Skip to content

feat: return 403 with descriptive error for catalog visibility restricted courses#38121

Merged
ormsbee merged 5 commits intoopenedx:masterfrom
mitodl:anas/display-detailed-error-message
Apr 2, 2026
Merged

feat: return 403 with descriptive error for catalog visibility restricted courses#38121
ormsbee merged 5 commits intoopenedx:masterfrom
mitodl:anas/display-detailed-error-message

Conversation

@Anas12091101
Copy link
Copy Markdown
Contributor

Description

Description

When a course has its catalog_visibility set to "none", non-staff users attempting to access it via the Learning MFE receive a generic "Course not found" 404 error, which provides no useful information about why access was denied. This change introduces a new CatalogVisibilityError access response type and ensures it propagates through the access control chain, so the API returns a 403 with a meaningful message instead.

The core of the fix is in the access control layer. The _has_catalog_visibility() helper now returns a typed CatalogVisibilityError instead of a bare ACCESS_DENIED. The can_see_about_page() and can_see_in_catalog() functions were restructured to preserve this typed error through their logic (the previous or-chain pattern would discard it). A new handler in check_course_access_with_redirect catches CatalogVisibilityError and raises a CourseAccessRedirect with the error attached.

On the API side, CourseHomeMetadataView now catches CourseAccessRedirect exceptions from course_detail() and converts them into a DRF PermissionDenied response (HTTP 403), including the user_message and error_code from the access error. This allows frontend clients to display a descriptive message like "This course is not currently accessible. The course team has restricted access to this content." instead of a confusing "Course not found" error.

Useful information to include:

  • Which edX user roles will this change impact? Learners, Course Authors
  • Include screenshots for changes to the UI (ideally, both "before" and "after" screenshots, if applicable).

Before

Screenshot 2026-03-06 at 5 06 22 PM

After

Screenshot 2026-03-06 at 5 05 46 PM
  • Provide links to the description of corresponding configuration changes. Remember to correctly annotate these
    changes.

Supporting information

Learning MFE PR: openedx/frontend-app-learning#1871

Testing Instructions

  1. Checkout to this learning MFE branch: https://github.com/mitodl/frontend-app-learning/tree/anas/display-detailed-error-message
  2. Ensure COURSE_ABOUT_VISIBILITY_PERMISSION = 'see_about_page' is set in your LMS settings.
  3. In Studio (Authoring MFE), open a course and go to Settings > Advanced Settings.
  4. Find the Course visibility In catalog field and set it to "none". Save.
  5. Log in as a non-staff user (or use an incognito window with a learner account).
  6. Navigate to the course in the Learning MFE, e.g. http://local.openedx.io:8000/learning/course/course-v1:Org+Course+Run/home.
  7. Alternatively, hit the API directly: GET /api/course_home/course_metadata/course-v1:Org+Course+Run.

Expected (after this change):

  • The API returns HTTP 403 with a JSON body containing:
    • "detail": "This course is not currently accessible. The course team has restricted access to this content. Please contact the course team for further assistance."
    • "error_code": "not_visible_in_catalog"

Expected (before this change):

  • The API returned HTTP 404 with "detail": "Course not found.".
  1. Log in as a staff/admin user and repeat step 4 or 5.

    • The course should load normally (HTTP 200) — staff access bypasses catalog visibility restrictions.
  2. Set Course Visibility In Catalog back to "both" and verify the course loads normally for all users.

Deadline

"None"

Other information

Include anything else that will help reviewers and consumers understand the change.

  • Does this change depend on other changes elsewhere?
  • Any special concerns or limitations? For example: deprecations, migrations, security, or accessibility.
  • If your database migration can't be rolled back easily.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Mar 6, 2026
@openedx-webhooks
Copy link
Copy Markdown

Thanks for the pull request, @Anas12091101!

This repository is currently maintained by @openedx/wg-maintenance-openedx-platform.

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.

Copy link
Copy Markdown
Contributor

@ormsbee ormsbee left a comment

Choose a reason for hiding this comment

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

Minor questions and request. Thank you!

Returns whether the given course has the given visibility type
"""
return ACCESS_GRANTED if course.catalog_visibility == visibility_type else ACCESS_DENIED
from lms.djangoapps.courseware.access_response import CatalogVisibilityError
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.

[Question]: Why is this import local to the function?

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.

Moved it to the top in 2d02a9e

"""
return ACCESS_GRANTED if course.catalog_visibility == visibility_type else ACCESS_DENIED
from lms.djangoapps.courseware.access_response import CatalogVisibilityError
return ACCESS_GRANTED if course.catalog_visibility == visibility_type else CatalogVisibilityError()
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.

[Question]: What kinds of values can visibility_type be?

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.

_has_catalog_visibility is only ever called with "both" or "about", never "none". When catalog_visibility="none", neither check matches, so CatalogVisibilityError is returned implicitly.

return ACCESS_GRANTED
if _has_staff_access_to_block(user, courselike, courselike.id):
return ACCESS_GRANTED
return catalog_response
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.

Please put a comment here to explain why this isn't equivalent to returning ACCESS_DENIED (like you did with can_see_about_page().

@Anas12091101 Anas12091101 force-pushed the anas/display-detailed-error-message branch from 6b18b50 to 2d02a9e Compare April 1, 2026 10:43
@ormsbee ormsbee merged commit df0ec65 into openedx:master Apr 2, 2026
48 checks passed
@github-project-automation github-project-automation bot moved this from Ready for Review to Done in Contributions Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants