Defining an Access Policy

The Access Policy controls the authorization of a given request and is enforced at the viewset-level. Access policies are based on the AccessPolicy from drf-access-policy which uses policy statements described here.

Example Policy

Below is an example policy used by FileRemote, with an explanation of its effect below that:

[
    {
        "action": ["list"],
        "principal": "authenticated",
        "effect": "allow",
    },
    {
        "action": ["create"],
        "principal": "authenticated",
        "effect": "allow",
        "condition": "has_model_perms:file.add_fileremote",
    },
    {
        "action": ["retrieve"],
        "principal": "authenticated",
        "effect": "allow",
        "condition": "has_model_or_obj_perms:file.view_fileremote",
    },
    {
        "action": ["update", "partial_update"],
        "principal": "authenticated",
        "effect": "allow",
        "condition": "has_model_or_obj_perms:file.change_fileremote",
    },
    {
        "action": ["destroy"],
        "principal": "authenticated",
        "effect": "allow",
        "condition": "has_model_or_obj_perms:file.delete_fileremote",
    },
]

The above policy allows the following four cases, and denies all others by default. Overall this creates a “user isolation policy” whereby users with the file.add_fileremote permission can create FileRemote objects, and users can only read/modify/delete FileRemote objects they created.

Here’s a written explanation of the policy statements:

  • list is allowed by any authenticated user. Although users are allowed to perform an operation what they can list will still be restricted to only the objects that user can view.

  • create is allowed by any authenticated user with the file.add_fileremote permission.

  • retrieve (the detail view of an object) is allowed by an authenticated user who has the file.view_fileremote permission. Although users are allowed to perform an operation what they can list will still be restricted to only the objects that user can view.

  • update or partial_update is allowed by an authenticated user who has the file.change_fileremote permission.

  • destroy is allowed by any authenticated user with the file.delete_fileremote permission.

These names correspond with the default DRF viewset action names.

Authorization Conditions

Each policy statement can contain drf-access-policy conditions which is useful for verifying a user has one or more permissions. Pulp ships many built-in checks. See the Permission Checking Machinery documentation for more information on available checks.

When multiple conditions are present, all of them must return True for the request to be authorized.

Warning

The admin user created on installations prior to RBAC being enabled has is_superuser=True. Django assumes a superuser has any model-level permission even without it being assigned. Django’s permission checking machinery assumes superusers bypass authorization checks.

Custom ViewSet Actions

The action part of a policy statement can reference any custom action your viewset has. For example FileRepositoryViewSet has a sync custom action used by users to sync a given FileRepository. Below is an example of the default policy used to guard that action:

{
    "action": ["sync"],
    "principal": "authenticated",
    "effect": "allow",
    "condition": [
        "has_model_or_obj_perms:file.modify_repo_content",
        "has_remote_param_model_or_obj_perms:file.view_fileremote",
    ]
}

Storing an Access Policy in the DB

All access policies are stored in the database in the pulpcore.plugin.models.AccessPolicy model, which stores the policy statements described above. Here is a look at the AccessPolicy model:

class pulpcore.plugin.models.AccessPolicy(*args, **kwargs)

A model storing a viewset authorization policy and permission assignment of new objects created.

Fields
  • creation_hooks (models.JSONField) – A list of dictionaries identifying callables on the pulpcore.plugin.access_policy.AccessPolicyFromDB which can add user or group roles for newly created objects. This is a nullable field due to not all endpoints creating objects.

  • statements (models.JSONField) – A list of drf-access-policy statements.

  • viewset_name (models.TextField) – The name of the viewset this instance controls authorization for.

  • customized (BooleanField) – False if the AccessPolicy has been user-modified. True otherwise. Defaults to False.

  • queryset_scoping (models.JSONField) – A dictionary identifying a callable to perform the queryset scoping. This field can be null if the user doesn’t want to perform scoping.

