From 1c8ff9f53652a46ea438df11764ed01448335a9e Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Sun, 23 Oct 2022 03:21:44 +0100 Subject: [PATCH 01/73] Complete re-do app --- django_keycloak/__init__.py | 2 + django_keycloak/admin.py | 24 +- django_keycloak/api/serializers.py | 28 ++ django_keycloak/api/views.py | 42 +- django_keycloak/authentication.py | 38 +- django_keycloak/backends.py | 65 ++- django_keycloak/config.py | 37 ++ django_keycloak/connector.py | 57 +++ django_keycloak/decorators.py | 20 - django_keycloak/enums.py | 5 - django_keycloak/errors.py | 44 ++ django_keycloak/keycloak.py | 432 ------------------ .../commands/sync_keycloak_users.py | 8 +- django_keycloak/managers.py | 121 +---- django_keycloak/middleware.py | 108 +++-- django_keycloak/mixins.py | 11 +- django_keycloak/models.py | 21 +- django_keycloak/tests.py | 3 - django_keycloak/token.py | 178 ++++++++ django_keycloak/urls.py | 23 - setup.cfg | 4 +- 21 files changed, 552 insertions(+), 719 deletions(-) create mode 100644 django_keycloak/config.py create mode 100644 django_keycloak/connector.py delete mode 100644 django_keycloak/decorators.py delete mode 100644 django_keycloak/enums.py delete mode 100644 django_keycloak/keycloak.py delete mode 100644 django_keycloak/tests.py create mode 100644 django_keycloak/token.py delete mode 100644 django_keycloak/urls.py diff --git a/django_keycloak/__init__.py b/django_keycloak/__init__.py index b1829aa..05d385f 100644 --- a/django_keycloak/__init__.py +++ b/django_keycloak/__init__.py @@ -1 +1,3 @@ +from .token import Token + default_app_config = "django_keycloak.apps.DjangoKeycloakConfig" diff --git a/django_keycloak/admin.py b/django_keycloak/admin.py index 5de869e..df58eae 100644 --- a/django_keycloak/admin.py +++ b/django_keycloak/admin.py @@ -1,10 +1,12 @@ -from django.conf import settings +""" +Module to register models into Django admin dashboard. +""" from django.contrib import admin from django.contrib.auth import get_user_model from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ -from django_keycloak.urls import BASE_PATH, KEYCLOAK_ADMIN_USER_PAGE +from django_keycloak.config import settings User = get_user_model() @@ -31,15 +33,17 @@ class UserAdmin(admin.ModelAdmin): search_fields = ["username", "email"] def keycloak_link(self, obj): - config = settings.KEYCLOAK_CONFIG - label = obj.id - base_path = config.get("BASE_PATH", BASE_PATH) - host = f"{config.get('SERVER_URL')}{base_path}" - link = KEYCLOAK_ADMIN_USER_PAGE.format( - host=host, realm=config.get("REALM"), id=label - ) + + base_path = settings.BASE_PATH + server = settings.SERVER_URL + realm = settings.REALM + + link = f"{server}{base_path}/admin/master/console/#/{realm}/users/{obj.keycloak_identifier}/settings" + return format_html( - '{label}', link=link, label=label + '{label}', + link=link, + label=obj.keycloak_identifier, ) keycloak_link.short_description = _("keycloak link") diff --git a/django_keycloak/api/serializers.py b/django_keycloak/api/serializers.py index cc07a80..b0191fb 100644 --- a/django_keycloak/api/serializers.py +++ b/django_keycloak/api/serializers.py @@ -1,15 +1,43 @@ from django.contrib.auth import get_user_model from rest_framework import serializers +from rest_framework.exceptions import AuthenticationFailed, ValidationError +from django_keycloak import Token class GetTokenSerializer(serializers.Serializer): username = serializers.CharField(required=True, write_only=True) password = serializers.CharField(required=True, write_only=True) + def to_representation(self, instance): + """ + Return an "access" and "refresh" token if the given credentials are correct. + Otherwise return an authentication error + """ + token = Token.from_credentials(instance["username"], instance["password"]) + if not token: + raise AuthenticationFailed + + return { + "access": token.access_token, + "refresh": token.refresh_token, + } + class RefreshTokenSerializer(serializers.Serializer): refresh_token = serializers.CharField(required=True, write_only=True) + def to_representation(self, instance): + """ + Return an "access" token from the "refresh" token. + If the refresh token is not valid return a validation error. + """ + token = Token.from_refresh_token(instance["refresh_token"]) + if not token: + raise ValidationError + return { + "access": token.access_token, + } + class KeycloakUserAutoIdSerializer(serializers.ModelSerializer): """ diff --git a/django_keycloak/api/views.py b/django_keycloak/api/views.py index 294bfe1..bd599b8 100644 --- a/django_keycloak/api/views.py +++ b/django_keycloak/api/views.py @@ -1,9 +1,8 @@ from django.contrib.auth import get_user_model -from rest_framework import mixins +from rest_framework import mixins, permissions from rest_framework import status -from rest_framework import viewsets +from rest_framework import viewsets, generics from rest_framework.decorators import action -from rest_framework.generics import GenericAPIView from rest_framework.response import Response from django_keycloak.api.filters import DRYPermissionFilter @@ -12,41 +11,26 @@ RefreshTokenSerializer, KeycloakUserAutoIdSerializer, ) -from django_keycloak.keycloak import Connect -class GetTokenAPIView(GenericAPIView): - serializer_class = GetTokenSerializer - permission_classes = () +class BaseTokenAPIView(generics.GenericAPIView): + serializer_class = None + # Allow anonymous users + permission_classes = [permissions.AllowAny] def post(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() - serializer = serializer_class(data=request.data, context={"request": request}) + serializer = serializer_class(data=request.data) serializer.is_valid(raise_exception=True) + return Response(serializer.data) - keycloak = Connect() - data = keycloak.get_token_from_credentials( - request.data.get("username"), request.data.get("password") - ) - return Response(data) +class GetTokenAPIView(BaseTokenAPIView): + serializer_class = GetTokenSerializer -class RefreshTokenAPIView(GenericAPIView): +class RefreshTokenAPIView(BaseTokenAPIView): serializer_class = RefreshTokenSerializer - permission_classes = () - - def post(self, request, *args, **kwargs): - serializer_class = self.get_serializer_class() - serializer = serializer_class(data=request.data, context={"request": request}) - serializer.is_valid(raise_exception=True) - - keycloak = Connect() - data = keycloak.refresh_token_from_credentials( - request.data.get("refresh_token") - ) - - return Response(data) class UserProfileAPIView(viewsets.GenericViewSet, mixins.RetrieveModelMixin): @@ -55,7 +39,9 @@ class UserProfileAPIView(viewsets.GenericViewSet, mixins.RetrieveModelMixin): filter_backends = (DRYPermissionFilter,) @action( - detail=False, methods=["GET"], serializer_class=KeycloakUserAutoIdSerializer + detail=False, + methods=["GET"], + serializer_class=KeycloakUserAutoIdSerializer, ) def me(self, request): """ diff --git a/django_keycloak/authentication.py b/django_keycloak/authentication.py index 7de0446..b415d3f 100644 --- a/django_keycloak/authentication.py +++ b/django_keycloak/authentication.py @@ -1,9 +1,13 @@ +""" +Custom authentication class for Django Rest Framework. +""" from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser -from rest_framework import exceptions, HTTP_HEADER_ENCODING +from rest_framework import HTTP_HEADER_ENCODING from rest_framework.authentication import BaseAuthentication +from rest_framework.exceptions import AuthenticationFailed -from django_keycloak.keycloak import Connect +from django_keycloak import Token class KeycloakAuthentication(BaseAuthentication): @@ -13,9 +17,6 @@ class KeycloakAuthentication(BaseAuthentication): authenticate_header = "Bearer" - def __init__(self): - self.keycloak = Connect() - @staticmethod def get_authorization_header(request): """ @@ -42,18 +43,23 @@ def authenticate(self, request): """ auth_header = self.get_authorization_header(request) - if auth_header: - token = self.get_token(request) - if token: - try: - user = get_user_model().objects.get_by_keycloak_id( - self.keycloak.get_user_id(token) - ) - except get_user_model().DoesNotExist: - raise exceptions.AuthenticationFailed("Invalid or expired token.") + # If request does not contain an authorization header, return + # an anonymous user + if not auth_header: + return AnonymousUser(), None + + # Otherwise try to create a "Token" object from the request token + token = Token.from_access_token(self.get_token(request)) + # If token is None it is not valid, immediately return error + # avoid the database query + if not token: + raise AuthenticationFailed + + # Get the associated user by keycloak id + user = get_user_model().objects.get_by_keycloak_id(token.user_id) - return user, token - return AnonymousUser(), None + # Return the user and the associated access token + return user, token.access_token def authenticate_header(self, request): """ diff --git a/django_keycloak/backends.py b/django_keycloak/backends.py index 53b7f35..6a94d51 100644 --- a/django_keycloak/backends.py +++ b/django_keycloak/backends.py @@ -1,38 +1,67 @@ +""" +Module containing custom Django authentication backends. +""" +from typing import Optional, Union from django.contrib.auth.backends import RemoteUserBackend from django.contrib.auth import get_user_model -from django_keycloak.keycloak import Connect from django_keycloak.models import KeycloakUserAutoId +from django_keycloak import Token +from django_keycloak.models import KeycloakUser, KeycloakUserAutoId class KeycloakAuthenticationBackend(RemoteUserBackend): - def authenticate(self, request, username=None, password=None): - keycloak = Connect() - token = keycloak.get_token_from_credentials(username, password).get( - "access_token" - ) - User = get_user_model() - if not keycloak.is_token_active(token): + """ + Custom remote backend for Keycloak + """ + + def authenticate( + self, + request, + username: Optional[str] = None, + password: Optional[str] = None, + ): + """ + Authenticates an user by credentials, and + updates it's information (first name, last name, email). + If user does not exist it is created with appropriate permissions. + """ + + # Create token from the provided credentials and check if + # credentials were valid + token = Token.from_credentials(username, password) # type: ignore + + # Check for non-existing or unactive token + if (not token) or (not token.active): + # credentials were not valid return + + # Get the user model + User: Union[KeycloakUser, KeycloakUserAutoId] = get_user_model() # type: ignore + + # try to get user from database try: user = User.objects.get(username=username) if isinstance(user, KeycloakUserAutoId): - # Get user info based on token - user_info = keycloak.get_user_info(token) + # Get user information from token + user_info = token.user_info - # Get user info and update + # Update local user information based on Keycloak + # information from token user.first_name = user_info.get("given_name") user.last_name = user_info.get("family_name") user.email = user_info.get("email") + except User.DoesNotExist: - user = User.objects.create_user(username, password) - if keycloak.has_superuser_perm(token): - user.is_staff = True - user.is_superuser = True + # If user does not exist create in database + # `create_from_token` takes cares of password hashing + user = User.objects.create_from_token(username, password) + + if token.superuser: + user.is_staff = user.is_superuser = True else: - user.is_staff = False - user.is_superuser = False - user.save() + user.is_staff = user.is_superuser = False + user.save() return user def get_user(self, user_identifier): diff --git a/django_keycloak/config.py b/django_keycloak/config.py new file mode 100644 index 0000000..1ee138a --- /dev/null +++ b/django_keycloak/config.py @@ -0,0 +1,37 @@ +""" +Module to interact with django settings +""" +from django.conf import settings as django_settings + +# Get settings + +from dataclasses import dataclass, field +from typing import Optional, List + + +@dataclass +class Settings: + SERVER_URL: str + REALM: str + CLIENT_ID: str + CLIENT_SECRET_KEY: str + CLIENT_ADMIN_ROLE: str + REALM_ADMIN_ROLE: str + EXEMPT_URIS: Optional[List] = field(default_factory=[]) + INTERNAL_URL: Optional[str] = None + BASE_PATH: Optional[str] = "" + GRAPHQL_ENDPOINT: Optional[str] = "graphql/" + + def __init__(self, **vars) -> None: + + for key, value in vars.items(): + setattr(self, key, value) + + +__desired_variables = Settings.__annotations__.keys() +__defined_variables = getattr(django_settings, "KEYCLOAK_CONFIG", {}) +# Create a dict of the values of the settings defined in django +__values = {key: __defined_variables.get(key) for key in __desired_variables} + +# The exported settings object +settings = Settings(**__values) diff --git a/django_keycloak/connector.py b/django_keycloak/connector.py new file mode 100644 index 0000000..5894304 --- /dev/null +++ b/django_keycloak/connector.py @@ -0,0 +1,57 @@ +""" +Module to interact with Keycloak Admin API +""" +from keycloak.exceptions import KeycloakAuthenticationError, KeycloakGetError + + +from keycloak.keycloak_admin import KeycloakAdmin +from django_keycloak.errors import ( + KeycloakNoServiceAccountRolesError, + KeycloakMissingServiceAccountRolesError, +) + +from django_keycloak.config import settings + +# Variables for parent constructor +SERVER_URL = settings.SERVER_URL +INTERNAL_URL = settings.INTERNAL_URL +BASE_PATH = settings.BASE_PATH +REAL_NAME = settings.REALM +CLIENT_ID = settings.CLIENT_ID +CLIENT_SECRET_KEY = settings.CLIENT_SECRET_KEY + +# Decide URL (internal url overrides serverl url) +URL = INTERNAL_URL if INTERNAL_URL else SERVER_URL +# Add base path +URL += BASE_PATH + +try: + KeycloakAdminConnector = KeycloakAdmin( + server_url=URL, + client_id=CLIENT_ID, + realm_name=REAL_NAME, + client_secret_key=CLIENT_SECRET_KEY, + ) + + # # Check for 403 unknown error (missing required roles) + # except if "unknown_error" in str(error): + # raise KeycloakMissingServiceAccountRolesError(CLIENT_ID) +except KeycloakAuthenticationError as error: + # Check if the error is due to service account not being enabled + if "Client not enabled to retrieve service account" in str(error): + raise KeycloakNoServiceAccountRolesError(CLIENT_ID) + + # Otherwise re-throw the original error + else: + raise error + +# Try to call a users method +# if error occurs a required role is missing +# https://github.com/marcospereirampj/python-keycloak/issues/87 +try: + KeycloakAdminConnector.users_count() +except KeycloakGetError as error: + if "unknown_error" in str(error): + raise KeycloakMissingServiceAccountRolesError(CLIENT_ID) + else: + raise error diff --git a/django_keycloak/decorators.py b/django_keycloak/decorators.py deleted file mode 100644 index 421b025..0000000 --- a/django_keycloak/decorators.py +++ /dev/null @@ -1,20 +0,0 @@ -from requests import HTTPError - -from .errors import KeycloakAPIError - - -def keycloak_api_error_handler(f): - """ - Decorator to wrapp all keycloak api calls - NOTE: remember to call res.raise_for_status() after the request - """ - - def wrapper(*args, **kwargs): - try: - f(*args, **kwargs) - except HTTPError as err: - raise KeycloakAPIError( - status=err.response.status_code, message=err.response.json() - ) - - return wrapper diff --git a/django_keycloak/enums.py b/django_keycloak/enums.py deleted file mode 100644 index 3028bef..0000000 --- a/django_keycloak/enums.py +++ /dev/null @@ -1,5 +0,0 @@ -USER_ACTION__VERIFY_EMAIL = "VERIFY_EMAIL" -USER_ACTION__UPDATE_PROFILE = "UPDATE_PROFILE" -USER_ACTION__CONFIGURE_TOTP = "CONFIGURE_TOTP" -USER_ACTION__UPDATE_PASSWORD = "UPDATE_PASSWORD" -USER_ACTION__TERMS_AND_CONDITIONS = "TERMS_AND_CONDITIONS" diff --git a/django_keycloak/errors.py b/django_keycloak/errors.py index 5f636e6..cf53dfd 100644 --- a/django_keycloak/errors.py +++ b/django_keycloak/errors.py @@ -6,3 +6,47 @@ class KeycloakAPIError(Exception): def __init__(self, status, message): self.status = status self.message = message + + +class KeycloakNoServiceAccountRolesError(Exception): + """ + Raised when the Keycloak server is not configured for + "Service account roles" for a particular client. + """ + + def __init__(self, keycloak_client: str): + super().__init__( + ( + "'Service account roles' setting not enabled. " + f"Please enable this authentication workflow for client '{keycloak_client}'." + ) + ) + + +class KeycloakMissingServiceAccountRolesError(Exception): + """ + Raised when the Keycloak server has service account roles enabled, + but a necessary role is missing. + """ + + def __init__(self, keycloak_client: str): + super().__init__( + ( + "'Service account roles' setting is enabled, " + "but role 'manage-users' is missing. " + f"To enable it go to '{keycloak_client}' client --> Service accounts roles " + " --> Assign role --> Filter by clients --> and add 'manage-users'." + ) + ) + + +class KeycloakMissingSettingError(Exception): + """ + When a setting is missing + """ + + def __init__(self, setting_name): + self.setting_name = setting_name + super().__init__( + f"Could not find setting '{self.setting_name}' in 'KEYCLOAK_CONFIG' settings." + ) diff --git a/django_keycloak/keycloak.py b/django_keycloak/keycloak.py deleted file mode 100644 index 0c2dc74..0000000 --- a/django_keycloak/keycloak.py +++ /dev/null @@ -1,432 +0,0 @@ -from datetime import datetime, timezone -from functools import cached_property -from typing import Dict, List, Optional, Union -from urllib.parse import urlparse - -import jwt -import requests -from django.conf import settings -from jwt import PyJWKClient -from jwt.exceptions import InvalidTokenError -from cachetools.func import ttl_cache - -from .decorators import keycloak_api_error_handler -from .urls import ( - BASE_PATH, - KEYCLOAK_CREATE_USER, - KEYCLOAK_DELETE_USER, - KEYCLOAK_GET_TOKEN, - KEYCLOAK_GET_USER_BY_ID, - KEYCLOAK_GET_USER_CLIENT_ROLES_BY_ID, - KEYCLOAK_GET_USERS, - KEYCLOAK_INTROSPECT_TOKEN, - KEYCLOAK_OPENID_CONFIG, - KEYCLOAK_UPDATE_USER, - KEYCLOAK_USER_INFO, -) - - -class Connect: - """ - Keycloak connection and methods - """ - - def __init__( - self, - server_url=None, - base_path=None, - realm=None, - client_id=None, - client_uuid=None, - client_secret_key=None, - internal_url=None, - ): - # Load configuration from settings + args - try: - self.config = settings.KEYCLOAK_CONFIG - except AttributeError: - raise Exception("Missing KEYCLOAK_CONFIG on settings file.") - try: - self.server_url = server_url or self.config.get("SERVER_URL") - self.base_path = base_path or self.config.get("BASE_PATH", BASE_PATH) - self.realm = realm or self.config.get("REALM") - self.client_uuid = client_uuid or self.config.get("CLIENT_UUID") - self.client_id = client_id or self.config.get("CLIENT_ID") - self.client_secret_key = client_secret_key or self.config.get( - "CLIENT_SECRET_KEY" - ) - self.internal_url = internal_url or self.config.get("INTERNAL_URL") - self.client_admin_role = self.config.get("CLIENT_ADMIN_ROLE", "admin") - self.realm_admin_role = self.config.get("REALM_ADMIN_ROLE", "admin") - self.graphql_endpoint = self.config.get("GRAPHQL_ENDPOINT", None) - self.exempt_uris = self.config.get("EXEMPT_URIS", []) - # Flag if the token should be introspected or decoded - self.enable_token_decoding = self.config.get("DECODE_TOKEN", False) - # The percentage of a tokens valid duration until a new token is requested - self.token_timeout_factor = self.config.get("TOKEN_TIMEOUT_FACTOR", 0.9) - # A token belonging to a user and the respective introspection - self.cached_token = None - self.cached_introspect = None - # A token belonging to the client to perform user management - self._client_token = None - # Audiences for the different JWT uses - self.client_jwt_audience = self.config.get( - "CLIENT_JWT_AUDIENCE", "realm-management" - ) - self.user_jwt_audience = self.config.get("USER_JWT_AUDIENCE", "account") - - except KeyError as err: - raise Exception("KEYCLOAK configuration is not defined.") from err - - if not self.server_url: - raise Exception("SERVER_URL is not defined.") - - if not self.realm: - raise Exception("REALM is not defined.") - - if not self.client_id: - raise Exception("CLIENT_ID is not defined.") - - if not self.client_secret_key: - raise Exception("CLIENT_SECRET_KEY is not defined.") - - @cached_property - def openid_config(self): - """Collects the identity provider's configuration""" - return self._get_openid_config() - - @cached_property - def jwks_client(self): - """Collects the identity provider's public keys""" - return PyJWKClient(self.openid_config["jwks_uri"]) - - def introspect(self, token): - """ - @param token: request token - @return: introspected token - """ - if self.cached_introspect and self.cached_token == token: - return self.cached_introspect - - payload = { - "token": token, - "client_id": self.client_id, - "grant_type": "client_credentials", - "client_secret": self.client_secret_key, - } - server_url, headers = self._make_form_request_config() - - response = requests.request( - "POST", - KEYCLOAK_INTROSPECT_TOKEN.format(server_url, self.realm), - data=payload, - headers=headers, - ) - self.cached_token = token - self.cached_introspect = response.json() - return self.cached_introspect - - def get_token_from_credentials(self, username, password): - """ - Get Token for a user from credentials - """ - payload = { - "grant_type": "password", - "client_id": self.client_id, - "client_secret": self.client_secret_key, - "username": username, - "password": password, - "scope": "openid", - } - server_url, headers = self._make_form_request_config() - - response = requests.request( - "POST", - KEYCLOAK_GET_TOKEN.format(server_url, self.realm), - data=payload, - headers=headers, - ) - return response.json() - - def refresh_token_from_credentials(self, refresh_token): - """ - Refresh token - """ - payload = { - "grant_type": "refresh_token", - "client_id": self.client_id, - "client_secret": self.client_secret_key, - "refresh_token": refresh_token, - } - server_url, headers = self._make_form_request_config() - - response = requests.request( - "POST", - KEYCLOAK_GET_TOKEN.format(server_url, self.realm), - data=payload, - headers=headers, - ) - return response.json() - - def get_token(self): - """ - Get a client token based on client credentials - @return: - """ - - if not self._is_client_token_timed_out(): - return self._client_token - - payload = { - "grant_type": "client_credentials", - "client_id": self.client_id, - "client_secret": self.client_secret_key, - } - server_url, headers = self._make_form_request_config() - - response = requests.request( - "POST", - KEYCLOAK_GET_TOKEN.format(server_url, self.realm), - data=payload, - headers=headers, - ) - self._client_token = response.json().get("access_token") - return self._client_token - - def get_users(self, **params): - """ - Get users for realm - @return: - """ - server_url, headers = self._make_secure_json_request_config() - - response = requests.request( - "GET", - KEYCLOAK_GET_USERS.format(server_url, self.realm), - headers=headers, - params=params, - ) - return response.json() - - def get_user_info(self, token): - """ - Get user information token - """ - if self.enable_token_decoding: - return self._decode_token(token, self.user_jwt_audience) - - server_url, headers = self._make_secure_request_config(token) - response = requests.request( - "GET", - KEYCLOAK_USER_INFO.format(server_url, self.realm), - headers=headers, - ) - return response.json() - - def get_user_id(self, token): - """ - Verify if introspect token is active. - """ - if self.enable_token_decoding: - token_data = self._decode_token(token, self.user_jwt_audience) - else: - token_data = self.introspect(token) - return token_data.get("sub", None) - - def is_token_active(self, token): - """ - Verify if introspect token is active. - """ - if self.enable_token_decoding: - return bool(self._decode_token(token, audience=self.user_jwt_audience)) - introspect_token = self.introspect(token) - return introspect_token.get("active", False) - - def client_roles(self, token): - """ - Get client roles from token - """ - if self.enable_token_decoding: - token_data = self._decode_token(token, self.user_jwt_audience) - else: - token_data = self.introspect(token) - client_resources = token_data.get("resource_access").get(self.client_id) - return client_resources.get("roles", []) if client_resources else [] - - def realm_roles(self, token): - """ - Get realm roles from token - """ - if self.enable_token_decoding: - token_data = self._decode_token(token, self.user_jwt_audience) - else: - token_data = self.introspect(token) - return token_data.get("realm_access").get("roles", None) - - def client_scope(self, token): - """ - Get client scope from token - """ - return self.introspect(token).get("scope").split(" ") - - def has_superuser_perm(self, token): - """ - Check if token belongs to a user with superuser permissions - """ - if self.client_admin_role in self.client_roles(token): - return True - if self.realm_admin_role in self.realm_roles(token): - return True - return False - - def get_user_info_by_id(self, user_id): - """ - Get user info from the id - """ - server_url, headers = self._make_secure_json_request_config() - - response = requests.request( - "GET", - KEYCLOAK_GET_USER_BY_ID.format(server_url, self.realm, user_id), - headers=headers, - ) - return response.json() - - def get_user_client_roles_by_id(self, user_id): - """ - Get user client roles from the id - """ - - server_url, headers = self._make_secure_json_request_config() - - response = requests.request( - "GET", - KEYCLOAK_GET_USER_CLIENT_ROLES_BY_ID.format( - server_url, self.realm, user_id, self.client_uuid - ), - headers=headers, - ) - return response.json() - - @keycloak_api_error_handler - def update_user(self, user_id, **values): - """ - Update user with values - """ - server_url, headers = self._make_secure_json_request_config() - - current_user = self.get_user_info_by_id(user_id) - data = { - **current_user, - **values, - } - - url = KEYCLOAK_UPDATE_USER.format(server_url, self.realm, user_id) - res = requests.put(url, headers=headers, json=data) - res.raise_for_status() - return data - - @keycloak_api_error_handler - def create_user(self, **values): - server_url, headers = self._make_secure_json_request_config() - - url = KEYCLOAK_CREATE_USER.format(server_url, self.realm) - res = requests.post(url, headers=headers, json=values) - res.raise_for_status() - return res.headers["Location"].split("/")[-1] - - @keycloak_api_error_handler - def delete_user(self, user_id): - server_url, headers = self._make_secure_json_request_config() - - url = KEYCLOAK_DELETE_USER.format(server_url, self.realm, user_id) - res = requests.delete(url, headers=headers) - res.raise_for_status() - - def _is_client_token_timed_out(self): - """ - Checks if the cached token is timed out as given by the timeout factor - - If there is no cached token, returns true. Timeout is true if the current time is - later than (expiration time - issue time) * timeout factor + issue time. - """ - if not self._client_token: - return True - - # Get the public key from the identity provider, i.e., the keycloak server - decoded = self._decode_token(self._client_token, self.client_jwt_audience) - if not decoded: - return True - exp = datetime.fromtimestamp(decoded["exp"], tz=timezone.utc) - iat = datetime.fromtimestamp(decoded["iat"], tz=timezone.utc) - now = datetime.now(tz=timezone.utc) - - is_timed_out = (exp - iat) * self.token_timeout_factor + iat < now - return is_timed_out - - def _get_openid_config(self): - server_url, headers = self._make_json_request_config() - - response = requests.request( - "GET", - KEYCLOAK_OPENID_CONFIG.format(server_url, self.realm), - headers=headers, - ) - return response.json() - - def _make_secure_request_config(self, token): - server_url, headers = self._make_request_config() - if not token: - token = self.get_token() - headers["Authorization"] = f"Bearer {token}" - return [server_url, headers] - - def _make_secure_json_request_config(self, token=None): - server_url, headers = self._make_request_config() - if not token: - token = self.get_token() - headers["Content-Type"] = "application/json" - headers["Authorization"] = f"Bearer {token}" - return [server_url, headers] - - def _make_form_request_config(self): - server_url, headers = self._make_request_config() - headers["Content-Type"] = "application/x-www-form-urlencoded" - return [server_url, headers] - - def _make_json_request_config(self): - server_url, headers = self._make_request_config() - headers["Content-Type"] = "application/json" - return [server_url, headers] - - def _make_request_config(self): - server_url = f"{self.server_url}{self.base_path}" - headers = {} - if self.internal_url: - server_url = f"{self.internal_url}{self.base_path}" - headers["HOST"] = urlparse(self.server_url).netloc - return [server_url, headers] - - @ttl_cache(maxsize=128, ttl=60) - def _decode_token( - self, token: str, audience: Union[str, List[str]], raise_error=False - ) -> Optional[Dict]: - """ - Attempts to decode the token. Returns a Dict on success or none on failure - """ - try: - # Get the public key from the identity provider, i.e., the keycloak server - public_key = self.jwks_client.get_signing_key_from_jwt(token).key - # Decode and verify the JWT with the server's public key specified in the - # JWT's header, the required audience and the allowed crypto algorithms it - # published - return jwt.decode( - token, - public_key, - audience=audience, - algorithms=self.openid_config["id_token_signing_alg_values_supported"], - ) - except InvalidTokenError as err: - if raise_error: - raise err - return None diff --git a/django_keycloak/management/commands/sync_keycloak_users.py b/django_keycloak/management/commands/sync_keycloak_users.py index 1a39e9d..64ccd75 100644 --- a/django_keycloak/management/commands/sync_keycloak_users.py +++ b/django_keycloak/management/commands/sync_keycloak_users.py @@ -3,17 +3,19 @@ from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model -from django_keycloak.keycloak import Connect +from django_keycloak.connector import KeycloakAdminConnector class Command(BaseCommand): help = "Synchronize users with keycloak" def handle(self, *args, **options): - keycloak = Connect() + User = get_user_model() - remote_users = set([user.get("id") for user in keycloak.get_users()]) + remote_users = set( + [user.get("id") for user in KeycloakAdminConnector.get_users()] + ) local_users = set(str(_u.id) for _u in User.objects.all()) users_to_remove = local_users.difference(remote_users) diff --git a/django_keycloak/managers.py b/django_keycloak/managers.py index ee6747c..7e73683 100644 --- a/django_keycloak/managers.py +++ b/django_keycloak/managers.py @@ -1,90 +1,31 @@ +""" +Module containing custom object managers +""" from django.contrib.auth.models import UserManager -from django.utils import timezone - -from django_keycloak.keycloak import Connect +from django_keycloak import Token class KeycloakUserManager(UserManager): - def __init__(self, *args, **kwargs): - self.keycloak = Connect() - super().__init__(*args, **kwargs) - - def _create_user_on_keycloak( - self, - username, - email, - password=None, - first_name=None, - last_name=None, - enabled=True, - actions=None, - ): - """Creates user on keycloak server, No state is changed on local db""" - keycloak = Connect() - values = {"username": username, "email": email, "enabled": enabled} - if password is not None: - values["credentials"] = [ - {"type": "password", "value": password, "temporary": False} - ] - if first_name is not None: - values["firstName"] = first_name - if last_name is not None: - values["lastName"] = last_name - if actions is not None: - values["requiredActions"] = actions - keycloak.create_user(**values) - return keycloak.get_users(username=username)[0] - - def create_user(self, username, password=None, **kwargs): + def create_from_token(self, token: Token, **kwargs): """ - Creates a local user if the user exists on keycloak + Create a new local database user from a valid token. """ - token = self.keycloak.get_token_from_credentials(username, password).get( - "access_token" - ) - if token is None: - raise ValueError("Wrong credentials") - user = self.create_from_token(token, password) - return user - def create_superuser(self, username, password=None, **kwargs): - """ - Creates a local super user if the user exists on keycloak and is superuser - """ - token = self.keycloak.get_token_from_credentials(username, password).get( - "access_token" - ) - if token is None: - raise ValueError("Wrong credentials") - if not self.keycloak.has_superuser_perm(token): - raise ValueError("You are not an administrator") - - user = self.create_from_token(token, password) - user.save(using=self._db) - return user - - def create_from_token(self, token, password=None, **kwargs): - """ - Create a new user from a valid token - """ - if not self.keycloak.is_token_active(token): - raise ValueError("Invalid token") - - user_info = self.keycloak.get_user_info(token) + # Get user info from token + user_info = token.user_info # set admin permissions if user is admin - is_staff = False - is_superuser = False - if self.keycloak.has_superuser_perm(token): - is_staff = True - is_superuser = True + if token.superuser: + is_staff = is_superuser = True + else: + is_staff = is_superuser = False + # Create the django user from the token information user = self.model( id=user_info.get("sub"), username=user_info.get("preferred_username"), is_staff=is_staff, is_superuser=is_superuser, - date_joined=timezone.now(), **kwargs ) user.save(using=self._db) @@ -93,31 +34,22 @@ def create_from_token(self, token, password=None, **kwargs): def get_by_keycloak_id(self, keycloak_id): return self.get(id=keycloak_id) - def create_keycloak_user(self, *args, **kwargs): - keycloak_user = self._create_user_on_keycloak(*args, **kwargs) - return self.create( - id=keycloak_user.get("id"), - username=keycloak_user.get("username"), - ) - class KeycloakUserManagerAutoId(KeycloakUserManager): - def create_from_token(self, token, password=None, **kwargs): + def create_from_token(self, token: Token, **kwargs): """ - Create a new user from a valid token + Create a local new user from a valid token """ - if not self.keycloak.is_token_active(token): - raise ValueError("Invalid token") - user_info = self.keycloak.get_user_info(token) + user_info = token.user_info # set admin permissions if user is admin - is_staff = False - is_superuser = False - if self.keycloak.has_superuser_perm(token): - is_staff = True - is_superuser = True + if token.superuser: + is_staff = is_superuser = True + else: + is_staff = is_superuser = False + # Create the django user from the token information user = self.model( keycloak_id=user_info.get("sub"), username=user_info.get("preferred_username"), @@ -126,18 +58,13 @@ def create_from_token(self, token, password=None, **kwargs): email=user_info.get("email"), is_staff=is_staff, is_superuser=is_superuser, - date_joined=timezone.now(), **kwargs ) user.save(using=self._db) return user def get_by_keycloak_id(self, keycloak_id): + """ + Returns a local user by keycloak id + """ return self.get(keycloak_id=keycloak_id) - - def create_keycloak_user(self, *args, **kwargs): - keycloak_user = self._create_user_on_keycloak(*args, **kwargs) - return self.create( - username=keycloak_user.get("username"), - keycloak_id=keycloak_user.get("id"), - ) diff --git a/django_keycloak/middleware.py b/django_keycloak/middleware.py index 26980e6..d652d69 100644 --- a/django_keycloak/middleware.py +++ b/django_keycloak/middleware.py @@ -1,26 +1,38 @@ +""" +Module containing custom middleware to authenticate, create and +sync user information between keycloak and local database. +""" import logging import re - +from typing import Union from django.contrib.auth import get_user_model -from django.http.response import JsonResponse +from django.http import JsonResponse from django.utils.deprecation import MiddlewareMixin +from django_keycloak.models import KeycloakUserAutoId, KeycloakUser +from django_keycloak import Token +from django_keycloak.config import settings -from django_keycloak.keycloak import Connect -from django_keycloak.models import KeycloakUserAutoId +# Create a reusable no permission JSON response with 401 status code +NO_PERMISSION = lambda: JsonResponse( + {"detail": "Invalid credentials provided to perform this action."}, + status=401, +) class KeycloakMiddlewareMixin: - def append_user_info_to_request(self, request, token): + def append_user_info_to_request(self, request, token: Token): """Appends user info to the request""" if hasattr(request, "remote_user"): return request - user_info = self.keycloak.get_user_info(token) + user_info = token.user_info + + # add the remote user to request request.remote_user = { - "client_roles": self.keycloak.client_roles(token), - "realm_roles": self.keycloak.realm_roles(token), - "client_scope": self.keycloak.client_scope(token), + "client_roles": token.client_roles, + "realm_roles": token.realm_roles, + "client_scope": token.client_scopes, "name": user_info.get("name"), "given_name": user_info.get("given_name"), "family_name": user_info.get("family_name"), @@ -29,11 +41,12 @@ def append_user_info_to_request(self, request, token): "email_verified": user_info.get("email_verified"), } + # Get the user model + User: Union[KeycloakUser, KeycloakUserAutoId] = get_user_model() # type: ignore + # Create or update user info try: - user = get_user_model().objects.get_by_keycloak_id( - self.keycloak.get_user_id(token) - ) + user = User.objects.get_by_keycloak_id(token.user_id) # Only KeycloakUserAutoId stores the user details locally if isinstance(user, KeycloakUserAutoId): @@ -42,8 +55,10 @@ def append_user_info_to_request(self, request, token): user.email = user_info.get("email") user.save() - except get_user_model().DoesNotExist: - user = get_user_model().objects.create_from_token(token) + except User.DoesNotExist: + user = User.objects.create_from_token(token) + + # Add the local user to request request.user = user return request @@ -57,9 +72,9 @@ def is_auth_header_missing(request): def get_token(request): """Get the token from the HTTP request""" auth_header = request.META.get("HTTP_AUTHORIZATION").split() - if len(auth_header) == 2: + if len(auth_header) > 1: return auth_header[1] - return None + return auth_header[0] class KeycloakGrapheneMiddleware(KeycloakMiddlewareMixin): @@ -71,7 +86,6 @@ def __init__(self): logging.warning( "All functionality is provided by KeycloakMiddleware", DeprecationWarning, 2 ) - self.keycloak = Connect() def resolve(self, next, root, info, **kwargs): """ @@ -80,17 +94,17 @@ def resolve(self, next, root, info, **kwargs): request = info.context if self.is_auth_header_missing(request): - """Append anonymous user and continue""" + # Append anonymous user and continue return next(root, info, **kwargs) - token = self.get_token(request) - if token is None: - raise Exception("Invalid token structure. Must be 'Bearer '") + # Build token from request + token = Token.from_access_token(self.get_token(request)) - if not self.keycloak.is_token_active(token): - raise Exception("Invalid or expired token.") + # Check if Token was created + if not token: + return NO_PERMISSION() - info.context = self.append_user_info_to_request(request, token) + info.context = self.append_user_info_to_request(request, token.access_token) return next(root, info, **kwargs) @@ -104,7 +118,6 @@ class KeycloakMiddleware(KeycloakMiddlewareMixin, MiddlewareMixin): async_capable = False def __init__(self, get_response): - self.keycloak = Connect() # Django response self.get_response = get_response @@ -113,24 +126,28 @@ def __call__(self, request): """ To be executed before the view each request """ - # Skip auth for gql endpoint (it is done in KeycloakGrapheneMiddleware) - if self.is_graphql_endpoint(request): + # Skip auth in the following cases: + # 1. It is a graphql endpoint (handled by KeycloakGrapheneMiddleware) + # 2. It is a URL in "EXEMPT_URIS" + # 3. Request does not contain authorization header + # Also skip auth for "EXEMPT_URIS" defined in configs + if ( + self.is_graphql_endpoint(request) + or self.pass_auth(request) + or self.is_auth_header_missing(request) + ): return self.get_response(request) - if not self.is_auth_header_missing(request): - token = self.get_token(request) - if token is None: - return JsonResponse( - {"detail": "Invalid token structure. Must be 'Bearer '"}, - status=401, - ) - - if not self.keycloak.is_token_active(token): - return JsonResponse( - {"detail": "Invalid or expired token."}, - status=401, - ) - request = self.append_user_info_to_request(request, token) + # Otherwise validate the token + token = Token.from_access_token(self.get_token(request)) + + # If token is None, access token was not valid + if not token: + return NO_PERMISSION() + + # Add user info to request for a valid token + request = self.append_user_info_to_request(request, token) + return self.get_response(request) def pass_auth(self, request): @@ -138,17 +155,20 @@ def pass_auth(self, request): Check if the current URI path needs to skip authorization """ path = request.path_info.lstrip("/") - return any(re.match(m, path) for m in self.keycloak.exempt_uris) + exempt_uris = settings.EXEMPT_URIS + + return any(re.match(m, path) for m in exempt_uris) def is_graphql_endpoint(self, request): """ Check if the request path belongs to a graphql endpoint """ - if self.keycloak.graphql_endpoint is None: + graphql_endpoint = settings.GRAPHQL_ENDPOINT + if graphql_endpoint is None: return False path = request.path_info.lstrip("/") - is_graphql_endpoint = re.match(self.keycloak.graphql_endpoint, path) + is_graphql_endpoint = re.match(graphql_endpoint, path) if is_graphql_endpoint and request.method != "GET": return True diff --git a/django_keycloak/mixins.py b/django_keycloak/mixins.py index 28b6ce9..4fa9322 100644 --- a/django_keycloak/mixins.py +++ b/django_keycloak/mixins.py @@ -1,5 +1,5 @@ import logging -from django_keycloak.keycloak import Connect +from django_keycloak.connector import KeycloakAdminConnector class KeycloakTestMixin: @@ -33,11 +33,12 @@ def tearDown(self): # pylint: disable=invalid-name self.keycloak_cleanup() def keycloak_init(self): - self.keycloak = Connect() - self._start_users = {user.get("id") for user in self.keycloak.get_users()} + self._start_users = { + user.get("id") for user in KeycloakAdminConnector.get_users() + } def keycloak_cleanup(self): - new_users = {user.get("id") for user in self.keycloak.get_users()} + new_users = {user.get("id") for user in KeycloakAdminConnector.get_users()} users_to_remove = new_users.difference(self._start_users) for user_id in users_to_remove: - self.keycloak.delete_user(user_id) + KeycloakAdminConnector.delete_user(user_id) diff --git a/django_keycloak/models.py b/django_keycloak/models.py index e568e85..2483992 100644 --- a/django_keycloak/models.py +++ b/django_keycloak/models.py @@ -4,11 +4,15 @@ from django.utils.translation import gettext_lazy as _ from dry_rest_permissions.generics import authenticated_users -from django_keycloak.keycloak import Connect from .managers import KeycloakUserManager, KeycloakUserManagerAutoId +from .connector import KeycloakAdminConnector class AbstractKeycloakUser(AbstractBaseUser, PermissionsMixin): + """ + Abstract Keycloak user. + """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._cached_user_info = None @@ -36,7 +40,7 @@ class Meta(AbstractBaseUser.Meta): abstract = True def update_keycloak(self, email=None, first_name=None, last_name=None): - keycloak = Connect() + values = {} if email is not None: values["email"] = email @@ -44,11 +48,10 @@ def update_keycloak(self, email=None, first_name=None, last_name=None): values["firstName"] = first_name if last_name is not None: values["lastName"] = last_name - return keycloak.update_user(self.keycloak_identifier, **values) + return KeycloakAdminConnector.update_user(self.keycloak_identifier, **values) def delete_keycloak(self): - keycloak = Connect() - keycloak.delete_user(self.keycloak_identifier) + KeycloakAdminConnector.delete_user(self.keycloak_identifier) class KeycloakUser(AbstractKeycloakUser): @@ -74,8 +77,7 @@ def last_name(self): def _confirm_cache(self): if not self._cached_user_info: - keycloak = Connect() - self._cached_user_info = keycloak.get_user_info_by_id(self.id) + self._cached_user_info = KeycloakAdminConnector.get_user(self.id) class AbstractKeycloakUserAutoId(AbstractKeycloakUser): @@ -101,11 +103,6 @@ class AbstractKeycloakUserAutoId(AbstractKeycloakUser): def keycloak_identifier(self): return self.keycloak_id - def _confirm_cache(self): - if not self._cached_user_info: - keycloak = Connect() - self._cached_user_info = keycloak.get_user_info_by_id(self.keycloak_id) - class Meta: abstract = True diff --git a/django_keycloak/tests.py b/django_keycloak/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/django_keycloak/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/django_keycloak/token.py b/django_keycloak/token.py new file mode 100644 index 0000000..f1fa468 --- /dev/null +++ b/django_keycloak/token.py @@ -0,0 +1,178 @@ +""" +Module to interact with the Keycloak token API +""" +from __future__ import annotations +from typing import Any, Callable, TypeVar, cast, Optional +from dataclasses import dataclass +from keycloak.exceptions import KeycloakAuthenticationError, KeycloakPostError +from keycloak.keycloak_openid import KeycloakOpenID +from django_keycloak.config import settings + +F = TypeVar("F", bound=Callable[..., Any]) + + +def with_active_token_property(f: F) -> F: + @property + def wrapper(self, *args, **kwargs): + # Only allow the method if token is active + if not self.active: + return None + return f(self, *args, **kwargs) + + return cast(F, wrapper) + + +# Variables for parent constructor +SERVER_URL = settings.SERVER_URL +INTERNAL_URL = settings.INTERNAL_URL +BASE_PATH = settings.BASE_PATH +REAL_NAME = settings.REALM +CLIENT_ID = settings.CLIENT_ID +CLIENT_SECRET_KEY = settings.CLIENT_SECRET_KEY + +# Decide URL (internal url overrides serverl url) +URL = INTERNAL_URL if INTERNAL_URL else SERVER_URL +# Add base path +URL += BASE_PATH + +# Define keycloak openid instance +KEYCLOAK = KeycloakOpenID( + server_url=URL, + client_id=CLIENT_ID, + realm_name=REAL_NAME, + client_secret_key=CLIENT_SECRET_KEY, +) + + +@dataclass +class Token: + access_token: Optional[str] = None + refresh_token: Optional[str] = None + expires_in: Optional[int] = None + refresh_expires_in: Optional[int] = None + + # Helpers methods + + @staticmethod + def _parse_keycloak_response(keycloak_response: dict) -> dict: + """ + Builds a dictionary mapping internal values to keycloak API + values + """ + return { + "access_token": keycloak_response["access_token"], + "refresh_token": keycloak_response["refresh_token"], + "expires_in": keycloak_response["expires_in"], + "refresh_expires_in": keycloak_response["refresh_expires_in"], + } + + # Properties + + @property + def active(self): + """ + Returns a boolean indicating if the current access token is active or not. + """ + return KEYCLOAK.introspect(self.access_token).get("active", False) + + @with_active_token_property + def user_info(self) -> dict: + """ + Returns the user information contained on the provided access token. + """ + return KEYCLOAK.userinfo(self.access_token) + + @with_active_token_property + def user_id(self) -> str: + """ + Returns the Keycloak user id + """ + return self.user_info.get("sub") # type: ignore + + @with_active_token_property + def superuser(self) -> bool: + """ + Returns a boolean indicating if the user has admin + permissions + """ + if (settings.CLIENT_ADMIN_ROLE in self.client_roles) or ( # type: ignore + settings.REALM_ADMIN_ROLE in self.realm_roles + ): # type: ignore + return True + + return False + + @with_active_token_property + def client_roles(self) -> list: + """ + Returns the client roles based on the provided access token. + """ + return ( + KEYCLOAK.introspect(self.access_token) + .get("resource_access") + .get(settings.CLIENT_ID) + .get("roles", []) + ) + + @with_active_token_property + def realm_roles(self) -> list: + """ + Returns the realm roles based on the access token. + """ + return ( + KEYCLOAK.introspect(self.access_token) + .get("realm_access", {}) + .get("roles", []) + ) + + @with_active_token_property + def client_scopes(self) -> list: + """ + Returns the client scope based on the access token. + """ + return KEYCLOAK.introspect(self.access_token).get("scope", "").split(" ") + + # Methods + + @classmethod + def from_credentials(cls, username: str, password: str) -> Optional[Token]: # type: ignore + """ + Creates a `Token` object from a set of user credentials. + Returns `None` if authentication fails. + """ + try: + keycloak_response = KEYCLOAK.token(username, password) + return cls(**cls._parse_keycloak_response(keycloak_response)) + # Catch authentication error (invalid credentials), + # and post error (account not completed.) + except (KeycloakAuthenticationError, KeycloakPostError): + return None + + @classmethod + def from_access_token(cls, access_token: str) -> Optional[Token]: + """ + Creates a `Token` object from an existing access token. + Returns `None` if token is not active. + """ + instance = cls(access_token=access_token) + return instance if instance.active else None + + @classmethod + def from_refresh_token(cls, refresh_token: str) -> Optional[Token]: + """ + Creates a `Token` from the provided refresh token. + """ + instance = cls(refresh_token=refresh_token) + instance.refresh() + return instance + + def refresh(self) -> None: + """ + Refreshes the `access_token` with `refresh_token`. + """ + if self.refresh_token: + mapping = self._parse_keycloak_response( + KEYCLOAK.refresh_token(self.refresh_token) + ) + for key, value in mapping.items(): + setattr(self, key, value) diff --git a/django_keycloak/urls.py b/django_keycloak/urls.py deleted file mode 100644 index 253a3c7..0000000 --- a/django_keycloak/urls.py +++ /dev/null @@ -1,23 +0,0 @@ -# KEYCLOAK SERVER -BASE_PATH = "/auth" - -# REST API -KEYCLOAK_INTROSPECT_TOKEN = "{}/realms/{}/protocol/openid-connect/token/introspect" -KEYCLOAK_USER_INFO = "{}/realms/{}/protocol/openid-connect/userinfo" -KEYCLOAK_GET_USERS = "{}/admin/realms/{}/users" -KEYCLOAK_GET_TOKEN = "{}/realms/{}/protocol/openid-connect/token" -KEYCLOAK_GET_USER_BY_ID = "{}/admin/realms/{}/users/{}" -KEYCLOAK_GET_USER_CLIENT_ROLES_BY_ID = ( - "{}/admin/realms/{}/users/{}/role-mappings/clients/{}" -) -KEYCLOAK_UPDATE_USER = "{}/admin/realms/{}/users/{}" -KEYCLOAK_CREATE_USER = "{}/admin/realms/{}/users" -KEYCLOAK_SEND_ACTIONS_EMAIL = "{}/admin/realms/{}/users/{}/execute-actions-email" -KEYCLOAK_DELETE_USER = "{}/admin/realms/{}/users/{}" -KEYCLOAK_OPENID_CONFIG = "{}/realms/{}/.well-known/openid-configuration" - - -# ADMIN CONSOLE -KEYCLOAK_ADMIN_USER_PAGE = ( - "{host}/auth/admin/master/console/#/realms/{realm}/users/{id}" -) diff --git a/setup.cfg b/setup.cfg index 7a389e4..652ac56 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,5 @@ install_requires = Django >= "2.2" djangorestframework >= "3.0" dry-rest-permissions >= "0.1" - PyJWT >= "2.3" - requests >= "2.0" - cachetools >= "5.0" + python-keycloak == 2.6.0 From b56ee031abe547eb65b74df61e689ea142997d93 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Sun, 23 Oct 2022 19:22:17 +0100 Subject: [PATCH 02/73] Make middleware async capable --- django_keycloak/config.py | 5 ++--- django_keycloak/middleware.py | 26 +++++++++----------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/django_keycloak/config.py b/django_keycloak/config.py index 1ee138a..acee3f9 100644 --- a/django_keycloak/config.py +++ b/django_keycloak/config.py @@ -1,13 +1,12 @@ """ Module to interact with django settings """ +from dataclasses import dataclass, field +from typing import Optional, List from django.conf import settings as django_settings # Get settings -from dataclasses import dataclass, field -from typing import Optional, List - @dataclass class Settings: diff --git a/django_keycloak/middleware.py b/django_keycloak/middleware.py index d652d69..3fe87c7 100644 --- a/django_keycloak/middleware.py +++ b/django_keycloak/middleware.py @@ -21,8 +21,10 @@ class KeycloakMiddlewareMixin: def append_user_info_to_request(self, request, token: Token): - """Appends user info to the request""" - + """ + Appends user info to the request + """ + # Check if already appended in a previous request if hasattr(request, "remote_user"): return request @@ -114,17 +116,9 @@ class KeycloakMiddleware(KeycloakMiddlewareMixin, MiddlewareMixin): Middleware to validate Keycloak access based on REST validations """ - sync_capable = True - async_capable = False - - def __init__(self, get_response): - - # Django response - self.get_response = get_response - - def __call__(self, request): + def process_request(self, request): """ - To be executed before the view each request + To be executed before the view each request. """ # Skip auth in the following cases: # 1. It is a graphql endpoint (handled by KeycloakGrapheneMiddleware) @@ -136,19 +130,17 @@ def __call__(self, request): or self.pass_auth(request) or self.is_auth_header_missing(request) ): - return self.get_response(request) + return # Otherwise validate the token - token = Token.from_access_token(self.get_token(request)) + token = Token.from_access_token(KeycloakMiddleware.get_token(request)) # If token is None, access token was not valid if not token: return NO_PERMISSION() # Add user info to request for a valid token - request = self.append_user_info_to_request(request, token) - - return self.get_response(request) + self.append_user_info_to_request(request, token) def pass_auth(self, request): """ From f806de702484bb45bacf024f378001b35c064f09 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Sun, 23 Oct 2022 19:30:45 +0100 Subject: [PATCH 03/73] Add missing return --- django_keycloak/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_keycloak/middleware.py b/django_keycloak/middleware.py index 3fe87c7..e7be07c 100644 --- a/django_keycloak/middleware.py +++ b/django_keycloak/middleware.py @@ -140,7 +140,7 @@ def process_request(self, request): return NO_PERMISSION() # Add user info to request for a valid token - self.append_user_info_to_request(request, token) + return self.append_user_info_to_request(request, token) def pass_auth(self, request): """ From 00946e3d57b6d0179e5138889e188cf3cf1300b3 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Sun, 23 Oct 2022 20:20:37 +0100 Subject: [PATCH 04/73] Add method to check info based on decoded token --- django_keycloak/config.py | 3 +-- django_keycloak/token.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/django_keycloak/config.py b/django_keycloak/config.py index acee3f9..3aa656c 100644 --- a/django_keycloak/config.py +++ b/django_keycloak/config.py @@ -6,8 +6,6 @@ from django.conf import settings as django_settings # Get settings - - @dataclass class Settings: SERVER_URL: str @@ -20,6 +18,7 @@ class Settings: INTERNAL_URL: Optional[str] = None BASE_PATH: Optional[str] = "" GRAPHQL_ENDPOINT: Optional[str] = "graphql/" + DECODE_TOKEN: Optional[bool] = False def __init__(self, **vars) -> None: diff --git a/django_keycloak/token.py b/django_keycloak/token.py index f1fa468..351f9b6 100644 --- a/django_keycloak/token.py +++ b/django_keycloak/token.py @@ -53,6 +53,30 @@ class Token: # Helpers methods + @staticmethod + def get_token_info(access_token: str) -> dict: + """ + Gets the information from a token either using token decode + or introspect, depending on `DECODE_TOKEN` setting. + """ + # If user enabled `DECODE_TOKEN` using local decoding + if settings.DECODE_TOKEN: + return KEYCLOAK.decode_token( + access_token, + key=( + "-----BEGIN PUBLIC KEY-----\n" + + KEYCLOAK.public_key() + + "\n-----END PUBLIC KEY-----" + ), + options={ + "verify_signature": True, + "verify_aud": False, + "verify_exp": True, + }, + ) + # Otherwise hit the Keycloak API for info + return KEYCLOAK.introspect(access_token) + @staticmethod def _parse_keycloak_response(keycloak_response: dict) -> dict: """ @@ -108,7 +132,7 @@ def client_roles(self) -> list: Returns the client roles based on the provided access token. """ return ( - KEYCLOAK.introspect(self.access_token) + Token.get_token_info(self.access_token) .get("resource_access") .get(settings.CLIENT_ID) .get("roles", []) @@ -120,7 +144,7 @@ def realm_roles(self) -> list: Returns the realm roles based on the access token. """ return ( - KEYCLOAK.introspect(self.access_token) + Token.get_token_info(self.access_token) .get("realm_access", {}) .get("roles", []) ) @@ -130,7 +154,7 @@ def client_scopes(self) -> list: """ Returns the client scope based on the access token. """ - return KEYCLOAK.introspect(self.access_token).get("scope", "").split(" ") + return Token.get_token_info(self.access_token).get("scope", "").split(" ") # Methods From 5d8184b3f7a5f91bee0cb97768703d76c89bdd7f Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Sun, 23 Oct 2022 21:53:10 +0100 Subject: [PATCH 05/73] Remove return statement from middleware --- django_keycloak/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_keycloak/middleware.py b/django_keycloak/middleware.py index e7be07c..3fe87c7 100644 --- a/django_keycloak/middleware.py +++ b/django_keycloak/middleware.py @@ -140,7 +140,7 @@ def process_request(self, request): return NO_PERMISSION() # Add user info to request for a valid token - return self.append_user_info_to_request(request, token) + self.append_user_info_to_request(request, token) def pass_auth(self, request): """ From 9fac107d806e5e7d2f225a9ac71acb2156742882 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Mon, 24 Oct 2022 09:55:48 +0100 Subject: [PATCH 06/73] Improve error handling --- django_keycloak/connector.py | 7 ++----- django_keycloak/errors.py | 26 ++++++++++---------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/django_keycloak/connector.py b/django_keycloak/connector.py index 5894304..8025382 100644 --- a/django_keycloak/connector.py +++ b/django_keycloak/connector.py @@ -33,13 +33,10 @@ client_secret_key=CLIENT_SECRET_KEY, ) - # # Check for 403 unknown error (missing required roles) - # except if "unknown_error" in str(error): - # raise KeycloakMissingServiceAccountRolesError(CLIENT_ID) except KeycloakAuthenticationError as error: # Check if the error is due to service account not being enabled if "Client not enabled to retrieve service account" in str(error): - raise KeycloakNoServiceAccountRolesError(CLIENT_ID) + raise KeycloakNoServiceAccountRolesError from error # Otherwise re-throw the original error else: @@ -52,6 +49,6 @@ KeycloakAdminConnector.users_count() except KeycloakGetError as error: if "unknown_error" in str(error): - raise KeycloakMissingServiceAccountRolesError(CLIENT_ID) + raise KeycloakMissingServiceAccountRolesError from error else: raise error diff --git a/django_keycloak/errors.py b/django_keycloak/errors.py index cf53dfd..4f90624 100644 --- a/django_keycloak/errors.py +++ b/django_keycloak/errors.py @@ -1,3 +1,9 @@ +""" +Module containing custom errors. +""" +from django_keycloak.config import settings + + class KeycloakAPIError(Exception): """ This should be raised on KeycloakAPIErrors @@ -14,11 +20,11 @@ class KeycloakNoServiceAccountRolesError(Exception): "Service account roles" for a particular client. """ - def __init__(self, keycloak_client: str): + def __init__(self): super().__init__( ( "'Service account roles' setting not enabled. " - f"Please enable this authentication workflow for client '{keycloak_client}'." + f"Please enable this authentication workflow for client '{settings.CLIENT_ID}'." ) ) @@ -29,24 +35,12 @@ class KeycloakMissingServiceAccountRolesError(Exception): but a necessary role is missing. """ - def __init__(self, keycloak_client: str): + def __init__(self): super().__init__( ( "'Service account roles' setting is enabled, " "but role 'manage-users' is missing. " - f"To enable it go to '{keycloak_client}' client --> Service accounts roles " + f"To enable it go to realm '{settings.REALM}' --> '{settings.CLIENT_ID}' client --> Service accounts roles " " --> Assign role --> Filter by clients --> and add 'manage-users'." ) ) - - -class KeycloakMissingSettingError(Exception): - """ - When a setting is missing - """ - - def __init__(self, setting_name): - self.setting_name = setting_name - super().__init__( - f"Could not find setting '{self.setting_name}' in 'KEYCLOAK_CONFIG' settings." - ) From 6c420089564df7221205548e68f99d7974ba533e Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Mon, 24 Oct 2022 10:17:18 +0100 Subject: [PATCH 07/73] Fix user creation --- django_keycloak/backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_keycloak/backends.py b/django_keycloak/backends.py index 6a94d51..b41f045 100644 --- a/django_keycloak/backends.py +++ b/django_keycloak/backends.py @@ -54,7 +54,7 @@ def authenticate( except User.DoesNotExist: # If user does not exist create in database # `create_from_token` takes cares of password hashing - user = User.objects.create_from_token(username, password) + user = User.objects.create_from_token(token) if token.superuser: user.is_staff = user.is_superuser = True From dbfad0775d30da64bdca203b50a5f9f424b6339a Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Mon, 24 Oct 2022 10:42:12 +0100 Subject: [PATCH 08/73] Fix error in getting client roles --- django_keycloak/token.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django_keycloak/token.py b/django_keycloak/token.py index 351f9b6..aa0440c 100644 --- a/django_keycloak/token.py +++ b/django_keycloak/token.py @@ -133,8 +133,8 @@ def client_roles(self) -> list: """ return ( Token.get_token_info(self.access_token) - .get("resource_access") - .get(settings.CLIENT_ID) + .get("resource_access", {}) + .get(settings.CLIENT_ID, {}) .get("roles", []) ) From 2bef36db06f70fb548f4e0045027be7f843b553b Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Mon, 24 Oct 2022 15:04:57 +0100 Subject: [PATCH 09/73] Simplify Authentication, Add basic auth support, reduce token API calls --- django_keycloak/authentication.py | 59 +++++++--------------------- django_keycloak/config.py | 15 ++++++- django_keycloak/middleware.py | 65 ++++++++++++++++++++++--------- django_keycloak/token.py | 59 ++++++++++++++++++---------- 4 files changed, 111 insertions(+), 87 deletions(-) diff --git a/django_keycloak/authentication.py b/django_keycloak/authentication.py index b415d3f..81e6440 100644 --- a/django_keycloak/authentication.py +++ b/django_keycloak/authentication.py @@ -4,54 +4,29 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser from rest_framework import HTTP_HEADER_ENCODING -from rest_framework.authentication import BaseAuthentication +from rest_framework.authentication import TokenAuthentication from rest_framework.exceptions import AuthenticationFailed - +from rest_framework.authentication import get_authorization_header from django_keycloak import Token +from typing import Union -class KeycloakAuthentication(BaseAuthentication): +class KeycloakAuthentication(TokenAuthentication): """ - All authentication classes should extend BaseAuthentication. + A custom token authentication class for Keycloak. """ - authenticate_header = "Bearer" - - @staticmethod - def get_authorization_header(request): - """ - Return request's 'Authorization:' header, as a bytestring. - Hide some test client ickyness where the header can be unicode. - """ - auth = request.META.get("HTTP_AUTHORIZATION", b"") - if isinstance(auth, str): - # Work around django test client oddness - auth = auth.encode(HTTP_HEADER_ENCODING) - return auth - - @staticmethod - def get_token(request): - """Get the token from the HTTP request""" - auth_header = request.META.get("HTTP_AUTHORIZATION").split() - if len(auth_header) == 2: - return auth_header[1] - return None + keyword = "Bearer" - def authenticate(self, request): + def authenticate_credentials(self, access_token: str): """ - Authenticate the request and return a two-tuple of (user, token). + Overrides `authenticate_credentials` to provide custom + Keycloak authentication for a given Bearer token in a request. """ - auth_header = self.get_authorization_header(request) + # Try to build a Token instance from the provided access token in request + token: Union[Token, None] = Token.from_access_token(access_token) - # If request does not contain an authorization header, return - # an anonymous user - if not auth_header: - return AnonymousUser(), None - - # Otherwise try to create a "Token" object from the request token - token = Token.from_access_token(self.get_token(request)) - # If token is None it is not valid, immediately return error - # avoid the database query + # Check for valid Token instance if not token: raise AuthenticationFailed @@ -59,12 +34,4 @@ def authenticate(self, request): user = get_user_model().objects.get_by_keycloak_id(token.user_id) # Return the user and the associated access token - return user, token.access_token - - def authenticate_header(self, request): - """ - Return a string to be used as the value of the `WWW-Authenticate` - header in a `401 Unauthenticated` response, or `None` if the - authentication scheme should return `403 Permission Denied` responses. - """ - return self.authenticate_header + return (user, token.access_token) diff --git a/django_keycloak/config.py b/django_keycloak/config.py index 3aa656c..a9c0b15 100644 --- a/django_keycloak/config.py +++ b/django_keycloak/config.py @@ -18,7 +18,10 @@ class Settings: INTERNAL_URL: Optional[str] = None BASE_PATH: Optional[str] = "" GRAPHQL_ENDPOINT: Optional[str] = "graphql/" + # Flag if the token should be introspected or decoded DECODE_TOKEN: Optional[bool] = False + # The percentage of a tokens valid duration until a new token is requested + TOKEN_TIMEOUT_FACTOR: Optional[float] = 0.9 def __init__(self, **vars) -> None: @@ -27,9 +30,17 @@ def __init__(self, **vars) -> None: __desired_variables = Settings.__annotations__.keys() -__defined_variables = getattr(django_settings, "KEYCLOAK_CONFIG", {}) +__defined_variables = getattr( + django_settings, + "KEYCLOAK_CONFIG", + {}, +) # Create a dict of the values of the settings defined in django -__values = {key: __defined_variables.get(key) for key in __desired_variables} +__values = { + key: __defined_variables.get(key) + for key in __desired_variables + if key in __defined_variables +} # The exported settings object settings = Settings(**__values) diff --git a/django_keycloak/middleware.py b/django_keycloak/middleware.py index 3fe87c7..f49a817 100644 --- a/django_keycloak/middleware.py +++ b/django_keycloak/middleware.py @@ -2,9 +2,10 @@ Module containing custom middleware to authenticate, create and sync user information between keycloak and local database. """ +import base64 import logging import re -from typing import Union +from typing import Union, Optional from django.contrib.auth import get_user_model from django.http import JsonResponse from django.utils.deprecation import MiddlewareMixin @@ -20,6 +21,31 @@ class KeycloakMiddlewareMixin: + def get_token_from_request(self, request) -> Optional[Token]: + """ + Get the value of "HTTTP_AUTHORIZATION" request header. + If the authorization is "Bearer" it tries to produce a "Token" + instance from it. + If the authorization is "Basic" (username+password) it tries + to authenticate the user + """ + auth_type, value = request.META.get("HTTP_AUTHORIZATION").split() + + if auth_type == "Basic": + decoded_username, decoded_password = ( + base64.b64decode(value).decode("utf-8").split(":") + ) + # Try to build a Token instance from decoded credentials + token = Token.from_credentials(decoded_username, decoded_password) + if token: + # Convert the request "Basic" auth to "Bearer" with access token + request.META["HTTP_AUTHORIZATION"] = f"Bearer {token.access_token}" + + elif auth_type == "Bearer": + token = Token.from_access_token(value) + + return token + def append_user_info_to_request(self, request, token: Token): """ Appends user info to the request @@ -66,13 +92,15 @@ def append_user_info_to_request(self, request, token: Token): return request @staticmethod - def is_auth_header_missing(request): + def has_auth_header(request) -> bool: """Check if exists an authentication header in the HTTP request""" - return "HTTP_AUTHORIZATION" not in request.META + return "HTTP_AUTHORIZATION" in request.META @staticmethod def get_token(request): - """Get the token from the HTTP request""" + """ + Get the token from the HTTP request + """ auth_header = request.META.get("HTTP_AUTHORIZATION").split() if len(auth_header) > 1: return auth_header[1] @@ -95,18 +123,17 @@ def resolve(self, next, root, info, **kwargs): """ request = info.context - if self.is_auth_header_missing(request): + if self.has_auth_header(request): # Append anonymous user and continue return next(root, info, **kwargs) - # Build token from request - token = Token.from_access_token(self.get_token(request)) + token: Union[Token, None] = self.get_token_from_request(request) # Check if Token was created - if not token: - return NO_PERMISSION() - - info.context = self.append_user_info_to_request(request, token.access_token) + if token: + info.context = self.append_user_info_to_request(request, token.access_token) + else: + del request.META["HTTP_AUTHORIZATION"] return next(root, info, **kwargs) @@ -128,19 +155,19 @@ def process_request(self, request): if ( self.is_graphql_endpoint(request) or self.pass_auth(request) - or self.is_auth_header_missing(request) + or not self.has_auth_header(request) ): return - # Otherwise validate the token - token = Token.from_access_token(KeycloakMiddleware.get_token(request)) + token: Union[Token, None] = self.get_token_from_request(request) # If token is None, access token was not valid - if not token: - return NO_PERMISSION() - - # Add user info to request for a valid token - self.append_user_info_to_request(request, token) + if token: + # Add user info to request for a valid token + self.append_user_info_to_request(request, token) + # If token building failed delete the authorization header. + else: + del request.META["HTTP_AUTHORIZATION"] def pass_auth(self, request): """ diff --git a/django_keycloak/token.py b/django_keycloak/token.py index aa0440c..0dcc58c 100644 --- a/django_keycloak/token.py +++ b/django_keycloak/token.py @@ -2,6 +2,7 @@ Module to interact with the Keycloak token API """ from __future__ import annotations +from datetime import datetime, timezone from typing import Any, Callable, TypeVar, cast, Optional from dataclasses import dataclass from keycloak.exceptions import KeycloakAuthenticationError, KeycloakPostError @@ -48,11 +49,35 @@ def wrapper(self, *args, **kwargs): class Token: access_token: Optional[str] = None refresh_token: Optional[str] = None - expires_in: Optional[int] = None - refresh_expires_in: Optional[int] = None # Helpers methods + @staticmethod + def _introspect(access_token: str) -> dict: + """ + Hits the Keycloak API for access token introspection + """ + return KEYCLOAK.introspect(access_token) + + @staticmethod + def _token_decode(access_token: str) -> dict: + """ + Performs token decoding to extract token information + """ + return KEYCLOAK.decode_token( + access_token, + key=( + "-----BEGIN PUBLIC KEY-----\n" + + KEYCLOAK.public_key() + + "\n-----END PUBLIC KEY-----" + ), + options={ + "verify_signature": True, + "verify_aud": False, + "verify_exp": True, + }, + ) + @staticmethod def get_token_info(access_token: str) -> dict: """ @@ -61,21 +86,9 @@ def get_token_info(access_token: str) -> dict: """ # If user enabled `DECODE_TOKEN` using local decoding if settings.DECODE_TOKEN: - return KEYCLOAK.decode_token( - access_token, - key=( - "-----BEGIN PUBLIC KEY-----\n" - + KEYCLOAK.public_key() - + "\n-----END PUBLIC KEY-----" - ), - options={ - "verify_signature": True, - "verify_aud": False, - "verify_exp": True, - }, - ) + return Token._token_decode(access_token) # Otherwise hit the Keycloak API for info - return KEYCLOAK.introspect(access_token) + return Token._introspect(access_token) @staticmethod def _parse_keycloak_response(keycloak_response: dict) -> dict: @@ -86,18 +99,24 @@ def _parse_keycloak_response(keycloak_response: dict) -> dict: return { "access_token": keycloak_response["access_token"], "refresh_token": keycloak_response["refresh_token"], - "expires_in": keycloak_response["expires_in"], - "refresh_expires_in": keycloak_response["refresh_expires_in"], } # Properties - @property def active(self): """ Returns a boolean indicating if the current access token is active or not. """ - return KEYCLOAK.introspect(self.access_token).get("active", False) + # Get token info via decode + tokeninfo = Token._token_decode(self.access_token) + expires_at = datetime.fromtimestamp(tokeninfo["exp"], tz=timezone.utc) + issued_at = datetime.fromtimestamp(tokeninfo["iat"], tz=timezone.utc) + now = datetime.now(tz=timezone.utc) + is_active = ( + expires_at - issued_at + ) * settings.TOKEN_TIMEOUT_FACTOR + issued_at > now + + return is_active @with_active_token_property def user_info(self) -> dict: From 7354dcf2e4a9875eabf082da3893373eb8c6026e Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Mon, 24 Oct 2022 15:46:09 +0100 Subject: [PATCH 10/73] Set bearer token to dummy value in case of failed basic auth --- django_keycloak/middleware.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/django_keycloak/middleware.py b/django_keycloak/middleware.py index f49a817..515e9db 100644 --- a/django_keycloak/middleware.py +++ b/django_keycloak/middleware.py @@ -7,18 +7,11 @@ import re from typing import Union, Optional from django.contrib.auth import get_user_model -from django.http import JsonResponse from django.utils.deprecation import MiddlewareMixin from django_keycloak.models import KeycloakUserAutoId, KeycloakUser from django_keycloak import Token from django_keycloak.config import settings -# Create a reusable no permission JSON response with 401 status code -NO_PERMISSION = lambda: JsonResponse( - {"detail": "Invalid credentials provided to perform this action."}, - status=401, -) - class KeycloakMiddlewareMixin: def get_token_from_request(self, request) -> Optional[Token]: @@ -40,6 +33,9 @@ def get_token_from_request(self, request) -> Optional[Token]: if token: # Convert the request "Basic" auth to "Bearer" with access token request.META["HTTP_AUTHORIZATION"] = f"Bearer {token.access_token}" + else: + # Setup an invalid dummy bearer token + request.META["HTTP_AUTHORIZATION"] = "Bearer not-valid-token" elif auth_type == "Bearer": token = Token.from_access_token(value) @@ -132,8 +128,6 @@ def resolve(self, next, root, info, **kwargs): # Check if Token was created if token: info.context = self.append_user_info_to_request(request, token.access_token) - else: - del request.META["HTTP_AUTHORIZATION"] return next(root, info, **kwargs) @@ -165,9 +159,6 @@ def process_request(self, request): if token: # Add user info to request for a valid token self.append_user_info_to_request(request, token) - # If token building failed delete the authorization header. - else: - del request.META["HTTP_AUTHORIZATION"] def pass_auth(self, request): """ From 839563d96cf4594b850b32ea92c580c6d387bf9c Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Tue, 25 Oct 2022 12:12:39 +0100 Subject: [PATCH 11/73] Improve Settings module --- django_keycloak/config.py | 24 +++++++++++++++--------- django_keycloak/errors.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/django_keycloak/config.py b/django_keycloak/config.py index a9c0b15..a0d0afb 100644 --- a/django_keycloak/config.py +++ b/django_keycloak/config.py @@ -1,13 +1,19 @@ """ Module to interact with django settings """ +import re from dataclasses import dataclass, field from typing import Optional, List from django.conf import settings as django_settings +from django_keycloak.errors import KeycloakMissingSettingError # Get settings @dataclass class Settings: + """ + Django Keycloak settings container + """ + SERVER_URL: str REALM: str CLIENT_ID: str @@ -23,11 +29,6 @@ class Settings: # The percentage of a tokens valid duration until a new token is requested TOKEN_TIMEOUT_FACTOR: Optional[float] = 0.9 - def __init__(self, **vars) -> None: - - for key, value in vars.items(): - setattr(self, key, value) - __desired_variables = Settings.__annotations__.keys() __defined_variables = getattr( @@ -37,10 +38,15 @@ def __init__(self, **vars) -> None: ) # Create a dict of the values of the settings defined in django __values = { - key: __defined_variables.get(key) + key: value for key in __desired_variables if key in __defined_variables + and (value := __defined_variables.get(key)) is not None } - -# The exported settings object -settings = Settings(**__values) +try: + # The exported settings object + settings = Settings(**__values) +except TypeError as e: + # Get missing variables with regex + missing_required_vars = re.findall("'([^']*)'", str(e)) + raise KeycloakMissingSettingError(" / ".join(missing_required_vars)) from e diff --git a/django_keycloak/errors.py b/django_keycloak/errors.py index 4f90624..13a74c3 100644 --- a/django_keycloak/errors.py +++ b/django_keycloak/errors.py @@ -14,6 +14,20 @@ def __init__(self, status, message): self.message = message +class KeycloakMissingSettingError(Exception): + """ + Raised when a given Django Keycloak setting(s) is missing. + """ + + def __init__(self, setting: str): + super().__init__( + ( + f"The following settings are missing: '{setting}' ", + "Please add them in 'KEYCLOAK_CONFIG' inside Django settings", + ) + ) + + class KeycloakNoServiceAccountRolesError(Exception): """ Raised when the Keycloak server is not configured for From 1ce61512ca009734c615350ee95cd0cba594ffd2 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Tue, 25 Oct 2022 12:21:28 +0100 Subject: [PATCH 12/73] Fix Circular import errors --- django_keycloak/config.py | 5 +++-- django_keycloak/errors.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/django_keycloak/config.py b/django_keycloak/config.py index a0d0afb..7f2ad50 100644 --- a/django_keycloak/config.py +++ b/django_keycloak/config.py @@ -5,7 +5,6 @@ from dataclasses import dataclass, field from typing import Optional, List from django.conf import settings as django_settings -from django_keycloak.errors import KeycloakMissingSettingError # Get settings @dataclass @@ -47,6 +46,8 @@ class Settings: # The exported settings object settings = Settings(**__values) except TypeError as e: + import django_keycloak.errors as errors + # Get missing variables with regex missing_required_vars = re.findall("'([^']*)'", str(e)) - raise KeycloakMissingSettingError(" / ".join(missing_required_vars)) from e + raise errors.KeycloakMissingSettingError(" / ".join(missing_required_vars)) from e diff --git a/django_keycloak/errors.py b/django_keycloak/errors.py index 13a74c3..7b12e71 100644 --- a/django_keycloak/errors.py +++ b/django_keycloak/errors.py @@ -1,7 +1,7 @@ """ Module containing custom errors. """ -from django_keycloak.config import settings +import django_keycloak.config as config class KeycloakAPIError(Exception): @@ -22,7 +22,7 @@ class KeycloakMissingSettingError(Exception): def __init__(self, setting: str): super().__init__( ( - f"The following settings are missing: '{setting}' ", + f"The following settings are missing: '{setting}' " "Please add them in 'KEYCLOAK_CONFIG' inside Django settings", ) ) @@ -38,7 +38,7 @@ def __init__(self): super().__init__( ( "'Service account roles' setting not enabled. " - f"Please enable this authentication workflow for client '{settings.CLIENT_ID}'." + f"Please enable this authentication workflow for client '{config.settings.CLIENT_ID}'." ) ) @@ -54,7 +54,7 @@ def __init__(self): ( "'Service account roles' setting is enabled, " "but role 'manage-users' is missing. " - f"To enable it go to realm '{settings.REALM}' --> '{settings.CLIENT_ID}' client --> Service accounts roles " + f"To enable it go to realm '{config.settings.REALM}' --> '{config.settings.CLIENT_ID}' client --> Service accounts roles " " --> Assign role --> Filter by clients --> and add 'manage-users'." ) ) From 19a94d307a906c5655607b387e3082073c802746 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Tue, 25 Oct 2022 12:23:02 +0100 Subject: [PATCH 13/73] Fix Error msg format --- django_keycloak/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_keycloak/errors.py b/django_keycloak/errors.py index 7b12e71..f5af706 100644 --- a/django_keycloak/errors.py +++ b/django_keycloak/errors.py @@ -23,7 +23,7 @@ def __init__(self, setting: str): super().__init__( ( f"The following settings are missing: '{setting}' " - "Please add them in 'KEYCLOAK_CONFIG' inside Django settings", + "Please add them in 'KEYCLOAK_CONFIG' inside Django settings" ) ) From 1da3ee82607e0eb8ecea2e3771948da7302068f0 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 25 Oct 2022 13:28:41 +0200 Subject: [PATCH 14/73] Update token and settins handling Why: - Simplify how settings class is constructed - Use decoding token for active check This change addresses the need by: - Adding comments to settins - Simplifying creating settings dataclass from Django settings - Adding caching to decoding/introspecting token - Simplifying is_active token check --- django_keycloak/config.py | 47 ++++---- django_keycloak/connector.py | 30 ++---- django_keycloak/token.py | 204 ++++++++++++++++++----------------- setup.cfg | 4 +- 4 files changed, 142 insertions(+), 143 deletions(-) diff --git a/django_keycloak/config.py b/django_keycloak/config.py index a9c0b15..65a4edb 100644 --- a/django_keycloak/config.py +++ b/django_keycloak/config.py @@ -2,45 +2,48 @@ Module to interact with django settings """ from dataclasses import dataclass, field -from typing import Optional, List +from typing import List, Optional + from django.conf import settings as django_settings + # Get settings @dataclass class Settings: + # Base URL to the Keycloak server from the client application https://auth.example.com SERVER_URL: str + # The Keycloak realm in which this client is registered REALM: str + # The ID of this client in the above Keycloak realm CLIENT_ID: str + # The secret for this confidential client CLIENT_SECRET_KEY: str + # The name of the admin role for the client CLIENT_ADMIN_ROLE: str + # The name of the admin role for the realm REALM_ADMIN_ROLE: str + # Regex formatted URLs to skip authentication for (uses re.match()) EXEMPT_URIS: Optional[List] = field(default_factory=[]) + # Overrides SERVER_URL for Keycloak admin calls INTERNAL_URL: Optional[str] = None - BASE_PATH: Optional[str] = "" + # Override default Keycloak base path (/auth/) + BASE_PATH: Optional[str] = "/auth/" + # Regex formatted URL to excempt the GraphQL URL""" GRAPHQL_ENDPOINT: Optional[str] = "graphql/" # Flag if the token should be introspected or decoded DECODE_TOKEN: Optional[bool] = False - # The percentage of a tokens valid duration until a new token is requested - TOKEN_TIMEOUT_FACTOR: Optional[float] = 0.9 - - def __init__(self, **vars) -> None: - - for key, value in vars.items(): - setattr(self, key, value) + # Flag if the audience in the token should be verified + VERIFY_AUDIENCE: Optional[bool] = True + # Flag if the user info has been included in the token + USER_INFO_IN_TOKEN: Optional[bool] = True + # Derived setting of the SERVER/INTERNAL_URL and BASE_PATH + KEYCLOAK_URL: str = field(init=False) + def __post_init__(self) -> None: + # Decide URL (internal url overrides serverl url) + URL = self.INTERNAL_URL if self.INTERNAL_URL else self.SERVER_URL + self.KEYCLOAK_URL = f"{URL}{self.BASE_PATH}" -__desired_variables = Settings.__annotations__.keys() -__defined_variables = getattr( - django_settings, - "KEYCLOAK_CONFIG", - {}, -) -# Create a dict of the values of the settings defined in django -__values = { - key: __defined_variables.get(key) - for key in __desired_variables - if key in __defined_variables -} # The exported settings object -settings = Settings(**__values) +settings = Settings(**django_settings.KEYCLOAK_CONFIG) diff --git a/django_keycloak/connector.py b/django_keycloak/connector.py index 8025382..56c088c 100644 --- a/django_keycloak/connector.py +++ b/django_keycloak/connector.py @@ -2,37 +2,21 @@ Module to interact with Keycloak Admin API """ from keycloak.exceptions import KeycloakAuthenticationError, KeycloakGetError - - from keycloak.keycloak_admin import KeycloakAdmin + +from django_keycloak.config import settings from django_keycloak.errors import ( - KeycloakNoServiceAccountRolesError, KeycloakMissingServiceAccountRolesError, + KeycloakNoServiceAccountRolesError, ) -from django_keycloak.config import settings - -# Variables for parent constructor -SERVER_URL = settings.SERVER_URL -INTERNAL_URL = settings.INTERNAL_URL -BASE_PATH = settings.BASE_PATH -REAL_NAME = settings.REALM -CLIENT_ID = settings.CLIENT_ID -CLIENT_SECRET_KEY = settings.CLIENT_SECRET_KEY - -# Decide URL (internal url overrides serverl url) -URL = INTERNAL_URL if INTERNAL_URL else SERVER_URL -# Add base path -URL += BASE_PATH - try: KeycloakAdminConnector = KeycloakAdmin( - server_url=URL, - client_id=CLIENT_ID, - realm_name=REAL_NAME, - client_secret_key=CLIENT_SECRET_KEY, + server_url=settings.KEYCLOAK_URL, + client_id=settings.CLIENT_ID, + realm_name=settings.REALM, + client_secret_key=settings.CLIENT_SECRET_KEY, ) - except KeycloakAuthenticationError as error: # Check if the error is due to service account not being enabled if "Client not enabled to retrieve service account" in str(error): diff --git a/django_keycloak/token.py b/django_keycloak/token.py index 0dcc58c..f1391dd 100644 --- a/django_keycloak/token.py +++ b/django_keycloak/token.py @@ -2,93 +2,87 @@ Module to interact with the Keycloak token API """ from __future__ import annotations -from datetime import datetime, timezone -from typing import Any, Callable, TypeVar, cast, Optional + from dataclasses import dataclass -from keycloak.exceptions import KeycloakAuthenticationError, KeycloakPostError +from typing import Optional + +from cachetools.func import ttl_cache +from jose.exceptions import JOSEError +from keycloak.exceptions import ( + KeycloakAuthenticationError, + KeycloakError, + KeycloakPostError, +) from keycloak.keycloak_openid import KeycloakOpenID -from django_keycloak.config import settings - -F = TypeVar("F", bound=Callable[..., Any]) - - -def with_active_token_property(f: F) -> F: - @property - def wrapper(self, *args, **kwargs): - # Only allow the method if token is active - if not self.active: - return None - return f(self, *args, **kwargs) - - return cast(F, wrapper) - - -# Variables for parent constructor -SERVER_URL = settings.SERVER_URL -INTERNAL_URL = settings.INTERNAL_URL -BASE_PATH = settings.BASE_PATH -REAL_NAME = settings.REALM -CLIENT_ID = settings.CLIENT_ID -CLIENT_SECRET_KEY = settings.CLIENT_SECRET_KEY -# Decide URL (internal url overrides serverl url) -URL = INTERNAL_URL if INTERNAL_URL else SERVER_URL -# Add base path -URL += BASE_PATH +from django_keycloak.config import settings # Define keycloak openid instance KEYCLOAK = KeycloakOpenID( - server_url=URL, - client_id=CLIENT_ID, - realm_name=REAL_NAME, - client_secret_key=CLIENT_SECRET_KEY, + server_url=settings.KEYCLOAK_URL, + client_id=settings.CLIENT_ID, + realm_name=settings.REALM, + client_secret_key=settings.CLIENT_SECRET_KEY, ) -@dataclass class Token: - access_token: Optional[str] = None - refresh_token: Optional[str] = None + def __init__(self, access_token=None, refresh_token=None): + self.access_token: Optional[str] = access_token + self.refresh_token: Optional[str] = refresh_token - # Helpers methods + @property + @ttl_cache(maxsize=1, ttl=60) + def public_key(self): + """ + Formats the public - @staticmethod - def _introspect(access_token: str) -> dict: + Raises: + KeycloakError: On Keycloak API errors """ - Hits the Keycloak API for access token introspection + + return f"-----BEGIN PUBLIC KEY-----\n{KEYCLOAK.public_key()}\n-----END PUBLIC KEY-----" + + @ttl_cache(maxsize=1, ttl=60) + def get_access_token_info(self) -> dict: """ - return KEYCLOAK.introspect(access_token) + Gets the information from a token either using token decode + or introspect, depending on `DECODE_TOKEN` setting. - @staticmethod - def _token_decode(access_token: str) -> dict: - """ - Performs token decoding to extract token information - """ - return KEYCLOAK.decode_token( - access_token, - key=( - "-----BEGIN PUBLIC KEY-----\n" - + KEYCLOAK.public_key() - + "\n-----END PUBLIC KEY-----" - ), - options={ - "verify_signature": True, - "verify_aud": False, - "verify_exp": True, - }, - ) + Raises: + JOSEError: On expired or invalid tokens + KeycloakError: On expired / invalid tokens or Keycloak API errors + """ + # If user enabled `DECODE_TOKEN` using local decoding + if settings.DECODE_TOKEN: + return KEYCLOAK.decode_token( + self.access_token, + key=self.public_key, + options={"verify_aud": settings.VERIFY_AUDIENCE}, + ) + # Otherwise hit the Keycloak API for info + return KEYCLOAK.introspect(self.access_token) - @staticmethod - def get_token_info(access_token: str) -> dict: + @ttl_cache(maxsize=1, ttl=60) + def get_refresh_token_info(self) -> dict: """ Gets the information from a token either using token decode or introspect, depending on `DECODE_TOKEN` setting. + + Raises: + JOSEError: On expired or invalid tokens + KeycloakError: On expired / invalid tokens or Keycloak API errors """ # If user enabled `DECODE_TOKEN` using local decoding + # If user enabled `DECODE_TOKEN` using local decoding if settings.DECODE_TOKEN: - return Token._token_decode(access_token) + return KEYCLOAK.decode_token( + self.refresh_token, + key=self.public_key, + options={"verify_aud": settings.VERIFY_AUDIENCE}, + ) # Otherwise hit the Keycloak API for info - return Token._introspect(access_token) + return KEYCLOAK.introspect(self.refresh_token) @staticmethod def _parse_keycloak_response(keycloak_response: dict) -> dict: @@ -97,46 +91,56 @@ def _parse_keycloak_response(keycloak_response: dict) -> dict: values """ return { - "access_token": keycloak_response["access_token"], - "refresh_token": keycloak_response["refresh_token"], + "access_token": keycloak_response.get("access_token"), + "refresh_token": keycloak_response.get("refresh_token"), } # Properties @property - def active(self): + def is_active(self) -> bool: """ Returns a boolean indicating if the current access token is active or not. """ - # Get token info via decode - tokeninfo = Token._token_decode(self.access_token) - expires_at = datetime.fromtimestamp(tokeninfo["exp"], tz=timezone.utc) - issued_at = datetime.fromtimestamp(tokeninfo["iat"], tz=timezone.utc) - now = datetime.now(tz=timezone.utc) - is_active = ( - expires_at - issued_at - ) * settings.TOKEN_TIMEOUT_FACTOR + issued_at > now - - return is_active + try: + info = self.get_access_token_info() + except (JOSEError, KeycloakError): + return False + # Keycloak introspections return {"active": bool} + if "active" in info: + return info["active"] + return True - @with_active_token_property def user_info(self) -> dict: """ Returns the user information contained on the provided access token. + + When DECODE_TOKEN and USER_INFO_IN_TOKEN are enabled the entire token is returned + + Raises: + JOSEError: On expired or invalid tokens + KeycloakError: On expired / invalid tokens or Keycloak API errors """ + if settings.DECODE_TOKEN and settings.USER_INFO_IN_TOKEN: + return self.get_access_token_info() return KEYCLOAK.userinfo(self.access_token) - @with_active_token_property def user_id(self) -> str: """ Returns the Keycloak user id + + Raises: + JOSEError: On expired or invalid tokens + KeycloakError: On expired / invalid tokens or Keycloak API errors """ - return self.user_info.get("sub") # type: ignore + return self.user_info().get("sub") # type: ignore - @with_active_token_property - def superuser(self) -> bool: + def has_superuser_perm(self) -> bool: """ - Returns a boolean indicating if the user has admin - permissions + Check if token belongs to a user with superuser permissions + + Raises: + JOSEError: On expired or invalid tokens + KeycloakError: On expired / invalid tokens or Keycloak API errors """ if (settings.CLIENT_ADMIN_ROLE in self.client_roles) or ( # type: ignore settings.REALM_ADMIN_ROLE in self.realm_roles @@ -145,35 +149,40 @@ def superuser(self) -> bool: return False - @with_active_token_property def client_roles(self) -> list: """ Returns the client roles based on the provided access token. + + Raises: + JOSEError: On expired or invalid tokens + KeycloakError: On expired / invalid tokens or Keycloak API errors """ return ( - Token.get_token_info(self.access_token) + Token.get_access_token_info() .get("resource_access", {}) .get(settings.CLIENT_ID, {}) .get("roles", []) ) - @with_active_token_property def realm_roles(self) -> list: """ Returns the realm roles based on the access token. + + Raises: + JOSEError: On expired or invalid tokens + KeycloakError: On expired / invalid tokens or Keycloak API errors """ - return ( - Token.get_token_info(self.access_token) - .get("realm_access", {}) - .get("roles", []) - ) + return Token.get_access_token_info().get("realm_access", {}).get("roles", []) - @with_active_token_property def client_scopes(self) -> list: """ Returns the client scope based on the access token. + + Raises: + JOSEError: On expired or invalid tokens + KeycloakError: On expired / invalid tokens or Keycloak API errors """ - return Token.get_token_info(self.access_token).get("scope", "").split(" ") + return Token.get_access_token_info().get("scope", "").split(" ") # Methods @@ -198,7 +207,7 @@ def from_access_token(cls, access_token: str) -> Optional[Token]: Returns `None` if token is not active. """ instance = cls(access_token=access_token) - return instance if instance.active else None + return instance if instance.is_active else None @classmethod def from_refresh_token(cls, refresh_token: str) -> Optional[Token]: @@ -207,11 +216,14 @@ def from_refresh_token(cls, refresh_token: str) -> Optional[Token]: """ instance = cls(refresh_token=refresh_token) instance.refresh() - return instance + return instance if instance.is_active else None def refresh(self) -> None: """ Refreshes the `access_token` with `refresh_token`. + + Raises: + KeycloakError: On Keycloak API errors """ if self.refresh_token: mapping = self._parse_keycloak_response( diff --git a/setup.cfg b/setup.cfg index 652ac56..89f481f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,5 +21,5 @@ install_requires = Django >= "2.2" djangorestframework >= "3.0" dry-rest-permissions >= "0.1" - python-keycloak == 2.6.0 - + python-keycloak >= 2.6.0 + cachetools >= "5.0.0" From c52502ae70d904dabaf3ea449f427201c4223bd1 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 25 Oct 2022 14:51:59 +0200 Subject: [PATCH 15/73] Fix bugs in Token --- django_keycloak/token.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/django_keycloak/token.py b/django_keycloak/token.py index f1391dd..8d8b084 100644 --- a/django_keycloak/token.py +++ b/django_keycloak/token.py @@ -3,7 +3,6 @@ """ from __future__ import annotations -from dataclasses import dataclass from typing import Optional from cachetools.func import ttl_cache @@ -53,6 +52,8 @@ def get_access_token_info(self) -> dict: JOSEError: On expired or invalid tokens KeycloakError: On expired / invalid tokens or Keycloak API errors """ + if not self.access_token: + return {} # If user enabled `DECODE_TOKEN` using local decoding if settings.DECODE_TOKEN: return KEYCLOAK.decode_token( @@ -73,7 +74,8 @@ def get_refresh_token_info(self) -> dict: JOSEError: On expired or invalid tokens KeycloakError: On expired / invalid tokens or Keycloak API errors """ - # If user enabled `DECODE_TOKEN` using local decoding + if not self.refresh_token: + return {} # If user enabled `DECODE_TOKEN` using local decoding if settings.DECODE_TOKEN: return KEYCLOAK.decode_token( @@ -142,8 +144,8 @@ def has_superuser_perm(self) -> bool: JOSEError: On expired or invalid tokens KeycloakError: On expired / invalid tokens or Keycloak API errors """ - if (settings.CLIENT_ADMIN_ROLE in self.client_roles) or ( # type: ignore - settings.REALM_ADMIN_ROLE in self.realm_roles + if (settings.CLIENT_ADMIN_ROLE in self.client_roles()) or ( # type: ignore + settings.REALM_ADMIN_ROLE in self.realm_roles() ): # type: ignore return True @@ -158,7 +160,7 @@ def client_roles(self) -> list: KeycloakError: On expired / invalid tokens or Keycloak API errors """ return ( - Token.get_access_token_info() + self.get_access_token_info() .get("resource_access", {}) .get(settings.CLIENT_ID, {}) .get("roles", []) @@ -172,7 +174,7 @@ def realm_roles(self) -> list: JOSEError: On expired or invalid tokens KeycloakError: On expired / invalid tokens or Keycloak API errors """ - return Token.get_access_token_info().get("realm_access", {}).get("roles", []) + return self.get_access_token_info().get("realm_access", {}).get("roles", []) def client_scopes(self) -> list: """ @@ -182,9 +184,7 @@ def client_scopes(self) -> list: JOSEError: On expired or invalid tokens KeycloakError: On expired / invalid tokens or Keycloak API errors """ - return Token.get_access_token_info().get("scope", "").split(" ") - - # Methods + return self.get_access_token_info().get("scope", "").split(" ") @classmethod def from_credentials(cls, username: str, password: str) -> Optional[Token]: # type: ignore From 7c2980e156b4eedf6711a33c5f4fc56301d5ba63 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Tue, 25 Oct 2022 15:21:50 +0100 Subject: [PATCH 16/73] Simplify configs parsing --- django_keycloak/config.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/django_keycloak/config.py b/django_keycloak/config.py index d5e1bfc..4d77237 100644 --- a/django_keycloak/config.py +++ b/django_keycloak/config.py @@ -48,22 +48,18 @@ def __post_init__(self) -> None: self.KEYCLOAK_URL = f"{URL}{self.BASE_PATH}" -__desired_variables = Settings.__annotations__.keys() -__defined_variables = getattr( +# Get keycloak configs from django +__configs = getattr( django_settings, "KEYCLOAK_CONFIG", {}, ) -# Create a dict of the values of the settings defined in django -__values = { - key: value - for key in __desired_variables - if key in __defined_variables - and (value := __defined_variables.get(key)) is not None -} +# Filter out configs with `None` as values +__configs = {k: v for k, v in __configs.items() if v} + try: # The exported settings object - settings = Settings(**__values) + settings = Settings(**__configs) except TypeError as e: import django_keycloak.errors as errors From e1c2650860a0ffb79b96e5fa0021ab2e451667ba Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Tue, 25 Oct 2022 15:37:45 +0100 Subject: [PATCH 17/73] Fix None Variables Parsing --- django_keycloak/config.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/django_keycloak/config.py b/django_keycloak/config.py index 4d77237..364b421 100644 --- a/django_keycloak/config.py +++ b/django_keycloak/config.py @@ -49,13 +49,9 @@ def __post_init__(self) -> None: # Get keycloak configs from django -__configs = getattr( - django_settings, - "KEYCLOAK_CONFIG", - {}, -) +__configs = django_settings.KEYCLOAK_CONFIG # Filter out configs with `None` as values -__configs = {k: v for k, v in __configs.items() if v} +__configs = {k: v for k, v in __configs.items() if v is not None} try: # The exported settings object From 64a4f4c917f54c5c72d0c053e39d5cbec26c8ed9 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Tue, 25 Oct 2022 15:55:28 +0100 Subject: [PATCH 18/73] Change Superuser to property, remove graphene middleware --- django_keycloak/backends.py | 8 ++--- django_keycloak/managers.py | 4 +-- django_keycloak/middleware.py | 57 ++++++----------------------------- django_keycloak/token.py | 3 +- 4 files changed, 17 insertions(+), 55 deletions(-) diff --git a/django_keycloak/backends.py b/django_keycloak/backends.py index b41f045..979908f 100644 --- a/django_keycloak/backends.py +++ b/django_keycloak/backends.py @@ -31,7 +31,7 @@ def authenticate( token = Token.from_credentials(username, password) # type: ignore # Check for non-existing or unactive token - if (not token) or (not token.active): + if not token: # credentials were not valid return @@ -56,7 +56,7 @@ def authenticate( # `create_from_token` takes cares of password hashing user = User.objects.create_from_token(token) - if token.superuser: + if token.is_superuser: user.is_staff = user.is_superuser = True else: user.is_staff = user.is_superuser = False @@ -64,8 +64,8 @@ def authenticate( user.save() return user - def get_user(self, user_identifier): - User = get_user_model() + def get_user(self, user_identifier: str): + User: Union[KeycloakUser, KeycloakUserAutoId] = get_user_model() try: return User.objects.get(username=user_identifier) except User.DoesNotExist: diff --git a/django_keycloak/managers.py b/django_keycloak/managers.py index 7e73683..c254b71 100644 --- a/django_keycloak/managers.py +++ b/django_keycloak/managers.py @@ -15,7 +15,7 @@ def create_from_token(self, token: Token, **kwargs): user_info = token.user_info # set admin permissions if user is admin - if token.superuser: + if token.is_superuser: is_staff = is_superuser = True else: is_staff = is_superuser = False @@ -44,7 +44,7 @@ def create_from_token(self, token: Token, **kwargs): user_info = token.user_info # set admin permissions if user is admin - if token.superuser: + if token.is_superuser: is_staff = is_superuser = True else: is_staff = is_superuser = False diff --git a/django_keycloak/middleware.py b/django_keycloak/middleware.py index 515e9db..ad3cc11 100644 --- a/django_keycloak/middleware.py +++ b/django_keycloak/middleware.py @@ -3,7 +3,6 @@ sync user information between keycloak and local database. """ import base64 -import logging import re from typing import Union, Optional from django.contrib.auth import get_user_model @@ -13,7 +12,11 @@ from django_keycloak.config import settings -class KeycloakMiddlewareMixin: +class KeycloakMiddleware(MiddlewareMixin): + """ + Middleware to validate Keycloak access based on REST validations + """ + def get_token_from_request(self, request) -> Optional[Token]: """ Get the value of "HTTTP_AUTHORIZATION" request header. @@ -22,7 +25,10 @@ def get_token_from_request(self, request) -> Optional[Token]: If the authorization is "Basic" (username+password) it tries to authenticate the user """ - auth_type, value = request.META.get("HTTP_AUTHORIZATION").split() + auth_type, value, *_ = request.META.get("HTTP_AUTHORIZATION").split() + + if not auth_type or auth_type[0].lower() not in ["basic", "bearer"]: + return None if auth_type == "Basic": decoded_username, decoded_password = ( @@ -92,51 +98,6 @@ def has_auth_header(request) -> bool: """Check if exists an authentication header in the HTTP request""" return "HTTP_AUTHORIZATION" in request.META - @staticmethod - def get_token(request): - """ - Get the token from the HTTP request - """ - auth_header = request.META.get("HTTP_AUTHORIZATION").split() - if len(auth_header) > 1: - return auth_header[1] - return auth_header[0] - - -class KeycloakGrapheneMiddleware(KeycloakMiddlewareMixin): - """ - Middleware to validate Keycloak access based on Graphql validations - """ - - def __init__(self): - logging.warning( - "All functionality is provided by KeycloakMiddleware", DeprecationWarning, 2 - ) - - def resolve(self, next, root, info, **kwargs): - """ - Graphene Middleware to validate keycloak access - """ - request = info.context - - if self.has_auth_header(request): - # Append anonymous user and continue - return next(root, info, **kwargs) - - token: Union[Token, None] = self.get_token_from_request(request) - - # Check if Token was created - if token: - info.context = self.append_user_info_to_request(request, token.access_token) - - return next(root, info, **kwargs) - - -class KeycloakMiddleware(KeycloakMiddlewareMixin, MiddlewareMixin): - """ - Middleware to validate Keycloak access based on REST validations - """ - def process_request(self, request): """ To be executed before the view each request. diff --git a/django_keycloak/token.py b/django_keycloak/token.py index 55ed952..5c4d2d7 100644 --- a/django_keycloak/token.py +++ b/django_keycloak/token.py @@ -140,7 +140,8 @@ def user_id(self) -> str: """ return self.user_info().get("sub") # type: ignore - def has_superuser_perm(self) -> bool: + @property + def is_superuser(self) -> bool: """ Check if token belongs to a user with superuser permissions From 8c3e0d839e88c9d033309ef2c0dd61af237df664 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Tue, 25 Oct 2022 16:19:28 +0100 Subject: [PATCH 19/73] Remove validation auth header --- django_keycloak/middleware.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/django_keycloak/middleware.py b/django_keycloak/middleware.py index ad3cc11..630f02d 100644 --- a/django_keycloak/middleware.py +++ b/django_keycloak/middleware.py @@ -27,9 +27,6 @@ def get_token_from_request(self, request) -> Optional[Token]: """ auth_type, value, *_ = request.META.get("HTTP_AUTHORIZATION").split() - if not auth_type or auth_type[0].lower() not in ["basic", "bearer"]: - return None - if auth_type == "Basic": decoded_username, decoded_password = ( base64.b64decode(value).decode("utf-8").split(":") From 79e58f7561b854f2f1de6e73d8a3a608970fbb05 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Tue, 25 Oct 2022 16:47:06 +0100 Subject: [PATCH 20/73] Back to token properties --- django_keycloak/token.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/django_keycloak/token.py b/django_keycloak/token.py index 5c4d2d7..3cb2c02 100644 --- a/django_keycloak/token.py +++ b/django_keycloak/token.py @@ -116,6 +116,7 @@ def is_active(self) -> bool: return info["active"] return True + @property def user_info(self) -> dict: """ Returns the user information contained on the provided access token. @@ -130,6 +131,7 @@ def user_info(self) -> dict: return self.get_access_token_info() return KEYCLOAK.userinfo(self.access_token) + @property def user_id(self) -> str: """ Returns the Keycloak user id @@ -138,7 +140,7 @@ def user_id(self) -> str: JOSEError: On expired or invalid tokens KeycloakError: On expired / invalid tokens or Keycloak API errors """ - return self.user_info().get("sub") # type: ignore + return self.user_info.get("sub") # type: ignore @property def is_superuser(self) -> bool: @@ -149,13 +151,14 @@ def is_superuser(self) -> bool: JOSEError: On expired or invalid tokens KeycloakError: On expired / invalid tokens or Keycloak API errors """ - if (settings.CLIENT_ADMIN_ROLE in self.client_roles()) or ( # type: ignore - settings.REALM_ADMIN_ROLE in self.realm_roles() + if (settings.CLIENT_ADMIN_ROLE in self.client_roles) or ( # type: ignore + settings.REALM_ADMIN_ROLE in self.realm_roles ): # type: ignore return True return False + @property def client_roles(self) -> list: """ Returns the client roles based on the provided access token. @@ -171,6 +174,7 @@ def client_roles(self) -> list: .get("roles", []) ) + @property def realm_roles(self) -> list: """ Returns the realm roles based on the access token. @@ -181,6 +185,7 @@ def realm_roles(self) -> list: """ return self.get_access_token_info().get("realm_access", {}).get("roles", []) + @property def client_scopes(self) -> list: """ Returns the client scope based on the access token. From fd27187a80b20a5e212c3686556dd0cc51a6a6eb Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 23 Aug 2022 14:27:15 +0200 Subject: [PATCH 21/73] Prepare for poetry packaging Why: - Migrate to modern build system This change addresses the need by: - Adding poetry --- Pipfile | 20 + Pipfile.lock | 779 ++++++++++++++++++ pyproject.toml | 43 + setup.cfg | 25 - setup.py | 7 +- .../django_keycloak}/__init__.py | 0 .../django_keycloak}/admin.py | 0 .../django_keycloak}/api/__init__.py | 0 .../django_keycloak}/api/filters.py | 0 .../django_keycloak}/api/serializers.py | 0 .../django_keycloak}/api/urls.py | 0 .../django_keycloak}/api/views.py | 0 .../django_keycloak}/apps.py | 0 .../django_keycloak}/authentication.py | 0 .../django_keycloak}/backends.py | 0 .../django_keycloak}/config.py | 0 .../django_keycloak}/connector.py | 0 .../django_keycloak}/errors.py | 0 .../django_keycloak}/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/sync_keycloak_users.py | 0 .../django_keycloak}/managers.py | 0 .../django_keycloak}/middleware.py | 0 .../migrations/0001_initial.py | 0 .../0001_redo_migrations_0001to0005.py | 0 .../migrations/0002_auto_20210209_1503.py | 0 .../migrations/0003_auto_20210406_1426.py | 0 .../migrations/0004_keycloakuserautoid.py | 0 .../migrations/0005_auto_20211231_1702.py | 0 .../django_keycloak}/migrations/__init__.py | 0 .../django_keycloak}/mixins.py | 0 .../django_keycloak}/models.py | 0 .../django_keycloak}/tasks.py | 0 .../django_keycloak}/token.py | 0 34 files changed, 843 insertions(+), 31 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 pyproject.toml delete mode 100644 setup.cfg rename {django_keycloak => src/django_keycloak}/__init__.py (100%) rename {django_keycloak => src/django_keycloak}/admin.py (100%) rename {django_keycloak => src/django_keycloak}/api/__init__.py (100%) rename {django_keycloak => src/django_keycloak}/api/filters.py (100%) rename {django_keycloak => src/django_keycloak}/api/serializers.py (100%) rename {django_keycloak => src/django_keycloak}/api/urls.py (100%) rename {django_keycloak => src/django_keycloak}/api/views.py (100%) rename {django_keycloak => src/django_keycloak}/apps.py (100%) rename {django_keycloak => src/django_keycloak}/authentication.py (100%) rename {django_keycloak => src/django_keycloak}/backends.py (100%) rename {django_keycloak => src/django_keycloak}/config.py (100%) rename {django_keycloak => src/django_keycloak}/connector.py (100%) rename {django_keycloak => src/django_keycloak}/errors.py (100%) rename {django_keycloak => src/django_keycloak}/management/__init__.py (100%) rename {django_keycloak => src/django_keycloak}/management/commands/__init__.py (100%) rename {django_keycloak => src/django_keycloak}/management/commands/sync_keycloak_users.py (100%) rename {django_keycloak => src/django_keycloak}/managers.py (100%) rename {django_keycloak => src/django_keycloak}/middleware.py (100%) rename {django_keycloak => src/django_keycloak}/migrations/0001_initial.py (100%) rename {django_keycloak => src/django_keycloak}/migrations/0001_redo_migrations_0001to0005.py (100%) rename {django_keycloak => src/django_keycloak}/migrations/0002_auto_20210209_1503.py (100%) rename {django_keycloak => src/django_keycloak}/migrations/0003_auto_20210406_1426.py (100%) rename {django_keycloak => src/django_keycloak}/migrations/0004_keycloakuserautoid.py (100%) rename {django_keycloak => src/django_keycloak}/migrations/0005_auto_20211231_1702.py (100%) rename {django_keycloak => src/django_keycloak}/migrations/__init__.py (100%) rename {django_keycloak => src/django_keycloak}/mixins.py (100%) rename {django_keycloak => src/django_keycloak}/models.py (100%) rename {django_keycloak => src/django_keycloak}/tasks.py (100%) rename {django_keycloak => src/django_keycloak}/token.py (100%) diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..7e69378 --- /dev/null +++ b/Pipfile @@ -0,0 +1,20 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +Django = ">=2.2" +djangorestframework = ">=3.0" +dry-rest-permissions = ">=0.1" +PyJWT = {version = "~=2.4.0", extras = ["crypto"]} +requests = ">=2.0" + +[dev-packages] +build = "~=0.8" +twine = "~=4.0" +black = "~=22.6" +setuptools = "~=65.0" + +[requires] +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..b0d4d84 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,779 @@ +{ + "_meta": { + "hash": { + "sha256": "cf7c8d55ebcf5a2c3c1e42edc42858a8a0d964c0c37f6618ee12614a744d21a9" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "asgiref": { + "hashes": [ + "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", + "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" + ], + "markers": "python_version >= '3.7'", + "version": "==3.5.2" + }, + "backports.zoneinfo": { + "hashes": [ + "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf", + "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328", + "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546", + "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6", + "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570", + "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", + "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7", + "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", + "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722", + "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582", + "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc", + "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b", + "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", + "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08", + "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", + "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" + ], + "markers": "python_version < '3.9'", + "version": "==0.2.1" + }, + "certifi": { + "hashes": [ + "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", + "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==2022.6.15" + }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==2.1.1" + }, + "cryptography": { + "hashes": [ + "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59", + "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596", + "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3", + "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5", + "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab", + "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884", + "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82", + "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b", + "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441", + "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa", + "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d", + "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b", + "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a", + "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6", + "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157", + "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280", + "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282", + "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67", + "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8", + "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046", + "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327", + "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9" + ], + "version": "==37.0.4" + }, + "django": { + "hashes": [ + "sha256:031ccb717782f6af83a0063a1957686e87cb4581ea61b47b3e9addf60687989a", + "sha256:032f8a6fc7cf05ccd1214e4a2e21dfcd6a23b9d575c6573cacc8c67828dbe642" + ], + "index": "pypi", + "version": "==4.1" + }, + "djangorestframework": { + "hashes": [ + "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee", + "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa" + ], + "index": "pypi", + "version": "==3.13.1" + }, + "dry-rest-permissions": { + "hashes": [ + "sha256:1f40461184063390e5b24e9c5602eb8cc8c3c2433c796f39a5332065bfbddd2b", + "sha256:f3fe685760004ce182801602819b43ebfa922e587036f1f5a5c10ffcfa646039" + ], + "index": "pypi", + "version": "==0.1.10" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3.5'", + "version": "==3.3" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pyjwt": { + "extras": [ + "crypto" + ], + "hashes": [ + "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf", + "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba" + ], + "index": "pypi", + "version": "==2.4.0" + }, + "pytz": { + "hashes": [ + "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", + "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" + ], + "version": "==2022.2.1" + }, + "requests": { + "hashes": [ + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + ], + "index": "pypi", + "version": "==2.28.1" + }, + "sqlparse": { + "hashes": [ + "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", + "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" + ], + "markers": "python_version >= '3.5'", + "version": "==0.4.2" + }, + "urllib3": { + "hashes": [ + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "version": "==1.26.12" + } + }, + "develop": { + "attrs": { + "hashes": [ + "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", + "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + ], + "markers": "python_version >= '3.5'", + "version": "==22.1.0" + }, + "black": { + "hashes": [ + "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90", + "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c", + "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78", + "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4", + "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee", + "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e", + "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e", + "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6", + "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9", + "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c", + "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256", + "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f", + "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2", + "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c", + "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b", + "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807", + "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf", + "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def", + "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad", + "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d", + "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849", + "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69", + "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666" + ], + "index": "pypi", + "version": "==22.6.0" + }, + "bleach": { + "hashes": [ + "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a", + "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c" + ], + "markers": "python_version >= '3.7'", + "version": "==5.0.1" + }, + "build": { + "hashes": [ + "sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437", + "sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0" + ], + "index": "pypi", + "version": "==0.8.0" + }, + "cached-property": { + "hashes": [ + "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130", + "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0" + ], + "version": "==1.5.2" + }, + "cerberus": { + "hashes": [ + "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c" + ], + "version": "==1.3.4" + }, + "certifi": { + "hashes": [ + "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", + "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==2022.6.15" + }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, + "chardet": { + "hashes": [ + "sha256:0368df2bfd78b5fc20572bb4e9bb7fb53e2c094f60ae9993339e8671d0afb8aa", + "sha256:d3e64f022d254183001eccc5db4040520c0f23b1a3f33d6413e099eb7f126557" + ], + "markers": "python_version >= '3.6'", + "version": "==5.0.0" + }, + "charset-normalizer": { + "hashes": [ + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==2.1.1" + }, + "click": { + "hashes": [ + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.3" + }, + "colorama": { + "hashes": [ + "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", + "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.4.5" + }, + "commonmark": { + "hashes": [ + "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", + "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" + ], + "version": "==0.9.1" + }, + "cryptography": { + "hashes": [ + "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59", + "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596", + "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3", + "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5", + "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab", + "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884", + "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82", + "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b", + "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441", + "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa", + "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d", + "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b", + "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a", + "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6", + "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157", + "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280", + "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282", + "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67", + "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8", + "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046", + "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327", + "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9" + ], + "version": "==37.0.4" + }, + "distlib": { + "hashes": [ + "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe", + "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c" + ], + "version": "==0.3.5" + }, + "docutils": { + "hashes": [ + "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6", + "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc" + ], + "markers": "python_version >= '3.7'", + "version": "==0.19" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3.5'", + "version": "==3.3" + }, + "importlib-metadata": { + "hashes": [ + "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670", + "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23" + ], + "markers": "python_version >= '3.7'", + "version": "==4.12.0" + }, + "jeepney": { + "hashes": [ + "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", + "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.8.0" + }, + "keyring": { + "hashes": [ + "sha256:0d9973f8891850f1ade5f26aafd06bb16865fbbae3fc56b0defb6a14a2624003", + "sha256:10d2a8639663fe2090705a00b8c47c687cacdf97598ea9c11456679fa974473a" + ], + "markers": "python_version >= '3.7'", + "version": "==23.8.2" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "orderedmultidict": { + "hashes": [ + "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad", + "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3" + ], + "version": "==1.0.1" + }, + "packaging": { + "hashes": [ + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.9" + }, + "pathspec": { + "hashes": [ + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + ], + "version": "==0.9.0" + }, + "pep517": { + "hashes": [ + "sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b", + "sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59" + ], + "markers": "python_version >= '3.6'", + "version": "==0.13.0" + }, + "pip": { + "hashes": [ + "sha256:3fd1929db052f056d7a998439176d3333fa1b3f6c1ad881de1885c0717608a4b", + "sha256:b61a374b5bc40a6e982426aede40c9b5a08ff20e640f5b56977f4f91fed1e39a" + ], + "markers": "python_version >= '3.7'", + "version": "==22.2.2" + }, + "pip-shims": { + "hashes": [ + "sha256:089e3586a92b1b8dbbc16b2d2859331dc1c412d3e3dbcd91d80e6b30d73db96c", + "sha256:2ae9f21c0155ca5c37d2734eb5f9a7d98c4c42a122d1ba3eddbacc9d9ea9fbae" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.3" + }, + "pipenv-setup": { + "extras": [ + "black" + ], + "hashes": [ + "sha256:0def7ec3363f58b38a43dc59b2078fcee67b47301fd51a41b8e34e6f79812b1a", + "sha256:6ceda7145a3088494d8ca68fded4b0473022dc62eb786a021c137632c44298b5" + ], + "index": "pypi", + "version": "==3.2.0" + }, + "pipfile": { + "hashes": [ + "sha256:f7d9f15de8b660986557eb3cc5391aa1a16207ac41bc378d03f414762d36c984" + ], + "version": "==0.0.2" + }, + "pkginfo": { + "hashes": [ + "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594", + "sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.8.3" + }, + "platformdirs": { + "hashes": [ + "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", + "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" + ], + "markers": "python_version >= '3.7'", + "version": "==2.5.2" + }, + "plette": { + "extras": [ + "validation" + ], + "hashes": [ + "sha256:46402c03e36d6eadddad2a5125990e322dd74f98160c8f2dcd832b2291858a26", + "sha256:d6c9b96981b347bddd333910b753b6091a2c1eb2ef85bb373b4a67c9d91dca16" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.2.3" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pygments": { + "hashes": [ + "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1", + "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42" + ], + "markers": "python_version >= '3.6'", + "version": "==2.13.0" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.2" + }, + "readme-renderer": { + "hashes": [ + "sha256:07b7ea234e03e58f77cc222e206e6abb8f4c0435becce5104794ee591f9301c5", + "sha256:9fa416704703e509eeb900696751c908ddeb2011319d93700d8f18baff887a69" + ], + "markers": "python_version >= '3.7'", + "version": "==37.0" + }, + "requests": { + "hashes": [ + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + ], + "index": "pypi", + "version": "==2.28.1" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", + "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + ], + "version": "==0.9.1" + }, + "requirementslib": { + "hashes": [ + "sha256:28924cf11a2fa91adb03f8431d80c2a8c3dc386f1c48fb2be9a58e4c39072354", + "sha256:d26ec6ad45e1ffce9532303543996c9c71a99dc65f783908f112e3f2aae7e49c" + ], + "markers": "python_version >= '3.7'", + "version": "==1.6.9" + }, + "rfc3986": { + "hashes": [ + "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "rich": { + "hashes": [ + "sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb", + "sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca" + ], + "markers": "python_version < '4' and python_full_version >= '3.6.3'", + "version": "==12.5.1" + }, + "secretstorage": { + "hashes": [ + "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", + "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.3.3" + }, + "setuptools": { + "hashes": [ + "sha256:7f4bc85450898a09f76ebf28b72fa25bc7111f6c7d665d514a60bba9c75ef2a9", + "sha256:a3ca5857c89f82f5c9410e8508cb32f4872a3bafd4aa7ae122a24ca33bccc750" + ], + "markers": "python_version >= '3.7'", + "version": "==65.2.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "tomlkit": { + "hashes": [ + "sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c", + "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83" + ], + "markers": "python_version >= '3.6' and python_version < '4'", + "version": "==0.11.4" + }, + "twine": { + "hashes": [ + "sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e", + "sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0" + ], + "index": "pypi", + "version": "==4.0.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", + "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" + ], + "markers": "python_version < '3.10'", + "version": "==4.3.0" + }, + "urllib3": { + "hashes": [ + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "version": "==1.26.12" + }, + "vistir": { + "hashes": [ + "sha256:6506888420ce1842f70c314739ad9853eb5823d195b5bec4a16251d172a48fc4", + "sha256:66e56f31ede7181bef1406a394b07c7b4db9910698c8269d26a12815c8e3ccf0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.5.6" + }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, + "wheel": { + "hashes": [ + "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a", + "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.37.1" + }, + "zipp": { + "hashes": [ + "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2", + "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009" + ], + "markers": "python_version >= '3.7'", + "version": "==3.8.1" + } + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cddfdcc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools>=65.0.0", "wheel"] + +[project] +authors = [{name = "Moritz Ulmer", email = "moritz.ulmer@posteo.de"}] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "Topic :: Security", +] +dependencies = [ + "django >= 2.2", + "djangorestframework >= 3.0", + "dry-rest-permissions >= 0.1", + "pyjwt[crypto] ~= 2.4.0", + "requests >= 2.0", +] +description = "Middleware to allow authorization using Keycloak and Django" +keywords = ["keycloak", "django", "authorization"] +license = {file = "LICENSE"} +name = "django-keycloak-tere" +readme = "README.md" +requires-python = ">=3.8" +version = "1.2.0" + +[project.optional-dependencies] +dev = [ + "build ~= 0.8", + "twine ~= 4.0", + "black ~= 22.6", +] + +[tool.setuptools] +packages = ["django_keycloak"] +package-dir = {"django_keycloak" = "src/django_keycloak"} + +[project.urls] +Homepage = "https://github.com/moritz89/django-keycloak-tere" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 89f481f..0000000 --- a/setup.cfg +++ /dev/null @@ -1,25 +0,0 @@ -[metadata] -name = django_uw_keycloak -version = 1.2.1 -description = Middleware to allow authorization using Keycloak and Django -long_description = file: README.md -long_description_content_type = text/markdown; charset=UTF-8; variant=GFM -url = https://github.com/urbanplatform/django-keycloak-auth -author = Ubiwhere -author_email = urbanplatform@ubiwhere.com -license = MIT -classifiers = - Environment :: Web Environment - Framework :: Django - Intended Audience :: Developers - Topic :: Security - -[options] -include_package_data = true -packages = find: -install_requires = - Django >= "2.2" - djangorestframework >= "3.0" - dry-rest-permissions >= "0.1" - python-keycloak >= 2.6.0 - cachetools >= "5.0.0" diff --git a/setup.py b/setup.py index d04f632..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,3 @@ from setuptools import setup -import os -if os.environ.get("CI_COMMIT_TAG"): - version = os.environ["CI_COMMIT_TAG"] - setup(version=version) -else: - setup() +setup() diff --git a/django_keycloak/__init__.py b/src/django_keycloak/__init__.py similarity index 100% rename from django_keycloak/__init__.py rename to src/django_keycloak/__init__.py diff --git a/django_keycloak/admin.py b/src/django_keycloak/admin.py similarity index 100% rename from django_keycloak/admin.py rename to src/django_keycloak/admin.py diff --git a/django_keycloak/api/__init__.py b/src/django_keycloak/api/__init__.py similarity index 100% rename from django_keycloak/api/__init__.py rename to src/django_keycloak/api/__init__.py diff --git a/django_keycloak/api/filters.py b/src/django_keycloak/api/filters.py similarity index 100% rename from django_keycloak/api/filters.py rename to src/django_keycloak/api/filters.py diff --git a/django_keycloak/api/serializers.py b/src/django_keycloak/api/serializers.py similarity index 100% rename from django_keycloak/api/serializers.py rename to src/django_keycloak/api/serializers.py diff --git a/django_keycloak/api/urls.py b/src/django_keycloak/api/urls.py similarity index 100% rename from django_keycloak/api/urls.py rename to src/django_keycloak/api/urls.py diff --git a/django_keycloak/api/views.py b/src/django_keycloak/api/views.py similarity index 100% rename from django_keycloak/api/views.py rename to src/django_keycloak/api/views.py diff --git a/django_keycloak/apps.py b/src/django_keycloak/apps.py similarity index 100% rename from django_keycloak/apps.py rename to src/django_keycloak/apps.py diff --git a/django_keycloak/authentication.py b/src/django_keycloak/authentication.py similarity index 100% rename from django_keycloak/authentication.py rename to src/django_keycloak/authentication.py diff --git a/django_keycloak/backends.py b/src/django_keycloak/backends.py similarity index 100% rename from django_keycloak/backends.py rename to src/django_keycloak/backends.py diff --git a/django_keycloak/config.py b/src/django_keycloak/config.py similarity index 100% rename from django_keycloak/config.py rename to src/django_keycloak/config.py diff --git a/django_keycloak/connector.py b/src/django_keycloak/connector.py similarity index 100% rename from django_keycloak/connector.py rename to src/django_keycloak/connector.py diff --git a/django_keycloak/errors.py b/src/django_keycloak/errors.py similarity index 100% rename from django_keycloak/errors.py rename to src/django_keycloak/errors.py diff --git a/django_keycloak/management/__init__.py b/src/django_keycloak/management/__init__.py similarity index 100% rename from django_keycloak/management/__init__.py rename to src/django_keycloak/management/__init__.py diff --git a/django_keycloak/management/commands/__init__.py b/src/django_keycloak/management/commands/__init__.py similarity index 100% rename from django_keycloak/management/commands/__init__.py rename to src/django_keycloak/management/commands/__init__.py diff --git a/django_keycloak/management/commands/sync_keycloak_users.py b/src/django_keycloak/management/commands/sync_keycloak_users.py similarity index 100% rename from django_keycloak/management/commands/sync_keycloak_users.py rename to src/django_keycloak/management/commands/sync_keycloak_users.py diff --git a/django_keycloak/managers.py b/src/django_keycloak/managers.py similarity index 100% rename from django_keycloak/managers.py rename to src/django_keycloak/managers.py diff --git a/django_keycloak/middleware.py b/src/django_keycloak/middleware.py similarity index 100% rename from django_keycloak/middleware.py rename to src/django_keycloak/middleware.py diff --git a/django_keycloak/migrations/0001_initial.py b/src/django_keycloak/migrations/0001_initial.py similarity index 100% rename from django_keycloak/migrations/0001_initial.py rename to src/django_keycloak/migrations/0001_initial.py diff --git a/django_keycloak/migrations/0001_redo_migrations_0001to0005.py b/src/django_keycloak/migrations/0001_redo_migrations_0001to0005.py similarity index 100% rename from django_keycloak/migrations/0001_redo_migrations_0001to0005.py rename to src/django_keycloak/migrations/0001_redo_migrations_0001to0005.py diff --git a/django_keycloak/migrations/0002_auto_20210209_1503.py b/src/django_keycloak/migrations/0002_auto_20210209_1503.py similarity index 100% rename from django_keycloak/migrations/0002_auto_20210209_1503.py rename to src/django_keycloak/migrations/0002_auto_20210209_1503.py diff --git a/django_keycloak/migrations/0003_auto_20210406_1426.py b/src/django_keycloak/migrations/0003_auto_20210406_1426.py similarity index 100% rename from django_keycloak/migrations/0003_auto_20210406_1426.py rename to src/django_keycloak/migrations/0003_auto_20210406_1426.py diff --git a/django_keycloak/migrations/0004_keycloakuserautoid.py b/src/django_keycloak/migrations/0004_keycloakuserautoid.py similarity index 100% rename from django_keycloak/migrations/0004_keycloakuserautoid.py rename to src/django_keycloak/migrations/0004_keycloakuserautoid.py diff --git a/django_keycloak/migrations/0005_auto_20211231_1702.py b/src/django_keycloak/migrations/0005_auto_20211231_1702.py similarity index 100% rename from django_keycloak/migrations/0005_auto_20211231_1702.py rename to src/django_keycloak/migrations/0005_auto_20211231_1702.py diff --git a/django_keycloak/migrations/__init__.py b/src/django_keycloak/migrations/__init__.py similarity index 100% rename from django_keycloak/migrations/__init__.py rename to src/django_keycloak/migrations/__init__.py diff --git a/django_keycloak/mixins.py b/src/django_keycloak/mixins.py similarity index 100% rename from django_keycloak/mixins.py rename to src/django_keycloak/mixins.py diff --git a/django_keycloak/models.py b/src/django_keycloak/models.py similarity index 100% rename from django_keycloak/models.py rename to src/django_keycloak/models.py diff --git a/django_keycloak/tasks.py b/src/django_keycloak/tasks.py similarity index 100% rename from django_keycloak/tasks.py rename to src/django_keycloak/tasks.py diff --git a/django_keycloak/token.py b/src/django_keycloak/token.py similarity index 100% rename from django_keycloak/token.py rename to src/django_keycloak/token.py From dfb59a37a04a18277ae59cf572782254acc7b011 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Mon, 10 Oct 2022 16:54:55 +0200 Subject: [PATCH 22/73] Add CI test config Why: - Allow CI tests to be executed This change addresses the need by: - Adding test site - Adding Github action for test CI - Adding Keycloak service --- .github/workflows/test.yml | 57 + Pipfile | 20 - Pipfile.lock | 779 ------- poetry.lock | 929 ++++++++ pyproject.toml | 53 +- tests/realm-export.json | 2177 +++++++++++++++++++ tests/start.sh | 16 + tests/test_site/manage.py | 22 + tests/test_site/test_app/__init__.py | 0 tests/test_site/test_app/apps.py | 5 + tests/test_site/test_app/tests/__init__.py | 0 tests/test_site/test_app/tests/test_init.py | 30 + tests/test_site/test_site/__init__.py | 0 tests/test_site/test_site/asgi.py | 16 + tests/test_site/test_site/settings.py | 148 ++ tests/test_site/test_site/urls.py | 21 + tests/test_site/test_site/wsgi.py | 16 + 17 files changed, 3463 insertions(+), 826 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 Pipfile delete mode 100644 Pipfile.lock create mode 100644 poetry.lock create mode 100644 tests/realm-export.json create mode 100755 tests/start.sh create mode 100755 tests/test_site/manage.py create mode 100644 tests/test_site/test_app/__init__.py create mode 100644 tests/test_site/test_app/apps.py create mode 100644 tests/test_site/test_app/tests/__init__.py create mode 100644 tests/test_site/test_app/tests/test_init.py create mode 100644 tests/test_site/test_site/__init__.py create mode 100644 tests/test_site/test_site/asgi.py create mode 100644 tests/test_site/test_site/settings.py create mode 100644 tests/test_site/test_site/urls.py create mode 100644 tests/test_site/test_site/wsgi.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..dd9c87e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,57 @@ +name: Python package + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + env: + python-version: "3.8" + KEYCLOAK_HOST: localhost + KEYCLOAK_PORT: 8080 + KEYCLOAK_ADMIN_USER: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KEYCLOAK_REALM: test-realm + KEYCLOAK_CLIENT_ID: test-client + KEYCLOAK_CLIENT_SECRET_KEY: f6974574-c773-4554-826d-06946cd55e98 + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + + services: + keycloak: + image: keycloak:15 + ports: + - 8080:8080 + - 9990:9990 + options: >- + -b 0.0.0.0 + -Djboss.bind.address.management=0.0.0.0 + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + # - name: Set up the cache + # uses: actions/cache@v1 + # env: + # cache-name: cache-python-packages + # with: + # path: .venv + # key: ${{ matrix.python-version }}-${{ env.cache-name }} + # restore-keys: ${{ matrix.python-version }} + - name: Set up the project + run: | + pip install poetry + poetry config virtualenvs.in-project true + poetry install + - name: Test with pytest + run: ./tests/start.sh diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 7e69378..0000000 --- a/Pipfile +++ /dev/null @@ -1,20 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -Django = ">=2.2" -djangorestframework = ">=3.0" -dry-rest-permissions = ">=0.1" -PyJWT = {version = "~=2.4.0", extras = ["crypto"]} -requests = ">=2.0" - -[dev-packages] -build = "~=0.8" -twine = "~=4.0" -black = "~=22.6" -setuptools = "~=65.0" - -[requires] -python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index b0d4d84..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,779 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "cf7c8d55ebcf5a2c3c1e42edc42858a8a0d964c0c37f6618ee12614a744d21a9" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.8" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "asgiref": { - "hashes": [ - "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", - "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" - ], - "markers": "python_version >= '3.7'", - "version": "==3.5.2" - }, - "backports.zoneinfo": { - "hashes": [ - "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf", - "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328", - "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546", - "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6", - "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570", - "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", - "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7", - "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", - "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722", - "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582", - "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc", - "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b", - "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", - "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08", - "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", - "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" - ], - "markers": "python_version < '3.9'", - "version": "==0.2.1" - }, - "certifi": { - "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2022.6.15" - }, - "cffi": { - "hashes": [ - "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", - "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", - "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", - "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", - "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", - "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", - "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", - "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", - "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", - "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", - "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", - "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", - "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", - "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", - "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", - "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", - "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", - "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", - "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", - "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", - "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", - "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", - "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", - "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", - "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", - "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", - "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", - "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", - "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", - "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", - "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", - "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", - "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", - "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", - "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", - "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", - "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", - "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", - "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", - "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", - "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", - "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", - "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", - "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", - "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", - "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", - "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", - "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", - "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", - "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", - "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", - "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", - "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", - "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", - "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", - "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", - "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", - "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", - "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", - "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", - "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", - "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", - "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", - "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" - ], - "version": "==1.15.1" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2.1.1" - }, - "cryptography": { - "hashes": [ - "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59", - "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596", - "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3", - "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5", - "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab", - "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884", - "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82", - "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b", - "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441", - "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa", - "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d", - "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b", - "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a", - "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6", - "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157", - "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280", - "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282", - "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67", - "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8", - "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046", - "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327", - "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9" - ], - "version": "==37.0.4" - }, - "django": { - "hashes": [ - "sha256:031ccb717782f6af83a0063a1957686e87cb4581ea61b47b3e9addf60687989a", - "sha256:032f8a6fc7cf05ccd1214e4a2e21dfcd6a23b9d575c6573cacc8c67828dbe642" - ], - "index": "pypi", - "version": "==4.1" - }, - "djangorestframework": { - "hashes": [ - "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee", - "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa" - ], - "index": "pypi", - "version": "==3.13.1" - }, - "dry-rest-permissions": { - "hashes": [ - "sha256:1f40461184063390e5b24e9c5602eb8cc8c3c2433c796f39a5332065bfbddd2b", - "sha256:f3fe685760004ce182801602819b43ebfa922e587036f1f5a5c10ffcfa646039" - ], - "index": "pypi", - "version": "==0.1.10" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "version": "==2.21" - }, - "pyjwt": { - "extras": [ - "crypto" - ], - "hashes": [ - "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf", - "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba" - ], - "index": "pypi", - "version": "==2.4.0" - }, - "pytz": { - "hashes": [ - "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", - "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" - ], - "version": "==2022.2.1" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "index": "pypi", - "version": "==2.28.1" - }, - "sqlparse": { - "hashes": [ - "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", - "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" - ], - "markers": "python_version >= '3.5'", - "version": "==0.4.2" - }, - "urllib3": { - "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.12" - } - }, - "develop": { - "attrs": { - "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" - ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" - }, - "black": { - "hashes": [ - "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90", - "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c", - "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78", - "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4", - "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee", - "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e", - "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e", - "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6", - "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9", - "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c", - "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256", - "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f", - "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2", - "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c", - "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b", - "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807", - "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf", - "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def", - "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad", - "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d", - "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849", - "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69", - "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666" - ], - "index": "pypi", - "version": "==22.6.0" - }, - "bleach": { - "hashes": [ - "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a", - "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0.1" - }, - "build": { - "hashes": [ - "sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437", - "sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0" - ], - "index": "pypi", - "version": "==0.8.0" - }, - "cached-property": { - "hashes": [ - "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130", - "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0" - ], - "version": "==1.5.2" - }, - "cerberus": { - "hashes": [ - "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c" - ], - "version": "==1.3.4" - }, - "certifi": { - "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2022.6.15" - }, - "cffi": { - "hashes": [ - "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", - "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", - "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", - "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", - "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", - "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", - "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", - "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", - "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", - "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", - "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", - "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", - "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", - "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", - "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", - "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", - "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", - "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", - "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", - "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", - "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", - "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", - "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", - "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", - "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", - "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", - "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", - "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", - "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", - "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", - "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", - "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", - "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", - "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", - "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", - "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", - "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", - "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", - "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", - "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", - "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", - "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", - "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", - "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", - "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", - "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", - "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", - "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", - "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", - "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", - "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", - "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", - "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", - "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", - "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", - "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", - "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", - "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", - "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", - "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", - "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", - "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", - "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", - "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" - ], - "version": "==1.15.1" - }, - "chardet": { - "hashes": [ - "sha256:0368df2bfd78b5fc20572bb4e9bb7fb53e2c094f60ae9993339e8671d0afb8aa", - "sha256:d3e64f022d254183001eccc5db4040520c0f23b1a3f33d6413e099eb7f126557" - ], - "markers": "python_version >= '3.6'", - "version": "==5.0.0" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==2.1.1" - }, - "click": { - "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.3" - }, - "colorama": { - "hashes": [ - "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", - "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.5" - }, - "commonmark": { - "hashes": [ - "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", - "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" - ], - "version": "==0.9.1" - }, - "cryptography": { - "hashes": [ - "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59", - "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596", - "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3", - "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5", - "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab", - "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884", - "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82", - "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b", - "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441", - "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa", - "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d", - "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b", - "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a", - "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6", - "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157", - "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280", - "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282", - "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67", - "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8", - "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046", - "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327", - "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9" - ], - "version": "==37.0.4" - }, - "distlib": { - "hashes": [ - "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe", - "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c" - ], - "version": "==0.3.5" - }, - "docutils": { - "hashes": [ - "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6", - "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc" - ], - "markers": "python_version >= '3.7'", - "version": "==0.19" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "importlib-metadata": { - "hashes": [ - "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670", - "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23" - ], - "markers": "python_version >= '3.7'", - "version": "==4.12.0" - }, - "jeepney": { - "hashes": [ - "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", - "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.8.0" - }, - "keyring": { - "hashes": [ - "sha256:0d9973f8891850f1ade5f26aafd06bb16865fbbae3fc56b0defb6a14a2624003", - "sha256:10d2a8639663fe2090705a00b8c47c687cacdf97598ea9c11456679fa974473a" - ], - "markers": "python_version >= '3.7'", - "version": "==23.8.2" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "orderedmultidict": { - "hashes": [ - "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad", - "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3" - ], - "version": "==1.0.1" - }, - "packaging": { - "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.9" - }, - "pathspec": { - "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" - ], - "version": "==0.9.0" - }, - "pep517": { - "hashes": [ - "sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b", - "sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59" - ], - "markers": "python_version >= '3.6'", - "version": "==0.13.0" - }, - "pip": { - "hashes": [ - "sha256:3fd1929db052f056d7a998439176d3333fa1b3f6c1ad881de1885c0717608a4b", - "sha256:b61a374b5bc40a6e982426aede40c9b5a08ff20e640f5b56977f4f91fed1e39a" - ], - "markers": "python_version >= '3.7'", - "version": "==22.2.2" - }, - "pip-shims": { - "hashes": [ - "sha256:089e3586a92b1b8dbbc16b2d2859331dc1c412d3e3dbcd91d80e6b30d73db96c", - "sha256:2ae9f21c0155ca5c37d2734eb5f9a7d98c4c42a122d1ba3eddbacc9d9ea9fbae" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.3" - }, - "pipenv-setup": { - "extras": [ - "black" - ], - "hashes": [ - "sha256:0def7ec3363f58b38a43dc59b2078fcee67b47301fd51a41b8e34e6f79812b1a", - "sha256:6ceda7145a3088494d8ca68fded4b0473022dc62eb786a021c137632c44298b5" - ], - "index": "pypi", - "version": "==3.2.0" - }, - "pipfile": { - "hashes": [ - "sha256:f7d9f15de8b660986557eb3cc5391aa1a16207ac41bc378d03f414762d36c984" - ], - "version": "==0.0.2" - }, - "pkginfo": { - "hashes": [ - "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594", - "sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.8.3" - }, - "platformdirs": { - "hashes": [ - "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", - "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" - ], - "markers": "python_version >= '3.7'", - "version": "==2.5.2" - }, - "plette": { - "extras": [ - "validation" - ], - "hashes": [ - "sha256:46402c03e36d6eadddad2a5125990e322dd74f98160c8f2dcd832b2291858a26", - "sha256:d6c9b96981b347bddd333910b753b6091a2c1eb2ef85bb373b4a67c9d91dca16" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.2.3" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "version": "==2.21" - }, - "pygments": { - "hashes": [ - "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1", - "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42" - ], - "markers": "python_version >= '3.6'", - "version": "==2.13.0" - }, - "pyparsing": { - "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "readme-renderer": { - "hashes": [ - "sha256:07b7ea234e03e58f77cc222e206e6abb8f4c0435becce5104794ee591f9301c5", - "sha256:9fa416704703e509eeb900696751c908ddeb2011319d93700d8f18baff887a69" - ], - "markers": "python_version >= '3.7'", - "version": "==37.0" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "index": "pypi", - "version": "==2.28.1" - }, - "requests-toolbelt": { - "hashes": [ - "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", - "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" - ], - "version": "==0.9.1" - }, - "requirementslib": { - "hashes": [ - "sha256:28924cf11a2fa91adb03f8431d80c2a8c3dc386f1c48fb2be9a58e4c39072354", - "sha256:d26ec6ad45e1ffce9532303543996c9c71a99dc65f783908f112e3f2aae7e49c" - ], - "markers": "python_version >= '3.7'", - "version": "==1.6.9" - }, - "rfc3986": { - "hashes": [ - "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", - "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "rich": { - "hashes": [ - "sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb", - "sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca" - ], - "markers": "python_version < '4' and python_full_version >= '3.6.3'", - "version": "==12.5.1" - }, - "secretstorage": { - "hashes": [ - "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", - "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.3.3" - }, - "setuptools": { - "hashes": [ - "sha256:7f4bc85450898a09f76ebf28b72fa25bc7111f6c7d665d514a60bba9c75ef2a9", - "sha256:a3ca5857c89f82f5c9410e8508cb32f4872a3bafd4aa7ae122a24ca33bccc750" - ], - "markers": "python_version >= '3.7'", - "version": "==65.2.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "tomlkit": { - "hashes": [ - "sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c", - "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83" - ], - "markers": "python_version >= '3.6' and python_version < '4'", - "version": "==0.11.4" - }, - "twine": { - "hashes": [ - "sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e", - "sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0" - ], - "index": "pypi", - "version": "==4.0.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", - "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" - ], - "markers": "python_version < '3.10'", - "version": "==4.3.0" - }, - "urllib3": { - "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.12" - }, - "vistir": { - "hashes": [ - "sha256:6506888420ce1842f70c314739ad9853eb5823d195b5bec4a16251d172a48fc4", - "sha256:66e56f31ede7181bef1406a394b07c7b4db9910698c8269d26a12815c8e3ccf0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.5.6" - }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, - "wheel": { - "hashes": [ - "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a", - "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.37.1" - }, - "zipp": { - "hashes": [ - "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2", - "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009" - ], - "markers": "python_version >= '3.7'", - "version": "==3.8.1" - } - } -} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..c97d529 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,929 @@ +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "asgiref" +version = "3.5.2" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "asttokens" +version = "2.0.8" +description = "Annotate AST trees with source code positions" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid (<=2.5.3)", "pytest"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "backports.zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +tzdata = ["tzdata"] + +[[package]] +name = "black" +version = "22.10.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cachetools" +version = "5.2.0" +description = "Extensible memoizing collections and decorators" +category = "main" +optional = false +python-versions = "~=3.7" + +[[package]] +name = "certifi" +version = "2022.9.24" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.5.0" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "38.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "Django" +version = "4.1.2" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +asgiref = ">=3.5.2,<4" +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +sqlparse = ">=0.2.2" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "djangorestframework" +version = "3.14.0" +description = "Web APIs for Django, made easy." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +django = ">=3.0" +pytz = "*" + +[[package]] +name = "dry-rest-permissions" +version = "0.1.10" +description = "Rules based permissions for the Django Rest Framework" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "executing" +version = "1.1.1" +description = "Get the currently executing AST node of a frame, and other information" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +tests = ["asttokens", "littleutils", "pytest", "rich"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "ipdb" +version = "0.13.9" +description = "IPython-enabled pdb" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +decorator = {version = "*", markers = "python_version > \"3.6\""} +ipython = {version = ">=7.17.0", markers = "python_version > \"3.6\""} +setuptools = "*" +toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} + +[[package]] +name = "ipython" +version = "8.5.0" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">3.0.1,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["Sphinx (>=1.3)", "black", "curio", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.19)", "pandas", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "testpath", "trio"] +black = ["black"] +doc = ["Sphinx (>=1.3)"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test_extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + +[[package]] +name = "jedi" +version = "0.18.1" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pathspec" +version = "0.10.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.31" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "Pygments" +version = "2.13.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "PyJWT" +version = "2.4.0" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cryptography = {version = ">=3.3.1", optional = true, markers = "extra == \"crypto\""} + +[package.extras] +crypto = ["cryptography (>=3.3.1)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.3.1)", "mypy", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pytz" +version = "2022.4" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "setuptools" +version = "65.4.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sqlparse" +version = "0.4.3" +description = "A non-validating SQL parser." +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "stack-data" +version = "0.5.1" +description = "Extract data from python stack frames and tracebacks for informative displays" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +asttokens = "*" +executing = "*" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "traitlets" +version = "5.4.0" +description = "" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pre-commit", "pytest"] + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tzdata" +version = "2022.4" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" + +[[package]] +name = "urllib3" +version = "1.26.12" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + +[metadata] +lock-version = "1.1" +python-versions = ">=3.8,<4" +content-hash = "f90692d72c9e82171db116b5ec209e1c973e69e313f0bb45a9042305755ee14c" + +[metadata.files] +appnope = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] +asgiref = [ + {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, + {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, +] +asttokens = [ + {file = "asttokens-2.0.8-py2.py3-none-any.whl", hash = "sha256:e3305297c744ae53ffa032c45dc347286165e4ffce6875dc662b205db0623d86"}, + {file = "asttokens-2.0.8.tar.gz", hash = "sha256:c61e16246ecfb2cde2958406b4c8ebc043c9e6d73aaa83c941673b35e5d3a76b"}, +] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] +"backports.zoneinfo" = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] +black = [ + {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, + {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, + {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, + {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, + {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, + {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, + {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, + {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, + {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, + {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, + {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, + {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, + {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, + {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, + {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, + {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, + {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, + {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, + {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, + {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, + {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, +] +cachetools = [ + {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"}, + {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, +] +certifi = [ + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, +] +cffi = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +coverage = [ + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, +] +cryptography = [ + {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, + {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, + {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, + {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, + {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, + {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, + {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, +] +decorator = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] +Django = [ + {file = "Django-4.1.2-py3-none-any.whl", hash = "sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793"}, + {file = "Django-4.1.2.tar.gz", hash = "sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f"}, +] +djangorestframework = [ + {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, + {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, +] +dry-rest-permissions = [ + {file = "dry-rest-permissions-0.1.10.tar.gz", hash = "sha256:1f40461184063390e5b24e9c5602eb8cc8c3c2433c796f39a5332065bfbddd2b"}, + {file = "dry_rest_permissions-0.1.10-py2.py3-none-any.whl", hash = "sha256:f3fe685760004ce182801602819b43ebfa922e587036f1f5a5c10ffcfa646039"}, +] +executing = [ + {file = "executing-1.1.1-py2.py3-none-any.whl", hash = "sha256:236ea5f059a38781714a8bfba46a70fad3479c2f552abee3bbafadc57ed111b8"}, + {file = "executing-1.1.1.tar.gz", hash = "sha256:b0d7f8dcc2bac47ce6e39374397e7acecea6fdc380a6d5323e26185d70f38ea8"}, +] +idna = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] +ipdb = [ + {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, +] +ipython = [ + {file = "ipython-8.5.0-py3-none-any.whl", hash = "sha256:6f090e29ab8ef8643e521763a4f1f39dc3914db643122b1e9d3328ff2e43ada2"}, + {file = "ipython-8.5.0.tar.gz", hash = "sha256:097bdf5cd87576fd066179c9f7f208004f7a6864ee1b20f37d346c0bcb099f84"}, +] +jedi = [ + {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, + {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, +] +matplotlib-inline = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +parso = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] +pathspec = [ + {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, + {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, +] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.31-py3-none-any.whl", hash = "sha256:9696f386133df0fc8ca5af4895afe5d78f5fcfe5258111c2a79a1c3e41ffa96d"}, + {file = "prompt_toolkit-3.0.31.tar.gz", hash = "sha256:9ada952c9d1787f52ff6d5f3484d0b4df8952787c087edf6a1f7c2cb1ea88148"}, +] +ptyprocess = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] +pure-eval = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +Pygments = [ + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, +] +PyJWT = [ + {file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"}, + {file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"}, +] +pytz = [ + {file = "pytz-2022.4-py2.py3-none-any.whl", hash = "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91"}, + {file = "pytz-2022.4.tar.gz", hash = "sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174"}, +] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] +setuptools = [ + {file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"}, + {file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +sqlparse = [ + {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, + {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, +] +stack-data = [ + {file = "stack_data-0.5.1-py3-none-any.whl", hash = "sha256:5120731a18ba4c82cefcf84a945f6f3e62319ef413bfc210e32aca3a69310ba2"}, + {file = "stack_data-0.5.1.tar.gz", hash = "sha256:95eb784942e861a3d80efd549ff9af6cf847d88343a12eb681d7157cfcb6e32b"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +traitlets = [ + {file = "traitlets-5.4.0-py3-none-any.whl", hash = "sha256:93663cc8236093d48150e2af5e2ed30fc7904a11a6195e21bab0408af4e6d6c8"}, + {file = "traitlets-5.4.0.tar.gz", hash = "sha256:3f2c4e435e271592fe4390f1746ea56836e3a080f84e7833f0f801d9613fec39"}, +] +typing-extensions = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] +tzdata = [ + {file = "tzdata-2022.4-py2.py3-none-any.whl", hash = "sha256:74da81ecf2b3887c94e53fc1d466d4362aaf8b26fc87cda18f22004544694583"}, + {file = "tzdata-2022.4.tar.gz", hash = "sha256:ada9133fbd561e6ec3d1674d3fba50251636e918aa97bd59d63735bef5a513bb"}, +] +urllib3 = [ + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] diff --git a/pyproject.toml b/pyproject.toml index cddfdcc..ea31688 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,17 @@ [build-system] -build-backend = "setuptools.build_meta" -requires = ["setuptools>=65.0.0", "wheel"] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" -[project] -authors = [{name = "Moritz Ulmer", email = "moritz.ulmer@posteo.de"}] +[tool.poetry] +name = "django_uw_keycloak" +version = "1.2.0" +description = "Middleware to allow authorization using Keycloak and Django" +authors = [ + "Ubiwhere ", + "Moritz Ulmer " +] +license = "MIT" +repository = "https://github.com/urbanplatform/django-keycloak-auth" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", @@ -13,31 +21,22 @@ classifiers = [ "Intended Audience :: Developers", "Topic :: Security", ] -dependencies = [ - "django >= 2.2", - "djangorestframework >= 3.0", - "dry-rest-permissions >= 0.1", - "pyjwt[crypto] ~= 2.4.0", - "requests >= 2.0", -] -description = "Middleware to allow authorization using Keycloak and Django" keywords = ["keycloak", "django", "authorization"] -license = {file = "LICENSE"} -name = "django-keycloak-tere" readme = "README.md" -requires-python = ">=3.8" -version = "1.2.0" - -[project.optional-dependencies] -dev = [ - "build ~= 0.8", - "twine ~= 4.0", - "black ~= 22.6", +packages = [ + { include = "src" } ] -[tool.setuptools] -packages = ["django_keycloak"] -package-dir = {"django_keycloak" = "src/django_keycloak"} +[tool.poetry.dependencies] +python = ">=3.8,<4" +django = ">=2.2" +djangorestframework = ">=3.0" +dry-rest-permissions = ">=0.1" +pyjwt = {version = "~=2.4.0", extras = ["crypto"]} +requests = ">=2.28" +cachetools = ">=5.0" -[project.urls] -Homepage = "https://github.com/moritz89/django-keycloak-tere" +[tool.poetry.dev-dependencies] +black = "~=22.6" +coverage = "~=6.4" +ipdb = "*" \ No newline at end of file diff --git a/tests/realm-export.json b/tests/realm-export.json new file mode 100644 index 0000000..ea9c097 --- /dev/null +++ b/tests/realm-export.json @@ -0,0 +1,2177 @@ +{ + "id": "test-realm", + "realm": "test-realm", + "displayName": "test-realm", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "305fdfc7-5e49-4797-8cb7-db3c483a678b", + "name": "default-roles-test-realm", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ] + }, + "clientRole": false, + "containerId": "test-realm", + "attributes": {} + }, + { + "id": "291b1491-fb37-4b78-9c88-01595e814efe", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "test-realm", + "attributes": {} + }, + { + "id": "8504ebf9-516d-4175-87e5-b6a4481201e5", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "test-realm", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "9f5ea67a-dd42-4d6d-bbac-fbb39bf392f2", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "ac20b228-484b-43c1-b136-f975597c71b9", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "d0b13bca-7007-49b1-adff-1a67903e2d78", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "dbaa223d-01d8-40c2-ba6a-b659b592b981", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "be3f4f7f-0ef4-4a5c-9d0b-c64ef96cc92d", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "3c5420f3-edc4-4f41-969b-cc871a04ee4f", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "d40c4da9-9feb-4fc3-bd92-e9c0152944cb", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "93b16ea5-db53-4709-8b27-0deb18d74c5a", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "5c3eae4e-1d14-4f68-ae9a-0e4d279db58b", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "e7b9ddba-9f0a-44c8-9428-efbd027fc484", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "8a693097-dea2-49b1-a26f-d524a38a89f2", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "6c2c51e9-ef0c-4fb2-ba99-1baf8882637e", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "fe3f05a4-a35b-4af1-bc2c-9b5d68fe1b61", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "70cd1697-4693-428e-a556-727d554bab55", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "create-client", + "view-clients", + "view-authorization", + "view-users", + "manage-authorization", + "manage-users", + "view-realm", + "query-clients", + "impersonation", + "query-users", + "view-events", + "manage-realm", + "manage-identity-providers", + "manage-clients", + "view-identity-providers", + "query-groups", + "manage-events", + "query-realms" + ] + } + }, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "b9fd96e0-24f2-4e68-9476-19dc51badd6e", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "fc6d2ab2-4801-49ba-a3ad-e28b921ad887", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "ed42ccd8-bf88-4bf7-9799-ac65a6e168f9", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "6861be93-094d-4da7-975b-cc7f7834227a", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "f828a3ae-1989-4d4f-a1ba-c9ac70cf3cca", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "test-client": [ + { + "id": "f18bdd68-1922-4131-8e36-9a28dbbd1a20", + "name": "admin", + "composite": false, + "clientRole": true, + "containerId": "42f4e729-0845-47fa-b810-73effdac174d", + "attributes": {} + } + ], + "account-console": [], + "broker": [], + "account": [ + { + "id": "44fd5143-9ce6-448f-91c0-27c503c2ac31", + "name": "manage-account", + "composite": false, + "clientRole": true, + "containerId": "b94a34e4-07ed-430a-937a-524ec283f133", + "attributes": {} + }, + { + "id": "7071637d-b154-4ad9-b85b-28c5a2c38ab9", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "b94a34e4-07ed-430a-937a-524ec283f133", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "305fdfc7-5e49-4797-8cb7-db3c483a678b", + "name": "default-roles-test-realm", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "test-realm" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "55c9883f-5f3e-42ce-91b8-6077fa1aedcc", + "createdTimestamp": 1665408867130, + "username": "service-account-test-client", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "test-client", + "disableableCredentialTypes": [], + "requiredActions": [], + "clientRoles": { + "realm-management": [ + "view-users", + "manage-users" + ] + }, + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "b94a34e4-07ed-430a-937a-524ec283f133", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/test-realm/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/test-realm/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "0b32a005-e2dd-4a9b-bd47-bd489dc0acb8", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/test-realm/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/test-realm/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9f74ca03-cd65-4335-8755-d1735ba19e3e", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "55b4d2a3-f1be-4b7d-9944-bcd5b60447db", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "e2d1436d-b71e-4184-a234-348beeb7447e", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "68488b0b-5f97-487c-a819-5611c4fd5eec", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/test-realm/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/test-realm/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "c62260f4-4039-4132-b38b-8af5eaa10017", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "42f4e729-0845-47fa-b810-73effdac174d", + "clientId": "test-client", + "name": "Test client", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "f6974574-c773-4554-826d-06946cd55e98", + "redirectUris": [ + "https://www.keycloak.org/app/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "e9e07797-737f-4dad-a797-8123627de266", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "d9e50279-8961-4352-b748-1dafeb707d25", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "ed88c706-fc65-4b50-bf05-f849b4c0532a", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "b5497d53-fb7e-4cc3-81ff-68f2061d079e", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "cf829ad2-4347-4436-a016-18da66e54c79", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "2847ef05-5d1e-4211-b16a-5d474834c528", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "0617dc30-f043-491c-b2fb-67ecc64e9bce", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "5ffaa1a0-56f3-4395-a81a-160f58edc37c", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "6eeb72f3-ca3d-4fa3-be75-4aa8c69ea6ab", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "b98f31b5-eb47-4429-a74a-3b6d6ca9c5d6", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "5b6a6b2d-f599-41b7-b0c8-bbecfd13280e", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "dd5a0d47-8d1b-49c8-9c47-679a705cb59a", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "05a1cfcd-ac93-4157-9a9c-3a83bb8c94aa", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "2d468904-10b1-4f15-a6fe-b9318eb7af1a", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "53b02a27-6b63-4f10-9b87-428e3c453a1a", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "d5a02ddc-02d1-451f-9c7e-97b33fda2109", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "c7b33cb5-dc05-489f-bf8e-b582d24a89c8", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ae072dfb-ea99-4104-be08-1e9c43461fb9", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "cd54095f-dd0a-493a-be31-ca7755041668", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "3bb08518-f6c5-43fc-b564-13252ee346eb", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "438070db-06a2-448b-a06b-7b7d5d5c9187", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "af3d04d0-55cd-4b9d-b7ff-86f9ea2abcbb", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "b6f75950-e949-4754-aa61-3b8a4058d1da", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "ea344f21-4abb-4b61-872c-96bf22bdaf3d", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "db9793b8-1e8b-477a-8309-a686834e408f", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "2a975880-75ac-4512-9358-66fd806bbd1a", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "7c382312-410c-4c96-b553-e45fc7d6e188", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "d08fd728-cb3d-4fa3-bb02-1d1405700825", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "603eacd7-f079-4a8c-85dd-5ac65028489f", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "0df553b5-5545-4f2a-adc3-dfe4eb67c5c0", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "0ecba014-1e7e-42dc-9298-04ddd1ecdab8", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "be082211-af0e-40b8-823b-0c21f1002b4f", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "31adf350-6c29-4660-8f53-cfd01a5efc19", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "8b52ec84-2e3a-404d-861a-80f21b1f23c6", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "04dc1aa9-7843-4f40-a6af-f0e8c0b0ceeb", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "f7ede40e-84cd-4910-a643-7fb2a7c9e1f8", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "ecb17418-c4c6-4e20-aab6-73995a07e3e3", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "6d60697d-d03e-44ce-a551-30fbc82e992b", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "email", + "profile", + "roles", + "web-origins", + "role_list" + ], + "defaultOptionalClientScopes": [ + "phone", + "offline_access", + "microprofile-jwt", + "address" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "2988f987-fd17-4c9f-9772-29ebf1b11f9f", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "cb241f81-6b2a-42a4-b87a-c434af0eb504", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "60e96658-7cb0-4a85-9410-fc2f578a7eb9", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "61e8844b-a17d-4b9b-b2f3-661204457955", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "92d52e91-38d3-4b8f-9fcf-ef945a7f4ba6", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "aaea42fb-5359-43ef-9329-c3e1f9270f57", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "saml-role-list-mapper", + "oidc-usermodel-property-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + }, + { + "id": "c94c9225-c457-4007-be98-8ec05412f847", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "bc0a5284-7510-4502-8b9e-bd7f6b8b080d", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "b3c90897-d62a-45f5-b29e-ceb0cd9b0dc7", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "75990713-490d-41fd-b509-43e6f06c718b", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "keyUse": [ + "sig" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "39138abc-1e8b-4836-87b0-93e418b7ab45", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "bfa7d911-0d07-48f4-a70f-e7b0cbd509c5", + "name": "rsa-enc-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "keyUse": [ + "enc" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "fcc694db-3503-48d0-a070-7592e5960c83", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "ca5c4bd5-d1fe-42f4-9dd8-c85166fb34c3", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "45d64d38-3480-452e-85cc-88ae9469de3d", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9e27e34b-488c-49a0-bc9e-116ee63d374b", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "0859f079-dbba-4233-bd4b-b78a0b366563", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9b8fe625-9fa0-4e12-b6bf-66e374f4d2a0", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "5bb82ccb-ff18-4c18-9f1f-93c089ebb604", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "3a0dddca-15c2-423f-8869-5f917b72e057", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "49c1227a-7b96-48bd-ba3f-80b4eaef570b", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "ffb6e551-7cfa-4aa9-abe2-92c44bcc3b91", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "feafea9a-a4ff-47db-88a0-175ee2e4923d", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "93085ce4-a863-4164-8f8d-8dfac4a779d1", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "a57140a1-f727-4c79-b15c-10568864b947", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "1967775a-815b-476a-b085-f884eb63be77", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "e8b4c7aa-eecb-4505-a860-8983db1ad7ce", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d471d051-e717-44cf-a687-4ca8fc8abb63", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "4264d059-b004-4cad-a3c0-2acaf8ce7877", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "5e130404-b690-40da-81b1-3b08ab719853", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "8d295e45-bf68-4dc0-bc6e-9796c8e891a6", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "f6066e87-aabd-40f7-bd63-c592663b3aa2", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "990640bb-bf5d-46b9-a586-f6c90a62cabe", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "5b19c33b-0b5d-44dc-aff4-14afb731f1be", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "3303300b-ea00-4ff1-b50c-6a51728815a8", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "userProfileEnabled": "false", + "clientSessionMaxLifespan": "0", + "parRequestUriLifespan": "60", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5" + }, + "keycloakVersion": "15.0.2", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file diff --git a/tests/start.sh b/tests/start.sh new file mode 100755 index 0000000..c90e8d4 --- /dev/null +++ b/tests/start.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Exit with nonzero exit code if anything fails +set -eo pipefail +# Ensure the script is running in this directory +cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" + +echo "Waiting for Keycloak to launch on $KEYCLOAK_HOST:$KEYCLOAK_PORT..." +# Abort after 10 seconds to avoid blocking (-m --> max time) +while ! curl -s -f -o /dev/null -m 2 "http://$KEYCLOAK_HOST:$KEYCLOAK_PORT/auth/realms/master"; do + echo "Waiting..." + sleep 2 & + wait +done + +poetry run test_site/manage.py test test_app \ No newline at end of file diff --git a/tests/test_site/manage.py b/tests/test_site/manage.py new file mode 100755 index 0000000..7d0b206 --- /dev/null +++ b/tests/test_site/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_site.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/tests/test_site/test_app/__init__.py b/tests/test_site/test_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_site/test_app/apps.py b/tests/test_site/test_app/apps.py new file mode 100644 index 0000000..f168fd5 --- /dev/null +++ b/tests/test_site/test_app/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TestAppConfig(AppConfig): + name = "test_app" diff --git a/tests/test_site/test_app/tests/__init__.py b/tests/test_site/test_app/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_site/test_app/tests/test_init.py b/tests/test_site/test_app/tests/test_init.py new file mode 100644 index 0000000..03e67ff --- /dev/null +++ b/tests/test_site/test_app/tests/test_init.py @@ -0,0 +1,30 @@ +from django.conf import settings +from django.contrib.auth import get_user_model +from django.test import TestCase +from django_keycloak.mixins import KeycloakTestMixin +from django_keycloak.models import KeycloakUser + + +class TestInit(KeycloakTestMixin, TestCase): + def setUp(self): + # import ipdb; ipdb.set_trace() + self.keycloak_init() + + def tearDown(self): + self.keycloak_cleanup() + + def test_pass(self): + self.assertTrue(True) + + def test_model(self): + user_a = get_user_model().objects.create_keycloak_user( + username="ownerA", + email="user@example.com", + password="PWowNerA0!", + first_name="Owner", + last_name="AAAA", + ) + self.keycloak.update_user(user_a.id, emailVerified=True) + valid_token = self.keycloak.get_token_from_credentials( + username="ownerA", password="PWowNerA0!" + ) diff --git a/tests/test_site/test_site/__init__.py b/tests/test_site/test_site/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_site/test_site/asgi.py b/tests/test_site/test_site/asgi.py new file mode 100644 index 0000000..781494f --- /dev/null +++ b/tests/test_site/test_site/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for test_site project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_site.settings") + +application = get_asgi_application() diff --git a/tests/test_site/test_site/settings.py b/tests/test_site/test_site/settings.py new file mode 100644 index 0000000..c52592b --- /dev/null +++ b/tests/test_site/test_site/settings.py @@ -0,0 +1,148 @@ +""" +Django settings for test_site project. + +Generated by 'django-admin startproject' using Django 4.1.2. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +import os +import sys +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent +sys.path.append(os.path.abspath(os.path.join(BASE_DIR, "../../src"))) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-ep3)qk(t@*3+-f=p9%1q4o0u*vxoaf3)@t)r29t70wpyh+q(%m" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_keycloak", + "test_app", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django_keycloak.middleware.KeycloakMiddleware", +] + +ROOT_URLCONF = "test_site.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "test_site.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +# Authentication +AUTH_USER_MODEL = "django_keycloak.KeycloakUser" + +AUTHENTICATION_BACKENDS = ("django_keycloak.backends.KeycloakAuthenticationBackend",) + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# KEYCLOAK + +keycloak_server = f"http://{os.getenv('KEYCLOAK_HOST')}:{os.getenv('KEYCLOAK_PORT')}" + +KEYCLOAK_CONFIG = { + "SERVER_URL": keycloak_server, + "INTERNAL_URL": keycloak_server, + "REALM": os.getenv("KEYCLOAK_REALM"), + "CLIENT_ID": os.getenv("KEYCLOAK_CLIENT_ID"), + "CLIENT_SECRET_KEY": os.getenv("KEYCLOAK_CLIENT_SECRET_KEY"), + "CLIENT_ADMIN_ROLE": "admin", + "REALM_ADMIN_ROLE": "admin", + "EXEMPT_URIS": [], + "DECODE_TOKEN": "true", +} diff --git a/tests/test_site/test_site/urls.py b/tests/test_site/test_site/urls.py new file mode 100644 index 0000000..af6025c --- /dev/null +++ b/tests/test_site/test_site/urls.py @@ -0,0 +1,21 @@ +"""test_site URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/tests/test_site/test_site/wsgi.py b/tests/test_site/test_site/wsgi.py new file mode 100644 index 0000000..80ec01f --- /dev/null +++ b/tests/test_site/test_site/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for test_site project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_site.settings") + +application = get_wsgi_application() From abece858df1b550ff26838b48488016f75e8e70e Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Mon, 10 Oct 2022 16:57:51 +0200 Subject: [PATCH 23/73] Update Keycloak docker image URL --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd9c87e..c377a2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: services: keycloak: - image: keycloak:15 + image: quay.io/keycloak/keycloak:15 ports: - 8080:8080 - 9990:9990 From 3cdd42fcd9c41662e46d2ffe022b13635e0f4f78 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Mon, 10 Oct 2022 17:02:16 +0200 Subject: [PATCH 24/73] Fix image name --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c377a2a..6fcbffe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Python package +name: Run CI tests on: push: @@ -9,7 +9,7 @@ on: - master jobs: - build: + test: runs-on: ubuntu-latest env: python-version: "3.8" @@ -26,7 +26,7 @@ jobs: services: keycloak: - image: quay.io/keycloak/keycloak:15 + image: quay.io/keycloak/keycloak:19.0.3 ports: - 8080:8080 - 9990:9990 From 04317498b1d38242f72be0af8282a735333d5687 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Mon, 10 Oct 2022 17:05:52 +0200 Subject: [PATCH 25/73] Remove options --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6fcbffe..11e5d64 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,9 +30,9 @@ jobs: ports: - 8080:8080 - 9990:9990 - options: >- - -b 0.0.0.0 - -Djboss.bind.address.management=0.0.0.0 + # options: >- + # -b=0.0.0.0 + # -Djboss.bind.address.management=0.0.0.0 steps: - uses: actions/checkout@v3 From 4c35d78d0b0fe8e0e4e94efb10f55d6b91ab8439 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 10:11:17 +0200 Subject: [PATCH 26/73] Try legacy Docker image --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11e5d64..d829386 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: services: keycloak: - image: quay.io/keycloak/keycloak:19.0.3 + image: quay.io/keycloak/keycloak:19.0.3-legacy ports: - 8080:8080 - 9990:9990 From 7db7a7a9affb1394afa90d865acd9e50f7c87d71 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 10:30:10 +0200 Subject: [PATCH 27/73] Import realms to Keycloak --- .github/workflows/test.yml | 4 +--- tests/start.sh | 27 +++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d829386..bf52d2c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,9 +30,6 @@ jobs: ports: - 8080:8080 - 9990:9990 - # options: >- - # -b=0.0.0.0 - # -Djboss.bind.address.management=0.0.0.0 steps: - uses: actions/checkout@v3 @@ -50,6 +47,7 @@ jobs: # restore-keys: ${{ matrix.python-version }} - name: Set up the project run: | + sudo apt-get install curl jq pip install poetry poetry config virtualenvs.in-project true poetry install diff --git a/tests/start.sh b/tests/start.sh index c90e8d4..94143c9 100755 --- a/tests/start.sh +++ b/tests/start.sh @@ -5,12 +5,35 @@ set -eo pipefail # Ensure the script is running in this directory cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" -echo "Waiting for Keycloak to launch on $KEYCLOAK_HOST:$KEYCLOAK_PORT..." +KEYCLOAK_URL=http://$KEYCLOAK_HOST:$KEYCLOAK_PORT +echo "Waiting for Keycloak to launch on $KEYCLOAK_URL..." # Abort after 10 seconds to avoid blocking (-m --> max time) -while ! curl -s -f -o /dev/null -m 2 "http://$KEYCLOAK_HOST:$KEYCLOAK_PORT/auth/realms/master"; do +while ! curl -s -f -o /dev/null -m 2 "$KEYCLOAK_URL/auth/realms/master"; do echo "Waiting..." sleep 2 & wait done +echo "Importing debug Keycloak setup" +# Get an access token +KEYCLOAK_TOKEN=$(curl -s \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=admin-cli" \ + -d "username=$KEYCLOAK_ADMIN_USER" \ + -d "password=$KEYCLOAK_ADMIN_PASSWORD" \ + "$KEYCLOAK_URL/auth/realms/master/protocol/openid-connect/token" \ + | jq -r .access_token) +# Combine the realm and user config and send it to the Keycloak server +HTTP_CODE=$(curl -X POST --data-binary "@realm-export.json" \ + -s -o /dev/null -w "%{http_code}" \ + -H "Content-Type: application/json" \ + -H "Authorization: bearer $KEYCLOAK_TOKEN" \ + "$KEYCLOAK_URL/auth/admin/realms") +if ((HTTP_CODE < 200 || HTTP_CODE >= 300)); then + echo "Failed to import the $KEYCLOAK_REALM realm ($HTTP_CODE)" + exit 1 +fi +unset KEYCLOAK_TOKEN + poetry run test_site/manage.py test test_app \ No newline at end of file From fe2b0ccec5fb4cbc0bb0f77432aec9a2f959d6c0 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 10:34:57 +0200 Subject: [PATCH 28/73] Add debug to start script --- tests/start.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/start.sh b/tests/start.sh index 94143c9..5abf598 100755 --- a/tests/start.sh +++ b/tests/start.sh @@ -1,7 +1,7 @@ #!/bin/bash # Exit with nonzero exit code if anything fails -set -eo pipefail +set -exo pipefail # Ensure the script is running in this directory cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" @@ -24,6 +24,7 @@ KEYCLOAK_TOKEN=$(curl -s \ -d "password=$KEYCLOAK_ADMIN_PASSWORD" \ "$KEYCLOAK_URL/auth/realms/master/protocol/openid-connect/token" \ | jq -r .access_token) +echo "$KEYCLOAK_TOKEN" # Combine the realm and user config and send it to the Keycloak server HTTP_CODE=$(curl -X POST --data-binary "@realm-export.json" \ -s -o /dev/null -w "%{http_code}" \ From 92a36c9f3d4841a30ba09dadddaa2cde8577be9f Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 10:40:08 +0200 Subject: [PATCH 29/73] Debug keycloak import --- tests/start.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/start.sh b/tests/start.sh index 5abf598..a16ef23 100755 --- a/tests/start.sh +++ b/tests/start.sh @@ -16,14 +16,15 @@ done echo "Importing debug Keycloak setup" # Get an access token -KEYCLOAK_TOKEN=$(curl -s \ +KEYCLOAK_TOKEN_RESPONSE=$(curl -s \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=password" \ -d "client_id=admin-cli" \ -d "username=$KEYCLOAK_ADMIN_USER" \ -d "password=$KEYCLOAK_ADMIN_PASSWORD" \ - "$KEYCLOAK_URL/auth/realms/master/protocol/openid-connect/token" \ - | jq -r .access_token) + "$KEYCLOAK_URL/auth/realms/master/protocol/openid-connect/token") +echo "$KEYCLOAK_TOKEN_RESPONSE" +KEYCLOAK_TOKEN=$(echo "$KEYCLOAK_TOKEN_RESPONSE" | jq -r .access_token) echo "$KEYCLOAK_TOKEN" # Combine the realm and user config and send it to the Keycloak server HTTP_CODE=$(curl -X POST --data-binary "@realm-export.json" \ From c00d4619197810cee1b9180b06eab7302b4a3cba Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 10:43:33 +0200 Subject: [PATCH 30/73] Add env options to keycloak docker --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf52d2c..ce9ab31 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,9 @@ jobs: ports: - 8080:8080 - 9990:9990 + options: >- + -e "KEYCLOAK_USER=$KEYCLOAK_ADMIN_USER" + -e "KEYCLOAK_PASSWORD=$KEYCLOAK_ADMIN_PASSWORD" steps: - uses: actions/checkout@v3 From 7072c70f9515b9a7ba0a79f3225579289945193e Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 10:47:03 +0200 Subject: [PATCH 31/73] Hardcode KC credentials --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce9ab31..de82135 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,8 +31,8 @@ jobs: - 8080:8080 - 9990:9990 options: >- - -e "KEYCLOAK_USER=$KEYCLOAK_ADMIN_USER" - -e "KEYCLOAK_PASSWORD=$KEYCLOAK_ADMIN_PASSWORD" + -e "KEYCLOAK_USER=admin" + -e "KEYCLOAK_PASSWORD=admin" steps: - uses: actions/checkout@v3 From 868c898f85af0a04ade6033e8f44c69d70b68f6c Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 10:58:11 +0200 Subject: [PATCH 32/73] Enable cache in Github action --- .github/workflows/test.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de82135..c84931b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,17 +40,16 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - # - name: Set up the cache - # uses: actions/cache@v1 - # env: - # cache-name: cache-python-packages - # with: - # path: .venv - # key: ${{ matrix.python-version }}-${{ env.cache-name }} - # restore-keys: ${{ matrix.python-version }} + - name: Set up the cache + uses: actions/cache@v1 + env: + cache-name: cache-python-packages + with: + path: .venv + key: ${{ matrix.python-version }}-${{ env.cache-name }} + restore-keys: ${{ matrix.python-version }} - name: Set up the project run: | - sudo apt-get install curl jq pip install poetry poetry config virtualenvs.in-project true poetry install From c009b334dd2e800aecf8da0ba47ffdf939960312 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:04:16 +0200 Subject: [PATCH 33/73] Test env variables --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c84931b..fb8bc53 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,8 +31,8 @@ jobs: - 8080:8080 - 9990:9990 options: >- - -e "KEYCLOAK_USER=admin" - -e "KEYCLOAK_PASSWORD=admin" + -e "KEYCLOAK_USER=${{env.KEYCLOAK_ADMIN_PASSWORD}}" + -e "KEYCLOAK_PASSWORD=${{env.KEYCLOAK_ADMIN_PASSWORD}}" steps: - uses: actions/checkout@v3 From 694904857bac0bba9ce0ca4796a99353ea0f9055 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:06:38 +0200 Subject: [PATCH 34/73] Test env variables 2 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fb8bc53..f70457d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,8 +31,8 @@ jobs: - 8080:8080 - 9990:9990 options: >- - -e "KEYCLOAK_USER=${{env.KEYCLOAK_ADMIN_PASSWORD}}" - -e "KEYCLOAK_PASSWORD=${{env.KEYCLOAK_ADMIN_PASSWORD}}" + -e "KEYCLOAK_USER=${{ env.KEYCLOAK_ADMIN_PASSWORD }}" + -e "KEYCLOAK_PASSWORD=${{ env.KEYCLOAK_ADMIN_PASSWORD }}" steps: - uses: actions/checkout@v3 From eb76a70194536b8bd3e8212bb537abfce109cbb8 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:07:09 +0200 Subject: [PATCH 35/73] Test env variables 3 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f70457d..68e473c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,8 +31,8 @@ jobs: - 8080:8080 - 9990:9990 options: >- - -e "KEYCLOAK_USER=${{ env.KEYCLOAK_ADMIN_PASSWORD }}" - -e "KEYCLOAK_PASSWORD=${{ env.KEYCLOAK_ADMIN_PASSWORD }}" + -e "KEYCLOAK_USER=${{ KEYCLOAK_ADMIN_PASSWORD }}" + -e "KEYCLOAK_PASSWORD=${{ KEYCLOAK_ADMIN_PASSWORD }}" steps: - uses: actions/checkout@v3 From 2de424f3c94c9f3de2e8a00fddb0d7c31ad4781e Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:08:55 +0200 Subject: [PATCH 36/73] Test env variables 4 --- .github/workflows/test.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68e473c..2d4e0a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,18 +8,18 @@ on: branches: - master +env: + KEYCLOAK_HOST: localhost + KEYCLOAK_PORT: 8080 + KEYCLOAK_ADMIN_USER: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KEYCLOAK_REALM: test-realm + KEYCLOAK_CLIENT_ID: test-client + KEYCLOAK_CLIENT_SECRET_KEY: f6974574-c773-4554-826d-06946cd55e98 + jobs: test: runs-on: ubuntu-latest - env: - python-version: "3.8" - KEYCLOAK_HOST: localhost - KEYCLOAK_PORT: 8080 - KEYCLOAK_ADMIN_USER: admin - KEYCLOAK_ADMIN_PASSWORD: admin - KEYCLOAK_REALM: test-realm - KEYCLOAK_CLIENT_ID: test-client - KEYCLOAK_CLIENT_SECRET_KEY: f6974574-c773-4554-826d-06946cd55e98 strategy: matrix: python-version: ["3.8", "3.9", "3.10"] @@ -31,8 +31,8 @@ jobs: - 8080:8080 - 9990:9990 options: >- - -e "KEYCLOAK_USER=${{ KEYCLOAK_ADMIN_PASSWORD }}" - -e "KEYCLOAK_PASSWORD=${{ KEYCLOAK_ADMIN_PASSWORD }}" + -e "KEYCLOAK_USER=${{ env.KEYCLOAK_ADMIN_PASSWORD }}" + -e "KEYCLOAK_PASSWORD=${{ env.KEYCLOAK_ADMIN_PASSWORD }}" steps: - uses: actions/checkout@v3 From 4b974389a19c582ffc7aa63d6e5e167ca0f31878 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:11:35 +0200 Subject: [PATCH 37/73] Revert env variable test --- .github/workflows/test.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d4e0a4..71b6b3e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,18 +8,17 @@ on: branches: - master -env: - KEYCLOAK_HOST: localhost - KEYCLOAK_PORT: 8080 - KEYCLOAK_ADMIN_USER: admin - KEYCLOAK_ADMIN_PASSWORD: admin - KEYCLOAK_REALM: test-realm - KEYCLOAK_CLIENT_ID: test-client - KEYCLOAK_CLIENT_SECRET_KEY: f6974574-c773-4554-826d-06946cd55e98 - jobs: test: runs-on: ubuntu-latest + env: + KEYCLOAK_HOST: localhost + KEYCLOAK_PORT: 8080 + KEYCLOAK_ADMIN_USER: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KEYCLOAK_REALM: test-realm + KEYCLOAK_CLIENT_ID: test-client + KEYCLOAK_CLIENT_SECRET_KEY: f6974574-c773-4554-826d-06946cd55e98 strategy: matrix: python-version: ["3.8", "3.9", "3.10"] @@ -31,8 +30,8 @@ jobs: - 8080:8080 - 9990:9990 options: >- - -e "KEYCLOAK_USER=${{ env.KEYCLOAK_ADMIN_PASSWORD }}" - -e "KEYCLOAK_PASSWORD=${{ env.KEYCLOAK_ADMIN_PASSWORD }}" + -e "KEYCLOAK_USER=admin" + -e "KEYCLOAK_PASSWORD=admin" steps: - uses: actions/checkout@v3 From a2267dff4734d86c1dafa1eda9c8188c561d3f2c Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:19:25 +0200 Subject: [PATCH 38/73] Test keycloak tag matrix --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 71b6b3e..c04b64b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,10 +22,11 @@ jobs: strategy: matrix: python-version: ["3.8", "3.9", "3.10"] + keycloak-tag: ["19.0.3-legacy", "15.1.1"] services: keycloak: - image: quay.io/keycloak/keycloak:19.0.3-legacy + image: quay.io/keycloak/keycloak:${{ matrix.keycloak-tag }} ports: - 8080:8080 - 9990:9990 From 48713c1a3e3cffa640054f6f714e6c906e966b0d Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:22:56 +0200 Subject: [PATCH 39/73] Expand keycloak tags, reduce Python tags --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c04b64b..c9d7f86 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,8 +21,8 @@ jobs: KEYCLOAK_CLIENT_SECRET_KEY: f6974574-c773-4554-826d-06946cd55e98 strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] - keycloak-tag: ["19.0.3-legacy", "15.1.1"] + python-version: ["3.8", "3.10"] + keycloak-tag: ["15.1.1", "16.1.1", "17.0.1-legacy", "18.0.2-legacy", "19.0.3-legacy"] services: keycloak: From 8ce354843e76b52db7e69e0af5311ed5e36d7574 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:27:39 +0200 Subject: [PATCH 40/73] Update cache key with lock hash --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9d7f86..586ee99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,7 @@ jobs: cache-name: cache-python-packages with: path: .venv - key: ${{ matrix.python-version }}-${{ env.cache-name }} + key: poetry-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }} restore-keys: ${{ matrix.python-version }} - name: Set up the project run: | From d28886221c1a904d540efb43eb6b84bd63b79b7d Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:30:04 +0200 Subject: [PATCH 41/73] Update cache config --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 586ee99..f0b2614 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,7 +47,6 @@ jobs: with: path: .venv key: poetry-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }} - restore-keys: ${{ matrix.python-version }} - name: Set up the project run: | pip install poetry From bb75377b4f0fa1c46020691ff1079e4017cd3006 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:34:22 +0200 Subject: [PATCH 42/73] Test cache changes --- .github/workflows/test.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0b2614..599d009 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,8 +42,9 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set up the cache uses: actions/cache@v1 - env: - cache-name: cache-python-packages + id: cached-poetry-dependencies + # env: + # cache-name: cache-python-packages with: path: .venv key: poetry-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }} @@ -51,6 +52,8 @@ jobs: run: | pip install poetry poetry config virtualenvs.in-project true - poetry install + - name: Install Poetry packages + run: poetry install + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - name: Test with pytest run: ./tests/start.sh From b5711fcbb995315e8aa81209adc9058cdd81b958 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Tue, 11 Oct 2022 11:38:12 +0200 Subject: [PATCH 43/73] Undo optimizations --- .github/workflows/test.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 599d009..f0b2614 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,9 +42,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set up the cache uses: actions/cache@v1 - id: cached-poetry-dependencies - # env: - # cache-name: cache-python-packages + env: + cache-name: cache-python-packages with: path: .venv key: poetry-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }} @@ -52,8 +51,6 @@ jobs: run: | pip install poetry poetry config virtualenvs.in-project true - - name: Install Poetry packages - run: poetry install - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + poetry install - name: Test with pytest run: ./tests/start.sh From 9ced1942073dd3ae80598622bd0c9014fcb8629c Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Fri, 21 Oct 2022 10:21:10 +0200 Subject: [PATCH 44/73] Fix package version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ea31688..331cb1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "django_uw_keycloak" -version = "1.2.0" +version = "1.2.1" description = "Middleware to allow authorization using Keycloak and Django" authors = [ "Ubiwhere ", From b1600c94360a917cb2005bdc38895755d521511c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sim=C3=A3o=20Silva?= Date: Sun, 23 Oct 2022 23:25:07 +0100 Subject: [PATCH 45/73] Added tests with Python 3.7 and 3.9 and Keycloak latest major versions >=13 --- .github/workflows/test.yml | 56 +- tests/realm-export-13-14.json | 2169 +++++++++++++++++++++++++++++++++ tests/start.sh | 15 +- 3 files changed, 2217 insertions(+), 23 deletions(-) create mode 100644 tests/realm-export-13-14.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0b2614..862f5ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,9 +20,19 @@ jobs: KEYCLOAK_CLIENT_ID: test-client KEYCLOAK_CLIENT_SECRET_KEY: f6974574-c773-4554-826d-06946cd55e98 strategy: + max-parallel: 2 matrix: - python-version: ["3.8", "3.10"] - keycloak-tag: ["15.1.1", "16.1.1", "17.0.1-legacy", "18.0.2-legacy", "19.0.3-legacy"] + python-version: ["3.7", "3.8", "3.9", "3.10"] + keycloak-tag: + [ + "13.0.1", + "14.0.0", + "15.1.1", + "16.1.1", + "17.0.1-legacy", + "18.0.2-legacy", + "19.0.3-legacy", + ] services: keycloak: @@ -35,22 +45,26 @@ jobs: -e "KEYCLOAK_PASSWORD=admin" steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Set up the cache - uses: actions/cache@v1 - env: - cache-name: cache-python-packages - with: - path: .venv - key: poetry-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }} - - name: Set up the project - run: | - pip install poetry - poetry config virtualenvs.in-project true - poetry install - - name: Test with pytest - run: ./tests/start.sh + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Set up the cache + uses: actions/cache@v1 + env: + cache-name: cache-python-packages + with: + path: .venv + key: poetry-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }} + + - name: Set up the project + run: | + pip install poetry + poetry config virtualenvs.in-project true + poetry install + + - name: Test with pytest + run: ./tests/start.sh diff --git a/tests/realm-export-13-14.json b/tests/realm-export-13-14.json new file mode 100644 index 0000000..98c2a23 --- /dev/null +++ b/tests/realm-export-13-14.json @@ -0,0 +1,2169 @@ +{ + "id": "test-realm", + "realm": "test-realm", + "displayName": "test-realm", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "305fdfc7-5e49-4797-8cb7-db3c483a678b", + "name": "default-roles-test-realm", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ] + }, + "clientRole": false, + "containerId": "test-realm", + "attributes": {} + }, + { + "id": "291b1491-fb37-4b78-9c88-01595e814efe", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "test-realm", + "attributes": {} + }, + { + "id": "8504ebf9-516d-4175-87e5-b6a4481201e5", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "test-realm", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "9f5ea67a-dd42-4d6d-bbac-fbb39bf392f2", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "ac20b228-484b-43c1-b136-f975597c71b9", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "d0b13bca-7007-49b1-adff-1a67903e2d78", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "dbaa223d-01d8-40c2-ba6a-b659b592b981", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "be3f4f7f-0ef4-4a5c-9d0b-c64ef96cc92d", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "3c5420f3-edc4-4f41-969b-cc871a04ee4f", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "d40c4da9-9feb-4fc3-bd92-e9c0152944cb", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "93b16ea5-db53-4709-8b27-0deb18d74c5a", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "5c3eae4e-1d14-4f68-ae9a-0e4d279db58b", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "e7b9ddba-9f0a-44c8-9428-efbd027fc484", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "8a693097-dea2-49b1-a26f-d524a38a89f2", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "6c2c51e9-ef0c-4fb2-ba99-1baf8882637e", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "fe3f05a4-a35b-4af1-bc2c-9b5d68fe1b61", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "70cd1697-4693-428e-a556-727d554bab55", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "create-client", + "view-clients", + "view-authorization", + "view-users", + "manage-authorization", + "manage-users", + "view-realm", + "query-clients", + "impersonation", + "query-users", + "view-events", + "manage-realm", + "manage-identity-providers", + "manage-clients", + "view-identity-providers", + "query-groups", + "manage-events", + "query-realms" + ] + } + }, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "b9fd96e0-24f2-4e68-9476-19dc51badd6e", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "fc6d2ab2-4801-49ba-a3ad-e28b921ad887", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "ed42ccd8-bf88-4bf7-9799-ac65a6e168f9", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "6861be93-094d-4da7-975b-cc7f7834227a", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + }, + { + "id": "f828a3ae-1989-4d4f-a1ba-c9ac70cf3cca", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "test-client": [ + { + "id": "f18bdd68-1922-4131-8e36-9a28dbbd1a20", + "name": "admin", + "composite": false, + "clientRole": true, + "containerId": "42f4e729-0845-47fa-b810-73effdac174d", + "attributes": {} + } + ], + "account-console": [], + "broker": [], + "account": [ + { + "id": "44fd5143-9ce6-448f-91c0-27c503c2ac31", + "name": "manage-account", + "composite": false, + "clientRole": true, + "containerId": "b94a34e4-07ed-430a-937a-524ec283f133", + "attributes": {} + }, + { + "id": "7071637d-b154-4ad9-b85b-28c5a2c38ab9", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "b94a34e4-07ed-430a-937a-524ec283f133", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "305fdfc7-5e49-4797-8cb7-db3c483a678b", + "name": "default-roles-test-realm", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "test-realm" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "55c9883f-5f3e-42ce-91b8-6077fa1aedcc", + "createdTimestamp": 1665408867130, + "username": "service-account-test-client", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "test-client", + "disableableCredentialTypes": [], + "requiredActions": [], + "clientRoles": { + "realm-management": [ + "view-users", + "manage-users" + ] + }, + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "b94a34e4-07ed-430a-937a-524ec283f133", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/test-realm/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/test-realm/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "0b32a005-e2dd-4a9b-bd47-bd489dc0acb8", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/test-realm/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/test-realm/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9f74ca03-cd65-4335-8755-d1735ba19e3e", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "55b4d2a3-f1be-4b7d-9944-bcd5b60447db", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "e2d1436d-b71e-4184-a234-348beeb7447e", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "59bf406c-0924-44fc-ac9c-0dd66e7cdde5", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "68488b0b-5f97-487c-a819-5611c4fd5eec", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/test-realm/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/test-realm/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "c62260f4-4039-4132-b38b-8af5eaa10017", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "42f4e729-0845-47fa-b810-73effdac174d", + "clientId": "test-client", + "name": "Test client", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "f6974574-c773-4554-826d-06946cd55e98", + "redirectUris": [ + "https://www.keycloak.org/app/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "e9e07797-737f-4dad-a797-8123627de266", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "d9e50279-8961-4352-b748-1dafeb707d25", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "ed88c706-fc65-4b50-bf05-f849b4c0532a", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "b5497d53-fb7e-4cc3-81ff-68f2061d079e", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "cf829ad2-4347-4436-a016-18da66e54c79", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "2847ef05-5d1e-4211-b16a-5d474834c528", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "0617dc30-f043-491c-b2fb-67ecc64e9bce", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "5ffaa1a0-56f3-4395-a81a-160f58edc37c", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "6eeb72f3-ca3d-4fa3-be75-4aa8c69ea6ab", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "b98f31b5-eb47-4429-a74a-3b6d6ca9c5d6", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "5b6a6b2d-f599-41b7-b0c8-bbecfd13280e", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "dd5a0d47-8d1b-49c8-9c47-679a705cb59a", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "05a1cfcd-ac93-4157-9a9c-3a83bb8c94aa", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "2d468904-10b1-4f15-a6fe-b9318eb7af1a", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "53b02a27-6b63-4f10-9b87-428e3c453a1a", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "d5a02ddc-02d1-451f-9c7e-97b33fda2109", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "c7b33cb5-dc05-489f-bf8e-b582d24a89c8", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ae072dfb-ea99-4104-be08-1e9c43461fb9", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "cd54095f-dd0a-493a-be31-ca7755041668", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "3bb08518-f6c5-43fc-b564-13252ee346eb", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "438070db-06a2-448b-a06b-7b7d5d5c9187", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "af3d04d0-55cd-4b9d-b7ff-86f9ea2abcbb", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "b6f75950-e949-4754-aa61-3b8a4058d1da", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "ea344f21-4abb-4b61-872c-96bf22bdaf3d", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "db9793b8-1e8b-477a-8309-a686834e408f", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "2a975880-75ac-4512-9358-66fd806bbd1a", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "7c382312-410c-4c96-b553-e45fc7d6e188", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "d08fd728-cb3d-4fa3-bb02-1d1405700825", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "603eacd7-f079-4a8c-85dd-5ac65028489f", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "0df553b5-5545-4f2a-adc3-dfe4eb67c5c0", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "0ecba014-1e7e-42dc-9298-04ddd1ecdab8", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "be082211-af0e-40b8-823b-0c21f1002b4f", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "31adf350-6c29-4660-8f53-cfd01a5efc19", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "8b52ec84-2e3a-404d-861a-80f21b1f23c6", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "04dc1aa9-7843-4f40-a6af-f0e8c0b0ceeb", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "f7ede40e-84cd-4910-a643-7fb2a7c9e1f8", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "ecb17418-c4c6-4e20-aab6-73995a07e3e3", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "6d60697d-d03e-44ce-a551-30fbc82e992b", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "email", + "profile", + "roles", + "web-origins", + "role_list" + ], + "defaultOptionalClientScopes": [ + "phone", + "offline_access", + "microprofile-jwt", + "address" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "2988f987-fd17-4c9f-9772-29ebf1b11f9f", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "cb241f81-6b2a-42a4-b87a-c434af0eb504", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "60e96658-7cb0-4a85-9410-fc2f578a7eb9", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "61e8844b-a17d-4b9b-b2f3-661204457955", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "92d52e91-38d3-4b8f-9fcf-ef945a7f4ba6", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "aaea42fb-5359-43ef-9329-c3e1f9270f57", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "saml-role-list-mapper", + "oidc-usermodel-property-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper" + ] + } + }, + { + "id": "c94c9225-c457-4007-be98-8ec05412f847", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "bc0a5284-7510-4502-8b9e-bd7f6b8b080d", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "75990713-490d-41fd-b509-43e6f06c718b", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "keyUse": [ + "sig" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "39138abc-1e8b-4836-87b0-93e418b7ab45", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "bfa7d911-0d07-48f4-a70f-e7b0cbd509c5", + "name": "rsa-enc-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "keyUse": [ + "enc" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "fcc694db-3503-48d0-a070-7592e5960c83", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "ca5c4bd5-d1fe-42f4-9dd8-c85166fb34c3", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "45d64d38-3480-452e-85cc-88ae9469de3d", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9e27e34b-488c-49a0-bc9e-116ee63d374b", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "0859f079-dbba-4233-bd4b-b78a0b366563", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9b8fe625-9fa0-4e12-b6bf-66e374f4d2a0", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "5bb82ccb-ff18-4c18-9f1f-93c089ebb604", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "3a0dddca-15c2-423f-8869-5f917b72e057", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "49c1227a-7b96-48bd-ba3f-80b4eaef570b", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "ffb6e551-7cfa-4aa9-abe2-92c44bcc3b91", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "feafea9a-a4ff-47db-88a0-175ee2e4923d", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "93085ce4-a863-4164-8f8d-8dfac4a779d1", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "a57140a1-f727-4c79-b15c-10568864b947", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "1967775a-815b-476a-b085-f884eb63be77", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "e8b4c7aa-eecb-4505-a860-8983db1ad7ce", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d471d051-e717-44cf-a687-4ca8fc8abb63", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "4264d059-b004-4cad-a3c0-2acaf8ce7877", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "5e130404-b690-40da-81b1-3b08ab719853", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "8d295e45-bf68-4dc0-bc6e-9796c8e891a6", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "f6066e87-aabd-40f7-bd63-c592663b3aa2", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "990640bb-bf5d-46b9-a586-f6c90a62cabe", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "5b19c33b-0b5d-44dc-aff4-14afb731f1be", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "3303300b-ea00-4ff1-b50c-6a51728815a8", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "userProfileEnabled": "false", + "clientSessionMaxLifespan": "0", + "parRequestUriLifespan": "60", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5" + }, + "keycloakVersion": "15.0.2", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file diff --git a/tests/start.sh b/tests/start.sh index a16ef23..2c7b155 100755 --- a/tests/start.sh +++ b/tests/start.sh @@ -26,8 +26,19 @@ KEYCLOAK_TOKEN_RESPONSE=$(curl -s \ echo "$KEYCLOAK_TOKEN_RESPONSE" KEYCLOAK_TOKEN=$(echo "$KEYCLOAK_TOKEN_RESPONSE" | jq -r .access_token) echo "$KEYCLOAK_TOKEN" + +# Get Keycloak server version to add the right config file +KEYCLOAK_VERSION="$(curl -v -X GET -H "Content-Type: application/json" -H "Authorization: bearer $KEYCLOAK_TOKEN" "$KEYCLOAK_URL/auth/admin/serverinfo" | jq '.systemInfo.version')" +echo "Keycloak version: $KEYCLOAK_VERSION" +VERSION_MAJOR="$(echo $KEYCLOAK_VERSION | tr -d \" | cut -d . -f 1)" +if [ $VERSION_MAJOR -le 14 ]; then + REALM_FILE="realm-export-13-14.json" +else + REALM_FILE="realm-export.json" +fi + # Combine the realm and user config and send it to the Keycloak server -HTTP_CODE=$(curl -X POST --data-binary "@realm-export.json" \ +HTTP_CODE=$(curl -X POST --data-binary "@$REALM_FILE" \ -s -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -H "Authorization: bearer $KEYCLOAK_TOKEN" \ @@ -38,4 +49,4 @@ if ((HTTP_CODE < 200 || HTTP_CODE >= 300)); then fi unset KEYCLOAK_TOKEN -poetry run test_site/manage.py test test_app \ No newline at end of file +poetry run test_site/manage.py test test_app From af22b1d011f4ef2d69459f9634b219acc424e43e Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Wed, 26 Oct 2022 11:11:42 +0200 Subject: [PATCH 46/73] Add urls to test middleware Why: - Test the Keycloak middleware stack This change addresses the need by: - Adding URLs and views to the test app - Adding tests to query the views --- tests/start.sh | 66 ++++++++++--------- tests/test_site/test_app/tests/test_init.py | 2 - .../test_app/tests/test_middleware.py | 26 ++++++++ tests/test_site/test_app/urls.py | 8 +++ tests/test_site/test_app/views.py | 18 +++++ tests/test_site/test_site/urls.py | 3 +- 6 files changed, 89 insertions(+), 34 deletions(-) create mode 100644 tests/test_site/test_app/tests/test_middleware.py create mode 100644 tests/test_site/test_app/urls.py create mode 100644 tests/test_site/test_app/views.py diff --git a/tests/start.sh b/tests/start.sh index 2c7b155..0b90804 100755 --- a/tests/start.sh +++ b/tests/start.sh @@ -14,39 +14,43 @@ while ! curl -s -f -o /dev/null -m 2 "$KEYCLOAK_URL/auth/realms/master"; do wait done -echo "Importing debug Keycloak setup" -# Get an access token -KEYCLOAK_TOKEN_RESPONSE=$(curl -s \ - -H "Content-Type: application/x-www-form-urlencoded" \ - -d "grant_type=password" \ - -d "client_id=admin-cli" \ - -d "username=$KEYCLOAK_ADMIN_USER" \ - -d "password=$KEYCLOAK_ADMIN_PASSWORD" \ - "$KEYCLOAK_URL/auth/realms/master/protocol/openid-connect/token") -echo "$KEYCLOAK_TOKEN_RESPONSE" -KEYCLOAK_TOKEN=$(echo "$KEYCLOAK_TOKEN_RESPONSE" | jq -r .access_token) -echo "$KEYCLOAK_TOKEN" +if (($(curl -s -o /dev/null -w "%{http_code}" "$KEYCLOAK_URL/auth/realms/$KEYCLOAK_REALM") != 200)); then + echo "Importing debug Keycloak setup" + # Get an access token + KEYCLOAK_TOKEN_RESPONSE=$(curl -s \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=admin-cli" \ + -d "username=$KEYCLOAK_ADMIN_USER" \ + -d "password=$KEYCLOAK_ADMIN_PASSWORD" \ + "$KEYCLOAK_URL/auth/realms/master/protocol/openid-connect/token") + echo "$KEYCLOAK_TOKEN_RESPONSE" + KEYCLOAK_TOKEN=$(echo "$KEYCLOAK_TOKEN_RESPONSE" | jq -r .access_token) + echo "$KEYCLOAK_TOKEN" -# Get Keycloak server version to add the right config file -KEYCLOAK_VERSION="$(curl -v -X GET -H "Content-Type: application/json" -H "Authorization: bearer $KEYCLOAK_TOKEN" "$KEYCLOAK_URL/auth/admin/serverinfo" | jq '.systemInfo.version')" -echo "Keycloak version: $KEYCLOAK_VERSION" -VERSION_MAJOR="$(echo $KEYCLOAK_VERSION | tr -d \" | cut -d . -f 1)" -if [ $VERSION_MAJOR -le 14 ]; then - REALM_FILE="realm-export-13-14.json" -else - REALM_FILE="realm-export.json" -fi + # Get Keycloak server version to add the right config file + KEYCLOAK_VERSION="$(curl -v -X GET -H "Content-Type: application/json" -H "Authorization: bearer $KEYCLOAK_TOKEN" "$KEYCLOAK_URL/auth/admin/serverinfo" | jq '.systemInfo.version')" + echo "Keycloak version: $KEYCLOAK_VERSION" + VERSION_MAJOR="$(echo "$KEYCLOAK_VERSION" | tr -d \" | cut -d . -f 1)" + if [ "$VERSION_MAJOR" -le 14 ]; then + REALM_FILE="realm-export-13-14.json" + else + REALM_FILE="realm-export.json" + fi -# Combine the realm and user config and send it to the Keycloak server -HTTP_CODE=$(curl -X POST --data-binary "@$REALM_FILE" \ - -s -o /dev/null -w "%{http_code}" \ - -H "Content-Type: application/json" \ - -H "Authorization: bearer $KEYCLOAK_TOKEN" \ - "$KEYCLOAK_URL/auth/admin/realms") -if ((HTTP_CODE < 200 || HTTP_CODE >= 300)); then - echo "Failed to import the $KEYCLOAK_REALM realm ($HTTP_CODE)" - exit 1 + # Combine the realm and user config and send it to the Keycloak server + HTTP_CODE=$(curl -X POST --data-binary "@$REALM_FILE" \ + -s -o /dev/null -w "%{http_code}" \ + -H "Content-Type: application/json" \ + -H "Authorization: bearer $KEYCLOAK_TOKEN" \ + "$KEYCLOAK_URL/auth/admin/realms") + if ((HTTP_CODE < 200 || HTTP_CODE >= 300)); then + echo "Failed to import the $KEYCLOAK_REALM realm ($HTTP_CODE)" + exit 1 + fi + unset KEYCLOAK_TOKEN +else + echo "Realm '$KEYCLOAK_REALM' already imported into Keycloak" fi -unset KEYCLOAK_TOKEN poetry run test_site/manage.py test test_app diff --git a/tests/test_site/test_app/tests/test_init.py b/tests/test_site/test_app/tests/test_init.py index 03e67ff..2a9acc8 100644 --- a/tests/test_site/test_app/tests/test_init.py +++ b/tests/test_site/test_app/tests/test_init.py @@ -2,12 +2,10 @@ from django.contrib.auth import get_user_model from django.test import TestCase from django_keycloak.mixins import KeycloakTestMixin -from django_keycloak.models import KeycloakUser class TestInit(KeycloakTestMixin, TestCase): def setUp(self): - # import ipdb; ipdb.set_trace() self.keycloak_init() def tearDown(self): diff --git a/tests/test_site/test_app/tests/test_middleware.py b/tests/test_site/test_app/tests/test_middleware.py new file mode 100644 index 0000000..3cb0ae6 --- /dev/null +++ b/tests/test_site/test_app/tests/test_middleware.py @@ -0,0 +1,26 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse +from django_keycloak.mixins import KeycloakTestMixin + + +class TestMiddleware(KeycloakTestMixin, TestCase): + def setUp(self): + self.keycloak_init() + self.user_a = get_user_model().objects.create_keycloak_user( + username="ownerA", + email="user@example.com", + password="PWowNerA0!", + first_name="Owner", + last_name="AAAA", + ) + + def tearDown(self): + self.keycloak_cleanup() + + def test_simple_api_call(self): + response = self.client.get(reverse("test_app:simple")) + self.assertEqual(response.json()["status"], "ok") + + def test_user_auth(self): + pass diff --git a/tests/test_site/test_app/urls.py b/tests/test_site/test_app/urls.py new file mode 100644 index 0000000..d1db658 --- /dev/null +++ b/tests/test_site/test_app/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from test_app.views import Simple + +app_name = "test_app" + +urlpatterns = [ + path("simple/", Simple.as_view(), name="simple"), +] diff --git a/tests/test_site/test_app/views.py b/tests/test_site/test_app/views.py new file mode 100644 index 0000000..25e19fa --- /dev/null +++ b/tests/test_site/test_app/views.py @@ -0,0 +1,18 @@ +from rest_framework.response import Response +from rest_framework.views import APIView + + +class Simple(APIView): + def get(self, request): + return Response({"status": "ok"}) + + +class WhoAmI(APIView): + def get(self, request): + user_data = { + "username": request.context.user.username, + "email": request.context.user.email, + "first_name": request.context.user.first_name, + "last_name": request.context.user.last_name, + } + return Response(user_data) diff --git a/tests/test_site/test_site/urls.py b/tests/test_site/test_site/urls.py index af6025c..822af89 100644 --- a/tests/test_site/test_site/urls.py +++ b/tests/test_site/test_site/urls.py @@ -14,8 +14,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), + path("test-app/", include("test_app.urls")), ] From fed46f22a8071f025d0458cd1be3301ced4ce239 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Wed, 26 Oct 2022 15:39:28 +0200 Subject: [PATCH 47/73] Fix package src location --- pyproject.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 331cb1c..c8c1af3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,12 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "django_uw_keycloak" -version = "1.2.1" +version = "2.0.0" description = "Middleware to allow authorization using Keycloak and Django" authors = [ "Ubiwhere ", - "Moritz Ulmer " + "Moritz Ulmer ", + "Diogo Silva ", ] license = "MIT" repository = "https://github.com/urbanplatform/django-keycloak-auth" @@ -24,17 +25,16 @@ classifiers = [ keywords = ["keycloak", "django", "authorization"] readme = "README.md" packages = [ - { include = "src" } + { include = "django_keycloak", from = "src" } ] [tool.poetry.dependencies] -python = ">=3.8,<4" +python = ">=3.7,<4" django = ">=2.2" djangorestframework = ">=3.0" dry-rest-permissions = ">=0.1" -pyjwt = {version = "~=2.4.0", extras = ["crypto"]} -requests = ">=2.28" -cachetools = ">=5.0" +python-keycloak = ">=2.6.0" +cachetools = ">=5.0.0" [tool.poetry.dev-dependencies] black = "~=22.6" From 6fcde4a3aed6885b4ac8f298f36b8e0d1925a25e Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Wed, 26 Oct 2022 13:53:36 +0100 Subject: [PATCH 48/73] Remove setup and teardown from test mixin --- src/django_keycloak/mixins.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/django_keycloak/mixins.py b/src/django_keycloak/mixins.py index 4fa9322..fdf0224 100644 --- a/src/django_keycloak/mixins.py +++ b/src/django_keycloak/mixins.py @@ -1,4 +1,3 @@ -import logging from django_keycloak.connector import KeycloakAdminConnector @@ -11,27 +10,8 @@ class KeycloakTestMixin: Usage: In the test class, derive from this mixin and call keycloak_init/teardown in the setUp and tearDown functions. - - class LoginTests(KeycloakTestMixin, TestCase): - def setUp(self): - keycloak_init() - ... - - def tearDown(self): - keycloak_teardown() - ... """ - def setUp(self): # pylint: disable=invalid-name - logging.warning("Please call keycloak_init() manually", DeprecationWarning, 2) - self.keycloak_init() - - def tearDown(self): # pylint: disable=invalid-name - logging.warning( - "Please call keycloak_cleanup() manually", DeprecationWarning, 2 - ) - self.keycloak_cleanup() - def keycloak_init(self): self._start_users = { user.get("id") for user in KeycloakAdminConnector.get_users() From a16e0ba5ca50b94c46d4ea7923ff0eee35f77a44 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Wed, 26 Oct 2022 15:44:18 +0200 Subject: [PATCH 49/73] Update poetry.lock --- poetry.lock | 474 +++++++++++++++++++++------------------------------- 1 file changed, 193 insertions(+), 281 deletions(-) diff --git a/poetry.lock b/poetry.lock index c97d529..2cec8e6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,22 +14,11 @@ category = "main" optional = false python-versions = ">=3.7" -[package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] - -[[package]] -name = "asttokens" -version = "2.0.8" -description = "Annotate AST trees with source code positions" -category = "dev" -optional = false -python-versions = "*" - [package.dependencies] -six = "*" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -test = ["astroid (<=2.5.3)", "pytest"] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "backcall" @@ -39,17 +28,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "backports.zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -tzdata = ["tzdata"] - [[package]] name = "black" version = "22.10.0" @@ -64,6 +42,7 @@ mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] @@ -88,17 +67,6 @@ category = "main" optional = false python-versions = ">=3.6" -[[package]] -name = "cffi" -version = "1.15.1" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pycparser = "*" - [[package]] name = "charset-normalizer" version = "2.1.1" @@ -120,14 +88,15 @@ python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" @@ -140,25 +109,6 @@ python-versions = ">=3.7" [package.extras] toml = ["tomli"] -[[package]] -name = "cryptography" -version = "38.0.1" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.12" - -[package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools-rust (>=0.11.4)"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] - [[package]] name = "decorator" version = "5.1.1" @@ -169,17 +119,16 @@ python-versions = ">=3.5" [[package]] name = "Django" -version = "4.1.2" -description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +version = "3.2.16" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.6" [package.dependencies] -asgiref = ">=3.5.2,<4" -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +asgiref = ">=3.3.2,<4" +pytz = "*" sqlparse = ">=0.2.2" -tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] argon2 = ["argon2-cffi (>=19.1.0)"] @@ -206,15 +155,19 @@ optional = false python-versions = "*" [[package]] -name = "executing" -version = "1.1.1" -description = "Get the currently executing AST node of a frame, and other information" -category = "dev" +name = "ecdsa" +version = "0.18.0" +description = "ECDSA cryptographic signature library (pure python)" +category = "main" optional = false -python-versions = "*" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[package.dependencies] +six = ">=1.9.0" [package.extras] -tests = ["asttokens", "littleutils", "pytest", "rich"] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] [[package]] name = "idna" @@ -224,6 +177,23 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "importlib-metadata" +version = "5.0.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +perf = ["ipython"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] + [[package]] name = "ipdb" version = "0.13.9" @@ -240,11 +210,11 @@ toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} [[package]] name = "ipython" -version = "8.5.0" +version = "7.34.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" [package.dependencies] appnope = {version = "*", markers = "sys_platform == \"darwin\""} @@ -255,14 +225,13 @@ jedi = ">=0.16" matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} pickleshare = "*" -prompt-toolkit = ">3.0.1,<3.1.0" -pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" +pygments = "*" +setuptools = ">=18.5" +traitlets = ">=4.2" [package.extras] -all = ["Sphinx (>=1.3)", "black", "curio", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.19)", "pandas", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "testpath", "trio"] -black = ["black"] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] doc = ["Sphinx (>=1.3)"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] @@ -270,8 +239,7 @@ nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] -test_extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.19)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] +test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments", "requests", "testpath"] [[package]] name = "jedi" @@ -378,23 +346,12 @@ optional = false python-versions = "*" [[package]] -name = "pure-eval" -version = "0.2.2" -description = "Safely evaluate AST nodes without side effects" -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "*" [[package]] name = "Pygments" @@ -408,25 +365,43 @@ python-versions = ">=3.6" plugins = ["importlib-metadata"] [[package]] -name = "PyJWT" -version = "2.4.0" -description = "JSON Web Token implementation in Python" +name = "python-jose" +version = "3.3.0" +description = "JOSE implementation in Python" category = "main" optional = false -python-versions = ">=3.6" +python-versions = "*" [package.dependencies] -cryptography = {version = ">=3.3.1", optional = true, markers = "extra == \"crypto\""} +ecdsa = "!=0.15" +pyasn1 = "*" +rsa = "*" [package.extras] -crypto = ["cryptography (>=3.3.1)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.3.1)", "mypy", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] +pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] + +[[package]] +name = "python-keycloak" +version = "2.6.0" +description = "python-keycloak is a Python package providing access to the Keycloak API." +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +python-jose = ">=3.3.0,<4.0.0" +requests = ">=2.20.0,<3.0.0" +requests-toolbelt = ">=0.9.1,<0.10.0" +urllib3 = ">=1.26.0,<2.0.0" + +[package.extras] +docs = ["Sphinx (>=5.0.2,<6.0.0)", "alabaster (>=0.7.12,<0.8.0)", "commonmark (>=0.9.1,<0.10.0)", "m2r2 (>=0.3.2,<0.4.0)", "mock (>=4.0.3,<5.0.0)", "readthedocs-sphinx-ext (>=2.1.8,<3.0.0)", "recommonmark (>=0.7.1,<0.8.0)", "sphinx-autoapi (>=1.8.4,<2.0.0)", "sphinx-rtd-theme (>=1.0.0,<2.0.0)"] [[package]] name = "pytz" -version = "2022.4" +version = "2022.5" description = "World timezone definitions, modern and historical" category = "main" optional = false @@ -450,9 +425,31 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-toolbelt" +version = "0.9.1" +description = "A utility belt for advanced users of python-requests" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "setuptools" -version = "65.4.1" +version = "65.5.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false @@ -467,7 +464,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" @@ -479,22 +476,6 @@ category = "main" optional = false python-versions = ">=3.5" -[[package]] -name = "stack-data" -version = "0.5.1" -description = "Extract data from python stack frames and tracebacks for informative displays" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -asttokens = "*" -executing = "*" -pure-eval = "*" - -[package.extras] -tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] - [[package]] name = "toml" version = "0.10.2" @@ -513,30 +494,31 @@ python-versions = ">=3.7" [[package]] name = "traitlets" -version = "5.4.0" +version = "5.5.0" description = "" category = "dev" optional = false python-versions = ">=3.7" [package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["pre-commit", "pytest"] [[package]] -name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +name = "typed-ast" +version = "1.5.4" +description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [[package]] -name = "tzdata" -version = "2022.4" -description = "Provider of IANA time zone data" +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=2" +python-versions = ">=3.7" [[package]] name = "urllib3" @@ -559,10 +541,22 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "zipp" +version = "3.10.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + [metadata] lock-version = "1.1" -python-versions = ">=3.8,<4" -content-hash = "f90692d72c9e82171db116b5ec209e1c973e69e313f0bb45a9042305755ee14c" +python-versions = ">=3.7,<4" +content-hash = "87685917c8dd02e85e7c15934ea2dc45e090f5d2088d9aaee805ebd6dce17652" [metadata.files] appnope = [ @@ -573,32 +567,10 @@ asgiref = [ {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, ] -asttokens = [ - {file = "asttokens-2.0.8-py2.py3-none-any.whl", hash = "sha256:e3305297c744ae53ffa032c45dc347286165e4ffce6875dc662b205db0623d86"}, - {file = "asttokens-2.0.8.tar.gz", hash = "sha256:c61e16246ecfb2cde2958406b4c8ebc043c9e6d73aaa83c941673b35e5d3a76b"}, -] backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] -"backports.zoneinfo" = [ - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] black = [ {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, @@ -630,72 +602,6 @@ certifi = [ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, ] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] charset-normalizer = [ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, @@ -705,8 +611,8 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coverage = [ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, @@ -760,41 +666,13 @@ coverage = [ {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] -cryptography = [ - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, - {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, - {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, - {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, -] decorator = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] Django = [ - {file = "Django-4.1.2-py3-none-any.whl", hash = "sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793"}, - {file = "Django-4.1.2.tar.gz", hash = "sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f"}, + {file = "Django-3.2.16-py3-none-any.whl", hash = "sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121"}, + {file = "Django-3.2.16.tar.gz", hash = "sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d"}, ] djangorestframework = [ {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, @@ -804,20 +682,24 @@ dry-rest-permissions = [ {file = "dry-rest-permissions-0.1.10.tar.gz", hash = "sha256:1f40461184063390e5b24e9c5602eb8cc8c3c2433c796f39a5332065bfbddd2b"}, {file = "dry_rest_permissions-0.1.10-py2.py3-none-any.whl", hash = "sha256:f3fe685760004ce182801602819b43ebfa922e587036f1f5a5c10ffcfa646039"}, ] -executing = [ - {file = "executing-1.1.1-py2.py3-none-any.whl", hash = "sha256:236ea5f059a38781714a8bfba46a70fad3479c2f552abee3bbafadc57ed111b8"}, - {file = "executing-1.1.1.tar.gz", hash = "sha256:b0d7f8dcc2bac47ce6e39374397e7acecea6fdc380a6d5323e26185d70f38ea8"}, +ecdsa = [ + {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, + {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +importlib-metadata = [ + {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"}, + {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"}, +] ipdb = [ {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, ] ipython = [ - {file = "ipython-8.5.0-py3-none-any.whl", hash = "sha256:6f090e29ab8ef8643e521763a4f1f39dc3914db643122b1e9d3328ff2e43ada2"}, - {file = "ipython-8.5.0.tar.gz", hash = "sha256:097bdf5cd87576fd066179c9f7f208004f7a6864ee1b20f37d346c0bcb099f84"}, + {file = "ipython-7.34.0-py3-none-any.whl", hash = "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e"}, + {file = "ipython-7.34.0.tar.gz", hash = "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6"}, ] jedi = [ {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, @@ -859,33 +741,41 @@ ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] -pure-eval = [ - {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, - {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, ] Pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] -PyJWT = [ - {file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"}, - {file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"}, +python-jose = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] +python-keycloak = [ + {file = "python-keycloak-2.6.0.tar.gz", hash = "sha256:08c530ff86f631faccb8033d9d9345cc3148cb2cf132ff7564f025292e4dbd96"}, + {file = "python_keycloak-2.6.0-py3-none-any.whl", hash = "sha256:a1ce102b978beb56d385319b3ca20992b915c2c12d15a2d0c23f1104882f3fb6"}, ] pytz = [ - {file = "pytz-2022.4-py2.py3-none-any.whl", hash = "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91"}, - {file = "pytz-2022.4.tar.gz", hash = "sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174"}, + {file = "pytz-2022.5-py2.py3-none-any.whl", hash = "sha256:335ab46900b1465e714b4fda4963d87363264eb662aab5e65da039c25f1f5b22"}, + {file = "pytz-2022.5.tar.gz", hash = "sha256:c4d88f472f54d615e9cd582a5004d1e5f624854a6a27a6211591c251f22a6914"}, ] requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] +requests-toolbelt = [ + {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, + {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, +] +rsa = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] setuptools = [ - {file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"}, - {file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"}, + {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, + {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -895,10 +785,6 @@ sqlparse = [ {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"}, {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"}, ] -stack-data = [ - {file = "stack_data-0.5.1-py3-none-any.whl", hash = "sha256:5120731a18ba4c82cefcf84a945f6f3e62319ef413bfc210e32aca3a69310ba2"}, - {file = "stack_data-0.5.1.tar.gz", hash = "sha256:95eb784942e861a3d80efd549ff9af6cf847d88343a12eb681d7157cfcb6e32b"}, -] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -908,17 +794,39 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] traitlets = [ - {file = "traitlets-5.4.0-py3-none-any.whl", hash = "sha256:93663cc8236093d48150e2af5e2ed30fc7904a11a6195e21bab0408af4e6d6c8"}, - {file = "traitlets-5.4.0.tar.gz", hash = "sha256:3f2c4e435e271592fe4390f1746ea56836e3a080f84e7833f0f801d9613fec39"}, + {file = "traitlets-5.5.0-py3-none-any.whl", hash = "sha256:1201b2c9f76097195989cdf7f65db9897593b0dfd69e4ac96016661bb6f0d30f"}, + {file = "traitlets-5.5.0.tar.gz", hash = "sha256:b122f9ff2f2f6c1709dab289a05555be011c87828e911c0cf4074b85cb780a79"}, +] +typed-ast = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] -tzdata = [ - {file = "tzdata-2022.4-py2.py3-none-any.whl", hash = "sha256:74da81ecf2b3887c94e53fc1d466d4362aaf8b26fc87cda18f22004544694583"}, - {file = "tzdata-2022.4.tar.gz", hash = "sha256:ada9133fbd561e6ec3d1674d3fba50251636e918aa97bd59d63735bef5a513bb"}, -] urllib3 = [ {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, @@ -927,3 +835,7 @@ wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] +zipp = [ + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, +] From 7a4acafeeafc674f2a0ca2fdb3061dbc81a07036 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Wed, 26 Oct 2022 14:46:07 +0100 Subject: [PATCH 50/73] Remove setup and teardown --- django_keycloak/mixins.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/django_keycloak/mixins.py b/django_keycloak/mixins.py index 4fa9322..fdf0224 100644 --- a/django_keycloak/mixins.py +++ b/django_keycloak/mixins.py @@ -1,4 +1,3 @@ -import logging from django_keycloak.connector import KeycloakAdminConnector @@ -11,27 +10,8 @@ class KeycloakTestMixin: Usage: In the test class, derive from this mixin and call keycloak_init/teardown in the setUp and tearDown functions. - - class LoginTests(KeycloakTestMixin, TestCase): - def setUp(self): - keycloak_init() - ... - - def tearDown(self): - keycloak_teardown() - ... """ - def setUp(self): # pylint: disable=invalid-name - logging.warning("Please call keycloak_init() manually", DeprecationWarning, 2) - self.keycloak_init() - - def tearDown(self): # pylint: disable=invalid-name - logging.warning( - "Please call keycloak_cleanup() manually", DeprecationWarning, 2 - ) - self.keycloak_cleanup() - def keycloak_init(self): self._start_users = { user.get("id") for user in KeycloakAdminConnector.get_users() From 3902f471a4ceddbeb46de02f023edc85c1f244c9 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Wed, 26 Oct 2022 15:49:10 +0200 Subject: [PATCH 51/73] Upgrade GitHub action cache step to v3 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 862f5ad..5341e76 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,7 +53,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set up the cache - uses: actions/cache@v1 + uses: actions/cache@v3 env: cache-name: cache-python-packages with: From 4938a10a6d85f41b08e02db3850aacb39b697cd9 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Wed, 26 Oct 2022 16:24:47 +0100 Subject: [PATCH 52/73] Improve config parsing --- src/django_keycloak/config.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/django_keycloak/config.py b/src/django_keycloak/config.py index 364b421..96cf995 100644 --- a/src/django_keycloak/config.py +++ b/src/django_keycloak/config.py @@ -26,7 +26,7 @@ class Settings: # The name of the admin role for the realm REALM_ADMIN_ROLE: str # Regex formatted URLs to skip authentication for (uses re.match()) - EXEMPT_URIS: Optional[List] = field(default_factory=[]) + EXEMPT_URIS: Optional[List] = field(default_factory=list) # Overrides SERVER_URL for Keycloak admin calls INTERNAL_URL: Optional[str] = None # Override default Keycloak base path (/auth/) @@ -51,14 +51,23 @@ def __post_init__(self) -> None: # Get keycloak configs from django __configs = django_settings.KEYCLOAK_CONFIG # Filter out configs with `None` as values -__configs = {k: v for k, v in __configs.items() if v is not None} - +__configs = { + k: v + for k, v in __configs.items() + if v is not None and k in Settings.__annotations__.keys() +} try: # The exported settings object settings = Settings(**__configs) + except TypeError as e: import django_keycloak.errors as errors - # Get missing variables with regex - missing_required_vars = re.findall("'([^']*)'", str(e)) - raise errors.KeycloakMissingSettingError(" / ".join(missing_required_vars)) from e + if "required positional argument" in str(e): + # Get missing variables with regex + missing_required_vars = re.findall("'([^']*)'", str(e)) + raise errors.KeycloakMissingSettingError( + " / ".join(missing_required_vars) + ) from e + else: + raise e From e72f410d15e0ecd5e40fb5a46e37e40af5a6034e Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Wed, 26 Oct 2022 16:53:56 +0100 Subject: [PATCH 53/73] Remove deprecated graphql related stuff --- README.md | 5 +---- src/django_keycloak/config.py | 2 -- src/django_keycloak/middleware.py | 26 +++----------------------- 3 files changed, 4 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 29afa09..2823637 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,6 @@ This package should only be used in projects starting from scratch, since it ove 'CLIENT_ADMIN_ROLE': '', 'REALM_ADMIN_ROLE': '', 'EXEMPT_URIS': [], # URIS to be ignored by the package - 'GRAPHQL_ENDPOINT': 'graphql/' # Default graphQL endpoint } ``` @@ -51,9 +50,7 @@ This package should only be used in projects starting from scratch, since it ove AUTH_USER_MODEL = "django_keycloak.KeycloakUserAutoId" ``` -7. If you are using Graphene, add the `GRAPHQL_ENDPOINT` to settings and `KeycloakGrapheneMiddleware` to the Graphene's `MIDDLEWARE`. - -8. Configure Django-Rest-Framework authentication classes with `django_keycloak.authentication.KeycloakAuthentication`: +7. Configure Django-Rest-Framework authentication classes with `django_keycloak.authentication.KeycloakAuthentication`: ```python REST_FRAMEWORK = { diff --git a/src/django_keycloak/config.py b/src/django_keycloak/config.py index 96cf995..6ebb620 100644 --- a/src/django_keycloak/config.py +++ b/src/django_keycloak/config.py @@ -31,8 +31,6 @@ class Settings: INTERNAL_URL: Optional[str] = None # Override default Keycloak base path (/auth/) BASE_PATH: Optional[str] = "/auth/" - # Regex formatted URL to excempt the GraphQL URL""" - GRAPHQL_ENDPOINT: Optional[str] = "graphql/" # Flag if the token should be introspected or decoded DECODE_TOKEN: Optional[bool] = False # Flag if the audience in the token should be verified diff --git a/src/django_keycloak/middleware.py b/src/django_keycloak/middleware.py index 630f02d..35daa1c 100644 --- a/src/django_keycloak/middleware.py +++ b/src/django_keycloak/middleware.py @@ -100,15 +100,10 @@ def process_request(self, request): To be executed before the view each request. """ # Skip auth in the following cases: - # 1. It is a graphql endpoint (handled by KeycloakGrapheneMiddleware) - # 2. It is a URL in "EXEMPT_URIS" - # 3. Request does not contain authorization header + # 1. It is a URL in "EXEMPT_URIS" + # 2. Request does not contain authorization header # Also skip auth for "EXEMPT_URIS" defined in configs - if ( - self.is_graphql_endpoint(request) - or self.pass_auth(request) - or not self.has_auth_header(request) - ): + if self.pass_auth(request) or not self.has_auth_header(request): return token: Union[Token, None] = self.get_token_from_request(request) @@ -126,18 +121,3 @@ def pass_auth(self, request): exempt_uris = settings.EXEMPT_URIS return any(re.match(m, path) for m in exempt_uris) - - def is_graphql_endpoint(self, request): - """ - Check if the request path belongs to a graphql endpoint - """ - graphql_endpoint = settings.GRAPHQL_ENDPOINT - if graphql_endpoint is None: - return False - - path = request.path_info.lstrip("/") - is_graphql_endpoint = re.match(graphql_endpoint, path) - if is_graphql_endpoint and request.method != "GET": - return True - - return False From 5795e8ccd998c14d61e2ff669322589a173f65cc Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Wed, 26 Oct 2022 21:04:55 +0100 Subject: [PATCH 54/73] Add "create_user_on_keycloak" method --- src/django_keycloak/mixins.py | 28 +++++++++++++++++++ .../test_app/tests/test_middleware.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/django_keycloak/mixins.py b/src/django_keycloak/mixins.py index fdf0224..04cdabf 100644 --- a/src/django_keycloak/mixins.py +++ b/src/django_keycloak/mixins.py @@ -22,3 +22,31 @@ def keycloak_cleanup(self): users_to_remove = new_users.difference(self._start_users) for user_id in users_to_remove: KeycloakAdminConnector.delete_user(user_id) + + def create_user_on_keycloak( + self, + username, + email, + password=None, + first_name=None, + last_name=None, + enabled=True, + actions=None, + ): + """Creates user on keycloak server, No state is changed on local db""" + + values = {"username": username, "email": email, "enabled": enabled} + if password is not None: + values["credentials"] = [ + {"type": "password", "value": password, "temporary": False} + ] + if first_name is not None: + values["firstName"] = first_name + if last_name is not None: + values["lastName"] = last_name + if actions is not None: + values["requiredActions"] = actions + + newuser = KeycloakAdminConnector.create_user(payload=values) + raise ValueError(newuser) + return KeycloakAdminConnector.get_user(newuser["id"]) diff --git a/tests/test_site/test_app/tests/test_middleware.py b/tests/test_site/test_app/tests/test_middleware.py index 3cb0ae6..5d3d339 100644 --- a/tests/test_site/test_app/tests/test_middleware.py +++ b/tests/test_site/test_app/tests/test_middleware.py @@ -7,7 +7,7 @@ class TestMiddleware(KeycloakTestMixin, TestCase): def setUp(self): self.keycloak_init() - self.user_a = get_user_model().objects.create_keycloak_user( + self.user_a = self.create_user_on_keycloak( username="ownerA", email="user@example.com", password="PWowNerA0!", From 4768a9f2dc6311f112e174e557c8dc9508f6415e Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Wed, 26 Oct 2022 21:08:37 +0100 Subject: [PATCH 55/73] Fix Mixin --- src/django_keycloak/mixins.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/django_keycloak/mixins.py b/src/django_keycloak/mixins.py index 04cdabf..16410dd 100644 --- a/src/django_keycloak/mixins.py +++ b/src/django_keycloak/mixins.py @@ -32,7 +32,7 @@ def create_user_on_keycloak( last_name=None, enabled=True, actions=None, - ): + ) -> dict: """Creates user on keycloak server, No state is changed on local db""" values = {"username": username, "email": email, "enabled": enabled} @@ -47,6 +47,4 @@ def create_user_on_keycloak( if actions is not None: values["requiredActions"] = actions - newuser = KeycloakAdminConnector.create_user(payload=values) - raise ValueError(newuser) - return KeycloakAdminConnector.get_user(newuser["id"]) + return KeycloakAdminConnector.create_user(payload=values) From aab68390372c0fe57d75b248bf53c75ecb1f8f7c Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Wed, 26 Oct 2022 21:14:24 +0100 Subject: [PATCH 56/73] Fix Tests --- tests/test_site/test_app/tests/test_init.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/test_site/test_app/tests/test_init.py b/tests/test_site/test_app/tests/test_init.py index 2a9acc8..feaf281 100644 --- a/tests/test_site/test_app/tests/test_init.py +++ b/tests/test_site/test_app/tests/test_init.py @@ -1,7 +1,8 @@ from django.conf import settings -from django.contrib.auth import get_user_model from django.test import TestCase from django_keycloak.mixins import KeycloakTestMixin +from django_keycloak.connector import KeycloakAdminConnector +from django_keycloak import Token class TestInit(KeycloakTestMixin, TestCase): @@ -11,18 +12,13 @@ def setUp(self): def tearDown(self): self.keycloak_cleanup() - def test_pass(self): - self.assertTrue(True) - def test_model(self): - user_a = get_user_model().objects.create_keycloak_user( + user_a = self.create_user_on_keycloak( username="ownerA", email="user@example.com", password="PWowNerA0!", first_name="Owner", last_name="AAAA", ) - self.keycloak.update_user(user_a.id, emailVerified=True) - valid_token = self.keycloak.get_token_from_credentials( - username="ownerA", password="PWowNerA0!" - ) + KeycloakAdminConnector.update_user(user_a.id, {"emailVerified": True}) + valid_token = Token.from_credentials(username="ownerA", password="PWowNerA0!") From 22b426045646b62052750302ff838e0b16338ada Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Wed, 26 Oct 2022 21:20:47 +0100 Subject: [PATCH 57/73] Fix Mixin --- src/django_keycloak/mixins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/django_keycloak/mixins.py b/src/django_keycloak/mixins.py index 16410dd..c2c9411 100644 --- a/src/django_keycloak/mixins.py +++ b/src/django_keycloak/mixins.py @@ -47,4 +47,5 @@ def create_user_on_keycloak( if actions is not None: values["requiredActions"] = actions - return KeycloakAdminConnector.create_user(payload=values) + id = KeycloakAdminConnector.create_user(payload=values) + return KeycloakAdminConnector.get_user(id) From 0127a8babb0aec297d665c592e86ab3a85e2eb97 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Thu, 27 Oct 2022 11:26:57 +0200 Subject: [PATCH 58/73] Fix tests --- tests/test_site/test_app/tests/test_init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_site/test_app/tests/test_init.py b/tests/test_site/test_app/tests/test_init.py index feaf281..a89e068 100644 --- a/tests/test_site/test_app/tests/test_init.py +++ b/tests/test_site/test_app/tests/test_init.py @@ -1,8 +1,7 @@ -from django.conf import settings from django.test import TestCase -from django_keycloak.mixins import KeycloakTestMixin -from django_keycloak.connector import KeycloakAdminConnector from django_keycloak import Token +from django_keycloak.connector import KeycloakAdminConnector +from django_keycloak.mixins import KeycloakTestMixin class TestInit(KeycloakTestMixin, TestCase): @@ -20,5 +19,6 @@ def test_model(self): first_name="Owner", last_name="AAAA", ) - KeycloakAdminConnector.update_user(user_a.id, {"emailVerified": True}) + KeycloakAdminConnector.update_user(user_a["id"], {"emailVerified": True}) valid_token = Token.from_credentials(username="ownerA", password="PWowNerA0!") + self.assertTrue(valid_token) From 641ae088f3dbb10d0d914f16be23e1c13df849e4 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Thu, 27 Oct 2022 12:12:00 +0200 Subject: [PATCH 59/73] Add logging for token handling --- src/django_keycloak/config.py | 3 ++ src/django_keycloak/token.py | 13 ++++++-- .../test_app/tests/test_middleware.py | 15 +++++++-- tests/test_site/test_app/urls.py | 3 +- tests/test_site/test_app/views.py | 11 ++++--- tests/test_site/test_site/settings.py | 31 ++++++++++++++++++- 6 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/django_keycloak/config.py b/src/django_keycloak/config.py index 6ebb620..8d99b48 100644 --- a/src/django_keycloak/config.py +++ b/src/django_keycloak/config.py @@ -37,6 +37,9 @@ class Settings: VERIFY_AUDIENCE: Optional[bool] = True # Flag if the user info has been included in the token USER_INFO_IN_TOKEN: Optional[bool] = True + # Flag to show the traceback of debug logs + TRACE_DEBUG_LOGS: Optional[bool] = False + # Derived setting of the SERVER/INTERNAL_URL and BASE_PATH KEYCLOAK_URL: str = field(init=False) diff --git a/src/django_keycloak/token.py b/src/django_keycloak/token.py index 3cb2c02..88c45e2 100644 --- a/src/django_keycloak/token.py +++ b/src/django_keycloak/token.py @@ -3,6 +3,7 @@ """ from __future__ import annotations +import logging from typing import Optional from cachetools.func import ttl_cache @@ -24,6 +25,8 @@ client_secret_key=settings.CLIENT_SECRET_KEY, ) +logger = logging.getLogger(__name__) + class Token: def __init__( @@ -109,7 +112,10 @@ def is_active(self) -> bool: """ try: info = self.get_access_token_info() - except (JOSEError, KeycloakError): + except (JOSEError, KeycloakError) as err: + logger.debug( + f"{type(err).__name__}: {err.args}", exc_info=settings.TRACE_DEBUG_LOGS + ) return False # Keycloak introspections return {"active": bool} if "active" in info: @@ -207,7 +213,10 @@ def from_credentials(cls, username: str, password: str) -> Optional[Token]: # t return cls(**cls._parse_keycloak_response(keycloak_response)) # Catch authentication error (invalid credentials), # and post error (account not completed.) - except (KeycloakAuthenticationError, KeycloakPostError): + except (KeycloakAuthenticationError, KeycloakPostError) as err: + logger.debug( + f"{type(err).__name__}: {err.args}", exc_info=settings.TRACE_DEBUG_LOGS + ) return None @classmethod diff --git a/tests/test_site/test_app/tests/test_middleware.py b/tests/test_site/test_app/tests/test_middleware.py index 5d3d339..6eac9af 100644 --- a/tests/test_site/test_app/tests/test_middleware.py +++ b/tests/test_site/test_app/tests/test_middleware.py @@ -1,4 +1,5 @@ -from django.contrib.auth import get_user_model +from unittest import skip + from django.test import TestCase from django.urls import reverse from django_keycloak.mixins import KeycloakTestMixin @@ -7,7 +8,7 @@ class TestMiddleware(KeycloakTestMixin, TestCase): def setUp(self): self.keycloak_init() - self.user_a = self.create_user_on_keycloak( + self.keycloak_user = self.create_user_on_keycloak( username="ownerA", email="user@example.com", password="PWowNerA0!", @@ -22,5 +23,13 @@ def test_simple_api_call(self): response = self.client.get(reverse("test_app:simple")) self.assertEqual(response.json()["status"], "ok") + @skip def test_user_auth(self): - pass + self.assertTrue(False) + + +class TestErrorHandling(TestCase): + def test_invalid_auth_token(self): + header = {"HTTP_AUTHORIZATION": "Bearer DummyJWT"} + response = self.client.get(reverse("test_app:who_am_i"), **header) + self.assertTrue(response.json()["is_anonymous"]) diff --git a/tests/test_site/test_app/urls.py b/tests/test_site/test_app/urls.py index d1db658..20d8360 100644 --- a/tests/test_site/test_app/urls.py +++ b/tests/test_site/test_app/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from test_app.views import Simple +from test_app.views import Simple, WhoAmI app_name = "test_app" urlpatterns = [ path("simple/", Simple.as_view(), name="simple"), + path("who-am-i/", WhoAmI.as_view(), name="who_am_i"), ] diff --git a/tests/test_site/test_app/views.py b/tests/test_site/test_app/views.py index 25e19fa..95709f7 100644 --- a/tests/test_site/test_app/views.py +++ b/tests/test_site/test_app/views.py @@ -9,10 +9,13 @@ def get(self, request): class WhoAmI(APIView): def get(self, request): + if request.user.is_anonymous: + return Response({"is_anonymous": True}) user_data = { - "username": request.context.user.username, - "email": request.context.user.email, - "first_name": request.context.user.first_name, - "last_name": request.context.user.last_name, + "is_anonymous": False, + "username": request.user.username, + "email": request.user.email, + "first_name": request.user.first_name, + "last_name": request.user.last_name, } return Response(user_data) diff --git a/tests/test_site/test_site/settings.py b/tests/test_site/test_site/settings.py index c52592b..f524205 100644 --- a/tests/test_site/test_site/settings.py +++ b/tests/test_site/test_site/settings.py @@ -14,6 +14,34 @@ import sys from pathlib import Path +LOGGING = { + "version": 1, + "formatters": { + "line_scope": { + "format": f"%(levelname)s | %(name)s.%(funcName)s:%(lineno)s | %(message)s" + }, + }, + "handlers": { + "console_line_scope": { + "class": "logging.StreamHandler", + "formatter": "line_scope", + }, + }, + "loggers": { + "": { + "handlers": ["console_line_scope"], + "level": "INFO", + "propagate": False, + }, + "django_keycloak": { + "handlers": ["console_line_scope"], + "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), + "propagate": False, + }, + }, +} + + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent sys.path.append(os.path.abspath(os.path.join(BASE_DIR, "../../src"))) @@ -144,5 +172,6 @@ "CLIENT_ADMIN_ROLE": "admin", "REALM_ADMIN_ROLE": "admin", "EXEMPT_URIS": [], - "DECODE_TOKEN": "true", + "DECODE_TOKEN": True, + "TRACE_DEBUG_LOGS": True, } From 5379dda832ca33604e100660d8127c603cfca9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sim=C3=A3o=20Silva?= Date: Thu, 27 Oct 2022 12:00:07 +0100 Subject: [PATCH 60/73] Added tests with Python 3.11 and removed parallel restriction --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5341e76..40cdb59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,9 +20,8 @@ jobs: KEYCLOAK_CLIENT_ID: test-client KEYCLOAK_CLIENT_SECRET_KEY: f6974574-c773-4554-826d-06946cd55e98 strategy: - max-parallel: 2 matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] keycloak-tag: [ "13.0.1", From d880f2b5b58bb024854fcad5990dd8c95e7094a9 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Thu, 27 Oct 2022 12:31:48 +0100 Subject: [PATCH 61/73] Only connect to server if auth system is keycloak --- src/django_keycloak/connector.py | 56 ++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/django_keycloak/connector.py b/src/django_keycloak/connector.py index 56c088c..9d98a29 100644 --- a/src/django_keycloak/connector.py +++ b/src/django_keycloak/connector.py @@ -1,6 +1,7 @@ """ Module to interact with Keycloak Admin API """ +from django.contrib.auth import get_user_model from keycloak.exceptions import KeycloakAuthenticationError, KeycloakGetError from keycloak.keycloak_admin import KeycloakAdmin @@ -9,30 +10,37 @@ KeycloakMissingServiceAccountRolesError, KeycloakNoServiceAccountRolesError, ) +from django_keycloak.models import AbstractKeycloakUser -try: - KeycloakAdminConnector = KeycloakAdmin( - server_url=settings.KEYCLOAK_URL, - client_id=settings.CLIENT_ID, - realm_name=settings.REALM, - client_secret_key=settings.CLIENT_SECRET_KEY, - ) -except KeycloakAuthenticationError as error: - # Check if the error is due to service account not being enabled - if "Client not enabled to retrieve service account" in str(error): - raise KeycloakNoServiceAccountRolesError from error +# Only try to start connector if auth model user is one of this package +# user might be using conditional auth systems, and we dont want to connect +# to server if it does not exist +if not issubclass(get_user_model(), AbstractKeycloakUser): + KeycloakAdminConnector = None +else: + try: + KeycloakAdminConnector = KeycloakAdmin( + server_url=settings.KEYCLOAK_URL, + client_id=settings.CLIENT_ID, + realm_name=settings.REALM, + client_secret_key=settings.CLIENT_SECRET_KEY, + ) + except KeycloakAuthenticationError as error: + # Check if the error is due to service account not being enabled + if "Client not enabled to retrieve service account" in str(error): + raise KeycloakNoServiceAccountRolesError from error - # Otherwise re-throw the original error - else: - raise error + # Otherwise re-throw the original error + else: + raise error -# Try to call a users method -# if error occurs a required role is missing -# https://github.com/marcospereirampj/python-keycloak/issues/87 -try: - KeycloakAdminConnector.users_count() -except KeycloakGetError as error: - if "unknown_error" in str(error): - raise KeycloakMissingServiceAccountRolesError from error - else: - raise error + # Try to call a users method + # if error occurs a required role is missing + # https://github.com/marcospereirampj/python-keycloak/issues/87 + try: + KeycloakAdminConnector.users_count() + except KeycloakGetError as error: + if "unknown_error" in str(error): + raise KeycloakMissingServiceAccountRolesError from error + else: + raise error From ba8c0ee36a83afd10ed97e859c063c668b2d5fd8 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Thu, 27 Oct 2022 12:37:27 +0100 Subject: [PATCH 62/73] Fix Circular import --- src/django_keycloak/connector.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/django_keycloak/connector.py b/src/django_keycloak/connector.py index 9d98a29..24d6e17 100644 --- a/src/django_keycloak/connector.py +++ b/src/django_keycloak/connector.py @@ -10,12 +10,14 @@ KeycloakMissingServiceAccountRolesError, KeycloakNoServiceAccountRolesError, ) -from django_keycloak.models import AbstractKeycloakUser + +# Avoid circular import +import django_keycloak.models as models # Only try to start connector if auth model user is one of this package # user might be using conditional auth systems, and we dont want to connect # to server if it does not exist -if not issubclass(get_user_model(), AbstractKeycloakUser): +if not issubclass(get_user_model(), models.AbstractKeycloakUser): KeycloakAdminConnector = None else: try: From 54ee49ce1fdd81f080413aa0f61d933f88cf045b Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Thu, 27 Oct 2022 12:47:03 +0100 Subject: [PATCH 63/73] Change admin connection to lazy instance --- src/django_keycloak/authentication.py | 5 +--- src/django_keycloak/backends.py | 15 ++++++------ src/django_keycloak/connector.py | 35 +++++++++++---------------- src/django_keycloak/mixins.py | 17 +++++++------ src/django_keycloak/models.py | 7 +++--- 5 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/django_keycloak/authentication.py b/src/django_keycloak/authentication.py index 81e6440..5a88b5d 100644 --- a/src/django_keycloak/authentication.py +++ b/src/django_keycloak/authentication.py @@ -1,14 +1,11 @@ """ Custom authentication class for Django Rest Framework. """ +from typing import Union from django.contrib.auth import get_user_model -from django.contrib.auth.models import AnonymousUser -from rest_framework import HTTP_HEADER_ENCODING from rest_framework.authentication import TokenAuthentication from rest_framework.exceptions import AuthenticationFailed -from rest_framework.authentication import get_authorization_header from django_keycloak import Token -from typing import Union class KeycloakAuthentication(TokenAuthentication): diff --git a/src/django_keycloak/backends.py b/src/django_keycloak/backends.py index 979908f..7ff544f 100644 --- a/src/django_keycloak/backends.py +++ b/src/django_keycloak/backends.py @@ -4,9 +4,8 @@ from typing import Optional, Union from django.contrib.auth.backends import RemoteUserBackend from django.contrib.auth import get_user_model -from django_keycloak.models import KeycloakUserAutoId +from django_keycloak.models import KeycloakUserAutoId, KeycloakUser from django_keycloak import Token -from django_keycloak.models import KeycloakUser, KeycloakUserAutoId class KeycloakAuthenticationBackend(RemoteUserBackend): @@ -17,7 +16,7 @@ class KeycloakAuthenticationBackend(RemoteUserBackend): def authenticate( self, request, - username: Optional[str] = None, + remote_user: Optional[str] = None, password: Optional[str] = None, ): """ @@ -28,7 +27,7 @@ def authenticate( # Create token from the provided credentials and check if # credentials were valid - token = Token.from_credentials(username, password) # type: ignore + token = Token.from_credentials(remote_user, password) # type: ignore # Check for non-existing or unactive token if not token: @@ -40,7 +39,7 @@ def authenticate( # try to get user from database try: - user = User.objects.get(username=username) + user = User.objects.get(username=remote_user) if isinstance(user, KeycloakUserAutoId): # Get user information from token user_info = token.user_info @@ -64,12 +63,12 @@ def authenticate( user.save() return user - def get_user(self, user_identifier: str): + def get_user(self, user_id: str): User: Union[KeycloakUser, KeycloakUserAutoId] = get_user_model() try: - return User.objects.get(username=user_identifier) + return User.objects.get(username=user_id) except User.DoesNotExist: try: - return User.objects.get(id=user_identifier) + return User.objects.get(id=user_id) except User.DoesNotExist: return None diff --git a/src/django_keycloak/connector.py b/src/django_keycloak/connector.py index 24d6e17..7a21c94 100644 --- a/src/django_keycloak/connector.py +++ b/src/django_keycloak/connector.py @@ -1,7 +1,6 @@ """ Module to interact with Keycloak Admin API """ -from django.contrib.auth import get_user_model from keycloak.exceptions import KeycloakAuthenticationError, KeycloakGetError from keycloak.keycloak_admin import KeycloakAdmin @@ -11,22 +10,27 @@ KeycloakNoServiceAccountRolesError, ) -# Avoid circular import -import django_keycloak.models as models -# Only try to start connector if auth model user is one of this package -# user might be using conditional auth systems, and we dont want to connect -# to server if it does not exist -if not issubclass(get_user_model(), models.AbstractKeycloakUser): - KeycloakAdminConnector = None -else: +def KeycloakAdminConnector(): # noqa try: - KeycloakAdminConnector = KeycloakAdmin( + connector = KeycloakAdmin( server_url=settings.KEYCLOAK_URL, client_id=settings.CLIENT_ID, realm_name=settings.REALM, client_secret_key=settings.CLIENT_SECRET_KEY, ) + # Try to call a users method + # if error occurs a required role is missing + # https://github.com/marcospereirampj/python-keycloak/issues/87 + try: + connector.users_count() + except KeycloakGetError as error: + if "unknown_error" in str(error): + raise KeycloakMissingServiceAccountRolesError from error + else: + raise error + + return connector except KeycloakAuthenticationError as error: # Check if the error is due to service account not being enabled if "Client not enabled to retrieve service account" in str(error): @@ -35,14 +39,3 @@ # Otherwise re-throw the original error else: raise error - - # Try to call a users method - # if error occurs a required role is missing - # https://github.com/marcospereirampj/python-keycloak/issues/87 - try: - KeycloakAdminConnector.users_count() - except KeycloakGetError as error: - if "unknown_error" in str(error): - raise KeycloakMissingServiceAccountRolesError from error - else: - raise error diff --git a/src/django_keycloak/mixins.py b/src/django_keycloak/mixins.py index c2c9411..667588c 100644 --- a/src/django_keycloak/mixins.py +++ b/src/django_keycloak/mixins.py @@ -13,15 +13,16 @@ class KeycloakTestMixin: """ def keycloak_init(self): - self._start_users = { - user.get("id") for user in KeycloakAdminConnector.get_users() - } + connector = KeycloakAdminConnector() + + self._start_users = {user.get("id") for user in connector.get_users()} def keycloak_cleanup(self): - new_users = {user.get("id") for user in KeycloakAdminConnector.get_users()} + connector = KeycloakAdminConnector() + new_users = {user.get("id") for user in connector.get_users()} users_to_remove = new_users.difference(self._start_users) for user_id in users_to_remove: - KeycloakAdminConnector.delete_user(user_id) + connector.delete_user(user_id) def create_user_on_keycloak( self, @@ -34,7 +35,7 @@ def create_user_on_keycloak( actions=None, ) -> dict: """Creates user on keycloak server, No state is changed on local db""" - + connector = KeycloakAdminConnector() values = {"username": username, "email": email, "enabled": enabled} if password is not None: values["credentials"] = [ @@ -47,5 +48,5 @@ def create_user_on_keycloak( if actions is not None: values["requiredActions"] = actions - id = KeycloakAdminConnector.create_user(payload=values) - return KeycloakAdminConnector.get_user(id) + id = connector.create_user(payload=values) + return connector.get_user(id) diff --git a/src/django_keycloak/models.py b/src/django_keycloak/models.py index 2483992..f7c575d 100644 --- a/src/django_keycloak/models.py +++ b/src/django_keycloak/models.py @@ -15,6 +15,7 @@ class AbstractKeycloakUser(AbstractBaseUser, PermissionsMixin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.connector = KeycloakAdminConnector() self._cached_user_info = None id = models.UUIDField(_("keycloak_id"), unique=True, primary_key=True) @@ -48,10 +49,10 @@ def update_keycloak(self, email=None, first_name=None, last_name=None): values["firstName"] = first_name if last_name is not None: values["lastName"] = last_name - return KeycloakAdminConnector.update_user(self.keycloak_identifier, **values) + return self.connector.update_user(self.keycloak_identifier, **values) def delete_keycloak(self): - KeycloakAdminConnector.delete_user(self.keycloak_identifier) + self.connector.delete_user(self.keycloak_identifier) class KeycloakUser(AbstractKeycloakUser): @@ -77,7 +78,7 @@ def last_name(self): def _confirm_cache(self): if not self._cached_user_info: - self._cached_user_info = KeycloakAdminConnector.get_user(self.id) + self._cached_user_info = self.connector.get_user(self.id) class AbstractKeycloakUserAutoId(AbstractKeycloakUser): From 7bc11e3b4fae2430e6584bcb69a4818fa2d5a425 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Thu, 27 Oct 2022 14:28:41 +0100 Subject: [PATCH 64/73] Update test to lazy connector --- tests/test_site/test_app/tests/test_init.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_site/test_app/tests/test_init.py b/tests/test_site/test_app/tests/test_init.py index a89e068..ce9da99 100644 --- a/tests/test_site/test_app/tests/test_init.py +++ b/tests/test_site/test_app/tests/test_init.py @@ -6,6 +6,7 @@ class TestInit(KeycloakTestMixin, TestCase): def setUp(self): + self.connector = KeycloakAdminConnector() self.keycloak_init() def tearDown(self): @@ -19,6 +20,6 @@ def test_model(self): first_name="Owner", last_name="AAAA", ) - KeycloakAdminConnector.update_user(user_a["id"], {"emailVerified": True}) + self.connector.update_user(user_a["id"], {"emailVerified": True}) valid_token = Token.from_credentials(username="ownerA", password="PWowNerA0!") self.assertTrue(valid_token) From 7a44d4d0bac99589e8299a44156161f8fe67ddd7 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Thu, 27 Oct 2022 16:13:46 +0100 Subject: [PATCH 65/73] Update connector place --- src/django_keycloak/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/django_keycloak/models.py b/src/django_keycloak/models.py index f7c575d..2f1a8e6 100644 --- a/src/django_keycloak/models.py +++ b/src/django_keycloak/models.py @@ -15,7 +15,6 @@ class AbstractKeycloakUser(AbstractBaseUser, PermissionsMixin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.connector = KeycloakAdminConnector() self._cached_user_info = None id = models.UUIDField(_("keycloak_id"), unique=True, primary_key=True) @@ -41,7 +40,7 @@ class Meta(AbstractBaseUser.Meta): abstract = True def update_keycloak(self, email=None, first_name=None, last_name=None): - + connector = KeycloakAdminConnector() values = {} if email is not None: values["email"] = email @@ -49,10 +48,11 @@ def update_keycloak(self, email=None, first_name=None, last_name=None): values["firstName"] = first_name if last_name is not None: values["lastName"] = last_name - return self.connector.update_user(self.keycloak_identifier, **values) + return connector.update_user(self.keycloak_identifier, **values) def delete_keycloak(self): - self.connector.delete_user(self.keycloak_identifier) + connector = KeycloakAdminConnector() + connector.delete_user(self.keycloak_identifier) class KeycloakUser(AbstractKeycloakUser): From 24486d076896d630ec1dadae729331ea5a51259d Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Thu, 27 Oct 2022 16:15:11 +0100 Subject: [PATCH 66/73] Update connector --- src/django_keycloak/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/django_keycloak/models.py b/src/django_keycloak/models.py index 2f1a8e6..fe2dd57 100644 --- a/src/django_keycloak/models.py +++ b/src/django_keycloak/models.py @@ -77,8 +77,9 @@ def last_name(self): return self._cached_user_info.get("lastName") def _confirm_cache(self): + connector = KeycloakAdminConnector() if not self._cached_user_info: - self._cached_user_info = self.connector.get_user(self.id) + self._cached_user_info = connector.get_user(self.id) class AbstractKeycloakUserAutoId(AbstractKeycloakUser): From c32c78cd20e0488e04b6d7c5420742d0df3f644c Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Thu, 27 Oct 2022 17:32:15 +0100 Subject: [PATCH 67/73] Add docs for backend --- src/django_keycloak/backends.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/django_keycloak/backends.py b/src/django_keycloak/backends.py index 7ff544f..cc8170e 100644 --- a/src/django_keycloak/backends.py +++ b/src/django_keycloak/backends.py @@ -23,6 +23,13 @@ def authenticate( Authenticates an user by credentials, and updates it's information (first name, last name, email). If user does not exist it is created with appropriate permissions. + + Parameters + ---------- + remote_user: str + The keycloak username. + password: str + The keycloak password. """ # Create token from the provided credentials and check if From 3908c516881e4ed4929c6a8872e2de82ef3ec67c Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Thu, 27 Oct 2022 21:45:38 +0200 Subject: [PATCH 68/73] Add lazy loaded KeycloakAdmin --- src/django_keycloak/connector.py | 67 ++++++++++++------- .../commands/sync_keycloak_users.py | 8 +-- src/django_keycloak/mixins.py | 32 +++++---- src/django_keycloak/models.py | 11 ++- tests/start.sh | 10 +-- tests/test_site/test_app/tests/test_init.py | 5 +- 6 files changed, 74 insertions(+), 59 deletions(-) diff --git a/src/django_keycloak/connector.py b/src/django_keycloak/connector.py index 7a21c94..4e7a9ce 100644 --- a/src/django_keycloak/connector.py +++ b/src/django_keycloak/connector.py @@ -1,6 +1,8 @@ """ Module to interact with Keycloak Admin API """ +from typing import Dict, List + from keycloak.exceptions import KeycloakAuthenticationError, KeycloakGetError from keycloak.keycloak_admin import KeycloakAdmin @@ -10,32 +12,51 @@ KeycloakNoServiceAccountRolesError, ) +_args: List +_kwargs: Dict +_initialized: bool = False + + +class LazyKeycloakAdmin(KeycloakAdmin): + def __init__(self, *args, **kwargs): + global _args, _kwargs + _args, _kwargs = args, kwargs -def KeycloakAdminConnector(): # noqa - try: - connector = KeycloakAdmin( - server_url=settings.KEYCLOAK_URL, - client_id=settings.CLIENT_ID, - realm_name=settings.REALM, - client_secret_key=settings.CLIENT_SECRET_KEY, - ) - # Try to call a users method - # if error occurs a required role is missing - # https://github.com/marcospereirampj/python-keycloak/issues/87 + def __getattribute__(self, item): + global _initialized, _args, _kwargs + if not _initialized: + _initialized = True + self.handle_keycloak_init(_args, _kwargs) + # Calling the super class to avoid recursion + return super().__getattribute__(item) + + def handle_keycloak_init(self, args, kwargs): try: - connector.users_count() - except KeycloakGetError as error: - if "unknown_error" in str(error): - raise KeycloakMissingServiceAccountRolesError from error + super().__init__(*args, **kwargs) + # Try to call a users method + # if error occurs a required role is missing + # https://github.com/marcospereirampj/python-keycloak/issues/87 + try: + self.users_count() + except KeycloakGetError as error: + if "unknown_error" in str(error): + raise KeycloakMissingServiceAccountRolesError from error + else: + raise error + except KeycloakAuthenticationError as error: + # Check if the error is due to service account not being enabled + if "Client not enabled to retrieve service account" in str(error): + raise KeycloakNoServiceAccountRolesError from error + + # Otherwise re-throw the original error else: raise error - return connector - except KeycloakAuthenticationError as error: - # Check if the error is due to service account not being enabled - if "Client not enabled to retrieve service account" in str(error): - raise KeycloakNoServiceAccountRolesError from error - # Otherwise re-throw the original error - else: - raise error +# lazy_keycloak_admin = lazy_init(KeycloakAdmin)( +lazy_keycloak_admin = LazyKeycloakAdmin( + server_url=settings.KEYCLOAK_URL, + client_id=settings.CLIENT_ID, + realm_name=settings.REALM, + client_secret_key=settings.CLIENT_SECRET_KEY, +) diff --git a/src/django_keycloak/management/commands/sync_keycloak_users.py b/src/django_keycloak/management/commands/sync_keycloak_users.py index 64ccd75..f140a1f 100644 --- a/src/django_keycloak/management/commands/sync_keycloak_users.py +++ b/src/django_keycloak/management/commands/sync_keycloak_users.py @@ -1,9 +1,9 @@ import logging as log -from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand -from django_keycloak.connector import KeycloakAdminConnector +from django_keycloak.connector import lazy_keycloak_admin class Command(BaseCommand): @@ -13,9 +13,7 @@ def handle(self, *args, **options): User = get_user_model() - remote_users = set( - [user.get("id") for user in KeycloakAdminConnector.get_users()] - ) + remote_users = set([user.get("id") for user in lazy_keycloak_admin.get_users()]) local_users = set(str(_u.id) for _u in User.objects.all()) users_to_remove = local_users.difference(remote_users) diff --git a/src/django_keycloak/mixins.py b/src/django_keycloak/mixins.py index 667588c..2f56d93 100644 --- a/src/django_keycloak/mixins.py +++ b/src/django_keycloak/mixins.py @@ -1,4 +1,6 @@ -from django_keycloak.connector import KeycloakAdminConnector +from typing import Optional + +from django_keycloak.connector import lazy_keycloak_admin class KeycloakTestMixin: @@ -13,29 +15,25 @@ class KeycloakTestMixin: """ def keycloak_init(self): - connector = KeycloakAdminConnector() - - self._start_users = {user.get("id") for user in connector.get_users()} + self._start_users = {user.get("id") for user in lazy_keycloak_admin.get_users()} def keycloak_cleanup(self): - connector = KeycloakAdminConnector() - new_users = {user.get("id") for user in connector.get_users()} + new_users = {user.get("id") for user in lazy_keycloak_admin.get_users()} users_to_remove = new_users.difference(self._start_users) for user_id in users_to_remove: - connector.delete_user(user_id) + lazy_keycloak_admin.delete_user(user_id) def create_user_on_keycloak( self, - username, - email, - password=None, - first_name=None, - last_name=None, - enabled=True, - actions=None, + username: str, + email: str, + password: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + enabled: bool = True, + actions: Optional[str] = None, ) -> dict: """Creates user on keycloak server, No state is changed on local db""" - connector = KeycloakAdminConnector() values = {"username": username, "email": email, "enabled": enabled} if password is not None: values["credentials"] = [ @@ -48,5 +46,5 @@ def create_user_on_keycloak( if actions is not None: values["requiredActions"] = actions - id = connector.create_user(payload=values) - return connector.get_user(id) + user_id = lazy_keycloak_admin.create_user(payload=values) + return lazy_keycloak_admin.get_user(user_id) diff --git a/src/django_keycloak/models.py b/src/django_keycloak/models.py index fe2dd57..4e200be 100644 --- a/src/django_keycloak/models.py +++ b/src/django_keycloak/models.py @@ -4,8 +4,8 @@ from django.utils.translation import gettext_lazy as _ from dry_rest_permissions.generics import authenticated_users +from .connector import lazy_keycloak_admin from .managers import KeycloakUserManager, KeycloakUserManagerAutoId -from .connector import KeycloakAdminConnector class AbstractKeycloakUser(AbstractBaseUser, PermissionsMixin): @@ -40,7 +40,6 @@ class Meta(AbstractBaseUser.Meta): abstract = True def update_keycloak(self, email=None, first_name=None, last_name=None): - connector = KeycloakAdminConnector() values = {} if email is not None: values["email"] = email @@ -48,11 +47,10 @@ def update_keycloak(self, email=None, first_name=None, last_name=None): values["firstName"] = first_name if last_name is not None: values["lastName"] = last_name - return connector.update_user(self.keycloak_identifier, **values) + return lazy_keycloak_admin.update_user(self.keycloak_identifier, **values) def delete_keycloak(self): - connector = KeycloakAdminConnector() - connector.delete_user(self.keycloak_identifier) + lazy_keycloak_admin.delete_user(self.keycloak_identifier) class KeycloakUser(AbstractKeycloakUser): @@ -77,9 +75,8 @@ def last_name(self): return self._cached_user_info.get("lastName") def _confirm_cache(self): - connector = KeycloakAdminConnector() if not self._cached_user_info: - self._cached_user_info = connector.get_user(self.id) + self._cached_user_info = lazy_keycloak_admin.get_user(self.id) class AbstractKeycloakUserAutoId(AbstractKeycloakUser): diff --git a/tests/start.sh b/tests/start.sh index 0b90804..95061ee 100755 --- a/tests/start.sh +++ b/tests/start.sh @@ -1,7 +1,7 @@ #!/bin/bash # Exit with nonzero exit code if anything fails -set -exo pipefail +set -eo pipefail # Ensure the script is running in this directory cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" @@ -24,12 +24,14 @@ if (($(curl -s -o /dev/null -w "%{http_code}" "$KEYCLOAK_URL/auth/realms/$KEYCLO -d "username=$KEYCLOAK_ADMIN_USER" \ -d "password=$KEYCLOAK_ADMIN_PASSWORD" \ "$KEYCLOAK_URL/auth/realms/master/protocol/openid-connect/token") - echo "$KEYCLOAK_TOKEN_RESPONSE" KEYCLOAK_TOKEN=$(echo "$KEYCLOAK_TOKEN_RESPONSE" | jq -r .access_token) - echo "$KEYCLOAK_TOKEN" # Get Keycloak server version to add the right config file - KEYCLOAK_VERSION="$(curl -v -X GET -H "Content-Type: application/json" -H "Authorization: bearer $KEYCLOAK_TOKEN" "$KEYCLOAK_URL/auth/admin/serverinfo" | jq '.systemInfo.version')" + KEYCLOAK_VERSION="$(curl -s \ + -H "Content-Type: application/json" \ + -H "Authorization: bearer $KEYCLOAK_TOKEN" \ + "$KEYCLOAK_URL/auth/admin/serverinfo" \ + | jq '.systemInfo.version')" echo "Keycloak version: $KEYCLOAK_VERSION" VERSION_MAJOR="$(echo "$KEYCLOAK_VERSION" | tr -d \" | cut -d . -f 1)" if [ "$VERSION_MAJOR" -le 14 ]; then diff --git a/tests/test_site/test_app/tests/test_init.py b/tests/test_site/test_app/tests/test_init.py index ce9da99..083aee4 100644 --- a/tests/test_site/test_app/tests/test_init.py +++ b/tests/test_site/test_app/tests/test_init.py @@ -1,12 +1,11 @@ from django.test import TestCase from django_keycloak import Token -from django_keycloak.connector import KeycloakAdminConnector +from django_keycloak.connector import lazy_keycloak_admin from django_keycloak.mixins import KeycloakTestMixin class TestInit(KeycloakTestMixin, TestCase): def setUp(self): - self.connector = KeycloakAdminConnector() self.keycloak_init() def tearDown(self): @@ -20,6 +19,6 @@ def test_model(self): first_name="Owner", last_name="AAAA", ) - self.connector.update_user(user_a["id"], {"emailVerified": True}) + lazy_keycloak_admin.update_user(user_a["id"], {"emailVerified": True}) valid_token = Token.from_credentials(username="ownerA", password="PWowNerA0!") self.assertTrue(valid_token) From 99bf549fdca66998083812e87538ef8e847a7042 Mon Sep 17 00:00:00 2001 From: Moritz Ulmer Date: Fri, 28 Oct 2022 09:56:28 +0200 Subject: [PATCH 69/73] Add authentication test --- src/django_keycloak/connector.py | 1 - src/django_keycloak/middleware.py | 6 ++++-- .../test_app/tests/test_middleware.py | 19 +++++++++++++------ tests/test_site/test_app/views.py | 8 ++++---- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/django_keycloak/connector.py b/src/django_keycloak/connector.py index 4e7a9ce..b9bdb14 100644 --- a/src/django_keycloak/connector.py +++ b/src/django_keycloak/connector.py @@ -53,7 +53,6 @@ def handle_keycloak_init(self, args, kwargs): raise error -# lazy_keycloak_admin = lazy_init(KeycloakAdmin)( lazy_keycloak_admin = LazyKeycloakAdmin( server_url=settings.KEYCLOAK_URL, client_id=settings.CLIENT_ID, diff --git a/src/django_keycloak/middleware.py b/src/django_keycloak/middleware.py index 35daa1c..2f98a8e 100644 --- a/src/django_keycloak/middleware.py +++ b/src/django_keycloak/middleware.py @@ -4,12 +4,14 @@ """ import base64 import re -from typing import Union, Optional +from typing import Optional, Union + from django.contrib.auth import get_user_model from django.utils.deprecation import MiddlewareMixin -from django_keycloak.models import KeycloakUserAutoId, KeycloakUser + from django_keycloak import Token from django_keycloak.config import settings +from django_keycloak.models import KeycloakUser, KeycloakUserAutoId class KeycloakMiddleware(MiddlewareMixin): diff --git a/tests/test_site/test_app/tests/test_middleware.py b/tests/test_site/test_app/tests/test_middleware.py index 6eac9af..f20c90e 100644 --- a/tests/test_site/test_app/tests/test_middleware.py +++ b/tests/test_site/test_app/tests/test_middleware.py @@ -1,17 +1,17 @@ -from unittest import skip - from django.test import TestCase from django.urls import reverse +from django_keycloak.connector import lazy_keycloak_admin from django_keycloak.mixins import KeycloakTestMixin class TestMiddleware(KeycloakTestMixin, TestCase): def setUp(self): self.keycloak_init() + self.user_password = "PWowNerA0!" self.keycloak_user = self.create_user_on_keycloak( username="ownerA", email="user@example.com", - password="PWowNerA0!", + password=self.user_password, first_name="Owner", last_name="AAAA", ) @@ -23,13 +23,20 @@ def test_simple_api_call(self): response = self.client.get(reverse("test_app:simple")) self.assertEqual(response.json()["status"], "ok") - @skip def test_user_auth(self): - self.assertTrue(False) + tokens = lazy_keycloak_admin.keycloak_openid.token( + username=self.keycloak_user["username"], + password=self.user_password, + ) + header = {"HTTP_AUTHORIZATION": f"Bearer {tokens['access_token']}"} + response = self.client.get(reverse("test_app:who_am_i"), **header) + data = response.json() + self.assertFalse(data.pop("isAnonymous")) + self.assertDictContainsSubset(data, self.keycloak_user) class TestErrorHandling(TestCase): def test_invalid_auth_token(self): header = {"HTTP_AUTHORIZATION": "Bearer DummyJWT"} response = self.client.get(reverse("test_app:who_am_i"), **header) - self.assertTrue(response.json()["is_anonymous"]) + self.assertTrue(response.json()["isAnonymous"]) diff --git a/tests/test_site/test_app/views.py b/tests/test_site/test_app/views.py index 95709f7..07a0af4 100644 --- a/tests/test_site/test_app/views.py +++ b/tests/test_site/test_app/views.py @@ -10,12 +10,12 @@ def get(self, request): class WhoAmI(APIView): def get(self, request): if request.user.is_anonymous: - return Response({"is_anonymous": True}) + return Response({"isAnonymous": True}) user_data = { - "is_anonymous": False, + "isAnonymous": False, "username": request.user.username, "email": request.user.email, - "first_name": request.user.first_name, - "last_name": request.user.last_name, + "firstName": request.user.first_name, + "lastName": request.user.last_name, } return Response(user_data) From 164d24ef56983e747c7bca075c25c843870a68eb Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Fri, 28 Oct 2022 11:33:51 +0100 Subject: [PATCH 70/73] Improve Docs --- pyproject.toml | 1 - src/django_keycloak/admin.py | 4 +++- src/django_keycloak/authentication.py | 4 ++++ src/django_keycloak/backends.py | 8 ++++---- src/django_keycloak/connector.py | 18 ++++++++++++++++++ src/django_keycloak/mixins.py | 5 ++++- src/django_keycloak/token.py | 12 +++++++----- 7 files changed, 40 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c8c1af3..fe094ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ description = "Middleware to allow authorization using Keycloak and Django" authors = [ "Ubiwhere ", "Moritz Ulmer ", - "Diogo Silva ", ] license = "MIT" repository = "https://github.com/urbanplatform/django-keycloak-auth" diff --git a/src/django_keycloak/admin.py b/src/django_keycloak/admin.py index df58eae..7ff640f 100644 --- a/src/django_keycloak/admin.py +++ b/src/django_keycloak/admin.py @@ -33,7 +33,9 @@ class UserAdmin(admin.ModelAdmin): search_fields = ["username", "email"] def keycloak_link(self, obj): - + """ + Adds an hyperlink to django-admin, which open the Keycloak's user profiles on Keycloak's Admin Console. + """ base_path = settings.BASE_PATH server = settings.SERVER_URL realm = settings.REALM diff --git a/src/django_keycloak/authentication.py b/src/django_keycloak/authentication.py index 5a88b5d..7e31fcf 100644 --- a/src/django_keycloak/authentication.py +++ b/src/django_keycloak/authentication.py @@ -13,6 +13,10 @@ class KeycloakAuthentication(TokenAuthentication): A custom token authentication class for Keycloak. """ + # `keyword` refeers to expected prefix in HTTP + # Authentication header. We use `Bearer` because it + # is commonly used in authorization protocols, such + # as OAuth2 keyword = "Bearer" def authenticate_credentials(self, access_token: str): diff --git a/src/django_keycloak/backends.py b/src/django_keycloak/backends.py index cc8170e..60918ea 100644 --- a/src/django_keycloak/backends.py +++ b/src/django_keycloak/backends.py @@ -20,16 +20,16 @@ def authenticate( password: Optional[str] = None, ): """ - Authenticates an user by credentials, and - updates it's information (first name, last name, email). + Authenticates a user by credentials, and + updates their information (first name, last name, email). If user does not exist it is created with appropriate permissions. Parameters ---------- remote_user: str - The keycloak username. + The Keycloak's username. password: str - The keycloak password. + The Keycloak's password. """ # Create token from the provided credentials and check if diff --git a/src/django_keycloak/connector.py b/src/django_keycloak/connector.py index b9bdb14..9cb168b 100644 --- a/src/django_keycloak/connector.py +++ b/src/django_keycloak/connector.py @@ -18,11 +18,24 @@ class LazyKeycloakAdmin(KeycloakAdmin): + """ + Overrides `KeycloakAdmin` from `python-keycloak`, + to provide a mechanism to lazy load the connection to + Keycloak's server and re-use the same connection on + subsequent requests, thus eliminating the connection overhead. + """ + def __init__(self, *args, **kwargs): global _args, _kwargs _args, _kwargs = args, kwargs def __getattribute__(self, item): + """ + Intercepts a method call for `KeycloakAdmin`. + If no instance as been previously initialized, we + call the parent constructor to create a new one, and + save a flag to re-use the same instance on following requests. + """ global _initialized, _args, _kwargs if not _initialized: _initialized = True @@ -31,6 +44,10 @@ def __getattribute__(self, item): return super().__getattribute__(item) def handle_keycloak_init(self, args, kwargs): + """ + Calls the parent constructor to initialize a + Keycloak connection. + """ try: super().__init__(*args, **kwargs) # Try to call a users method @@ -53,6 +70,7 @@ def handle_keycloak_init(self, args, kwargs): raise error +# The exported module variable lazy_keycloak_admin = LazyKeycloakAdmin( server_url=settings.KEYCLOAK_URL, client_id=settings.CLIENT_ID, diff --git a/src/django_keycloak/mixins.py b/src/django_keycloak/mixins.py index 2f56d93..da0bc6f 100644 --- a/src/django_keycloak/mixins.py +++ b/src/django_keycloak/mixins.py @@ -33,7 +33,10 @@ def create_user_on_keycloak( enabled: bool = True, actions: Optional[str] = None, ) -> dict: - """Creates user on keycloak server, No state is changed on local db""" + """ + Creates user on Keycloak's server. + No state is changed on local database. + """ values = {"username": username, "email": email, "enabled": enabled} if password is not None: values["credentials"] = [ diff --git a/src/django_keycloak/token.py b/src/django_keycloak/token.py index 88c45e2..b24a456 100644 --- a/src/django_keycloak/token.py +++ b/src/django_keycloak/token.py @@ -41,7 +41,8 @@ def __init__( @ttl_cache(maxsize=1, ttl=60) def public_key(self): """ - Formats the public + Obtains the Keycloak's Public key, used for token + decodings. Raises: KeycloakError: On Keycloak API errors @@ -114,13 +115,14 @@ def is_active(self) -> bool: info = self.get_access_token_info() except (JOSEError, KeycloakError) as err: logger.debug( - f"{type(err).__name__}: {err.args}", exc_info=settings.TRACE_DEBUG_LOGS + "%s: %s", + type(err).__name__, + err.args, + exc_info=settings.TRACE_DEBUG_LOGS, ) return False # Keycloak introspections return {"active": bool} - if "active" in info: - return info["active"] - return True + return info["active"] if "active" in info else True @property def user_info(self) -> dict: From fca3b627d576d173e4f730ef44d63971340295e8 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Fri, 28 Oct 2022 11:42:31 +0100 Subject: [PATCH 71/73] Fix Typo --- src/django_keycloak/connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_keycloak/connector.py b/src/django_keycloak/connector.py index 9cb168b..d83e417 100644 --- a/src/django_keycloak/connector.py +++ b/src/django_keycloak/connector.py @@ -32,7 +32,7 @@ def __init__(self, *args, **kwargs): def __getattribute__(self, item): """ Intercepts a method call for `KeycloakAdmin`. - If no instance as been previously initialized, we + If no instance has been previously initialized, we call the parent constructor to create a new one, and save a flag to re-use the same instance on following requests. """ From b191719c6f7ee84be408c2894cc02f6fa31258db Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Fri, 28 Oct 2022 11:42:31 +0100 Subject: [PATCH 72/73] Fix Typo --- src/django_keycloak/backends.py | 2 +- src/django_keycloak/connector.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/django_keycloak/backends.py b/src/django_keycloak/backends.py index 60918ea..815fcf2 100644 --- a/src/django_keycloak/backends.py +++ b/src/django_keycloak/backends.py @@ -36,7 +36,7 @@ def authenticate( # credentials were valid token = Token.from_credentials(remote_user, password) # type: ignore - # Check for non-existing or unactive token + # Check for non-existing or inactive token if not token: # credentials were not valid return diff --git a/src/django_keycloak/connector.py b/src/django_keycloak/connector.py index 9cb168b..d83e417 100644 --- a/src/django_keycloak/connector.py +++ b/src/django_keycloak/connector.py @@ -32,7 +32,7 @@ def __init__(self, *args, **kwargs): def __getattribute__(self, item): """ Intercepts a method call for `KeycloakAdmin`. - If no instance as been previously initialized, we + If no instance has been previously initialized, we call the parent constructor to create a new one, and save a flag to re-use the same instance on following requests. """ From 882c463e5dd3a6af5821497b84af04ce5b113e54 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Fri, 28 Oct 2022 13:28:20 +0100 Subject: [PATCH 73/73] Simplify Expression --- src/django_keycloak/backends.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/django_keycloak/backends.py b/src/django_keycloak/backends.py index 815fcf2..f38bccb 100644 --- a/src/django_keycloak/backends.py +++ b/src/django_keycloak/backends.py @@ -62,10 +62,7 @@ def authenticate( # `create_from_token` takes cares of password hashing user = User.objects.create_from_token(token) - if token.is_superuser: - user.is_staff = user.is_superuser = True - else: - user.is_staff = user.is_superuser = False + user.is_staff = user.is_superuser = bool(token.is_superuser) user.save() return user