Skip to content

feat: Group-based permissions — Phase 1 schema (AccountType, Permission, PermissionGrant)#9547

Open
Subash-Mohan wants to merge 8 commits intomainfrom
group-permissions-phase1-migration
Open

feat: Group-based permissions — Phase 1 schema (AccountType, Permission, PermissionGrant)#9547
Subash-Mohan wants to merge 8 commits intomainfrom
group-permissions-phase1-migration

Conversation

@Subash-Mohan
Copy link
Contributor

@Subash-Mohan Subash-Mohan commented Mar 23, 2026

Description

  • Adds AccountType enum (standard, bot, ext_perm_user, service_account, anonymous) to classify
    how accounts are created and what interface they use — replaces the identity aspect of UserRole
  • Adds Permission enum (19 tokens in action:resource format, e.g. read:connectors,
    manage:agents, admin) for group-based authorization
  • Creates permission_grant table linking groups to permission tokens, with unique constraint on
    (group_id, permission) and audit columns (granted_by, granted_at)
  • Adds is_default column to user_group for protected default groups (Basic, Admins)
  • Adds nullable account_type column to user (backfill in a future phase)
  • Adds performance index on user__user_group(user_id) for user→groups lookups

How Has This Been Tested?

Tested by running migrations

Additional Options

  • [Optional] Please cherry-pick this PR to the latest release version.
  • [Optional] Override Linear Check

Summary by cubic

Phase 1 schema for group-based permissions: adds AccountType on users, Permission and GrantSource enums, and a permission_grant table with audit fields, soft delete, and a unique (group_id, permission) constraint. Also adds a default-group flag, a user→groups index, model validation to block implied read tokens, fixes the Alembic down_revision, and notes a TODO to backfill account_type before enforcing NOT NULL.

  • New Features

    • Added nullable account_type on user (backfill planned, then enforce NOT NULL).
    • Introduced Permission (19 tokens incl. admin) and GrantSource enums.
    • Created permission_grant table with (group_id, permission) uniqueness, grant_source, granted_by, granted_at, is_deleted; group FK cascades on delete.
    • Added is_default to user_group to protect default groups.
    • Added index on user__user_group(user_id) for faster user→groups lookups.
    • Model-level check rejects implied read tokens from being stored.
  • Refactors

    • Moved AccountType to onyx.db.enums for reuse.

Written for commit c0eed0f. Summary will update on new commits.

@Subash-Mohan Subash-Mohan requested a review from a team as a code owner March 23, 2026 09:43
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR introduces Phase 1 of the group-based permissions schema: a new AccountType enum on user, a Permission enum with 19 tokens, a GrantSource enum, the permission_grant table linking groups to permission tokens, an is_default flag on user_group, and a performance index on user__user_group(user_id). It is a schema-only change with no application logic yet.

Compared to the initial review pass, most critical concerns have been resolved:

  • The granted_by → user.id FK constraint is now present in the migration with ondelete="SET NULL".
  • The group_id FK has ondelete="CASCADE" at both the DB level and the ORM cascade="all, delete-orphan".
  • AccountType was correctly moved to onyx/db/enums.py (not schemas.py).
  • UniqueConstraint now has an explicit name="uq_permission_grant_group_permission".
  • The TODO comment follows the required TODO(name): format.
  • is_deleted has both a Python-side default=False and server_default=text("false").

Two design-level concerns from prior threads remain open without a developer reply and are worth tracking into Phase 2:

  • read: prefix ambiguity: READ_AGENT_ANALYTICS and READ_QUERY_HISTORY use the read: prefix but live in the "Toggle tokens" section — not in Permission.IMPLIED. Any future authorization code that uses a startswith("read:") heuristic to identify implied permissions would silently deny access for these two tokens.
  • String value collisions with UserRole: Permission.BASIC_ACCESS = "basic", Permission.FULL_ADMIN_PANEL_ACCESS = "admin", and AccountType.EXT_PERM_USER = "ext_perm_user" share identical string representations with existing UserRole members. Serialized JSON comparisons or string-typed dispatch during the coexistence phase could silently match the wrong enum.

Confidence Score: 3/5

  • Safe to merge as a schema-only Phase 1 change, but two unresolved design concerns should be tracked before authorization logic is built on top of this schema.
  • Most previously flagged structural issues (missing FK, missing cascade, missing constraint name, wrong module placement) have been addressed. The two remaining open concerns — read: prefix ambiguity and string-value collisions with UserRole — are not runtime bugs in this PR but represent semantic landmines for Phase 2 authorization code. They lower confidence because the schema choices made here will be very expensive to change once permission-checking logic is written against them.
  • backend/onyx/db/enums.py — the Permission enum's read: prefix ambiguity and UserRole string-value collisions should be resolved before Phase 2 authorization logic is written.

Important Files Changed

Filename Overview
backend/alembic/versions/25a5501dc766_group_permissions_phase1.py New Alembic migration adding account_type to user, is_default to user_group, the permission_grant table with proper FK/cascade/unique constraints, and an index on user__user_group(user_id). Previously flagged issues (missing granted_by FK, unnamed UniqueConstraint) have been resolved. Minor: live-app-code imports in migration are the accepted pattern per developer reply.
backend/onyx/db/enums.py Adds AccountType, GrantSource, and Permission enums. Two outstanding design concerns from prior review remain unaddressed: (1) read: prefix is used for both implied-only tokens and directly-grantable toggle tokens (READ_AGENT_ANALYTICS, READ_QUERY_HISTORY), creating ambiguity for future authorization code; (2) three Permission/AccountType string values collide with existing UserRole values ("basic", "admin", "ext_perm_user"), risking silent mismatches during the coexistence period.
backend/onyx/db/models.py Adds PermissionGrant ORM model and is_default to UserGroup. Previously flagged items largely resolved: granted_by now has ondelete="SET NULL", group FK has ondelete="CASCADE", UserGroup.permission_grants relationship has cascade="all, delete-orphan", is_deleted has both Python-side default and server_default. AccountType is still imported on a standalone line separate from the block importing Permission/GrantSource — cosmetically inconsistent but functional.

Entity Relationship Diagram

