Drf-yasg: Best practice for documenting permissions

Created on 1 May 2019  路  5Comments  路  Source: axnsan12/drf-yasg

Hi,

AFAIU there are two general use-cases for schema generation - schema for user, in which case I should use get_schema_view(public=False,) and second use-case - schema as API documentation, in which case I should use get_schema_view(public=True,).
Probably permissions documentation is not important in first case - use will see already filtered API subset, but is important in second case.

I'm generating documentation for project with lots of custom permissions. It's strange that there are view, fields and paginator inspectors, but no permissions inspectors.
I was wondering if there is some best-practice or recommended approach for this.

At the moment I use following hack, which does the job, but feels wrong:

class ViewInspector(SwaggerAutoSchema):
    """View inspector with some project-specific logic."""

    def get_summary_and_description(self):
        """Return summary and description extended with permission docs."""
        summary, description = super().get_summary_and_description()
        permissions_description = self._get_permissions_description()
        if permissions_description:
            description += permissions_description
        return summary, description

    def _get_permissions_description(self):
        permission_descriptions = []
        for permission_class in getattr(self.view, 'permission_classes', []):
            if hasattr(permission_class, 'get_description'):
                permission_descriptions.append(
                    permission_class.get_description(self.view))
            else:
                permission_descriptions.append(
                    permission_class.__doc__.replace('\n', ' ').strip())
        if permission_descriptions:
            return '\n**Permissions**:\n' + '\n'.join(
                '+ ' + description for description in permission_descriptions
            )
        else:
            return None
question

Most helpful comment

I've change something in the snippet, maybe it might be useful for others.

from typing import List, Optional

from collections import namedtuple

from drf_yasg.inspectors import SwaggerAutoSchema as DrfYasgSwaggerAutoSchema
from rest_framework.permissions import OperandHolder, SingleOperandHolder

PermissionItem = namedtuple('PermissionItem', ['name', 'doc_str'])


def _render_permission_item(item):
    return f'+ `{item.name}`: *{item.doc_str}*'


class SwaggerAutoSchema(DrfYasgSwaggerAutoSchema):
    """View inspector with some project-specific logic."""

    def get_summary_and_description(self):
        """Return summary and description extended with permission docs."""
        summary, description = super().get_summary_and_description()
        permissions_description = self._get_permissions_description()
        if permissions_description:
            description += permissions_description
        return summary, description

    def _handle_permission(
            self, permission_class) -> Optional[PermissionItem]:
        permission = None

        try:
            permission = PermissionItem(
                permission_class.__name__,
                permission_class.get_description(self.view),
            )
        except AttributeError:
            if permission_class.__doc__:
                permission = PermissionItem(
                    permission_class.__name__,
                    permission_class.__doc__.replace('\n', ' ').strip(),
                )

        return permission

    def _gather_permissions(self) -> List[PermissionItem]:
        items = []

        for permission_class in getattr(self.view, 'permission_classes', []):
            if isinstance(permission_class, OperandHolder):
                items.append(
                    self._handle_permission(permission_class.op1_class))
                items.append(
                    self._handle_permission(permission_class.op2_class))

            if isinstance(permission_class, SingleOperandHolder):
                items.append(
                    self._handle_permission(permission_class.op1_class))

            items.append(self._handle_permission(permission_class))

        return [i for i in items if i]

    def _get_permissions_description(self):
        permission_items = self._gather_permissions()

        if permission_items:
            return '\n\n**Permissions:**\n' + '\n'.join(
                _render_permission_item(item) for item in permission_items
            )
        else:
            return None

All 5 comments

There are no permission inspectors because there is no set way to describe permissions in swagger. The logic you showed in your snippet seems just fine if all you want is to show the required permissions in the description.

The closes thing to programatically defining permissions in swagger would be OAuth scopes, but I'm guessing that is not what you're after (and there is also no built-in support for those in drf-yasg).

I think it would be exceptionally useful if we somehow integrate this into drf_yasg. For example, we could automatically generate 403 responses based on permissions.

Anyway @K0Te approach quite good. Thanks!

I've change something in the snippet, maybe it might be useful for others.

from typing import List, Optional

from collections import namedtuple

from drf_yasg.inspectors import SwaggerAutoSchema as DrfYasgSwaggerAutoSchema
from rest_framework.permissions import OperandHolder, SingleOperandHolder

PermissionItem = namedtuple('PermissionItem', ['name', 'doc_str'])


def _render_permission_item(item):
    return f'+ `{item.name}`: *{item.doc_str}*'


class SwaggerAutoSchema(DrfYasgSwaggerAutoSchema):
    """View inspector with some project-specific logic."""

    def get_summary_and_description(self):
        """Return summary and description extended with permission docs."""
        summary, description = super().get_summary_and_description()
        permissions_description = self._get_permissions_description()
        if permissions_description:
            description += permissions_description
        return summary, description

    def _handle_permission(
            self, permission_class) -> Optional[PermissionItem]:
        permission = None

        try:
            permission = PermissionItem(
                permission_class.__name__,
                permission_class.get_description(self.view),
            )
        except AttributeError:
            if permission_class.__doc__:
                permission = PermissionItem(
                    permission_class.__name__,
                    permission_class.__doc__.replace('\n', ' ').strip(),
                )

        return permission

    def _gather_permissions(self) -> List[PermissionItem]:
        items = []

        for permission_class in getattr(self.view, 'permission_classes', []):
            if isinstance(permission_class, OperandHolder):
                items.append(
                    self._handle_permission(permission_class.op1_class))
                items.append(
                    self._handle_permission(permission_class.op2_class))

            if isinstance(permission_class, SingleOperandHolder):
                items.append(
                    self._handle_permission(permission_class.op1_class))

            items.append(self._handle_permission(permission_class))

        return [i for i in items if i]

    def _get_permissions_description(self):
        permission_items = self._gather_permissions()

        if permission_items:
            return '\n\n**Permissions:**\n' + '\n'.join(
                _render_permission_item(item) for item in permission_items
            )
        else:
            return None

@timur-orudzhov very useful. Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tiltec picture tiltec  路  4Comments

phihag picture phihag  路  5Comments

hnykda picture hnykda  路  3Comments

beaugunderson picture beaugunderson  路  5Comments

marcosgeorgopoulos picture marcosgeorgopoulos  路  4Comments