axnsan12 / drf-yasg

Automated generation of real Swagger/OpenAPI 2.0 schemas from Django REST Framework code.
https://drf-yasg.readthedocs.io/en/stable/
Other
3.4k stars 437 forks source link

Hiding APIs that require authentication #58

Closed lukik closed 6 years ago

lukik commented 6 years ago

I've setup DRF-YASG but it seems not able to hide APIs that require Authentication.

Below is the configuration.

public_apis = [ 
    # sample-end-point
    url(f'{URL_VERSION}/sample-end-point/', include(sample_end_point.urls)),  # requires authentication
]

  schema_view = get_schema_view(
      openapi.Info(
        title="Swagger Doc",
        default_version='v1',
        description="Test description",
        terms_of_service="https://www.google.com/policies/terms/",
        contact=openapi.Contact(email="contact@snippets.local"),
        license=openapi.License(name="BSD License"),
      ),
      validators=['flex', 'ssv'],
      permission_classes=(permissions.AllowAny,), 
      public=False,
      patterns=public_apis,
  )

urlpatterns = public_apis + [

    url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'),
    url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
    url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
]

With the above configuration, the sample-end-point does not appear on the SwaggerUi. It only shows the Authorize Button and text that says No operations defined in spec!. But if I change thepublic=False to public=True setting, the sample-end-point appears. But that is not what I want as this URL should be accessible only after one logs in.

Is there something am not understanding or a setting am missing?

My Swagger settings are as below:

SWAGGER_SETTINGS = {
    'USE_SESSION_AUTH': False,
    'SECURITY_DEFINITIONS': {
        'api_key': {
            'type': 'apiKey',
            'in': 'header',
            'name': 'Authorization'
        }
    },
    'APIS_SORTER': 'alpha',
    'SUPPORTED_SUBMIT_METHODS': ['get', 'post', 'put', 'delete', 'patch'],
    'OPERATIONS_SORTER': 'alpha'
}

I'd posted the issue on Stack Overflow

axnsan12 commented 6 years ago

Hello!

The public parameter is used to hide APIs from the generated specification according to the permissions of the user who views the API documentation page. With public=True, anyone who has access to view the swagger page can see ALL endpoints in the api, while with False, they would only see the ones they can actually call according to their current credentials.

If you want to restrict who has access to the page itself, that would be achived, as with any regular view, via permission_classes. The get_schema_view method also provides it as a convenience argument, so you could achieve what you want simply by replacing permission_classes=(permissions.AllowAny,), with, say, permission_classes=(permissions.IsAuthenticated,),, or whatever floats your boat.

The main thing to take away is that the permission_classes on the swagger view are unrelated to the public argument to the spec generator - one controls who can VIEW the spec, while the other controls what endpoints are included in it.

axnsan12 commented 6 years ago

Just now read the stack overflow question more closely - the error you are getting happens because you try to access the endpoint without authentication. I see that the error handling is not exactly on point for this case.

You could wrap the views in login_required: https://docs.djangoproject.com/en/2.0/topics/class-based-views/intro/#decorating-class-based-views

from django.contrib.auth.decorators import login_required, 

.. login_required(schema_view.with_ui('swagger', cache_timeout=None)) ...  # same for the other two
chgad commented 6 years ago

Great explanation, i stumbled upon this too. 👍✨

lukik commented 6 years ago

@axnsan12 thanks for the guide. However, I tried the following...

from django.contrib.auth.decorators import login_required

    schema_view = get_schema_view(
        openapi.Info(
          title="Swagger Doc",
          default_version='v1',
          description="Test description",
          terms_of_service="https://www.google.com/policies/terms/",
          contact=openapi.Contact(email="contact@snippets.local"),
          license=openapi.License(name="BSD License"),
        ),
        # validators=['flex', 'ssv'],
        permission_classes=(permissions.IsAuthenticated,),  # If I change the permission it throws an exception
        public=False,
        patterns=public_apis,
    )

And URLs, I added the login_required...

url(r'^docs(?P<format>.json|.yaml)$', login_required(schema_view.without_ui(cache_timeout=None)), name='schema-json'),
url(r'^docs/$', login_required(schema_view.with_ui('swagger', cache_timeout=None)), name='schema-swagger-ui'),
url(r'^redoc/$', login_required(schema_view.with_ui('redoc', cache_timeout=None)), name='schema-redoc'),

permissions.AllowAny: With permissions.AllowAny and public=false, the APIs that require to be hidden before someone has authenticated do not show but the Swagger view loads. Just that it does not have any API on it.

Any other Permission Type: With permissions.IsAuthenticated or -any other permission for that matter- , the swagger view fails with the error below.

  Internal Server Error: /swagger/
  Traceback (most recent call last):
    File "/usr/local/lib/python3.6/site-packages/django/core/handlers/exception.py", line 41, in inner
      response = get_response(request)
    File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py", line 217, in _get_response
      response = self.process_exception_by_middleware(e, request)
    File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py", line 215, in _get_response
      response = response.render()
    File "/usr/local/lib/python3.6/site-packages/django/template/response.py", line 107, in render
      self.content = self.rendered_content
    File "/usr/local/lib/python3.6/site-packages/rest_framework/response.py", line 72, in rendered_content
      ret = renderer.render(self.data, accepted_media_type, context)
    File "/usr/local/lib/python3.6/site-packages/drf_yasg/renderers.py", line 54, in render
      self.set_context(renderer_context, swagger)
    File "/usr/local/lib/python3.6/site-packages/drf_yasg/renderers.py", line 62, in set_context
      renderer_context['title'] = swagger.info.title
  AttributeError: 'dict' object has no attribute 'info'
  Internal Server Error: /swagger/

So am not sure what am doing wrong..

@chgad any tips?

axnsan12 commented 6 years ago

So, the 'dict' object has no attribute 'info' error you are getting happens because the renderer receives a plain dict and fails to check for non-200 staus code (here), so any outcome SchemaView#get except success would trigger the error.

My assumption was that your error happened because of lacking authentication, but it might also happen for other reasons, running with a debugger might help - the error details probably arrive as the swagger parameter of render.

I'll try to fix the error handling this weekend to avoid burying the error like this.

chgad commented 6 years ago

@axnsan12 I also got the same error. In my case it's permissions.IsAdminUser. Analysing it with my Debugger lead to the following statement : the local variable swagger is dictionary which only contains {'detail'."Authentication credentials were not provided."}.

After seeing this i first loggedin the admin panel and the tried again. Aaaaaaaaaaaaaaaaaaaaaaand it worked, so my first guess is, that the swagger Renderer does not comunicate with the Authenticationbackend correctly.

I'd like to help further but i don't see where the swagger variable enters and where it's origin is.

axnsan12 commented 6 years ago

@chgad it enters via the response object returned by the view, as is normal for django rest framework renderers (http://www.django-rest-framework.org/api-guide/renderers/).

The request and response are passed in renderer_context, and the problem is that swagger is expected to be a Swagger object, which is not the case for a non-200 response. I don't yet know exactly what would be the correct treatment, should probably somehow render an error page.

lukik commented 6 years ago

Finally worked.

Turned out the issue was that I had only rest_framework_jwt.authentication.JSONWebTokenAuthentication in REST_FRAMEWORK's DEFAULT_AUTHENTICATION_CLASSES. So I guess the request object sent after logging in via the Django Admin login view did not have a user so all permission classes kept failing and it would then lead to the above error?

So to solve it I added 'rest_framework.authentication.SessionAuthentication', so the login works okay and drf-yasg shows all its glory.

@axnsan12 good job on this. Really solved a big documentation gap.

ihd2911 commented 5 years ago

If i add rest_framework.authentication.SessionAuthentication, my apis start working without auth token in the header

trsh commented 4 years ago

@ihd2911 same here. Did you find a solution?

sandhuharjodh commented 4 years ago

No, it didn't. I had to change package

rau commented 2 years ago

@lukik

Finally worked.

Turned out the issue was that I had only rest_framework_jwt.authentication.JSONWebTokenAuthentication in REST_FRAMEWORK's DEFAULT_AUTHENTICATION_CLASSES. So I guess the request object sent after logging in via the Django Admin login view did not have a user so all permission classes kept failing and it would then lead to the above error?

So to solve it I added 'rest_framework.authentication.SessionAuthentication', so the login works okay and drf-yasg shows all its glory.

@axnsan12 good job on this. Really solved a big documentation gap.

Even after adding SessionAuthentication to DEFAULT_AUTHENTICATION_CLASSES, and even above TokenAuthentication, using any sort of permission_class with DRF_yasg tends to think it is an AnonymousUser trying to view the API. This happens even when I'm 100% logged in through Django admin. Any clue how I could fix this? I've tested it with my own custom permission and printing 'request.user', - it still shows as an AnonymousUser.

st9-8 commented 6 months ago

Finally worked.

Turned out the issue was that I had only rest_framework_jwt.authentication.JSONWebTokenAuthentication in REST_FRAMEWORK's DEFAULT_AUTHENTICATION_CLASSES. So I guess the request object sent after logging in via the Django Admin login view did not have a user so all permission classes kept failing and it would then lead to the above error?

So to solve it I added 'rest_framework.authentication.SessionAuthentication', so the login works okay and drf-yasg shows all its glory.

@axnsan12 good job on this. Really solved a big documentation gap.

You should not add SessionAuthentication directly in the REST_FRAMEWORK settings because it means that now even Session auth in Django admin can be considered as working authentication method by your API endpoints. Not sure it's what you want. Instead, the get_schema_view also has authentication_classes as kwargs. Pass SessionAuthentication as value for this key. Like that this authentication will be accepted to login in the api documentation, but not for your API endpoints.

Example:

schema_view = get_schema_view(
    openapi.Info(
        title="...",
        default_version='...',
        description="...",
        terms_of_service="",
        contact=openapi.Contact(email="...@...),
        license=openapi.License(name="..."),
    ),
    public=False,
    authentication_classes=(SessionAuthentication,),
    permission_classes=(permissions.IsAdminUser,),
)