By storing these in the database they are readable to users with a GET to /pulp/api/v3/access_policies/. Additionally users can PUT/PATCH modify them at /pulp/api/v3/access_policies/:uuid/. Users cannot modify create or delete an Access Policy in the database because only plugin writers create them and their viewset code expects a specific AccessPolicy instance to exist.

Shipping a Default Access Policy

To ship a default access policy, define a dictionary named DEFAULT_ACCESS_POLICY as a class attribute on a subclass of NamedModelViewSet containing all of statements and creation_hooks. The AccessPolicy instance will then be created in the pulp_migrate signal handler. In the same way you might want to specify a LOCKED_ROLES dictionary that will define roles as lists of permissions to be used in the access policy.

Here’s an example of code to define a default policy:

class FileRemoteViewSet(RemoteViewSet):

<...>
    DEFAULT_ACCESS_POLICY = {
        "statements": [
            {
                "action": ["list"],
                "principal": "authenticated",
                "effect": "allow",
            },
            {
                "action": ["create"],
                "principal": "authenticated",
                "effect": "allow",
                "condition": "has_model_perms:file.add_fileremote",
            },
            {
                "action": ["retrieve"],
                "principal": "authenticated",
                "effect": "allow",
                "condition": "has_model_or_obj_perms:file.view_fileremote",
            },
            {
                "action": ["update", "partial_update"],
                "principal": "authenticated",
                "effect": "allow",
                "condition": "has_model_or_obj_perms:file.change_fileremote",
            },
            {
                "action": ["destroy"],
                "principal": "authenticated",
                "effect": "allow",
                "condition": "has_model_or_obj_perms:file.delete_fileremote",
            },
        ],

        "creation_hooks": [
            {
                "function": "add_roles_for_object_creator",
                "parameters": {
                    "roles": "file.fileremote_owner",
                },
            },
        ],
    }
    LOCKED_ROLES = {
        "file.fileremote_owner": [
            "file.view_fileremote", "file.change_fileremote", "file.delete_fileremote"
        ],
        "file.fileremote_viewer": ["file.view_fileremote"],
    }
    <...>

For an explanation of the creation_hooks see the Shipping a Default New Object Policy documentation.

The attribute LOCKED_ROLES contains roles that are managed by the plugin author. Their name needs to be prefixed by the plugins app_label with a dot to prevent collisions. Roles defined there will be replicated and updated in the database after every migration. They are also marked locked=True to prevent being modified by users. The primary purpose of these roles is to allow plugin writers to refer to them in the default access policy.

Handling Objects created prior to RBAC

Prior to RBAC being enabled, admin was the only user and they have is_superuser=True which generally causes them to pass any permission check even without explicit permissions being assigned.

Viewset Enforcement

Pulp configures the DEFAULT_PERMISSION_CLASSES in the settings file to use pulpcore.plugin.access_policy.AccessPolicyFromDB by default. This ensures that by defining a DEFAULT_ACCESS_POLICY on your Viewset, Pulp will automatically save it to the database at migration-time, and your Viewset will be protected without additional effort.

This strategy allows users to completely customize or disable the DRF Permission checks Pulp uses like any typical DRF project would.

Also like a typical DRF project, individual Viewsets or views can also be customized to use a different Permission check by declaring the permission_classes check. For example, here is the StatusView which disables permission checks entirely as follows:

class StatusView(APIView):
    ...
    permission_classes = tuple()
    ...

Permission Checking Machinery

drf-access-policy provides a feature to enable conditional checks to be globally available as their docs describe here. Pulp enables the reusable_conditions in its settings.py file, allowing a variety of condition checks to be globally available. Pulp enables this as follows:

DRF_ACCESS_POLICY = {"reusable_conditions": ["pulpcore.app.global_access_conditions"]}

The pulpcore.app.global_access_conditions provides the following checks that are available for both users and plugin writers to use in their policies:

pulpcore.app.global_access_conditions.has_group_model_or_obj_perms(request, view, action, permission)

Checks whether a user has the requested object or model permission on the Group in the URL.

This check is meant to be used for relations nested beneath the group endpoint, e.g. the list of users to belong to that group. It will fail for other endpoints.

{
    ...
    "condition": "has_group_model_or_obj_perms:core.group_delete",
},
Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Group Permission to be checked. In the form app_label.codename, e.g. “core.group_change”.

Returns

True if the user has the Permission on the Group specified in the URL named by the permission at the model or object level. False otherwise.

pulpcore.app.global_access_conditions.has_group_obj_perms(request, view, action, permission)

Checks whether a user has the requested object permission on the Group in the URL.

This check is meant to be used for relations nested beneath the group endpoint, e.g. the list of users to belong to that group. It will fail for other endpoints.

{
    ...
    "condition": "has_group_obj_perms:core.group_delete",
},
Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Group Permission to be checked. In the form app_label.codename, e.g. “core.group_change”.

Returns

True if the user has the Permission on the Group specified in the URL named by the permission at object level. False otherwise.

pulpcore.app.global_access_conditions.has_model_or_obj_perms(request, view, action, permission)

Checks if the current user has either model-level or object-level permissions.

The object in this case is the one the action is operating on, e.g. the URL /pulp/api/v3/tasks/15939b47-6b6d-4613-a441-939ca4ba6e63/ is operating on the Task object with pk=15939b47-6b6d-4613-a441-939ca4ba6e63.

This is usable as a conditional check in an AccessPolicy. Here is an example checking for “file.view_fileremote” permission at either the model-level or object-level.

{
    ...
    "condition": "has_model_or_obj_perms:file.view_fileremote",
}
Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Permission to be checked. In the form app_label.codename, e.g. “core.delete_task”.

Returns

True if the user has the Permission named by the permission argument at the model-level

or on the object being operated on at the object-level. False otherwise.

pulpcore.app.global_access_conditions.has_model_perms(request, view, action, permission)

Checks if the current user has a model-level permission.

This is usable as a conditional check in an AccessPolicy. Here is an example checking for “file.add_fileremote” permission at the model-level.

{
    ...
    "condition": "has_model_perms:file.add_fileremote",
}
Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Permission to be checked. In the form app_label.codename, e.g. “core.delete_task”.

Returns

True if the user has the Permission named by the permission argument at the model-level.

False otherwise.

pulpcore.app.global_access_conditions.has_obj_perms(request, view, action, permission)

Checks if the current user has object-level permission on the specific object.

The object in this case is the one the action is operating on, e.g. the URL /pulp/api/v3/tasks/15939b47-6b6d-4613-a441-939ca4ba6e63/ is operating on the Task object with pk=15939b47-6b6d-4613-a441-939ca4ba6e63.

This is usable as a conditional check in an AccessPolicy. Here is an example checking for the “file.view_fileremote” permissions at the object-level.

{
    ...
    "condition": "has_obj_perms:file.view_fileremote",
}
Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Permission to be checked. In the form app_label.codename, e.g. “core.delete_task”.

Returns

True if the user has the Permission named by the permission argument on the object being

operated on at the object-level. False otherwise.

pulpcore.app.global_access_conditions.has_publication_param_model_or_obj_perms(request, view, action, permission)

Checks if the current user has object-level permission on the publication object.

The object in this case is the one specified by the publication parameter. For example when distributing the publication parameter is passed in as an argument.

This is usable as a conditional check in an AccessPolicy. Here is an example checking for the “file.view_filepublication” permissions at the object-level.

{
    ...
    "condition": "has_publication_param_model_or_obj_perms:file.view_filepublication",
}

Since it is checking a publication object the permission argument should be one of the following:

  • “file.view_filepublication” - Permission to view the FilePublication.

  • “file.delete_filepublication” - Permission to delete the FilePublication.

Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Permission to be checked. In the form app_label.codename, e.g. “core.delete_task”.

Returns

True if the user has the Permission named by the permission argument on the

publication parameter at the object-level or if there is no publication. False otherwise.

pulpcore.app.global_access_conditions.has_remote_param_model_or_obj_perms(request, view, action, permission)

Checks if the current user has either model-level or object-level permissions on the remote.

The object in this case is the one specified by the remote parameter. For example when syncing the remote parameter is passed in as an argument.

This is usable as a conditional check in an AccessPolicy. Here is an example checking for “file.view_fileremote” permission at either the model-level or object-level.

{
    ...
    "condition": "has_remote_param_model_or_obj_perms:file.view_fileremote",
}

Since it is checking a remote object the permission argument should be one of the following:

  • “file.change_fileremote” - Permission to change the FileRemote.

  • “file.view_fileremote” - Permission to view the FileRemote.

  • “file.delete_fileremote” - Permission to delete the FileRemote.

Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Permission to be checked. In the form app_label.codename, e.g. “core.delete_task”.

Returns

True if the user has the Permission named by the permission at the model-level or on the

argument on the remote parameter at the object-level. False otherwise.

pulpcore.app.global_access_conditions.has_remote_param_obj_perms(request, view, action, permission)

Checks if the current user has object-level permission on the remote object.

The object in this case is the one specified by the remote parameter. For example when syncing the remote parameter is passed in as an argument.

This is usable as a conditional check in an AccessPolicy. Here is an example checking for the “file.view_fileremote” permissions at the object-level.

{
    ...
    "condition": "has_remote_param_obj_perms:file.view_fileremote",
}

Since it is checking a remote object the permission argument should be one of the following:

  • “file.change_fileremote” - Permission to change the FileRemote.

  • “file.view_fileremote” - Permission to view the FileRemote.

  • “file.delete_fileremote” - Permission to delete the FileRemote.

Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Permission to be checked. In the form app_label.codename, e.g. “core.delete_task”.

Returns

True if the user has the Permission named by the permission argument on the remote

parameter at the object-level or if there is no remote. False otherwise.

pulpcore.app.global_access_conditions.has_repo_attr_model_or_obj_perms(request, view, action, permission)

Checks if the current user has model-level or object-level permissions on a repository.

The object in this case is the one specified by the repository attribute of a resource which is being operated on. For example, when deleting a repository version, a repository is its attribute.

This is usable as a conditional check in an AccessPolicy. Here is an example checking for the “file.view_filerepository” permissions at the object-level.

{
    ...
    "condition": "has_repo_attr_model_or_obj_perms:file.view_filerepository",
}

Since it is checking a repository object the permission argument should be one of the following:

  • “file.change_filerepository” - Permission to change the FileRepository.

  • “file.view_filerepository” - Permission to view the FileRepository.

  • “file.delete_filerepository” - Permission to delete the FileRepository.

  • any custom permission a plugin has defined for their repository.

Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the repository Permission to be checked. In the form app_label.codename, e.g. “file.delete_filerepository”.

Returns

True if the user has the Permission on the repository attribute named by the permission at the model or object level. False otherwise.

pulpcore.app.global_access_conditions.has_repo_attr_obj_perms(request, view, action, permission)

Checks if the current user has object-level permission on a repository attribute.

The object in this case is the one specified by the repository attribute of a resource which is being operated on. For example, when deleting a repository version, a repository is its attribute.

This is usable as a conditional check in an AccessPolicy. Here is an example checking for the “file.view_filerepository” permissions at the object-level.

{
    ...
    "condition": "has_repo_attr_obj_perms:file.view_filerepository",
}

Since it is checking a repository object the permission argument should be one of the following:

  • “file.change_filerepository” - Permission to change the FileRepository.

  • “file.view_filerepository” - Permission to view the FileRepository.

  • “file.delete_filerepository” - Permission to delete the FileRepository.

  • any custom permission a plugin has defined for their repository.

Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the repository Permission to be checked. In the form app_label.codename, e.g. “file.delete_filerepository”.

Returns

True if the user has the Permission named by the permission argument on the repository attribute at the object-level. False otherwise.

pulpcore.app.global_access_conditions.has_repo_or_repo_ver_param_model_or_obj_perms(request, view, action, permission)

Checks if the current user has object-level permission on the repository object.

The object in this case is the one specified by the repository or repository_version parameter. For example when publishing the repository parameter is passed in as an argument.

This is usable as a conditional check in an AccessPolicy. Here is an example checking for the “file.view_filerepository” permissions at the object-level.

{
    ...
    "condition": "has_repo_or_repo_ver_param_model_or_obj_perms:file.view_filerepository",
}

Since it is checking a repository object the permission argument should be one of the following:

  • “file.change_filerepository” - Permission to change the FileRepository.

  • “file.view_filerepository” - Permission to view the FileRepository.

  • “file.delete_filerepository” - Permission to delete the FileRepository.

  • “file.sync_filerepository” - Permission to sync the FileRepository.

Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Permission to be checked. In the form app_label.codename, e.g. “core.delete_task”.

Returns

True if the user has the Permission named by the permission argument on the

repository or repository_version parameter at the object-level or if there is no repository. False otherwise.

pulpcore.app.global_access_conditions.has_repository_model_or_obj_perms(request, view, action, permission)

Checks whether a user has the requested model or object permission on the repository in the URL.

This check is meant to be used for relations nested beneath a repository endpoint, e.g. the list of repository versions belonging to that repository. It will fail for other endpoints.

{
    ...
    "condition": "has_repository_model_or_obj_perms:file.filerepository_delete",
},
Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Repository Permission to be checked. In the form app_label.codename, e.g. “file.filerepository_change”.

Returns

True if the user has the Permission on the Repository specified in the URL named by the permission at model or object level. False otherwise.

pulpcore.app.global_access_conditions.has_repository_obj_perms(request, view, action, permission)

Checks whether a user has the requested object permission on the repository in the URL.

This check is meant to be used for relations nested beneath a repository endpoint, e.g. the list of repository versions belonging to that repository. It will fail for other endpoints.

{
    ...
    "condition": "has_repository_obj_perms:file.filerepository_delete",
},
Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Repository Permission to be checked. In the form app_label.codename, e.g. “file.filerepository_change”.

Returns

True if the user has the Permission on the Repository specified in the URL named by the permission at object level. False otherwise.

pulpcore.app.global_access_conditions.has_required_repo_perms_on_upload(request, view, action, permission)

Checks if the current user has permission to upload content to the repository object.

Since content queryset scoping prevents users from seeing orphaned content by default this also checks to make sure that any user that isn’t an admin has also supplied the repository parameter when performing a content upload.

This is usable as a conditional check in an AccessPolicy for content uploads. Here is an example checking for the “file.modify_filerepository” permission.

{
    ...
    "condition": "has_required_repo_perms_on_upload:file.modify_filerepository",
}
Parameters
  • request (rest_framework.request.Request) – The request being made.

  • view (subclass rest_framework.viewsets.GenericViewSet) – The view being checked for authorization.

  • action (str) – The action being performed, e.g. “destroy”.

  • permission (str) – The name of the Permission to be checked. In the form app_label.codename, e.g. “core.delete_task”.

Returns

True if the user has supplied the repository parameter and has the Permission on it

named by the permission argument, or is an admin. False otherwise.

Custom Permission Checks

Plugins can provide their own permission checks by defining them in a app.global_access_conditions module and adding an operation like

DRF_ACCESS_POLICY = {
    "dynaconf_merge_unique": True,
    "reusable_conditions": ["pulp_container.app.global_access_conditions"],
}

to their app.settings module.