# Pluggable access control both viewing and enrolling in a course

**URL:** <https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803>\
**Category:** Architecture\
**Tags:** api\
**Created:** [November 5, 2019, 11:36am UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803 "2019-11-05T11:36:50Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [November 5, 2019, 11:36am UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/1 "2019-11-05T11:36:51Z")

</div>

## Summary

I’m proposing a method to customize the access control in Open edX via plugins. This proposal is concerned about augmenting the course `load` and `enroll` permissions by providing django app and registering it using the Django settings.

## Access Control in Open edX

Enrollment access control is pretty much hard-coded in Open edX. The course view and enrollment are not an exception.

Often times, when the access control logic need to be changed, the modifications are also hardcoded  
which adds to the tech-debt that fork maintainers needs to carry on.

One example is the ability to make a Course Access Group feature which is something that  
a couple of Open edX providers have expressed interest in and also being requested by our customers.

## Pluggable Access Control

I’m trying to collect some architecture feedback before going and making such change in Open edX,  
but mostly I’m interested in stealing from the [LTI Consumer XBlock extensions](https://github.com/edx/xblock-lti-consumer#custom-lti-parameters) architecture.

You can also check [original pull request](https://github.com/edx/xblock-lti-consumer/pull/44) and how an [LTI Plugin](https://github.com/appsembler/tahoe-lti) looks like.

If you’re really into it check other related work in Open edX:

- [The registration extension form](https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/customize_registration_page.html#add-custom-fields-to-the-registration-page).
- The original hard-coded [Course Access Group](https://github.com/appsembler/edx-platform/compare/024e7aaa57a120097b7468b9cf97ec7cdca271db...metalogix/feature/courseAccessRestriction) that we developed for a customer in 2015.  
Focus on [`can_enroll()`](https://github.com/appsembler/edx-platform/compare/024e7aaa57a120097b7468b9cf97ec7cdca271db...metalogix/feature/courseAccessRestriction#diff-be0481503e8fd3f1d02e60ef92bc39f4R249) changes.

## Proposal

There’s a couple of ways to design the extension, I’m going for the most simple one that can work focusing on the [`_has_access_course`](https://github.com/edx/edx-platform/blob/c1274ba765e9911f2145c1415e3c7894d5e9bee0/lms/djangoapps/courseware/access.py#L287-L307) function.

Hypothetically, if we’d like to make the enrollment in the `course-v1:Uni+Demo+2020` course  
to be exclusive for users with`@example.edu` email we’d use the following configuration:

```auto
# my-configs/server-vars.yml
ACCESS_CONTROL_BACKENDS:
    course_enroll:
      NAME: 'my_access_plugins.backends.domain_based_course_enrollment'
      OPTIONS:
        enforce_domain_name_check: true
    course_load:
      NAME: 'my_access_plugins.backends.domain_based_course_enrollment'

```

And the backend would look like this:

```python
# my_access_plugins/backends.py
def domain_based_course_enrollment(user, courselike, default_access, options):
    if courselike.id == 'course-v1:Uni+Demo+2020' and not user.email.endswith('@example.edu'):
        return False

    if options.get('enforce_domain_name_check', False):
       # Check the DNS records for such email.
       pass

    return default_access

```

### Some Implementation Example

While the actual implementation will be different, here’s an approximate idea of what would it look like:

The settings would follow the same patten like other Open edX settings.

```python
# lms/envs/aws.py
ACCESS_CONTROL_BACKENDS = ENV_TOKENS.get('ACCESS_CONTROL_BACKENDS', {})

```

We also need a helper to handle the common logic for invoking the backends:

```python
# somewhere/helpers.py
def use_acl_backend(backend_name, user, resource, default_access):
    # Not doing too much error handling at the moment
    backend_path = ACCESS_CONTROL_BACKENDS.get(backend_name).get('NAME')
    backend_options = ACCESS_CONTROL_BACKENDS.get(backend_name).get('OPTIONS', {})

    if backend_path:
        backend = import_module(module_path)
        return backend(user, resource, default_access=default_access, options=backend_options)

    return default_access

```

This would be the actual use of the backends, it needs to put it in other places in the platform.

```python
# lms/djangoapps/courseware/access.py
def can_enroll(user, course):
    # Over-simplified function, since the actual `access.py` is pretty complex.
    # ...
    can_enroll = through_some_other_logic()
    # ...
    #
    return use_acl_backend('course_enroll', user, resource=course, default_access=can_enroll)

```

Let me know if you’d like me to expand with more details.

## Questions

- Tell me what do you think?
- How can we get this into Open edX?
- Do you see any issues with the proposal above?
- Any better alternative do you have in mind?
- Is this change has enough impact to require an OEP?

---

<div class="post-metadata">

**Author:** ![john](https://avatars.discourse-cdn.com/v4/letter/j/a3d4f5/32.png) [@john](https://discuss.openedx.org/u/john)\
**Post date:** [November 5, 2019, 11:51am UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/2 "2019-11-05T11:51:40Z")

</div>

@omar Would you please provide example code that uses this configuration?

---

<div class="post-metadata">

**Author:** ![regis](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/regis/32/2640_2.png) [@regis](https://discuss.openedx.org/u/regis)\
**Post date:** [November 5, 2019, 12:15pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/3 "2019-11-05T12:15:04Z")

</div>

> [@omar](#):
>
> In the `server-vars.yml` configs:
> 
> ```auto
> ACCESS_CONTROL_BACKENDS:
> course:
> enroll: 'my_access_plugins.backends.course_enroll'
> 
> ```

I understand your point, but can you please describe instead how the setting would affect the `envs/*.py` files instead of `server-vars.yml`? I feel like we are closer to the bone when talking about \*.py files, while the `server-vars.yml` file needs to be interepreted by multiple layers of ansible playbooks.

> [@omar](#):
>
> And the backend would look like this:
> 
> ```auto
> def course_enroll(user, action, courselike, default_access):
> if courselike.id == 'course-v1:Uni+Demo+2020' and not user.email.endswith('@example.edu'):
> return False
> 
> return default_access
> 
> ```

What’s the point of the `action` argument? If I understand correctly, in this case `action == "enroll"`, right?

> [@omar](#):
>
> Tell me what do you think?

This goes in the right direction. It’s funny, as I am currently working on Django authentication and what you are proposing seems to be similar to the [`AUTHENTICATION_BACKENDS`](https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends) setting.  
Maybe you should plan ahead and expect that access control backends will require optional settings, just like the authentication backends? In that case, you would have to write:

```auto
ACCESS_CONTROL_BACKENDS = {
    "enroll": [
        {"NAME": "my_access_plugins.backends.course_enroll", "OPTIONS": {...}}
    ]
}

```

> [@omar](#):
>
> How can we get this into Open edX?

You know the answer to that one, [right](https://github.com/edx/edx-platform/pulls)? 😉

> [@omar](#):
>
> - Do you see any issues with the proposal above?
> - Any better alternative do you have in mind?

No

> [@omar](#):
>
> Is this change has enough impact to require an OEP?

I certainly hope not.

---

<div class="post-metadata">

**Author:** ![cpennington](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/cpennington/32/21_2.png) [@cpennington](https://discuss.openedx.org/u/cpennington)\
**Post date:** [November 5, 2019, 4:45pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/4 "2019-11-05T16:45:31Z")

</div>

For performance reasons, I’ve been trying to push access checks over into [Bridgekeeper](https://github.com/excitedleigh/bridgekeeper). The rationale for that is described [in an ADR](https://github.com/edx/edx-platform/blob/master/lms/djangoapps/courseware/docs/decisions/0003-permissions-via-bridgekeeper.rst).

---

<div class="post-metadata">

**Author:** ![regis](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/regis/32/2640_2.png) [@regis](https://discuss.openedx.org/u/regis)\
**Post date:** [November 6, 2019, 8:32am UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/5 "2019-11-06T08:32:59Z")

</div>

```auto
Warning
Bridgekeeper (and these docs!) are a work in progress.

```

🤔

---

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [November 6, 2019, 12:32pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/6 "2019-11-06T12:32:55Z")

</div>

@john Thank you! Examples have been added, let me know if I should expand more to clarify the idea.

@regis Thank you, great notes. I’ve updated the post to the following:

- More code examples.
- I added the `OPTIONS` as you suggested. But kept it to a single backend per “action”, instead of having multiple ones. I’d like to keep it simple unless there’s a compelling use case to do otherwise.
- The `action` parameter is removed.

---

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [November 6, 2019, 12:37pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/7 "2019-11-06T12:37:34Z")

</div>

Thanks for sharing the decision @cpennington . That’s a major change to the existing access control. I have a couple of questions so I can decided how to steer this proposal:

- Is this decision final, and the edX architects have committed to do?
- Even if Bridgekeeper is an unmaintained tool that’s still officially “Work in Progress” (i.e. Regis concern)?
- What’s a reasonable timeline for such change to be completed? Any kind of estimation or target date would be good.
- Would this change be backward incompatible, or we’d like to keep the old method and allow switching between?
- Assuming that it would be done sometime soon, how do you think extensibility can be built into Bridgekeeper? A starting point for me to dig in this subject would be awesome.

---

<div class="post-metadata">

**Author:** ![cpennington](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/cpennington/32/21_2.png) [@cpennington](https://discuss.openedx.org/u/cpennington)\
**Post date:** [November 7, 2019, 4:16pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/8 "2019-11-07T16:16:45Z")

</div>

> [@omar](#):
>
> - Is this decision final, and the edX architects have committed to do?

It hasn’t come up as an OEP, so right now it’s a direction that I’d like to move edx-platform in, and I see benefits to adoption in other services. It’s certainly possible that the library we use could shift, but I think the key piece of allowing composable database filters to implement the all (or the majority) of access control is important, and is something that we should commit to.

> [@omar](#):
>
> - Even if Bridgekeeper is an unmaintained tool that’s still officially “Work in Progress” (i.e. Regis concern)?

The library itself is relatively small and self-contained, so I’m not super worried about either needing to do work on it ourselves (I’ve already PRed a small fix during my initial prototyping), or replacing with a more widely supported library if one comes along.

> [@omar](#):
>
> - What’s a reasonable timeline for such change to be completed? Any kind of estimation or target date would be good.

That’s an excellent question. Right now, we don’t have the whole project (converting all existing access control in edx-platform and/or other services) on a roadmap over here at edX. There are a some INCR tickets that I created for naming/migrating existing permissions (see [INCR-560](https://openedx.atlassian.net/browse/INCR-560) for details).

> [@omar](#):
>
> - Would this change be backward incompatible, or we’d like to keep the old method and allow switching between?

There are actually two things that need to change. First, we need to convert the places in edx-platform where we are implicitly checking a permission (by checking what group a user is in, or whether they are staff on a course) to use the standard django `user.has_perm` syntax. Those named permissions could then be implemented by wrapping the existing logic into a form compatible w/ either Bridgekeeper or django-rules (django-rules, of course, not having the capability for query-filtering). This step is both backwards compatible, and makes future work of changing how individual permissions are implemented easier.

The second step is to convert the existing `has_access` implementations to use a composable permissions framework (Bridgekeeper, for instance), so that we can take advantages of the performance improvements from those. As that work is completed, existing code that calls `has_access` won’t function the same (it wouldn’t be able to check the converted permissions.

> [@omar](#):
>
> - Assuming that it would be done sometime soon, how do you think extensibility can be built into Bridgekeeper? A starting point for me to dig in this subject would be awesome.

I think it would be pretty easy to set up your proposed extensibility points to work w/ Bridgekeeper. The standard setup for that is to add your `Rule` objects to a dictionary of `perms` (like in [https://github.com/edx/edx-platform/blob/master/lms/djangoapps/ccx/permissions.py](https://github.com/edx/edx-platform/blob/master/lms/djangoapps/ccx/permissions.py)). I could imagine your extension point either directly adding `Rule`s to `perms`, or being set up to augment whatever existing `Rule` is assigned to the permission by the core platform.

Hope that all helps!

---

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [November 8, 2019, 8:15am UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/9 "2019-11-08T08:15:15Z")

</div>

Thanks @cpennington for the detailed explanation. It really helps. I’ll do some homework and see how can I make something that works for now (Hawthorn/Ironwood) while is has some prep for the future Bridgekeeper changes.

---

<div class="post-metadata">

**Author:** ![braden](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/braden/32/46_2.png) [@braden](https://discuss.openedx.org/u/braden)\
**Post date:** [November 21, 2019, 4:51pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/10 "2019-11-21T16:51:20Z")

</div>

Just a quick note to say that (inspired by the edX ADR) we’ve been using bridgekeeper for the LabXchange project and are pretty happy with it. I am also [working on using it for Blockstore-based content libraries in edx-platform](https://github.com/edx/edx-platform/pull/22258) and should be merging that in the next week or two. The only snag I’ve hit so far is that edx-platform seems to be using an out of date fork of Bridgekeeper that doesn’t quite match the docs (some things have been made easier/simpler in new versions); I _think_ that’s only to support python 2 though so should be something that can be improved in the coming weeks after the switch to python 3.

---

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [November 22, 2019, 5:11am UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/11 "2019-11-22T05:11:46Z")

</div>

Thanks @braden! Great to know that it’s working well for community installations.

---

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [November 28, 2019, 11:07am UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/12 "2019-11-28T11:07:19Z")

</div>

I’d like to share some updates. I’ve now finished the first version of the [Access Control Bakends](https://github.com/appsembler/edx-platform/pull/491). Because we are still using Hawthorn, this is the most important version to support i.e. without `Bridgekeeper`.

Later on, I’ll follow up with `Bridgekeeper` support till the whole platform is converted.

---

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [December 11, 2019, 1:39pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/13 "2019-12-11T13:39:15Z")

</div>

@cpennington I’m still working on this and experimenting with both intermediate solution (Hawthorn) that keeps in mind the long term direction towards Bridgekeeper.

I’ve noticed that most of the course access in the platform is guarded by `has_access` except for the notable exception of `edx-search:course_discovery_search` ([link to GitHub](https://github.com/edx/edx-search/blob/4b5837a5d06131193af652ba208c6b50a57752f4/search/views.py#L147-L170)).

Do you know of any plan to put access control mechanism in front of that view? If there’s no such plan, what do you think of using a similar mechanism to [`SearchResultProcessor`](https://github.com/edx/edx-search/blob/4b5837a5d06131193af652ba208c6b50a57752f4/search/result_processor.py#L24-L36) that is used in `do_search`?

---

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [January 10, 2020, 11:51am UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/14 "2020-01-10T11:51:21Z")

</div>

I’d like to share some progress regarding the pluggable access control backends. I’ve been able to get something working for Hawthorn:

- **Access Control Backends:** [https://github.com/appsembler/edx-platform/pull/491](https://github.com/appsembler/edx-platform/pull/491)

As I have mentioned earlier I’ve noticed that `edx-search` does not check for access via the `has_access` function so I made a hack that I’d like to eventually refactor into something more solid to add such check:

- **Hack filter discovery results:** [https://github.com/appsembler/edx-search/pull/12](https://github.com/appsembler/edx-search/pull/12)

There’s still a good amount of work left mostly to coup with the changes that’ll released in Juniper. The most notable change is the use of [Bridgekeeper](https://github.com/excitedleigh/bridgekeeper). So please reach out to me whether here or via email [omar@appsembler.com](mailto:omar@appsembler.com) if you’d like to collaborate on this project.

---

<div class="post-metadata">

**Author:** ![cpennington](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/cpennington/32/21_2.png) [@cpennington](https://discuss.openedx.org/u/cpennington)\
**Post date:** [January 27, 2020, 3:48pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/15 "2020-01-27T15:48:02Z")

</div>

> [@omar](#):
>
> Do you know of any plan to put access control mechanism in front of that view? If there’s no such plan, what do you think of using a similar mechanism to [`SearchResultProcessor`](https://github.com/edx/edx-search/blob/4b5837a5d06131193af652ba208c6b50a57752f4/search/result_processor.py#L24-L36) that is used in `do_search` ?

I don’t know of any plans to change how access control works for `course_discovery_search`. I think that as long as you can do a small number of queries to fetch all the attributes needed for the permissions checks for _all_ the search results, then the performance won’t be terrible. Although pagination in the face of post-search access control still may lead to some funny results.

---

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [January 28, 2020, 8:16am UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/16 "2020-01-28T08:16:34Z")

</div>

> [@cpennington](#):
>
> I don’t know of any plans to change how access control works for `course_discovery_search` .

Thanks! So currently there’s no access control and my plan is to add that in a proper-ish way.

> [@cpennington](#):
>
> I think that as long as you can do a small number of queries to fetch all the attributes needed for the permissions checks for _all_ the search results, then the performance won’t be terrible. Although pagination in the face of post-search access control still may lead to some funny results.

👍

---

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [March 4, 2020, 1:15pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/17 "2020-03-04T13:15:41Z")

</div>

Sharing more progress: We’re releasing an initial version of the [course access groups](https://github.com/appsembler/course-access-groups) plugin. I’ve also started to contribute an [extension point](https://github.com/edx/edx-platform/pull/23296) to upstream.

The overall work is still in progress but the feature is shaping up well.

---

<div class="post-metadata">

**Author:** ![pdpinch](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/pdpinch/32/36_2.png) [@pdpinch](https://discuss.openedx.org/u/pdpinch)\
**Post date:** [March 4, 2020, 1:18pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/18 "2020-03-04T13:18:32Z")

</div>

Thanks for the update. Is it possible to use this scheme to deny _all_ self-enrollment? We have an open edX installation where we only want to allow enrollment via API.

---

<div class="post-metadata">

**Author:** ![omar](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/omar/32/7275_2.png) [@omar](https://discuss.openedx.org/u/omar)\
**Post date:** [March 5, 2020, 7:44am UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/19 "2020-03-05T07:44:46Z")

</div>

Yes, currently it supports the following actions via the `has_access` function:

```auto
course.enroll
course.instructor
course.load
course.load_forum
course.load_mobile
course.see_about_page
course.see_exists
course.see_in_catalog
course.staff

```

However, my understanding is that you can make a course “invite only” and then self-enrollments will not be possible. Could you please share more about your use case? I’d like to learn more about it.

---

<div class="post-metadata">

**Author:** ![pdpinch](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.openedx.org/pdpinch/32/36_2.png) [@pdpinch](https://discuss.openedx.org/u/pdpinch)\
**Post date:** [March 12, 2020, 6:35pm UTC](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803/20 "2020-03-12T18:35:45Z")

</div>

(not sure why I have so much trouble getting notifications from here. I apologize for delayed reply).

We tried setting invite only for all the course in our installation (we went so far as to write a site-wide feature flag to change the default to "True) but we found that it conflicted with enrollment API we were using. That is to say, setting the course to “invite only” prevented us from using the enrollment API. I suspect we were not using the right API (or the right user/permissions), but we haven’t revisited the issue since the last roadblock.

The use case is an open edX installation where enrollment is managed by a separate application. We don’t want any self-enrollment in edX.

[Next page](https://discuss.openedx.org/t/pluggable-access-control-both-viewing-and-enrolling-in-a-course/803.md?page=2)
