Repository navigation
feat: Piece 01: add RBAC core models and permissions - #302
Conversation
emiliano-go
left a comment
There was a problem hiding this comment.
Reviewed against #237. Scope is right, it rebases clean, and ruff, mypy and the suite pass on my side (308 passed with testcontainers up). A few things before this lands as the base for pieces 2 to 6, inline. Main ones: the selectin relations eager load roles on every user query, and the assignment tables have no delete path.
| user_roles: Mapped[list["UserRole"]] = relationship( | ||
| "UserRole", | ||
| back_populates="user", | ||
| lazy="selectin", |
There was a problem hiding this comment.
lazy="selectin" on user_roles eager loads roles and role permissions on every user query. Loading one user now costs 4 statements instead of 2. tests/unit/modules/tier/test_models.py already guards this exact case for Tier.users (must be lazy select). Make it lazy select and eager load only where the effective permissions query needs it.
| permissions: Mapped[list["RolePermission"]] = relationship( | ||
| "RolePermission", | ||
| back_populates="role", | ||
| lazy="selectin", |
There was a problem hiding this comment.
Same selectin issue on Role.permissions and Role.user_roles (and on the UserRole sides): fetching a role drags in its permissions and every assigned user. Lazy select here too.
| ) | ||
| role_id: Mapped[int] = mapped_column( | ||
| Integer, | ||
| ForeignKey("roles.id"), |
There was a problem hiding this comment.
These assignment FKs have no ondelete and the relationships have no cascade, so deleting a Role through the ORM tries to null a non-nullable FK. I reproduced IntegrityError: NOT NULL constraint failed: role_permission.role_id. api_keys/models.py already uses ondelete="CASCADE"; add it plus cascade="all, delete-orphan", passive_deletes=True.
| ForeignKey("roles.id"), | ||
| index=True, | ||
| ) | ||
| permission_name: Mapped[str] = mapped_column(String(100), index=True) |
There was a problem hiding this comment.
permission_name accepts any string, so a typo sticks around and the permission check in piece 4 silently never matches it. Validate against PermissionNames in the schemas/service, or add a CheckConstraint here while the constants live in this module.
| autoincrement=True, | ||
| init=False, | ||
| ) | ||
| name: Mapped[str] = mapped_column(String(50), nullable=False, unique=True, index=True) |
There was a problem hiding this comment.
Role soft-deletes but name is globally unique, so a deleted role keeps its name reserved and nothing filters is_deleted=False yet. Tier has the same shape and handles it in TierService; mirror that in piece 2 so the role CRUD does not corner itself.
| children: tuple[PermissionNode, ...] = field(default_factory=tuple) | ||
|
|
||
|
|
||
| PERMISSION_TREE: tuple[PermissionNode, ...] = ( |
There was a problem hiding this comment.
Nothing binds the tree to PermissionNames; every value is retyped by hand, so a typo or a missing child ships silently. Add a small test that each node is a real permission and each parent lists exactly its children.
| "PermissionNames", | ||
| "PermissionNode", | ||
| "PERMISSION_TREE", | ||
| ] |
There was a problem hiding this comment.
No tests in this piece, and it is the base everyone else builds on. We already have spots at tests/unit/modules/tier/test_models.py and .../user/test_models.py. At minimum: the lazy guard, the tree check, and a Role + RolePermission + UserRole round trip.
|
Thanks for the detailed review. I’ve addressed the actionable Piece 1 feedback in commit 1. Eager
|
|
@Vins01-07 nice, cascade and lazy selects look right. The lazy test misses Small stuff: the new test dir needs an I don't want to push the rest to piece 2 or 4, let's get it in here:
Rest is done, I'll coordinate CI and merge with the maintainer. |
|
Thanks for the review. I’ve addressed the requested RBAC Piece 1 changes in the latest commit ( Implemented:
Verification:
Two points were intentionally not changed in this Piece 1 implementation:
The current Piece 1 introduces the RBAC ORM models, but there is no Role CRUD/service/deletion path in this branch yet. The existing project pattern handles soft-delete dependency/business logic in the service layer (for example, TierService), while the database cascade applies to an actual DELETE. I therefore did not add speculative model events or modify the global SoftDeleteMixin to implement role-specific soft-delete behavior. That would affect the deletion architecture outside the scope of the core model implementation. The Role deletion service will need to explicitly handle
These are currently intentionally recognized as valid permission names because they are defined in I left this behavior unchanged because the current permission-tree structure treats those parent names as valid defined permission names. If the intended contract is that only leaf permissions may be assigned to a role, I can adjust the validator and tests accordingly. Please let me know if you want the role-deletion behavior or parent-permission semantics implemented differently from the above. |
|
@Vins01-07 nice. Few things and we're good. The five touched files lost their trailing newline ( You added the Since On the parents: I want leaf-only. Soft delete is fine at the service layer in piece 2, but make sure role deletion there filters Rest is done, I'll coordinate CI and merge with the maintainer. |
|
@emiliano-go Here is the complete checklist against your comments: ✅ Added the missing trailing newline to role/init.py. Verification Targeted RBAC tests: 6 passed, 1 skipped. Soft delete I have intentionally left Role soft-delete assignment handling for Piece 2, as discussed. The Role service/deletion flow there will filter is_deleted and clear the role assignments, since the database ON DELETE CASCADE only applies to a hard delete. So, for the Piece 1 items from this review, everything requested in your latest feedback has now been addressed and pushed. Thanks again for the detailed review and guidance. |
|
@Vins01-07 all good, one thing. The tree test is a no-op now. defined = {v for n, v in vars(PermissionNames).items() if not n.startswith("_") and isinstance(v, str)}
parents = {p.name for p in PERMISSION_TREE}
assert tree_values == defined - parentsThat keeps the leaf-only contract and the drift guard. Rest is done, I'll coordinate CI and merge with the maintainer. |
|
@emiliano-go Verified with: 6 tests passed, 1 skipped The fix is pushed in commit 31fdfc2. Thanks again! |
|
Well, @igorbenav , this is ready for merge IMO. |
|
@emiliano-go Since it’s ready for merge from your side, do you have a rough estimate of when the maintainer might be able to review and merge it? No rush, just wanted to get a sense of the timeline. |
|
@Vins01-07 I'd like to say a week tops, but can't really be sure. This PR is probably gonna be the first to merge as it allows for other PRs to build on top. I'm glad my reviews helped you, feel free to AMA. |
|
Hey @Vins01-07, thanks a lot for picking this up, and thanks @emiliano-go for the review rounds. The core shape is mostly right for what #237 asks for: three tables ( Before we merge the models as Piece 01, though, I'd like to settle the spec around them. I was researching what other projects do and: 1. A flat list of
|
31fdfc2 to
e72206a
Compare
RBAC Piece 01 — Implementation UpdateThank you for the detailed review and for clarifying the direction for Piece 01. I went through the requested redesign and updated the implementation to follow the revised RBAC architecture. The core changes requested in the review are now implemented: flat I also added and updated the relevant tests, rebased the branch onto the latest A few items remain unchecked because they require PostgreSQL or belong to the API-key follow-up work. I have explicitly documented the reason for each one below rather than marking anything complete without verification. 1. PermissionsThe previous permission-tree implementation has been removed and replaced with a flat Each module now owns its permissions through its own enum:
For example: @register_permissions("tier")
class TierPermission(StrEnum):
READ = "tier.read"
CREATE = "tier.create"
UPDATE = "tier.update"
DELETE = "tier.delete"A central registry in
The registry validates that:
Permission modules are automatically discovered by walking This removes the previous duplication between the permission tree and permission constants and gives each module ownership of its own permission definitions. Checklist
2. Models / DatabaseThe RBAC database model was updated to match the requested design.
The join tables are now:
The join tables use composite primary keys:
Surrogate IDs and redundant indexes were removed. The explicit constraint names were also removed so the project's SQLAlchemy naming convention can generate the names. Checklist
Why the last two remain uncheckedPostgreSQL is not available in the current environment. I verified the database port directly: Because PostgreSQL was unavailable, I could not honestly run and verify the migration or the database-level cascade behavior. I therefore left both items unchecked rather than claiming database behavior that was not actually tested. 3. Permission CheckingPermission checking now goes through crudauth using: require_permissions(*needed)which uses: auth.current_user(check=check)The implementation includes:
A protected route was also added using the new permission dependency. Checklist
Why request-local permission caching remains unchecked
However, after checking the actual crudauth API, the callback supplied to: auth.current_user(check=check)receives the Because of that API boundary, request-local caching cannot currently be guaranteed inside the I therefore left this unchecked rather than claiming that the requested caching behavior is fully implemented. 4. SecurityThe existing self-profile ownership rule has been preserved while adding the requested The resulting rules are:
Privilege-escalation protection was also added. A principal cannot delegate permissions they do not already possess. This applies to:
When assigning a role, the permissions contained in that role are checked against the assigner's effective permissions. Superusers bypass these restrictions. Checklist
Why API-key scope narrowing remains uncheckedThe existing API-key implementation still uses its separate legacy resource/action permission model. Moving API-key scopes onto the new flat I left this as follow-up work rather than mixing another permission-system migration into Piece 01. 5. Tests / VerificationThe focused RBAC tests are passing. Results
Checklist
6. Full Test SuiteThe complete test suite was also executed. Result: The two failures are: Both failures occur because the tests iterate over These failures are outside the RBAC changes. The RBAC-specific tests continue to pass. I did not modify the OAuth implementation just to make this RBAC PR pass. 7. DeliveryThe branch was rebased onto the latest upstream The completed implementation was committed as: The rebased branch was successfully pushed to the fork. The working tree is clean after the push. Checklist
Final ChecklistPermissions
Models / Database
Permission Checking
Security
Tests / Delivery
SummaryThe core RBAC Piece 01 requested in the review is now implemented. The implementation now contains:
The remaining unchecked items are explicitly documented:
Thank you again for the detailed review and for clarifying the intended architecture. The feedback helped make the permission model substantially cleaner, less duplicated, and easier to extend while keeping Piece 01 focused on the core RBAC implementation. |
…session, and make require_permissions overridable
|
I pushed a few things. A
The permission lookup opened its own
The join tables couldn't be built from ids: Registry validation was accepting things the column or a check can't handle: nested names like Smaller things: the escalation helpers now agree on how they treat an unregistered name (never delegable, ignored when stored, no Tests: 407 passing. The new ones sign in for real and cover the role granting Two things left for Piece 02: role deletion has to clear assignments and filter Thanks again for the work, and for sticking with the review rounds. For the person taking the next one, please wait a couple of days because I'm refactoring some things in the codebase. |
|
Thank you so much for giving me the opportunity to contribute to the project and for guiding me through the review process. I really appreciate the time you took to review my work and the feedback you shared throughout the process. This was my first open-source contribution, so getting the chance to work on Piece 01 and seeing it merged into the project means a lot to me. 🙌 For Piece 02, my friend will be taking that one forward. Whenever the codebase refactor is complete and Piece 02 is ready to be worked on, please mention/tag me again so I can make sure we're aligned and help coordinate with him. Thanks again for the opportunity and for being so welcoming to new contributors! 🚀 |
Summary
Role,RolePermission, andUserRolemodels.User.user_rolesrelationship.Verification
ruff checkpasses.git diff --checkpasses.Related to #237