%%{init: {'theme': 'neutral'}}%%
erDiagram
    USER {
        UUID id PK
        UserRole role
        AccountType account_type "nullable"
    }

    USER_GROUP {
        int id PK
        string name
        bool is_up_for_deletion
        bool is_default "NEW"
    }

    USER__USER_GROUP {
        int user_group_id PK,FK
        UUID user_id PK,FK
        bool is_curator
    }

    PERMISSION_GRANT {
        int id PK
        int group_id FK
        Permission permission
        GrantSource grant_source
        UUID granted_by FK "nullable"
        datetime granted_at
        bool is_deleted
    }

    USER ||--o{ USER__USER_GROUP : "belongs to"
    USER_GROUP ||--o{ USER__USER_GROUP : "contains"
    USER_GROUP ||--o{ PERMISSION_GRANT : "has (cascade delete)"
    USER ||--o{ PERMISSION_GRANT : "granted_by (SET NULL on delete)"
Loading

Reviews (8): Last reviewed commit: "feat: update unique constraint for permi..." | Re-trigger Greptile

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 4 files

Confidence score: 3/5

  • There is a concrete data-integrity risk in backend/alembic/versions/25a5501dc766_group_permissions_phase1.py: granted_by appears to reference a user but lacks a ForeignKeyConstraint, so orphaned or invalid references could be written without DB enforcement.
  • The redundant index in backend/alembic/versions/25a5501dc766_group_permissions_phase1.py is lower impact (performance/maintenance overhead), since UniqueConstraint("group_id", "permission") already supports group_id-leading lookups in PostgreSQL.
  • Given the medium severity and high confidence on the FK issue, this is not a critical blocker but does introduce meaningful regression risk if merged as-is.
  • Pay close attention to backend/alembic/versions/25a5501dc766_group_permissions_phase1.py - add the missing foreign key on granted_by and clean up the redundant index definition.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/alembic/versions/25a5501dc766_group_permissions_phase1.py">

<violation number="1" location="backend/alembic/versions/25a5501dc766_group_permissions_phase1.py:58">
P2: Missing `ForeignKeyConstraint` on `granted_by` — this column semantically references the `user` who granted the permission but has no FK, so the database won't enforce referential integrity. Add a foreign key to `user.id` (or document why it's intentionally omitted, e.g. to allow the granting user to be deleted without cascading).</violation>

<violation number="2" location="backend/alembic/versions/25a5501dc766_group_permissions_phase1.py:77">
P2: This index is redundant. The `UniqueConstraint("group_id", "permission")` on the same table already creates a composite index with `group_id` as the leading column, which PostgreSQL can use for `group_id`-only lookups. The extra index wastes disk space and adds write overhead without query benefit.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 23, 2026

🖼️ Visual Regression Report

Project Changed Added Removed Unchanged Report
admin 0 0 0 122 ✅ No changes
exclusive 0 0 0 8 ✅ No changes

Comment on lines +4000 to +4013
Enum(Permission, native_enum=False), nullable=False
)
grant_source: Mapped[GrantSource] = mapped_column(
Enum(GrantSource, native_enum=False), nullable=False
)
granted_by: Mapped[UUID | None] = mapped_column(
ForeignKey("user.id", ondelete="SET NULL"), nullable=True
)
granted_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)

group: Mapped["UserGroup"] = relationship(
"UserGroup", back_populates="permission_grants"
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 group_id FK and relationship missing cascade — will block group deletion

PermissionGrant is a first-class entity with its own integer PK (unlike the other association tables that use composite PKs to user_group.id). When the sync worker eventually hard-deletes a UserGroup after it's marked is_up_for_deletion=True, PostgreSQL will raise a FK constraint violation if any permission_grant rows reference that group.

Two fixes are needed:

  1. Add ondelete="CASCADE" to the FK so the DB will automatically clean up grants when the group is deleted:
Suggested change
Enum(Permission, native_enum=False), nullable=False
)
grant_source: Mapped[GrantSource] = mapped_column(
Enum(GrantSource, native_enum=False), nullable=False
)
granted_by: Mapped[UUID | None] = mapped_column(
ForeignKey("user.id", ondelete="SET NULL"), nullable=True
)
granted_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
group: Mapped["UserGroup"] = relationship(
"UserGroup", back_populates="permission_grants"
group_id: Mapped[int] = mapped_column(ForeignKey("user_group.id", ondelete="CASCADE"), nullable=False)
  1. Add cascade to the ORM relationship on UserGroup so ORM-level deletions also propagate:
permission_grants: Mapped[list["PermissionGrant"]] = relationship(
    "PermissionGrant", back_populates="group", cascade="all, delete-orphan"
)

Without both, any code path that deletes a UserGroup row (directly or via ORM) will be blocked silently or raise a runtime error once this table has data.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/onyx/db/models.py
Line: 4000-4013

Comment:
**`group_id` FK and relationship missing cascade — will block group deletion**

`PermissionGrant` is a first-class entity with its own integer PK (unlike the other association tables that use composite PKs to `user_group.id`). When the sync worker eventually hard-deletes a `UserGroup` after it's marked `is_up_for_deletion=True`, PostgreSQL will raise a FK constraint violation if any `permission_grant` rows reference that group.

Two fixes are needed:

1. Add `ondelete="CASCADE"` to the FK so the DB will automatically clean up grants when the group is deleted:

```suggestion
    group_id: Mapped[int] = mapped_column(ForeignKey("user_group.id", ondelete="CASCADE"), nullable=False)
```

2. Add `cascade` to the ORM relationship on `UserGroup` so ORM-level deletions also propagate:

```python
permission_grants: Mapped[list["PermissionGrant"]] = relationship(
    "PermissionGrant", back_populates="group", cascade="all, delete-orphan"
)
```

Without both, any code path that deletes a `UserGroup` row (directly or via ORM) will be blocked silently or raise a runtime error once this table has data.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

Choose a reason for hiding this comment

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

agree here

Comment on lines +4000 to +4013
Enum(Permission, native_enum=False), nullable=False
)
grant_source: Mapped[GrantSource] = mapped_column(
Enum(GrantSource, native_enum=False), nullable=False
)
granted_by: Mapped[UUID | None] = mapped_column(
ForeignKey("user.id", ondelete="SET NULL"), nullable=True
)
granted_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)

