Skip to content

feat: add role assignment audit trail#261

Open
mariajgrimaldi wants to merge 11 commits intomainfrom
MJG/audit
Open

feat: add role assignment audit trail#261
mariajgrimaldi wants to merge 11 commits intomainfrom
MJG/audit

Conversation

@mariajgrimaldi
Copy link
Copy Markdown
Member

@mariajgrimaldi mariajgrimaldi commented Apr 14, 2026

Description

Implements attribution-level auditability for role assignment operations, as described in ADR 0012.

The work covers two independent pieces:

Role lifecycle events: assign_role and remove_role now emit ROLE_ASSIGNMENT_CREATED and ROLE_ASSIGNMENT_DELETED after the database write commits (transaction.on_commit). Each event payload includes the namespaced subject, role, scope, the actor resolved from get_current_user(), and the operation timestamp. In non-request contexts (management commands, Celery tasks), the actor is None and treated as a system actor.

RoleAssignmentAudit model: 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 in RoleAssignmentAuditQuerySet.for_scope_namespace so it is available both in the admin and in API-level querysets without duplicating the namespace-prefix logic.

Also AUTHZ_POLICY_ATTRIBUTES_SEPARATOR moved from api/data.py to constants/ to remove a circular import introduced by the handler importing from models and api in the same module.

Note: requirements/base.txt pins openedx-events to a feature branch (MJG/authz-audit) while the upstream PR is open. This pin should be removed before merging.

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:

  1. Run python manage.py migrate to apply migration 0008_roleassignmentaudit.
  2. Assign and remove a role via the API or shell.
  3. Open /admin/openedx_authz/roleassignmentaudit/ and confirm the records appear with the correct operation, actor, and display fields.
  4. Use the scope-type filter in the sidebar to confirm it narrows results by namespace prefix.

Merge checklist:
Check off if complete or not applicable:

  • Version bumped
  • Changelog record added
  • Documentation updated (not only docstrings)
  • Fixup commits are squashed away
  • Unit tests added/updated
  • Manual testing instructions provided
  • Noted any: Concerns, dependencies, migration issues, deadlines, tickets

@openedx-webhooks openedx-webhooks added open-source-contribution PR author is not from Axim or 2U core contributor PR author is a Core Contributor (who may or may not have write access to this repo). labels Apr 14, 2026
@openedx-webhooks
Copy link
Copy Markdown

openedx-webhooks commented Apr 14, 2026

Thanks for the pull request, @mariajgrimaldi!

This repository is currently maintained by @openedx/committers-openedx-authz.

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 approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To 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:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where 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:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@github-project-automation github-project-automation bot moved this to Needs Triage in Contributions Apr 14, 2026
@mariajgrimaldi mariajgrimaldi changed the title Mjg/audit feat: add role assignment audit trail Apr 15, 2026
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.
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.
@mariajgrimaldi mariajgrimaldi requested review from a team, BryanttV, MaferMazu, bmtcril, dwong2708, rodmgwgu and wgu-taylor-payne and removed request for a team April 15, 2026 19:13
@mariajgrimaldi mariajgrimaldi marked this pull request as ready for review April 15, 2026 19:15
@@ -0,0 +1,7 @@
"""Shared low-level constants for openedx_authz.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not sure if this is the best place for this. Need to check if after all of my changes this is still needed.

@mariajgrimaldi
Copy link
Copy Markdown
Member Author

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

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?

Copy link
Copy Markdown
Member Author

@mariajgrimaldi mariajgrimaldi Apr 17, 2026

Choose a reason for hiding this comment

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

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(
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.

Does these need to be wrapped in a try/except? I seem to remember that these don't wrap themselves.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

max_length=20,
choices=[(op, op) for op in OPERATIONS],
)
subject = models.CharField(
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.

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?

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.

Given the filters defined the admin, I think the following indexes should be considered:

timestamp, subject, scope, and operation

Copy link
Copy Markdown
Member Author

@mariajgrimaldi mariajgrimaldi Apr 20, 2026

Choose a reason for hiding this comment

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

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!

Copy link
Copy Markdown
Member Author

@mariajgrimaldi mariajgrimaldi Apr 20, 2026

Choose a reason for hiding this comment

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

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

@mphilbrick211 mphilbrick211 moved this from Needs Triage to In Eng Review in Contributions Apr 16, 2026
@rodmgwgu
Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Contributor

@rodmgwgu rodmgwgu left a comment

Choose a reason for hiding this comment

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

Thanks for all this work, could you run make format on this please? there are some files that ruff will want to change. Thanks!

Comment thread openedx_authz/models/core.py Outdated
from openedx_authz.constants import AUTHZ_POLICY_ATTRIBUTES_SEPARATOR
from openedx_authz.engine.filter import Filter

User = get_user_model()
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.

It seems this is not being used, please remove.

max_length=20,
choices=[(op, op) for op in OPERATIONS],
)
subject = models.CharField(
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.

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(
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 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

@dwong2708 dwong2708 left a comment

Choose a reason for hiding this comment

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

LGTM after addressing Rod’s requested changes. Thanks!

@rodmgwgu
Copy link
Copy Markdown
Contributor

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

@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

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

Labels

core contributor PR author is a Core Contributor (who may or may not have write access to this repo). open-source-contribution PR author is not from Axim or 2U

Projects

Status: In Eng Review

Development

Successfully merging this pull request may close these issues.

6 participants