Conversation
|
Thanks for the pull request, @mariajgrimaldi! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
Emits ROLE_ASSIGNMENT_CREATED and ROLE_ASSIGNMENT_DELETED events after role assignment operations commit, so consumers can stay up to date on the authorization state of the system. Each event includes the subject, role, scope, and the actor performing the operation (via get_current_user()). Also adds the auditability ADR (0012) documenting the design decisions behind this feature.
Introduces RoleAssignmentAudit, a read-only log of role assignment and removal events. Records store namespaced key strings (subject, role, scope) rather than FK references to live objects, so the audit history survives deletions independently of the authorization state. - Model and migration (0008) - Handler wired to ROLE_ASSIGNMENT_CREATED/DELETED signals - Read-only Django admin with scope-type filter and namespace-stripped display - Display properties (subject_display, role_display, scope_display) on the model - AUTHZ_POLICY_ATTRIBUTES_SEPARATOR moved to constants/ to avoid circular import - ADR 0012 updated with independence guarantee, actor exception, and filter rationale
Add a `for_scope_namespace` queryset method so scope-type filtering is available both in the admin and in API-level querysets, without duplicating the namespace-prefix logic. The admin `ScopeTypeFilter` now delegates to this method instead of building the `scope__startswith` lookup directly.
The released version of openedx-events does not yet include the RoleAssignmentEventData event type needed by the audit trail feature. Pin to the feature branch until it is released. TODO: revert to a released version once MJG/authz-audit is merged and released to PyPI.
9e10685 to
f9b6043
Compare
Change RoleAssignmentAudit.actor from ForeignKey(User) to a plain CharField storing the username. Attribution of the operation is preserved unconditionally: deleting or retiring a user does not affect existing audit records, and the audit log has no dependency on the User table. The event payload carries actor as a username string resolved from get_current_user() at API call time. Update ADR-0012 to document the decision and its rationale.
6d13a07 to
ff6ddeb
Compare
| @@ -0,0 +1,7 @@ | |||
| """Shared low-level constants for openedx_authz. | |||
There was a problem hiding this comment.
Not sure if this is the best place for this. Need to check if after all of my changes this is still needed.
|
This PR only adds the building blocks for auditability which can be accessed only via the Django admin. Once we consolidate the model we can add a lightweight API for consumers! https://openedx.atlassian.net/wiki/spaces/OEPM/pages/6045859842/Spike+-+RBAC+AuthZ+-+Auditability?focusedCommentId=6260686849 @rodmgwgu: I'm not clear whether this is part of M2, what do you think? Also this only considers changes in roles assignments, our conversation is still pending: https://openedx.atlassian.net/wiki/spaces/OEPM/pages/6045859842/Spike+-+RBAC+AuthZ+-+Auditability?focusedCommentId=6057328643 |
| } | ||
|
|
||
| The actor is resolved from ``django_crum.get_current_user()`` at API call time. No callers | ||
| need to pass ``actor=`` explicitly. The username is stored as a plain string rather than a |
There was a problem hiding this comment.
Per OEP-30 this counts as PII, and while this might be a reasonable place to store that without retirement I'm not sure it gets us enough extra information to warrant the exceptions.
If either of the users retires we'll be able to track what they did in terms of this audit trail, but not tie it to other systems unless we also store the user id, which would basically nullify the retirements. It's a bit of a complicated issue and I can ask our legal folks about it but I think we get the most private behavior for free if we use user ids instead. What do you think?
There was a problem hiding this comment.
Thanks for raising this! We could definitely use actor_id instead, at the end of the day, this will still be a str (or int), so we'd have to go look in the users table either way for more detailed information. Also, now that I think about it, if the user is retired, the username is hashed, so we won't be able to find it easily. It makes sense to me then to use the user ID instead.
| # .. event_implemented_name: ROLE_ASSIGNMENT_CREATED | ||
| # .. event_type: org.openedx.authz.role_assignment.created | ||
| transaction.on_commit( | ||
| lambda: ROLE_ASSIGNMENT_CREATED.send_event( |
There was a problem hiding this comment.
Does these need to be wrapped in a try/except? I seem to remember that these don't wrap themselves.
There was a problem hiding this comment.
send robus is true by default so we're safe in prod: https://docs.openedx.org/projects/openedx-events/en/latest/reference/events-tooling.html#openedx_events.tooling.OpenEdxPublicSignal.send_event_with_custom_metadata
| max_length=20, | ||
| choices=[(op, op) for op in OPERATIONS], | ||
| ) | ||
| subject = models.CharField( |
There was a problem hiding this comment.
Do we want to add indexes on things that we know will be searched on frequently such as this one and the scope that's used in RoleAssignmentAuditQuerySet?
There was a problem hiding this comment.
Given the filters defined the admin, I think the following indexes should be considered:
timestamp, subject, scope, and operation
There was a problem hiding this comment.
I think the subject and scope are both indices worth testing out. For display, we could order by timestamp. Not sure how impactful the index per operations will be but it's worth looking into it!
There was a problem hiding this comment.
Did a brief Google search, and given the cardinality of operations is not high (only two values for now), I don't think we should make it an index. For exact matches for subject and like for scopes would be worth it.
Added here: a5edda0
The API for consumer is out of scope for this phase. On the "libraries allow public read", my opinion is that it should not be part of this as it is not managed by authz, but product did want that there, I'll discuss with Guillermo about that. |
rodmgwgu
left a comment
There was a problem hiding this comment.
Thanks for all this work, could you run make format on this please? there are some files that ruff will want to change. Thanks!
| from openedx_authz.constants import AUTHZ_POLICY_ATTRIBUTES_SEPARATOR | ||
| from openedx_authz.engine.filter import Filter | ||
|
|
||
| User = get_user_model() |
There was a problem hiding this comment.
It seems this is not being used, please remove.
| max_length=20, | ||
| choices=[(op, op) for op in OPERATIONS], | ||
| ) | ||
| subject = models.CharField( |
There was a problem hiding this comment.
Given the filters defined the admin, I think the following indexes should be considered:
timestamp, subject, scope, and operation
| # .. event_implemented_name: ROLE_ASSIGNMENT_CREATED | ||
| # .. event_type: org.openedx.authz.role_assignment.created | ||
| transaction.on_commit( | ||
| lambda: ROLE_ASSIGNMENT_CREATED.send_event( |
There was a problem hiding this comment.
This will execute even when role_assignment is False, which happens when the user already had the role assigned. I think we should check for this and not generate a new assignment event as this is a no-op.
There was a problem hiding this comment.
L225 covers what happens when role_assignment is false, so when we get here, we know for sure the role was assigned. Not sure if I'm missing something. In any case, I included tests for this: 969f795
dwong2708
left a comment
There was a problem hiding this comment.
LGTM after addressing Rod’s requested changes. Thanks!
@mariajgrimaldi I spoke with Guillermo and he agrees to keep this out of this phase (tracking the "libraries allow public read" toggle). I created this issue to review it in future phases: #265 |
Description
Implements attribution-level auditability for role assignment operations, as described in ADR 0012.
The work covers two independent pieces:
Role lifecycle events:
assign_roleandremove_rolenow emitROLE_ASSIGNMENT_CREATEDandROLE_ASSIGNMENT_DELETEDafter the database write commits (transaction.on_commit). Each event payload includes the namespaced subject, role, scope, the actor resolved fromget_current_user(), and the operation timestamp. In non-request contexts (management commands, Celery tasks), the actor isNoneand treated as a system actor.RoleAssignmentAuditmodel: An append-only log of role assignment and removal events. Records store namespaced key strings (not FK references to live objects), so the audit history survives deletions independently of the live authorization state. A Celery-compatible handler wired to both signals writes the record after each event. If the handler fails, the Casbin write and the signal are unaffected.The admin exposes the audit log as read-only, with a scope-type filter and namespace-stripped display fields (
subject_display,role_display,scope_display). Scope-type filtering lives inRoleAssignmentAuditQuerySet.for_scope_namespaceso it is available both in the admin and in API-level querysets without duplicating the namespace-prefix logic.Also
AUTHZ_POLICY_ATTRIBUTES_SEPARATORmoved fromapi/data.pytoconstants/to remove a circular import introduced by the handler importing frommodelsandapiin the same module.Testing instructions
Use the Postman collection to exercise the role assignment endpoints and confirm events are emitted, and audit records are created:
Open edX AuthZ Auditing.postman_collection.json
To verify the admin manually:
python manage.py migrateto apply migration0008_roleassignmentaudit./admin/openedx_authz/roleassignmentaudit/and confirm the records appear with the correct operation, actor, and display fields.Merge checklist:
Check off if complete or not applicable: