diff --git a/passbook/admin/views/groups.py b/passbook/admin/views/groups.py index 833949fd3..cfc99c18c 100644 --- a/passbook/admin/views/groups.py +++ b/passbook/admin/views/groups.py @@ -56,10 +56,11 @@ class GroupUpdateView( success_message = _("Successfully updated Group") -class GroupDeleteView(LoginRequiredMixin, DeleteMessageView): +class GroupDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView): """Delete group""" model = Group + permission_required = "passbook_flows.delete_group" template_name = "generic/delete.html" success_url = reverse_lazy("passbook_admin:groups") diff --git a/passbook/api/auth.py b/passbook/api/auth.py new file mode 100644 index 000000000..06eebd3ce --- /dev/null +++ b/passbook/api/auth.py @@ -0,0 +1,43 @@ +"""API Authentication""" +from base64 import b64decode +from typing import Any, Tuple, Union + +from django.utils.translation import gettext as _ +from rest_framework import HTTP_HEADER_ENCODING, exceptions +from rest_framework.authentication import BaseAuthentication, get_authorization_header +from rest_framework.request import Request + +from passbook.core.models import Token, TokenIntents, User + + +class PassbookTokenAuthentication(BaseAuthentication): + """Token-based authentication using HTTP Basic authentication""" + + def authenticate(self, request: Request) -> Union[Tuple[User, Any], None]: + """Token-based authentication using HTTP Basic authentication""" + auth = get_authorization_header(request).split() + + if not auth or auth[0].lower() != b"basic": + return None + + if len(auth) == 1: + msg = _("Invalid basic header. No credentials provided.") + raise exceptions.AuthenticationFailed(msg) + if len(auth) > 2: + msg = _( + "Invalid basic header. Credentials string should not contain spaces." + ) + raise exceptions.AuthenticationFailed(msg) + + header_data = b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(":") + + tokens = Token.filter_not_expired( + token_uuid=header_data[2], intent=TokenIntents.INTENT_API + ) + if not tokens.exists(): + raise exceptions.AuthenticationFailed(_("Invalid token.")) + + return (tokens.first().user, None) + + def authenticate_header(self, request: Request) -> str: + return 'Basic realm="passbook"' diff --git a/passbook/api/urls.py b/passbook/api/urls.py index 2115b64b2..a67986f8d 100644 --- a/passbook/api/urls.py +++ b/passbook/api/urls.py @@ -6,5 +6,5 @@ from passbook.api.v2.urls import urlpatterns as v2_urls urlpatterns = [ path("v1/", include(v1_urls)), - path("v2/", include(v2_urls)), + path("v2beta/", include(v2_urls)), ] diff --git a/passbook/root/settings.py b/passbook/root/settings.py index ba53846e0..bd53ba074 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -118,6 +118,9 @@ GUARDIAN_MONKEY_PATCH = False SWAGGER_SETTINGS = { "DEFAULT_INFO": "passbook.api.v2.urls.info", + "SECURITY_DEFINITIONS": { + "token": {"type": "apiKey", "name": "Authorization", "in": "header"} + }, } REST_FRAMEWORK = { @@ -128,13 +131,10 @@ REST_FRAMEWORK = { "rest_framework.filters.OrderingFilter", "rest_framework.filters.SearchFilter", ], - "DEFAULT_PERMISSION_CLASSES": ( - # 'rest_framework.permissions.IsAuthenticated', - "passbook.api.permissions.CustomObjectPermissions", - ), + "DEFAULT_PERMISSION_CLASSES": ("passbook.api.permissions.CustomObjectPermissions"), "DEFAULT_AUTHENTICATION_CLASSES": ( + "passbook.api.auth.PassbookTokenAuthentication", "rest_framework.authentication.SessionAuthentication", - # 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', ), } diff --git a/swagger.yaml b/swagger.yaml index 36e920f3f..bf7a60faf 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -6,16 +6,18 @@ info: license: name: MIT License version: v2 -basePath: /api/v2 +basePath: /api/v2beta consumes: - application/json produces: - application/json securityDefinitions: - Basic: - type: basic + token: + type: apiKey + name: Authorization + in: header security: - - Basic: [] + - token: [] paths: /audit/events/: get: