Skip to content

docs: propose custom_role and principal_group schema for [ADR6 (BDMS-1557)] - #1047

Open
likithabommasani21 wants to merge 4 commits into
stagingfrom
BDMS-1557-design-doc-permission-bundle-and-group-membership-schema-adr-6-follow-up
Open

likithabommasani21 wants to merge 4 commits into
stagingfrom
BDMS-1557-design-doc-permission-bundle-and-group-membership-schema-adr-6-follow-up

Conversation

@likithabommasani21

Copy link
Copy Markdown
Contributor

Why

This PR addresses the following problem / context:

  • ADR6 moves ownership of principal groups and their membership from Authentik into Ocotillo's database. Before implementation, this needs a schema proposal: how a group's identity/membership is stored, and how a reusable, editable permission bundle ("custom role") gets assigned to a group with live-reference semantics.

How

Implementation summary: the following was changed / added / removed:

  • Added docs/permission-bundle-and-group-membership-schema.md, proposing: principal_group/principal_group_member for group identity and membership, custom_role/custom_role_item for the permission bundle, and a new nullable custom_role_id on permission_grant for direct-grant vs. role-assignment rows.
  • Included a worked example (principal group, membership, role, and a later edit propagating with no re-sync).
  • Documented why custom_role was chosen over plain role (collision with the existing Role lexicon/enum).
  • Listed open questions, including naming confirmation for every proposed table/column, whether a bundle can be assigned to user/api_key directly, delegation boundaries, and whether a principal can hold multiple assignments.

Author Checklist

  • Changes are limited to the intended scope.
  • [N/A] Tests were added or updated where appropriate.
  • [N/A] Relevant tests and lint checks pass.
  • No secrets, credentials, or sensitive data were publicly exposed.
  • Documentation was updated where necessary.

Reviewer Checklist

  • Agree with the proposed table/column names (Open Questions 1-5), or propose alternatives.
  • Confirm whether a custom_role can be assigned to user/api_key directly, or only to a principal_group (Open Question 6).
  • Confirm whether delegation boundaries (ADR6 section 2) belong in this doc or a separate one (Open Question 7).
  • Confirm the worked example matches the intended mechanism.

Notes

Any special considerations, known limitations, risks, workarounds, or follow-up work to note?

  • This PR is the design doc only -- no implementation. Implementation gets its own ticket(s) once the open questions are resolved.

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Coverage

✅ 81.81% total — gate is 75%.

jirhiker commented Nov 9, 2026

Copy link
Copy Markdown
Member

Review: docs/permission-bundle-and-group-membership-schema.md

custom_role_item copies permission_grant. Both tables have the same five permission columns: capability, scope_type, scope_id, data_type, field_group. Only the holder is different. In custom_role_item, the holder is a role. In permission_grant, the holder is a principal.

Problems

  1. The proposal removes a safety constraint. db/permission_grant.py sets capability, scope_type and scope_id to NOT NULL. The code comment gives the reason: if scope_id has no value, the constraint fails and the row cannot give global access. A bundle-assignment row keeps these columns NULL. Thus, the columns must become nullable. After this change, only a CHECK constraint stops a partly filled row.
  2. Two tables need the same validation. Each table needs the lexicon foreign keys, the "*" rule for global scope, and the data-type checks. The two sets of rules can become different over time.
  3. The audit log does not record role edits. ADR5 section 3 says that the log must answer "who granted that, and when". A new custom_role_item row gives access to all members of each assigned group. But custom_role_item has no granted_by, granted_at, reason or end_date. In the worked example, permission_grant does not change. Thus, the audit log does not record the new access.
  4. The permission check needs two code paths. One path reads direct grants. The other path joins custom_role_item. The engine must then merge the two results.

Is the bundle necessary?

A principal_group with grants is already a bundle. Put the grants directly on the group. When you add a grant to the group, all members get it on their next request. This is the same live-reference result that the document wants.

The worked example shows the problem. It has one group, "AMMP Viewer", and one role, "AMMP Viewer Base Permissions". Each has one function, so the role adds no value.

A separate bundle is useful only when two or more groups, users or API keys use the same set of permissions. The document does not give an example of this. The example in open question 8 shows one group with two bundles. You can also do this with direct grants on the group.

Recommendation

  • Now: Remove custom_role and custom_role_item. Add principal_group and principal_group_member. Put grants in permission_grant with principal_type='principal_group'. Do not change the permission_grant constraints. Open questions 3 to 6 and 8 then become unnecessary.
  • Later, if more than one principal must use the same bundle: Do not copy the permission columns. Use one of these two options:
    • Make custom_role a principal type. The role's permissions are then permission_grant rows with principal_type='custom_role'. Record each assignment in a small custom_role_assignment table: role, principal, start and end dates, granted_by, reason. Validation and audit stay in one location.
    • Let a group be a member of a different group. This needs no new concept, but you must prevent cycles.

With both options, one table holds all permission definitions. That table has one set of constraints and one audit path.

Small error

The custom_role_item table in the worked example has an id column. The schema table above it does not have this column.


Generated by Claude Code

@TylerAdamMartinez TylerAdamMartinez 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.

Overall, looks great!


## Principal group schema

**`principal_group`** (proposed name) -- the group's identity:

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.

I don't think we need to mention "(proposed name)" in this document, since if this PR is approved, those will be the official names.

| principal_group_id | member_id | added_by |
|---|---|---|
| 1 | auth0\|sub-kelsey | jake |
| 1 | auth0\|sub-tyler | jake |

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.

🤠

@jacob-a-brown jacob-a-brown 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.

Overall I think that it looks good! Just some small comments.

I also think that, for the sake of being explicit, the schemas in ##Principal group schema should include the data types. It's pretty obvious from reading the tables, but since this defines the schemas the data types should be defined too.

To answer open questions:

  1. I think that principal_group is a good table name.
  2. I think that principal_group_member is a good table name. I don't know if member_id needs more than the bare sub since we can use that to identify the individual (?). What if it's an API key, though? Does that have an id that can get assigned to that field?
  3. I think that group assignment needs delegation boundaries once the schema is settled.
  4. I'm okay to keep ADR6's log table. Wouldn't we be able to query that to determine changes and who was present when? Creating something akin to StatusHistory seems like a lot of work for what we may be able to achieve by querying the table proposed by ADR6.

| `created_by` | Who created it |
| `created_at` | When |

**`principal_group_member`** -- its membership, one row per person:

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.

this is a many-to-many table? A table that relates a principal to a principal_group? If so that should be written out here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated

| Column | Meaning |
|---|---|
| `principal_group_id` | FK to `principal_group.id` |
| `member_id` | The Authentik-issued `sub` identifying the person |

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.

previously we we were identifying people/keys as principals. Should this be called principal_id?

@likithabommasani21 likithabommasani21 Nov 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For now I'd like to keep this as member_id rather than principal_id. It reads more clearly for what the column actually is (one person in this specific group), and it's not quite the same thing as permission_grant.principal_id, which can be a user, api, or a principal_group itself. If it turns out principal_id is the better name , happy to switch it .


| Column | Meaning |
|---|---|
| `principal_group_id` | FK to `principal_group.id` |

@ksmuczynski ksmuczynski Nov 11, 2026 •

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.

Consider id as the primary key instead of principal_group_id. The table name is typically included when referenced or called, so principal_group_member.id would be used instead of principal_group_member.principal_group_id. id is also used as the PK on most of the current tables, so we'd be keeping it consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, updated. principal_group_member now has its own id as primary key

@ksmuczynski
ksmuczynski self-requested a review November 11, 2026 21:10

@ksmuczynski ksmuczynski 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.

Responses to open questions:

  1. principal_group works as the table name.
  2. principal_group_member works as the table name. I'm not sure if we need a separate user table to display name/email per member. Maybe we keep as is and modify later if we receive feedback regarding the need for additional user information?
  3. I think we can figure group assignment boundaries later.
  4. Similar to question 2, maybe proceed with ADR 6's activity log and we could consider something more detailed if needed.

@ksmuczynski
ksmuczynski self-requested a review November 11, 2026 21:31

This branch has not been deployed

No deployments
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.

5 participants