feat: make can_tag_object's course check authorization-service aware - #39112
alezconsultant wants to merge 3 commits into
Conversation
can_change_object_tag_objectid in content_tagging/rules.py -- the predicate every direct
has_perm('oel_tagging.can_tag_object', ...) caller resolves through -- already checked the
new openedx-authz service for Content Library objects, but for course objects it only ever
checked the legacy has_studio_write_access role, regardless of whether that course had been
switched to the new authorization service. ObjectTagOrgView (Studio's own tag-editing
endpoint) already had the correct, toggle-aware behavior, but only inside that one view
class, so every other caller (including the CBE create-criterion endpoint, openedx-core#665)
silently got the wrong, legacy-only answer for switched-over courses.
Add a branch to can_change_object_tag_objectid, checked after the Content Library case and
before the legacy fallback: when should_use_course_authz_for_object says the course has been
switched, return authz_api.is_user_allowed directly, with no
fallback to legacy roles or org-admin access -- an exclusive switch, matching
ObjectTagOrgView's existing behavior exactly.
Delete ObjectTagOrgView.ensure_user_has_can_tag_object_permissions: the parent
ObjectTagView's own version (openedx-core) already resolves through
has_perm(oel_tagging.can_tag_object, ...), so this override had become pure duplication of
logic that now lives in rules.py. get_permissions and _authz_check stay, since
ensure_has_view_object_tag_permission (the separate, untouched view permission) and
ObjectTagTaxonomyOrgFilterBackend.filter_queryset both still read them directly.
Related to openedx/openedx-core#795
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks for the pull request, @alezconsultant! 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. |
mgwozdz-unicon
left a comment
There was a problem hiding this comment.
This looks pretty good. Just a few things came out of the review Claude and I did together:
Requested changes
1. content_tagging/rest_api/v1/views.py:209-214: the docstring addition to _authz_check explains why it's kept even though the sibling override was deleted, which is genuinely useful since openedx-core#795's own Technical Details section suggested deleting _authz_check, get_permissions, and ensure_user_has_can_tag_object_permissions together. So I'd keep the substance, but the wording references "the now-deleted ensure_user_has_can_tag_object_permissions override", and a future reader who never saw that method won't have any context for that phrase. Can you reword it to state the current dependency only, for example: "Kept: ensure_has_view_object_tag_permission below and ObjectTagTaxonomyOrgFilterBackend.filter_queryset (filters.py) still read this directly."
2. PR Description's Manual testing section: this needs more detail to be reproducible. It doesn't name the waffle flag (AUTHZ_COURSE_AUTHORING_FLAG), doesn't say how it was toggled per course, doesn't name the specific screen or URL used, and doesn't say concretely what "tagging succeeded" looked like. Can you add those specifics, along the lines of: which flag, how you set the per-course override, which screen you used (the Course Team tag editor), and what confirmed success (for example, tags saved and visible again after a reload, with no 403).
3. content_tagging/tests/test_rules.py:812, test_course_switched_org_admin_only_role_denied: this duplicates test_course_switched_legacy_only_role_denied right above it; both exercise the exact same code path. Can you remove it?
4. Test coverage for objects within a course: the updated docstring in rules.py now explicitly claims the new branch covers "a course (or an object within one)", i.e. an xblock inside a switched course, and the code supports that (should_use_course_authz_for_object resolves a BlockUsageLocator down to its course key). None of the new tests in TestRulesCourseAuthzPermissions pass an xblock object_id though, only a bare course key. Since this is now a documented claim, can you add a test case for it?
Nits
1. content_tagging/rules.py:220-225: this docstring addition reads as two short paragraphs where one would do, for example: "For a course (or object within one) switched to openedx-authz, this requires courses.manage_tags through that service alone, with no fallback to studio write or org-admin access; for any other course, xblock, etc. not yet switched, those checks still apply exactly as before." Not blocking, just a length nit.
2. content_tagging/rules.py:245-246: the inline comment above the new branch ("Once a course is switched to openedx-authz, it is the sole source of truth for this permission...") restates the docstring paragraph directly above it in the same function almost word for word. Worth dropping one of the two.
|
mgwozdz-unicon
left a comment
There was a problem hiding this comment.
Thank you for fixing those things. My final nit is just that I think the remaining lines of the comment on 209-212 of views.py could probably be deleted entirely, but feel free to keep it if you want.
|
@mgwozdz-unicon i think it's okay to remove it, done. |
Description
Makes
oel_tagging.can_tag_object's course check authorization-service aware(openedx/openedx-core#795).
can_change_object_tag_objectidincontent_tagging/rules.py— the predicate every directhas_perm('oel_tagging.can_tag_object', ...)caller resolves through — already checked the new openedx-authz service for ContentLibrary objects, but for course objects it only ever checked the legacy
has_studio_write_accessrole, regardless of whether that course had been switched to the new authorization service.
ObjectTagOrgViewalready had the correct, toggle-awarebehavior, but only inside that one view class — every other caller, including the CBE
create-criterion endpoint (openedx-core#665), silently got the wrong (legacy-only) answer for
switched-over courses. This closes that gap by giving the shared predicate the same
toggle-aware branch
ObjectTagOrgViewalready had, then removing the view's own now-duplicateoverride so the toggle check lives in one place.
Changes
content_tagging/rules.py:can_change_object_tag_objectidgets a new branch, checkedafter the existing Content Library case and before the legacy
has_studio_write_accessfallback. It calls
should_use_course_authz_for_object(object_id)(already used byObjectTagOrgView, fromcontent_tagging/auth.py); when the course has been switched, itreturns
authz_api.is_user_allowed(user.username, COURSES_MANAGE_TAGS.identifier, str(course_key))directly, with no fallback to legacy roles or org-admin access — anexclusive switch, matching
ObjectTagOrgView's existing behavior exactly. Docstring updatedto describe the new branch.
content_tagging/rest_api/v1/views.py:ObjectTagOrgView.ensure_user_has_can_tag_object_permissionsdeleted. Verified the parent
ObjectTagView.ensure_user_has_can_tag_object_permissions(openedx-core) already resolves through
user.has_perm("oel_tagging.can_tag_object", ...),so this override became pure duplication of the same logic now living in
rules.py.get_permissionsand_authz_checkare unchanged —_authz_check's docstring nowexplains why:
ensure_has_view_object_tag_permission(the separate, untouched viewpermission) and
ObjectTagTaxonomyOrgFilterBackend.filter_querysetboth still read itdirectly, so only the tagging-permission override became redundant, not this.
content_tagging/tests/test_rules.py: newTestRulesCourseAuthzPermissions, covering theticket's acceptance criteria directly against the predicate: a course switched to authz
grants access via
authz_api.is_user_allowedalone; a course not yet switched keeps resolving through thelegacy check unaffected; Content Library objects are
unaffected.
Tests
is_user_allowedcalled with the right identifier/scope.CourseStaffRolebut authz denies → denied; provesthe switch is exclusive, not an OR with a legacy course-level role.
exclusivity holds against org-admin fallback too, not just the course-level role.
is_user_allowedis never even called.MANAGE_LIBRARY_TAGS(regression guard on the untouched branch).Verification
pytest openedx/core/djangoapps/content_tagging/tests/test_rules.py openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py --no-cov:575 passed, 0 failed.
pylinton both changed source files and the changed test file: clean.Manual testing
Manual testing notes for #795
Since #665 hasn't been merged, it isn't
possible to test that endpoint for now. The best approach I came up with is to test the branch
from the CLI.
http://apps.local.openedx.io:2001/authoring/home.authz.enable_course_authoring, and set it toForce Off and enabled, on the Django Admin page:
http://studio.local.openedx.io:8001/admin/waffle_utils/waffleflagcourseoverridemodel/.(If you add a new user to a course that already has the flag Force On, it will automatically
grant that user the authz permission instead of a legacy one.)
http://studio.local.openedx.io:8001/admin/auth/user/add/.docker exec tutor_dev-cms-1 python manage.py cms shellTo test
ObjectTagOrgViewitself, curl or Postman is needed, since a user with the old (legacy)permissions can't even load the page for a course that uses the override flag.
To get the auth header (written with Claude's help):
For
authz_user, expected200:For
legacy_user, expected403. Log in again first withlegacy_user's credentials (repeatthe auth-header block above with
email=legacy_user@test.com, so$AUTH_HEADERreflectslegacy_user, notauthz_user), then:Related to openedx/openedx-core#795 and openedx/openedx-core#665
🤖 Generated with Claude Code