diff --git a/swh/deposit/api/common.py b/swh/deposit/api/common.py --- a/swh/deposit/api/common.py +++ b/swh/deposit/api/common.py @@ -18,7 +18,7 @@ from django.urls import reverse from django.utils import timezone from rest_framework import status -from rest_framework.authentication import BaseAuthentication +from rest_framework.authentication import BaseAuthentication, BasicAuthentication from rest_framework.permissions import BasePermission, IsAuthenticated from rest_framework.request import Request from rest_framework.views import APIView @@ -160,32 +160,34 @@ if not origin_url.startswith(provider_url): raise DepositError( FORBIDDEN, - f"Cannot create origin {origin_url}, it must start with " f"{provider_url}", + f"Cannot create origin {origin_url}, it must start with {provider_url}", ) -class AuthenticatedAPIView(APIView): - """Mixin intended as a based API view to enforce the basic - authentication check - - """ - - authentication_classes: Sequence[Type[BaseAuthentication]] = ( - KeycloakBasicAuthentication, - ) - permission_classes: Sequence[Type[BasePermission]] = ( - IsAuthenticated, - HasDepositPermission, - ) - - -class APIBase(APIConfig, AuthenticatedAPIView, metaclass=ABCMeta): +class APIBase(APIConfig, APIView, metaclass=ABCMeta): """Base deposit request class sharing multiple common behaviors. """ _client: Optional[DepositClient] = None + def __init__(self): + super().__init__() + auth_provider = self.config["authentication_provider"] + if auth_provider == "basic": + self.authentication_classes: Sequence[Type[BaseAuthentication]] = ( + BasicAuthentication, + ) + self.permission_classes: Sequence[Type[BasePermission]] = (IsAuthenticated,) + elif auth_provider == "keycloak": + self.authentication_classes: Sequence[Type[BaseAuthentication]] = ( + KeycloakBasicAuthentication, + ) + self.permission_classes: Sequence[Type[BasePermission]] = ( + IsAuthenticated, + HasDepositPermission, + ) + def _read_headers(self, request: Request) -> ParsedRequestHeaders: """Read and unify the necessary headers from the request (those are not stored in the same location or not properly formatted). diff --git a/swh/deposit/api/private/__init__.py b/swh/deposit/api/private/__init__.py --- a/swh/deposit/api/private/__init__.py +++ b/swh/deposit/api/private/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2020 The Software Heritage developers +# Copyright (C) 2017-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -6,9 +6,9 @@ from typing import Any, Dict, List, Tuple from rest_framework.permissions import AllowAny +from rest_framework.views import APIView from swh.deposit import utils -from swh.deposit.api.common import AuthenticatedAPIView from ...config import METADATA_TYPE, APIConfig from ...models import Deposit, DepositRequest @@ -60,14 +60,16 @@ return (aggregated_metadata, raw_metadata) -class APIPrivateView(APIConfig, AuthenticatedAPIView): +class APIPrivateView(APIConfig, APIView): """Mixin intended as private api (so no authentication) based API view (for the private ones). """ - authentication_classes = () - permission_classes = (AllowAny,) + def __init__(self): + super().__init__() + self.authentication_classes = () + self.permission_classes = (AllowAny,) def checks(self, req, collection_name, deposit=None): """Override default checks implementation to allow empty collection. diff --git a/swh/deposit/api/service_document.py b/swh/deposit/api/service_document.py --- a/swh/deposit/api/service_document.py +++ b/swh/deposit/api/service_document.py @@ -12,12 +12,16 @@ APIBase, ) from swh.deposit.config import COL_IRI -from swh.deposit.models import DepositCollection +from swh.deposit.models import DepositClient, DepositCollection class ServiceDocumentAPI(APIBase): def get(self, request, *args, **kwargs): - client = request.user + if isinstance(request.user, DepositClient): + client = request.user + else: + client = DepositClient.objects.get(username=request.user) + collections = {} for col_id in client.collections: col = DepositCollection.objects.get(pk=col_id) diff --git a/swh/deposit/settings/common.py b/swh/deposit/settings/common.py --- a/swh/deposit/settings/common.py +++ b/swh/deposit/settings/common.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2020 The Software Heritage developers +# Copyright (C) 2017-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -103,7 +103,9 @@ STATIC_URL = "/static/" REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": ("swh.deposit.auth.KeycloakBasicAuthentication",), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework.authentication.BasicAuthentication", + ), "EXCEPTION_HANDLER": "swh.deposit.exception.custom_exception_handler", } diff --git a/swh/deposit/settings/production.py b/swh/deposit/settings/production.py --- a/swh/deposit/settings/production.py +++ b/swh/deposit/settings/production.py @@ -1,4 +1,4 @@ -# Copyright (C) 2017-2019 The Software Heritage developers +# Copyright (C) 2017-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information @@ -8,7 +8,7 @@ from swh.core import config from .common import * # noqa -from .common import ALLOWED_HOSTS +from .common import ALLOWED_HOSTS, CACHES, REST_FRAMEWORK ALLOWED_HOSTS += ["deposit.softwareheritage.org"] # Setup support for proxy headers @@ -41,7 +41,7 @@ if not conf: raise ValueError("Production: configuration %s does not exist." % (config_file,)) -for key in ("scheduler", "private"): +for key in ("scheduler", "private", "authentication"): if not conf.get(key): raise ValueError( "Production: invalid configuration; missing %s config entry." % (key,) @@ -91,3 +91,24 @@ # https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-MEDIA_ROOT MEDIA_ROOT = private_conf.get("media_root") + +# Default authentication is http basic +authentication = conf["authentication"] + +# With the following, we delegate the authentication mechanism to keycloak +if authentication == "keycloak": + REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( + "swh.deposit.auth.KeycloakBasicAuthentication", + ) + + # Optional cache server + server_cache = conf.get("cache_uri") + if server_cache: + CACHES.update( + { + "default": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": server_cache, + } + } + ) diff --git a/swh/deposit/settings/testing.py b/swh/deposit/settings/testing.py --- a/swh/deposit/settings/testing.py +++ b/swh/deposit/settings/testing.py @@ -40,3 +40,8 @@ FILE_UPLOAD_HANDLERS = [ "django.core.files.uploadhandler.MemoryFileUploadHandler", ] + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ("swh.deposit.auth.KeycloakBasicAuthentication",), + "EXCEPTION_HANDLER": "swh.deposit.exception.custom_exception_handler", +} diff --git a/swh/deposit/tests/conftest.py b/swh/deposit/tests/conftest.py --- a/swh/deposit/tests/conftest.py +++ b/swh/deposit/tests/conftest.py @@ -137,6 +137,7 @@ "storage": swh_storage_backend_config, "storage_metadata": swh_storage_backend_config, "swh_authority_url": "http://deposit.softwareheritage.example/", + "authentication_provider": "keycloak", "keycloak": { "server_url": KEYCLOAK_SERVER_URL, "realm_name": KEYCLOAK_REALM_NAME,