group: Mapped["UserGroup"] = relationship(
"UserGroup", back_populates="permission_grants"
Copy link
Contributor

Choose a reason for hiding this comment

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

agree here

Comment on lines +4013 to +4015
is_deleted: Mapped[bool] = mapped_column(
Boolean, nullable=False, server_default=text("false")
)
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 is_deleted has no Python-side default

is_deleted uses server_default=text("false") for the DB but has no default=False for the Python side. When a PermissionGrant object is created in memory and its is_deleted attribute is accessed before the first flush, it will be None rather than False, which can cause subtle bugs (not grant.is_deleted evaluates to True for None).

Other boolean columns in the codebase (e.g., is_up_for_deletion, is_up_to_date on UserGroup) consistently provide both:

Suggested change
is_deleted: Mapped[bool] = mapped_column(
Boolean, nullable=False, server_default=text("false")
)
is_deleted: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False, server_default=text("false")
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/onyx/db/models.py
Line: 4013-4015

Comment:
**`is_deleted` has no Python-side `default`**

`is_deleted` uses `server_default=text("false")` for the DB but has no `default=False` for the Python side. When a `PermissionGrant` object is created in memory and its `is_deleted` attribute is accessed before the first flush, it will be `None` rather than `False`, which can cause subtle bugs (`not grant.is_deleted` evaluates to `True` for `None`).

Other boolean columns in the codebase (e.g., `is_up_for_deletion`, `is_up_to_date` on `UserGroup`) consistently provide both:

```suggestion
    is_deleted: Mapped[bool] = mapped_column(
        Boolean, nullable=False, default=False, server_default=text("false")
    )
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/onyx/db/models.py">

<violation number="1" location="backend/onyx/db/models.py:4013">
P1: Adding `is_deleted` without updating uniqueness/read filtering makes soft-deleted permission grants behave like active rows. This can block re-grants and leak revoked permissions into authorization reads.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@Subash-Mohan Subash-Mohan force-pushed the group-permissions-phase1-migration branch from 0718191 to f95e630 Compare March 24, 2026 11:56
@github-actions
Copy link
Contributor

Preview Deployment

Status Preview Commit Updated
https://onyx-preview-afj1ig29u-danswer.vercel.app f95e630 2026-03-24 11:59:31 UTC

Comment on lines +346 to +393
class Permission(str, PyEnum):
"""
Permission tokens for group-based authorization.
19 tokens total. full_admin_panel_access is an override —
if present, any permission check passes.
"""

# Basic (auto-granted to every new group)
BASIC_ACCESS = "basic"

# Read tokens — implied only, never granted directly
READ_CONNECTORS = "read:connectors"
READ_DOCUMENT_SETS = "read:document_sets"
READ_AGENTS = "read:agents"
READ_USERS = "read:users"

# Add / Manage pairs
ADD_AGENTS = "add:agents"
MANAGE_AGENTS = "manage:agents"
MANAGE_DOCUMENT_SETS = "manage:document_sets"
ADD_CONNECTORS = "add:connectors"
MANAGE_CONNECTORS = "manage:connectors"
MANAGE_LLMS = "manage:llms"

# Toggle tokens
READ_AGENT_ANALYTICS = "read:agent_analytics"
MANAGE_ACTIONS = "manage:actions"
READ_QUERY_HISTORY = "read:query_history"
MANAGE_USER_GROUPS = "manage:user_groups"
CREATE_USER_API_KEYS = "create:user_api_keys"
CREATE_SERVICE_ACCOUNT_API_KEYS = "create:service_account_api_keys"
CREATE_SLACK_DISCORD_BOTS = "create:slack_discord_bots"

# Override — any permission check passes
FULL_ADMIN_PANEL_ACCESS = "admin"

# Permissions that are implied by other grants and must never be stored
# directly in the permission_grant table.
IMPLIED: ClassVar[frozenset[Permission]]


Permission.IMPLIED = frozenset(
{
Permission.READ_CONNECTORS,
Permission.READ_DOCUMENT_SETS,
Permission.READ_AGENTS,
Permission.READ_USERS,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 String value collisions with UserRole enum during coexistence phase

Three new enum values share identical string representations with existing UserRole values:

New enum Value Existing UserRole
Permission.BASIC_ACCESS "basic" UserRole.BASIC = "basic"
Permission.FULL_ADMIN_PANEL_ACCESS "admin" UserRole.ADMIN = "admin"
AccountType.EXT_PERM_USER "ext_perm_user" UserRole.EXT_PERM_USER = "ext_perm_user"

The PR description explicitly states these new enums "replace the identity aspect of UserRole", so both systems will coexist during the migration. Any code that compares or dispatches on the raw string value (e.g., serialized JSON, API responses, string-typed DB columns shared between systems) could silently match the wrong enum. The most dangerous case is "admin" — if the new permission checker short-circuits on permission_value == "admin" it would also match an UserRole.ADMIN string that leaked into the wrong context.

Consider disambiguating the string representations to make the two systems unambiguous at the serialization boundary:

# Example disambiguated names
BASIC_ACCESS = "perm:basic"           # instead of "basic"
FULL_ADMIN_PANEL_ACCESS = "perm:admin" # instead of "admin"

and in AccountType:

EXT_PERM_USER = "acct:ext_perm_user"  # instead of "ext_perm_user"

If changing the string values is too disruptive at this stage, at minimum add a comment documenting the intentional overlap and the phase-out plan to alert future reviewers.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/onyx/db/enums.py
Line: 346-393

Comment:
**String value collisions with `UserRole` enum during coexistence phase**

Three new enum values share identical string representations with existing `UserRole` values:

| New enum | Value | Existing `UserRole` |
|---|---|---|
| `Permission.BASIC_ACCESS` | `"basic"` | `UserRole.BASIC = "basic"` |
| `Permission.FULL_ADMIN_PANEL_ACCESS` | `"admin"` | `UserRole.ADMIN = "admin"` |
| `AccountType.EXT_PERM_USER` | `"ext_perm_user"` | `UserRole.EXT_PERM_USER = "ext_perm_user"` |

The PR description explicitly states these new enums "replace the identity aspect of `UserRole`", so both systems will coexist during the migration. Any code that compares or dispatches on the raw string value (e.g., serialized JSON, API responses, string-typed DB columns shared between systems) could silently match the wrong enum. The most dangerous case is `"admin"` — if the new permission checker short-circuits on `permission_value == "admin"` it would also match an `UserRole.ADMIN` string that leaked into the wrong context.

Consider disambiguating the string representations to make the two systems unambiguous at the serialization boundary:

```python
# Example disambiguated names
BASIC_ACCESS = "perm:basic"           # instead of "basic"
FULL_ADMIN_PANEL_ACCESS = "perm:admin" # instead of "admin"
```

and in `AccountType`:
```python
EXT_PERM_USER = "acct:ext_perm_user"  # instead of "ext_perm_user"
```

If changing the string values is too disruptive at this stage, at minimum add a comment documenting the intentional overlap and the phase-out plan to alert future reviewers.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +371 to +375
READ_AGENT_ANALYTICS = "read:agent_analytics"
MANAGE_ACTIONS = "manage:actions"
READ_QUERY_HISTORY = "read:query_history"
MANAGE_USER_GROUPS = "manage:user_groups"
CREATE_USER_API_KEYS = "create:user_api_keys"
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 read: prefix semantics broken by non-implied toggle tokens

Permission.IMPLIED establishes the convention that all read:* prefixed tokens are implied-only and never stored directly in permission_grant. However, READ_AGENT_ANALYTICS = "read:agent_analytics" and READ_QUERY_HISTORY = "read:query_history" use the same read: prefix but are NOT in Permission.IMPLIED — they are explicitly grantable "Toggle tokens."

This breaks the semantic invariant: any future authorization code that uses a prefix heuristic (e.g., permission.value.startswith("read:") to determine implied permissions) would incorrectly skip checking permission_grant for these two tokens, causing silent denial of access. Conversely, code that naively adds all read:* tokens to IMPLIED in a future phase would unintentionally exclude them from direct grants.

Consider renaming these two tokens to avoid the ambiguous read: prefix — for example view:agent_analytics / view:query_history — or explicitly documenting the break from the naming convention with a code comment.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/onyx/db/enums.py
Line: 371-375

Comment:
**`read:` prefix semantics broken by non-implied toggle tokens**

`Permission.IMPLIED` establishes the convention that all `read:*` prefixed tokens are implied-only and never stored directly in `permission_grant`. However, `READ_AGENT_ANALYTICS = "read:agent_analytics"` and `READ_QUERY_HISTORY = "read:query_history"` use the same `read:` prefix but are NOT in `Permission.IMPLIED` — they are explicitly grantable "Toggle tokens."

This breaks the semantic invariant: any future authorization code that uses a prefix heuristic (e.g., `permission.value.startswith("read:")` to determine implied permissions) would incorrectly skip checking `permission_grant` for these two tokens, causing silent denial of access. Conversely, code that naively adds all `read:*` tokens to `IMPLIED` in a future phase would unintentionally exclude them from direct grants.

Consider renaming these two tokens to avoid the ambiguous `read:` prefix — for example `view:agent_analytics` / `view:query_history` — or explicitly documenting the break from the naming convention with a code comment.

How can I resolve this? If you propose a fix, please make it concise.

["user.id"],
ondelete="SET NULL",
),
sa.UniqueConstraint("group_id", "permission"),
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 UniqueConstraint missing explicit name parameter

sa.UniqueConstraint("group_id", "permission") has no name argument. PostgreSQL auto-generates a name (typically permission_grant_group_id_permission_key), but that name is not guaranteed to be stable across fresh installs vs. migrated databases, and any future migration that needs to drop_constraint must know the exact name.

Consistent constraint naming also makes alembic revision --autogenerate diffs cleaner and avoids false-positives. The same issue exists in the ORM model's __table_args__.

Suggested change
sa.UniqueConstraint("group_id", "permission"),
sa.UniqueConstraint("group_id", "permission", name="uq_permission_grant_group_permission"),

And correspondingly in models.py:

UniqueConstraint("group_id", "permission", name="uq_permission_grant_group_permission"),
Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/alembic/versions/25a5501dc766_group_permissions_phase1.py
Line: 91

Comment:
**`UniqueConstraint` missing explicit `name` parameter**

`sa.UniqueConstraint("group_id", "permission")` has no `name` argument. PostgreSQL auto-generates a name (typically `permission_grant_group_id_permission_key`), but that name is not guaranteed to be stable across fresh installs vs. migrated databases, and any future migration that needs to `drop_constraint` must know the exact name.

Consistent constraint naming also makes `alembic revision --autogenerate` diffs cleaner and avoids false-positives. The same issue exists in the ORM model's `__table_args__`.

```suggestion
        sa.UniqueConstraint("group_id", "permission", name="uq_permission_grant_group_permission"),
```

And correspondingly in `models.py`:

```python
UniqueConstraint("group_id", "permission", name="uq_permission_grant_group_permission"),
```

How can I resolve this? If you propose a fix, please make it concise.

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.

2 participants