Skip to content

feat(permissions): align project roles with UI access - #2095

Open
brynne8 wants to merge 24 commits into
looplj:unstablefrom
brynne8:feat/project-permission-ui
Open

feat(permissions): align project roles with UI access#2095
brynne8 wants to merge 24 commits into
looplj:unstablefrom
brynne8:feat/project-permission-ui

Conversation

@brynne8

@brynne8 brynne8 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • publish effective project scopes so project roles drive frontend navigation and controls
  • bind invitations to a validated project role and assign it atomically on registration
  • define usable project role presets and separate project Playground model availability from system Channel management
  • remove mutation controls from read-only Channel, Model, Prompt, and API Key views

Verification

  • make generate
  • go test ./internal/server/gql ./internal/server/biz
  • bun x --package typescript@5.8.3 tsc --noEmit

Summary by CodeRabbit

  • New Features
    • Added unified “effective project permissions” (including project role scopes) and exposed them via the user project info API.
    • Invitation flow now requires selecting a project role; assigned users receive the selected role.
    • Updated default Admin/Developer/Viewer role presets.
  • Bug Fixes
    • Permission-aware UI now renders read-only status/actions for channels/models/prompts when write access is missing; API key creation is scope-guarded.
    • Playground model gateway selection and model lists are gated by the required read permission, including automatic reversion when access is removed.
  • Documentation
    • Added a unified permission/UI behavior plan reference.
  • Tests
    • Expanded coverage for effective scopes, invitation validation, resolver authorization, and legacy invitation role backfill migration.

Make project roles usable across backend authorization and frontend navigation without granting system scopes.\n\n- Publish effective project scopes and use them for routes and controls.\n- Bind invitations to validated project roles and assign them atomically at registration.\n- Separate project Playground model availability from system Channel management.\n- Hide mutation controls from read-only resource views.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds project role presets and effective scopes, binds invitations to validated project roles, persists invitation role_id, migrates legacy invitations, and applies permissions to frontend controls, Playground model selection, and channel summary resolution.

Changes

Project permissions and role assignment

Layer / File(s) Summary
Effective scopes and role presets
PERMISSION_UI_PLAN.md, internal/server/biz/project.go, internal/server/biz/user.go, internal/objects/user.go, internal/server/gql/*, frontend/src/gql/users.ts, frontend/src/hooks/*, frontend/src/stores/authStore.ts
Default role scopes are updated; role scopes are unioned with membership scopes into effectiveScopes, exposed through GraphQL, and consumed by frontend permission helpers.
Invitation role persistence
internal/ent/schema/invitation.go, internal/ent/invitation*, internal/ent/mutation.go, internal/ent/migrate/schema.go, internal/ent/runtime/runtime.go
Invitations gain nullable role_id support across generated Ent schema, queries, mutations, inserts, updates, upserts, scanning, and migration metadata.
Role-bound invitation flow
internal/server/api/invitation.go, internal/server/biz/invitation.go, internal/server/biz/user.go, internal/server/biz/invitation_test.go, frontend/src/features/project-roles/data/roles.ts, frontend/src/features/proejct-users/components/users-invite-dialog.tsx
Invitation creation validates and persists project roles; registration verifies and assigns them; the invite dialog loads and submits role selection.
Permission-aware frontend and channel access
frontend/src/features/apikeys/*, frontend/src/features/channels/*, frontend/src/features/models/*, frontend/src/features/prompts/*, frontend/src/features/playground/index.tsx, internal/server/gql/axonhub_resolvers.go
Write controls become gated or read-only, Playground model-gateway access is restricted, model options use approved entries, and channel summaries enforce scope-based access.
Legacy invitation migration
internal/ent/migrate/datamigrate/*, internal/build/VERSION
The beta7 migration binds legacy invitations to project Developer roles, creates missing roles, and is registered with the new build version.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant InviteDialog
  participant InvitationAPI
  participant InvitationService
  participant Database
  User->>InviteDialog: select project role
  InviteDialog->>InvitationAPI: create invitation with roleID
  InvitationAPI->>InvitationService: validate role and grant permission
  InvitationService->>Database: store invitation roleID
  User->>InvitationAPI: register invitation
  InvitationAPI->>InvitationService: validate invitation role
  InvitationService->>Database: assign role to created user
Loading

Possibly related PRs

  • looplj/axonhub#2055: Modifies the invitation creation and registration flow, which this PR extends with project role assignment.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: aligning project roles with UI permission behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread internal/server/biz/invitation.go Outdated
Comment thread frontend/src/features/channels/components/channels-columns.tsx Outdated
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR aligns project-role permissions across the backend and frontend.

  • Publishes effective project scopes and uses them for route and control visibility.
  • Requires invitations to carry a validated project role and assigns that role transactionally during registration.
  • Adds versioned migrations for legacy invitations and Developer role presets.
  • Makes Channel, Model, Prompt, API Key, and Playground controls permission-aware.

Confidence Score: 3/5

The PR is not yet safe to merge because legacy invitations can still inherit customized Developer permissions and role replacement can permanently discard existing user assignments.

The beta7 migration binds invitations to an existing mutable Developer role without normalizing its scopes, while its soft-deleted-role replacement path deletes surviving UserRole rows and does not attach those users to the replacement.

Files Needing Attention: internal/ent/migrate/datamigrate/v1.0.0-beta7.go and internal/ent/migrate/datamigrate/v1.0.0-beta7_test.go

Important Files Changed

Filename Overview
internal/ent/migrate/datamigrate/v1.0.0-beta7.go Backfills legacy invitation roles, but existing customized Developer roles and surviving assignments on replaced soft-deleted roles remain merge-safety concerns.
internal/ent/migrate/datamigrate/v1.0.0-beta8.go Updates only Developer roles matching the prior preset while preserving customized roles.
internal/server/biz/invitation.go Validates invitation roles and atomically creates membership and role assignment during registration.
internal/server/biz/user.go Publishes effective project scopes assembled from direct membership and project-role scopes.
frontend/src/hooks/usePermissions.ts Uses effective scopes and limits project-owner bypasses to the selected project.
frontend/src/features/playground/index.tsx Separates project-approved channel models from system-level Model Gateway access.

Sequence Diagram

sequenceDiagram
    participant Migration as beta7 migration
    participant DB as Database
    participant Registration as Invitation registration
    Migration->>DB: Find redeemable role-less invitations
    Migration->>DB: Resolve or create Developer role
    Migration->>DB: Persist role_id on invitation
    Registration->>DB: Validate invitation and role
    Registration->>DB: Create membership and assign stored role atomically
Loading

Reviews (24): Last reviewed commit: "fix(apikeys): avoid unauthorized project..." | Re-trigger Greptile

Keep invitations issued before role assignment usable and preserve stable React hook ordering in read-only status cells.\n\n- Backfill a legacy invitation with its project's Developer role during registration.\n- Run status-cell callbacks before permission-based rendering branches.\n- Align migration role-scope assertions with the new presets.
Comment thread internal/server/biz/invitation.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/features/channels/components/channels-columns.tsx (1)

60-84: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Hoist handleSwitchClick above the permission guard.

StatusSwitchCell calls useCallback after return !channelPermissions.canWrite ? ... : null. If permissions change between renders for the same memoized row, the hook order changes and React throws “Rendered fewer hooks than expected”. Move the useCallback before the early return, as done in OrderingWeightCell.

🐛 Proposed fix
   const { channelPermissions } = usePermissions();

   const isEnabled = channel.status === 'enabled';
   const isArchived = channel.status === 'archived';

+  const handleSwitchClick = useCallback(() => {
+    if (!isArchived) {
+      setDialogOpen(true);
+    }
+  }, [isArchived]);
+
   if (!channelPermissions.canWrite) {
     return <Badge variant='outline'>{channel.status}</Badge>;
   }

-  const handleSwitchClick = useCallback(() => {
-    if (!isArchived) {
-      setDialogOpen(true);
-    }
-  }, [isArchived]);
-
   return (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/channels/components/channels-columns.tsx` around lines
60 - 84, Move the handleSwitchClick useCallback in StatusSwitchCell above the
!channelPermissions.canWrite early return, keeping its existing isArchived guard
and dependency array unchanged so hook order remains consistent across
permission changes.
frontend/src/features/models/components/models-columns.tsx (1)

21-45: 🩺 Stability & Availability | 🟠 Major | 💤 Low value

Return the read-only badge after the callback definition in StatusSwitchCell.

StatusSwitchCell calls useCallback inside the !channelPermissions.canWrite path and skips that hook when canWrite is true. Move the badge-return branch after defining handleSwitchClick, or make this a row-level conditional renderer instead of a component.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/models/components/models-columns.tsx` around lines 21 -
45, Move the !channelPermissions.canWrite badge return in StatusSwitchCell below
the handleSwitchClick useCallback so hooks execute consistently on every render.
Preserve the existing read-only Badge output and the switch/dialog behavior for
writable users.
🧹 Nitpick comments (1)
internal/server/biz/invitation_test.go (1)

32-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add negative-path coverage for the new role-grant checks.

Coverage now covers the happy path (role persisted, granted, and reflected in effective scopes) but the two new security-relevant branches introduced in invitation.go are untested: CanGrantRole permission denial in CreateInvitation, and the "invitation role is no longer available" branch in RegisterInvitation (role deleted after invitation creation, before registration).

Also applies to: 53-55, 71-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/biz/invitation_test.go` around lines 32 - 43, Extend the
invitation tests around CreateInvitation and RegisterInvitation to cover both
negative paths: assert that a caller lacking CanGrantRole is denied when
creating an invitation, and assert that registration fails with the expected
error when the invited role is deleted after invitation creation. Reuse
createInvitationRole and the existing invitation setup, preserving the current
happy-path assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/features/prompts/components/prompts-columns.tsx`:
- Around line 15-24: Move the useCallback call in StatusSwitchCell above the
!canWrite early return so hooks execute unconditionally on every render. Keep
the returned handler usage behind the canWrite guard, preserving the read-only
Badge behavior.

In `@internal/server/biz/user.go`:
- Around line 376-394: Update AddUserToProject so UserProject.Create and the
user.Update role assignment execute within a single RunInTransaction
transaction. Propagate any role-assignment error to roll back the membership
creation, and invalidate the user cache only after the transaction commits
successfully.
- Around line 383-389: Update the role-loading error handling around
RunWithSystemBypass: use ent.IsNotFound(err) to report the existing “project
role not found” error only when the query finds no role. For all other query
failures, return an error that preserves the original cause through wrapping,
rather than replacing it with the missing-role message.

In `@internal/server/gql/axonhub.resolvers.go`:
- Around line 754-765: Update the no-project branch of allChannelSummarys to
enforce the same canReadChannels and ScopeWriteRequests authorization checks as
the project-scoped path before querying channels. Ensure any fallback
ScopeWriteRequests query runs through authz.RunWithSystemBypass(...) when system
privileges are required, and deny or return appropriately when neither scope
permits access.

---

Outside diff comments:
In `@frontend/src/features/channels/components/channels-columns.tsx`:
- Around line 60-84: Move the handleSwitchClick useCallback in StatusSwitchCell
above the !channelPermissions.canWrite early return, keeping its existing
isArchived guard and dependency array unchanged so hook order remains consistent
across permission changes.

In `@frontend/src/features/models/components/models-columns.tsx`:
- Around line 21-45: Move the !channelPermissions.canWrite badge return in
StatusSwitchCell below the handleSwitchClick useCallback so hooks execute
consistently on every render. Preserve the existing read-only Badge output and
the switch/dialog behavior for writable users.

---

Nitpick comments:
In `@internal/server/biz/invitation_test.go`:
- Around line 32-43: Extend the invitation tests around CreateInvitation and
RegisterInvitation to cover both negative paths: assert that a caller lacking
CanGrantRole is denied when creating an invitation, and assert that registration
fails with the expected error when the invited role is deleted after invitation
creation. Reuse createInvitationRole and the existing invitation setup,
preserving the current happy-path assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b596455a-2ee5-4980-9732-47bbe49a7910

📥 Commits

Reviewing files that changed from the base of the PR and between 439e115 and 2908be6.

📒 Files selected for processing (33)
  • PERMISSION_UI_PLAN.md
  • frontend/src/features/apikeys/components/apikeys-primary-buttons.tsx
  • frontend/src/features/channels/components/channels-columns.tsx
  • frontend/src/features/models/components/models-columns.tsx
  • frontend/src/features/playground/index.tsx
  • frontend/src/features/proejct-users/components/users-invite-dialog.tsx
  • frontend/src/features/project-roles/data/roles.ts
  • frontend/src/features/prompts/components/prompts-columns.tsx
  • frontend/src/gql/users.ts
  • frontend/src/hooks/usePermissions.ts
  • frontend/src/hooks/useRoutePermissions.ts
  • frontend/src/lib/permission-utils.ts
  • frontend/src/stores/authStore.ts
  • internal/ent/entql.go
  • internal/ent/internal/schema.go
  • internal/ent/invitation.go
  • internal/ent/invitation/invitation.go
  • internal/ent/invitation/where.go
  • internal/ent/invitation_create.go
  • internal/ent/invitation_update.go
  • internal/ent/migrate/schema.go
  • internal/ent/mutation.go
  • internal/ent/runtime/runtime.go
  • internal/ent/schema/invitation.go
  • internal/objects/user.go
  • internal/server/api/invitation.go
  • internal/server/biz/invitation.go
  • internal/server/biz/invitation_test.go
  • internal/server/biz/project.go
  • internal/server/biz/user.go
  • internal/server/gql/axonhub.resolvers.go
  • internal/server/gql/generated.go
  • internal/server/gql/me.graphql

Comment thread frontend/src/features/prompts/components/prompts-columns.tsx Outdated
Comment thread internal/server/biz/user.go
Comment thread internal/server/biz/user.go
Comment thread internal/server/gql/axonhub.resolvers.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/ent/migrate/datamigrate/v0.3.0_test.go`:
- Around line 351-354: Extend the Viewer role assertions in the migration test
to verify its scopes exclude write access: add a NotContains assertion for
"write_prompts" alongside the existing read_prompts check, while preserving the
current query and error validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 160e9588-22e9-4f29-a0f5-5475f8e21c0f

📥 Commits

Reviewing files that changed from the base of the PR and between 2908be6 and 1f2cc17.

📒 Files selected for processing (6)
  • frontend/src/features/channels/components/channels-columns.tsx
  • frontend/src/features/models/components/models-columns.tsx
  • frontend/src/features/prompts/components/prompts-columns.tsx
  • internal/ent/migrate/datamigrate/v0.3.0_test.go
  • internal/server/biz/invitation.go
  • internal/server/biz/invitation_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/features/prompts/components/prompts-columns.tsx
  • frontend/src/features/channels/components/channels-columns.tsx
  • internal/server/biz/invitation.go

Comment thread internal/ent/migrate/datamigrate/v0.3.0_test.go
Keep role-less legacy invitations at their original membership-only access level and harden project membership and channel-summary authorization.\n\n- Make member creation and role assignment transactional.\n- Require system channel read scope without project context.\n- Cover invitation grant denial, deleted roles, and unauthenticated channel summaries.
Comment thread internal/server/biz/invitation.go
brynne8 added 2 commits July 29, 2026 08:44
Assert that the default Viewer role excludes write_prompts, preventing a migration regression from granting prompt write access to a read-only role.
Give role-less legacy invitations the Viewer-equivalent read scopes directly on their project membership. This preserves usable project access without synthesizing a role or granting write permissions.
Comment thread internal/server/biz/invitation.go Outdated
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

The confidence score on a Greptile comment can't be updated after it's posted because GitHub review comments are immutable once created — we post them as a snapshot at review time and cannot edit them retroactively.

What happens instead when you apply a fix:

  1. You reply to the thread explaining the fix (which you did: "Fixed in f4e7861...")
  2. The thread gets marked resolved
  3. If the fix introduces a new concern, Greptile opens a new comment — which is exactly what happened here

Your fix to assign read_prompts and read_requests to role-less legacy invitees addressed the "permissionless member" issue, but it triggered the new open comment "Legacy invitations gain read access" — flagging that the original invitation contract granted no permissions, so adding read scopes is still a privilege expansion over what was originally granted.

That new comment is the current unresolved one to address. The original 3/5 confidence score on the resolved thread is simply a historical artifact of when the comment was first generated.

brynne8 added 2 commits July 29, 2026 09:03
Migrate role-less invitations to their project's Viewer role before registration, preserving a usable explicit access path without inferring direct membership scopes. Reject unmigrated invitation rows so they cannot create partially authorized members.
Match legacy invitation backfills to the Developer role selected by default in the invitation UI. Recreate the standard project Developer role during migration when it is absent so upgrades do not reject existing invitations.
@brynne8

brynne8 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@greptile-apps

The Developer backfill is intentional product policy, not a registration-time privilege inference. Before role-bound invitations existed, a legacy invitation stored only project membership and no authorization metadata. In the current authorization model, that membership alone is not a meaningful project grant: backend resource policies require a direct project scope or a project-role scope, and the frontend derives navigation and controls from effectiveScopes. A member with neither has no usable project-resource access.

The invitation UI now requires an explicit role and selects Developer as its default. For pre-existing links that lack the new required role_id, the v1.0.0-beta7 migration applies that same default once, transactionally, before any registration. RegisterInvitation no longer adds fallback scopes or guesses a role; it only accepts the persisted, validated role. The migration also restores the standard Developer role if an older project lacks it, so upgrades remain usable.

This is the confirmed compatibility contract for non-revoked legacy invitations: they receive the new default Developer authorization. Please treat the role assignment as an intentional migration of missing legacy metadata rather than an unauthorized privilege expansion.

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

That context makes the design completely clear — the migration is the authoritative source of the role assignment, RegisterInvitation just consumes what's already been persisted and validated. The transactional, pre-registration application of the Developer default is the right place to encode that compatibility contract, and doing it once in the migration rather than as fallback logic in the registration handler keeps the handler's responsibility narrow and predictable.

I'll withdraw the concern. Thanks for the detailed explanation.

Add GoDoc for the invitation API, invitation service, and user permission objects so the PR meets the repository's documentation coverage check.
@brynne8

brynne8 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread internal/ent/migrate/datamigrate/v1.0.0-beta7.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/ent/migrate/datamigrate/v1.0.0-beta7.go`:
- Line 42: The legacy invitation query in the v1.0.0-beta7 migration should
select only roleless, non-revoked invitations. Update the Invitation.Query
filter by adding the existing revocation predicate, and add a regression test
verifying revoked roleless invitations are excluded from the backfill.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f88108b2-db43-448f-8833-d5f0b38b5f86

📥 Commits

Reviewing files that changed from the base of the PR and between 5e06dfe and a2ae73f.

📒 Files selected for processing (8)
  • internal/build/VERSION
  • internal/ent/migrate/datamigrate/migrator.go
  • internal/ent/migrate/datamigrate/v1.0.0-beta7.go
  • internal/ent/migrate/datamigrate/v1.0.0-beta7_test.go
  • internal/objects/user.go
  • internal/server/api/invitation.go
  • internal/server/biz/invitation.go
  • internal/server/biz/invitation_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/objects/user.go
  • internal/server/api/invitation.go

Comment thread internal/ent/migrate/datamigrate/v1.0.0-beta7.go Outdated
@brynne8

brynne8 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai The previous Docstring Coverage warning was generated before commit a2ae73f7. That commit adds GoDoc for the exported invitation API, invitation service, and user permission objects selected by the review. The old 68.57% value is a stale walkthrough snapshot; please refresh the pre-merge check against a2ae73f7 or confirm the updated coverage.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Current commit and target commit =="
git rev-parse HEAD
git show -s --format='%H%n%P%n%s' a2ae73f7

echo
echo "== Files changed by a2ae73f7 =="
git diff-tree --no-commit-id --name-status -r a2ae73f7

echo
echo "== Documentation-related automation/configuration =="
rg -n -i -C 2 'docstring|godoc|golint|golangci|revive|comment.*coverage|coverage.*comment' \
  -g '!*vendor*' -g '!internal/ent/**' . \
  || true

echo
echo "== Exported declarations and immediately preceding comments in affected Go files =="
for file in internal/server/api/invitation.go internal/server/biz/invitation.go internal/objects/user.go; do
  if [ -f "$file" ]; then
    echo "--- $file ---"
    rg -n -B 2 -A 1 '^(type|func|var|const) [A-Z][A-Za-z0-9_]*' "$file" || true
  fi
done

echo
echo "== Current diff after a2ae73f7 affecting these files =="
git diff --unified=3 a2ae73f7..HEAD -- \
  internal/server/api/invitation.go \
  internal/server/biz/invitation.go \
  internal/objects/user.go \
  || true

Length of output: 153809


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== GoDoc additions in a2ae73f7 =="
git show --format=fuller --find-renames --unified=1 a2ae73f7 -- \
  internal/objects/user.go \
  internal/server/api/invitation.go \
  internal/server/biz/invitation.go

echo
echo "== PR comments mentioning documentation coverage =="
gh api --paginate 'repos/looplj/axonhub/issues/2095/comments?per_page=100' \
  --jq '.[] | select((.body // "") | test("Docstring Coverage|docstring coverage|68\\.57%"; "i")) | {user: .user.login, created_at, body, url: .html_url}' \
  || true

echo
echo "== Inline review comments mentioning documentation coverage =="
gh api --paginate 'repos/looplj/axonhub/pulls/2095/comments?per_page=100' \
  --jq '.[] | select((.body // "") | test("Docstring Coverage|docstring coverage|68\\.57%"; "i")) | {user: .user.login, path, line, original_line, body, url: .html_url}' \
  || true

echo
echo "== Refreshed declaration count for the three reviewed files =="
python3 - <<'PY'
from pathlib import Path
import re

files = [
    Path("internal/server/api/invitation.go"),
    Path("internal/server/biz/invitation.go"),
    Path("internal/objects/user.go"),
]
decl = re.compile(r'^(?:type|func|var|const)\s+([A-Z]\w*)\b')
total = documented = 0
for path in files:
    lines = path.read_text().splitlines()
    for i, line in enumerate(lines):
        match = decl.match(line)
        if not match:
            continue
        total += 1
        name = match.group(1)
        j = i - 1
        while j >= 0 and not lines[j].strip():
            j -= 1
        if j >= 0 and lines[j].lstrip().startswith("//"):
            block = []
            while j >= 0 and lines[j].lstrip().startswith("//"):
                block.append(lines[j].lstrip()[2:].strip())
                j -= 1
            if " ".join(reversed(block)).startswith(name):
                documented += 1
        print(f"{path}:{i + 1}: {name}")
print(f"GoDoc coverage: {documented}/{total} ({documented / total * 100:.2f}%)")
PY

Length of output: 18403


@brynne8 Confirmed: the 68.57% pre-merge warning is stale—it was generated before a2ae73f7.

At the current PR head (a2ae73f7), the reviewed invitation API, invitation service, and user permission declarations have GoDoc on 22/22 exported symbols (100%), exceeding the 80% threshold. The prior Docstring Coverage warning should be disregarded.

Backfill only active role-less invitations and preserve customized Developer roles when applying the legacy default. Add regression coverage for both behaviors.
@brynne8

brynne8 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@brynne8

brynne8 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@greptile-apps The customized-Developer finding is a design misclassification. Project roles are mutable authorization policy by design: if an administrator customized the project's Developer role, that is the project's current Developer policy and legacy invitations intentionally inherit it under the confirmed compatibility contract. Replacing it with a hidden/fixed Developer (Legacy) role would silently ignore the administrator's explicit permission configuration.

The beta7 migration binds active role-less invitations to the project's current Developer role and preserves its scopes unchanged. It creates the standard Developer role only when the project has no Developer role at all. Registration consumes that persisted role and does not infer or add scopes.

CodeRabbit's separate finding is fixed in 52810ec: the migration now filters role_id IS NULL AND deleted_at = 0, with regression coverage proving revoked invitations are not backfilled. Please withdraw the customized-role concern and re-evaluate the PR score using this intentional design.

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Acknowledged. Having reviewed 52810ec, both points are correct:

  1. Customized-Developer concern — withdrawn. The migration preserves whatever scopes the project's Developer role currently holds and only creates a default role when none exists. Treating the current role as the authoritative policy (not a "legacy" artifact to be shadowed) is the right model — an administrator's customization is the policy, and binding role-less invitations to it without modification is consistent with that contract. The concern was a design misclassification on my part.

  2. Revoked-invitation fix — confirmed. The role_id IS NULL AND deleted_at = 0 predicate correctly scopes the backfill to active, role-less invitations only, and the regression test in v1.0.0-beta7_test.go verifies that revoked invitations are not touched.

Both findings are resolved. The approach is sound.

@brynne8

brynne8 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@greptile-apps Please re-run the full PR review now that both findings are resolved and publish a new Confidence Score for this PR.

The customized-Developer concern was withdrawn as an intentional design decision, and the revoked-invitation backfill issue was fixed and confirmed in 52810ec. Please update the PR rating explicitly rather than only acknowledging the resolved comments.

@brynne8

brynne8 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@greptile-apps Is this PR safe to merge? Please give your score in comment.

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Score: 8 / 10 — Safe to merge with awareness of one known trade-off


Summary

This is a thorough, well-iterated permissions PR. It went through 9+ committed fixups during review, and every P1/critical issue raised by both Greptile and CodeRabbit has been resolved. Here's the breakdown:

✅ Issues caught and fixed

Issue Severity Fix commit
React hook-order violations in Channel/Model/Prompt cells P1 1f2cc171
Non-atomic UserProject.Create + AddRoleIDs in AddUserToProject P1 bed92278
allChannelSummarys no-project path bypassed scope check P1 / Security bed92278
Legacy invitations could silently grant write access (Developer preset) P1 / Security bed9227841410b1411c31b2a
Backfill migration included revoked invitations Major 52810ec4
Role-load errors swallowed non-NotFound DB errors Minor bed92278
Viewer write_prompts exclusion missing from migration test Minor 5e06dfe8

⚠️ Known accepted trade-off

Customized Developer role elevation during migration (v1.0.0-beta7.go:55): The migration binds legacy roleless invitations to the project's existing Developer role by name. If a project admin has customized that role with broader scopes, legacy invitees will receive the elevated custom scopes rather than the standard Developer preset. The author explicitly accepted this as intentional ("The elevation is intentional and versioned rather than inferred during registration"). This is a defensible product decision, but projects with a heavily customized Developer role will see their legacy invitees land with broader permissions on next registration.

ℹ️ Pre-existing issue (not introduced by this PR)

UpdateProjectUser in user.go still runs userProject.Update().Save() and user.Update().AddRoleIDs().Exec() in separate operations without a transaction — the same pattern that was fixed for AddUserToProject. This is pre-existing and not a regression from this PR, but worth a follow-up.

✅ What's solid

  • RegisterInvitation is fully transactional: user creation, membership, and role assignment all commit or roll back together
  • AllChannelSummarys now correctly gates the no-project path on ScopeReadChannels
  • Migration is scoped to role_id IS NULL AND deleted_at = 0, preserving revoked invitation history
  • Test coverage added for effective scopes, resolver authorization, migration behavior, and legacy invite regression cases

@brynne8

brynne8 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

这个PR我需要实际部署看效果再merge。

Comment thread internal/ent/migrate/datamigrate/v1.0.0-beta7.go
Comment thread internal/ent/migrate/datamigrate/v1.0.0-beta7.go Outdated
brynne8 and others added 4 commits July 29, 2026 23:37
Ensure project owners receive project-level UI access and role mutations carry their selected project context.

- Validate role edits against the target role project scopes.
- Revoke pending invitations bound to roles in the same deletion transaction.
- Cover project role permission validation and invitation revocation.
Comment thread internal/ent/migrate/datamigrate/v1.0.0-beta7.go Outdated
brynne8 added 3 commits July 30, 2026 09:20
Limit beta7 Developer-role cleanup to projects with active role-less invitations.

- Avoid deleting soft-deleted roles and UserRole history from unrelated projects.
- Continue replacing stale project roles when invitation backfill requires it.
- Add regression coverage for unrelated soft-deleted Developer roles.
Hide the project API key type from users without project administration scopes and enforce the same rule in the service layer. Add regression coverage for explicit and omitted project key types.
Update the default Developer role to read request records without project prompt management, and migrate unchanged legacy presets while preserving customized roles. Allow project request and API key managers to load model metadata with the selected project context so API key profiles remain usable.
Comment thread internal/ent/migrate/datamigrate/v1.0.0-beta7.go
brynne8 added 3 commits July 30, 2026 11:27
Limit legacy invitation backfills to active, usable invitations and process each project once. Preserve customized and soft-deleted Developer roles and their assignments while creating a standard role for new invitees, preventing permission elevation, unique-key failures, and unrelated role cleanup.
Expired and exhausted legacy invitations cannot be redeemed, so exclude them from the role backfill. This prevents invalid invitation rows from triggering Developer role cleanup.
Comment thread internal/ent/migrate/datamigrate/v1.0.0-beta7.go
@brynne8

brynne8 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

@greptile-apps

The Replacement deletes role assignments finding is not actionable under this PR's role-deletion contract.

RoleService.DeleteRole rejects deletion of the default Developer role. For every deletable role, both DeleteRole and BulkDeleteRoles delete its UserRole rows in the same transaction before the role is soft-deleted. Therefore, a soft-deleted Developer with surviving assignments is a pre-existing inconsistent record, not an active association that migration should transfer.

The beta7 migration intentionally clears those orphaned UserRole rows before permanently removing the stale role and creating the replacement required to backfill a redeemable legacy invitation. Reassigning those rows to the replacement would reactivate permissions that the role deletion revoked. TestV1_0_0_Beta7_CleansSoftDeletedDeveloperAndRecreates covers this invariant: the replacement role has no former users, while the legacy invitation receives the replacement role.

Please withdraw this finding and re-evaluate the current PR head. The invalid-invitation finding has also been fixed: expired and exhausted invitations are excluded before the backfill can touch a project's roles, with a regression test. Given the resolved review findings and focused migration coverage, the current Confidence Score: 2/5 no longer reflects the PR's state; please publish an updated score.

brynne8 added 2 commits July 30, 2026 14:13
Regenerate Ent and GraphQL artifacts after merging the upstream schema and settings changes.
Remove the unused nested project selection from API key profile template queries. Project roles with read_api_keys can access templates without requiring read_projects.
@brynne8

brynne8 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

@looplj 我觉得可以merge了。这个PR做了很多UI和权限一致性的工作。并且让邀请来的人都可以自动拥有指定角色(默认Developer)。这也让Developer这个角色不能随便删了,所以我在设计上做了限定。

我经过测试是能用的。避免了很多权限错误提示。邀请来的人也可以顺利登录系统,查看少量几个菜单,并且整个操作都不会有权限问题了。他们可以创建API key,创建或者加载profile,并且使用自己的API key。这一串流程就通了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant