diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index e5f504fe..30afdc64 100644 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -94,6 +94,7 @@ jobs: run: | docker load --input /tmp/cvat_server/image.tar docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \ + -e USE_ALLAUTH_SOCIAL_ACCOUNTS="True" \ --entrypoint /bin/bash -u root cvat/server \ -c 'python manage.py spectacular --file /transfer/schema.yml' pip3 install --user -r cvat-sdk/gen/requirements.txt diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 39fb91ec..4c7b2b5c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,6 +97,7 @@ jobs: run: | docker load --input /tmp/cvat_server/image.tar docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \ + -e USE_ALLAUTH_SOCIAL_ACCOUNTS="True" \ --entrypoint /bin/bash -u root cvat/server \ -c 'python manage.py spectacular --file /transfer/schema.yml' pip3 install --user -r cvat-sdk/gen/requirements.txt diff --git a/cvat/apps/iam/adapters.py b/cvat/apps/iam/adapters.py index 649bf472..a53ce0c0 100644 --- a/cvat/apps/iam/adapters.py +++ b/cvat/apps/iam/adapters.py @@ -8,6 +8,7 @@ from django.conf import settings from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from allauth.account.adapter import DefaultAccountAdapter +from allauth.socialaccount.providers.amazon_cognito.views import AmazonCognitoOAuth2Adapter from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter from allauth.exceptions import ImmediateHttpResponse @@ -48,3 +49,7 @@ class GoogleAdapter(GoogleOAuth2Adapter): def get_callback_url(self, request, app): return settings.GOOGLE_CALLBACK_URL + +class AmazonCognitoOAuth2AdapterEx(AmazonCognitoOAuth2Adapter): + def get_callback_url(self, request, app): + return settings.AMAZON_COGNITO_REDIRECT_URI diff --git a/cvat/apps/iam/static/social_authentication/social-amazon-cognito-logo.svg b/cvat/apps/iam/static/social_authentication/social-amazon-cognito-logo.svg new file mode 100644 index 00000000..6d632fcd --- /dev/null +++ b/cvat/apps/iam/static/social_authentication/social-amazon-cognito-logo.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cvat/apps/iam/urls.py b/cvat/apps/iam/urls.py index ca6161b4..0ad4d31b 100644 --- a/cvat/apps/iam/urls.py +++ b/cvat/apps/iam/urls.py @@ -12,15 +12,14 @@ from dj_rest_auth.views import ( from allauth.account import app_settings as allauth_settings from cvat.apps.iam.views import ( - SigningView, RegisterViewEx, RulesView, ConfirmEmailViewEx, -) -from cvat.apps.iam.views import ( + SigningView, CognitoLogin, RegisterViewEx, RulesView, + ConfirmEmailViewEx, LoginViewEx, GitHubLogin, GoogleLogin, SocialAuthMethods, github_oauth2_login as github_login, github_oauth2_callback as github_callback, google_oauth2_login as google_login, google_oauth2_callback as google_callback, - LoginViewEx, GitHubLogin, GoogleLogin, - SocialAuthMethods, + amazon_cognito_oauth2_login as amazon_cognito_login, + amazon_cognito_oauth2_callback as amazon_cognito_callback, ) urlpatterns = [ @@ -58,6 +57,9 @@ if settings.IAM_TYPE == 'BASIC': path('google/login/', google_login, name='google_login'), path('google/login/callback/', google_callback, name='google_callback'), path('google/login/token', GoogleLogin.as_view()), + path('amazon-cognito/login/', amazon_cognito_login, name='amazon_cognito_login'), + path('amazon-cognito/login/callback/', amazon_cognito_callback, name='amazon_cognito_callback'), + path('amazon-cognito/login/token', CognitoLogin.as_view()), ] urlpatterns = [path('auth/', include(urlpatterns))] diff --git a/cvat/apps/iam/views.py b/cvat/apps/iam/views.py index 3d8a664c..d01bac58 100644 --- a/cvat/apps/iam/views.py +++ b/cvat/apps/iam/views.py @@ -28,13 +28,13 @@ from allauth.socialaccount.models import SocialLogin from allauth.socialaccount.providers.oauth2.views import OAuth2CallbackView, OAuth2LoginView from allauth.socialaccount.providers.oauth2.client import OAuth2Client from allauth.utils import get_request_param + from furl import furl from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiResponse, OpenApiParameter, extend_schema, inline_serializer, extend_schema_view from drf_spectacular.contrib.rest_auth import get_token_serializer_class -from cvat.apps.iam.adapters import GitHubAdapter, GoogleAdapter from .authentication import Signer from cvat.apps.iam.serializers import SocialLoginSerializerEx, SocialAuthMethodSerializer @@ -49,6 +49,12 @@ GoogleAdapter = ( else None ) +AmazonCognitoAdapter = ( + import_callable(settings.SOCIALACCOUNT_AMAZON_COGNITO_ADAPTER) + if settings.USE_ALLAUTH_SOCIAL_ACCOUNTS + else None +) + def get_context(request): from cvat.apps.organizations.models import Organization, Membership @@ -251,8 +257,11 @@ class OAuth2CallbackViewEx(OAuth2CallbackView): if not code: return HttpResponseBadRequest('Parameter code not found in request') + + provider = self.adapter.provider_id.replace('_', '-') + return HttpResponseRedirect( - f'{settings.SOCIAL_APP_LOGIN_REDIRECT_URL}/?provider={self.adapter.provider_id}&code={code}' + f'{settings.SOCIAL_APP_LOGIN_REDIRECT_URL}/?provider={provider}&code={code}' f'&auth_params={state.get("auth_params")}&process={state.get("process")}' f'&scope={state.get("scope")}') @@ -297,6 +306,17 @@ def github_oauth2_callback(*args, **kwargs): def google_oauth2_login(*args, **kwargs): return OAuth2LoginView.adapter_view(GoogleAdapter)(*args, **kwargs) +@extend_schema( + summary="Redirects to Amazon Cognito authentication page", + description="Redirects to the Amazon Cognito authentication page. " + "After successful authentication on the provider side, " + "a redirect to the callback endpoint is performed.", +) +@api_view(["GET"]) +@permission_classes([AllowAny]) +def amazon_cognito_oauth2_login(*args, **kwargs): + return OAuth2LoginView.adapter_view(AmazonCognitoAdapter)(*args, **kwargs) + @extend_schema( summary="Checks the authentication response from Google, redirects to the CVAT client if successful.", description="Accepts a request from Google with code and state query parameters. " @@ -315,6 +335,24 @@ def google_oauth2_callback(*args, **kwargs): return OAuth2CallbackViewEx.adapter_view(GoogleAdapter)(*args, **kwargs) +@extend_schema( + summary="Checks the authentication response from Amazon Cognito, redirects to the CVAT client if successful.", + description="Accepts a request from Amazon Cognito with code and state query parameters. " + "In case of successful authentication on the provider side, it will " + "redirect to the CVAT client", + parameters=[ + OpenApiParameter('code', description='Returned by google', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), + OpenApiParameter('state', description='Returned by google', + location=OpenApiParameter.QUERY, type=OpenApiTypes.STR), + ], +) +@api_view(["GET"]) +@permission_classes([AllowAny]) +def amazon_cognito_oauth2_callback(*args, **kwargs): + return OAuth2CallbackViewEx.adapter_view(AmazonCognitoAdapter)(*args, **kwargs) + + class ConfirmEmailViewEx(ConfirmEmailView): template_name = 'account/email/email_confirmation_signup_message.html' @@ -369,6 +407,10 @@ class GoogleLogin(SocialLoginViewEx): client_class = OAuth2Client callback_url = getattr(settings, 'GOOGLE_CALLBACK_URL', None) +class CognitoLogin(SocialLoginViewEx): + adapter_class = AmazonCognitoAdapter + client_class = OAuth2Client + callback_url = getattr(settings, 'AMAZON_COGNITO_REDIRECT_URI', None) @extend_schema_view( get=extend_schema( @@ -379,6 +421,7 @@ class GoogleLogin(SocialLoginViewEx): fields={ 'google': SocialAuthMethodSerializer(), 'github': SocialAuthMethodSerializer(), + 'amazon-cognito': SocialAuthMethodSerializer(), } )), } @@ -401,7 +444,7 @@ class SocialAuthMethods(views.APIView): getattr(settings, f'SOCIAL_AUTH_{provider.upper()}_CLIENT_ID', None) and getattr(settings, f'SOCIAL_AUTH_{provider.upper()}_CLIENT_SECRET', None) ) - icon_path = osp.join(settings.STATIC_ROOT, 'social_authentication', f'social-{provider}-logo.svg') + icon_path = osp.join(settings.STATIC_ROOT, 'social_authentication', f'social-{provider.replace("_", "-")}-logo.svg') if is_enabled and osp.exists(icon_path): with open(icon_path, 'r') as f: icon = f.read() @@ -413,6 +456,6 @@ class SocialAuthMethods(views.APIView): }) serializer.is_valid(raise_exception=True) - response[provider] = serializer.validated_data + response[provider.replace("_", "-")] = serializer.validated_data return Response(response) diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index 501ad395..76dfbd48 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -4,6 +4,7 @@ Django==3.2.16 django-appconf==1.0.4 django-auth-ldap==2.2.0 django-compressor==2.4 +dj-rest-auth[with_social]==2.2.5 django-rq==2.3.2 EasyProcess==0.3 Pillow==9.3.0 @@ -31,7 +32,6 @@ Pygments==2.7.4 drf-spectacular==0.22.1 Shapely==1.7.1 pdf2image==1.14.0 -dj-rest-auth[with_social]==2.2.4 opencv-python-headless==4.5.5.62 h5py==3.6.0 django-cors-headers==3.5.0 diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 2c715e90..8db205a7 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -111,20 +111,21 @@ INSTALLED_APPS = [ 'django_rq', 'compressor', 'django_sendfile', + "dj_rest_auth", + 'dj_rest_auth.registration', 'dj_pagination', 'rest_framework', 'rest_framework.authtoken', 'drf_spectacular', - 'dj_rest_auth', 'django.contrib.sites', 'allauth', 'allauth.account', 'corsheaders', 'allauth.socialaccount', # social providers + 'allauth.socialaccount.providers.amazon_cognito', 'allauth.socialaccount.providers.github', 'allauth.socialaccount.providers.google', - 'dj_rest_auth.registration', 'health_check', 'health_check.db', 'health_check.contrib.migrations', @@ -240,6 +241,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + ], }, }, @@ -287,6 +289,7 @@ AUTHENTICATION_BACKENDS = [ # https://github.com/pennersr/django-allauth ACCOUNT_EMAIL_VERIFICATION = 'none' ACCOUNT_AUTHENTICATION_METHOD = 'username_email' + # set UI url to redirect after a successful e-mail confirmation #changed from '/auth/login' to '/auth/email-confirmation' for email confirmation message ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '/auth/email-confirmation' @@ -639,6 +642,7 @@ if USE_ALLAUTH_SOCIAL_ACCOUNTS: SOCIALACCOUNT_ADAPTER = 'cvat.apps.iam.adapters.SocialAccountAdapterEx' SOCIALACCOUNT_GITHUB_ADAPTER = 'cvat.apps.iam.adapters.GitHubAdapter' SOCIALACCOUNT_GOOGLE_ADAPTER = 'cvat.apps.iam.adapters.GoogleAdapter' + SOCIALACCOUNT_AMAZON_COGNITO_ADAPTER = 'cvat.apps.iam.adapters.AmazonCognitoOAuth2AdapterEx' SOCIALACCOUNT_LOGIN_ON_GET = True # It's required to define email in the case when a user has a private hidden email. # (e.g in github account set keep my email addresses private) @@ -648,6 +652,7 @@ if USE_ALLAUTH_SOCIAL_ACCOUNTS: # custom variable because by default LOGIN_REDIRECT_URL will be used SOCIAL_APP_LOGIN_REDIRECT_URL = f'{CVAT_BASE_URL}/auth/login-with-social-app' + AMAZON_COGNITO_REDIRECT_URI = f'{CVAT_BASE_URL}/api/auth/amazon-cognito/login/callback/' GITHUB_CALLBACK_URL = f'{CVAT_BASE_URL}/api/auth/github/login/callback/' GOOGLE_CALLBACK_URL = f'{CVAT_BASE_URL}/api/auth/google/login/callback/' @@ -656,6 +661,13 @@ if USE_ALLAUTH_SOCIAL_ACCOUNTS: SOCIAL_AUTH_GITHUB_CLIENT_ID = os.getenv('SOCIAL_AUTH_GITHUB_CLIENT_ID') SOCIAL_AUTH_GITHUB_CLIENT_SECRET = os.getenv('SOCIAL_AUTH_GITHUB_CLIENT_SECRET') + + SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_ID = os.getenv('SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_ID') + SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_SECRET = os.getenv('SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_SECRET') + SOCIAL_AUTH_AMAZON_COGNITO_DOMAIN = os.getenv('SOCIAL_AUTH_AMAZON_COGNITO_DOMAIN') + + # Django allauth social account providers + # https://django-allauth.readthedocs.io/en/latest/providers.html SOCIALACCOUNT_PROVIDERS = { 'google': { 'APP': { @@ -681,4 +693,14 @@ if USE_ALLAUTH_SOCIAL_ACCOUNTS: # key with a capital letter will be used 'PUBLIC_NAME': 'GitHub', }, + 'amazon_cognito': { + 'DOMAIN': SOCIAL_AUTH_AMAZON_COGNITO_DOMAIN, + 'SCOPE': [ 'profile', 'email', 'openid'], + 'APP': { + 'client_id': SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_ID, + 'secret': SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_SECRET, + 'key': '' + }, + 'PUBLIC_NAME': 'Amazon Cognito', + } } diff --git a/cvat/settings/development.py b/cvat/settings/development.py index 7ae8a37b..7cfcbdaa 100644 --- a/cvat/settings/development.py +++ b/cvat/settings/development.py @@ -50,5 +50,6 @@ IAM_OPA_DATA_URL = f'{IAM_OPA_HOST}/v1/data' if USE_ALLAUTH_SOCIAL_ACCOUNTS: GITHUB_CALLBACK_URL = f'{UI_URL}/api/auth/github/login/callback/' GOOGLE_CALLBACK_URL = f'{UI_URL}/api/auth/google/login/callback/' + AMAZON_COGNITO_REDIRECT_URI = f'{UI_URL}/api/auth/amazon-cognito/login/callback/' SOCIALACCOUNT_CALLBACK_CANCELLED_URL = f'{UI_URL}/auth/login' SOCIAL_APP_LOGIN_REDIRECT_URL = f'{UI_URL}/auth/login-with-social-app' diff --git a/docker-compose.yml b/docker-compose.yml index c5fa5535..b9dae107 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,11 +40,17 @@ services: IAM_OPA_BUNDLE: '1' no_proxy: elasticsearch,kibana,logstash,nuclio,opa,${no_proxy:-} NUMPROCS: 1 - USE_ALLAUTH_SOCIAL_ACCOUNTS: "" - SOCIAL_AUTH_GOOGLE_CLIENT_ID: "" - SOCIAL_AUTH_GOOGLE_CLIENT_SECRET: "" - SOCIAL_AUTH_GITHUB_CLIENT_ID: "" - SOCIAL_AUTH_GITHUB_CLIENT_SECRET: "" + USE_ALLAUTH_SOCIAL_ACCOUNTS: + # Google enviroment variables + SOCIAL_AUTH_GOOGLE_CLIENT_ID: + SOCIAL_AUTH_GOOGLE_CLIENT_SECRET: + # GitHub enviroment variables + SOCIAL_AUTH_GITHUB_CLIENT_ID: + SOCIAL_AUTH_GITHUB_CLIENT_SECRET: + # Amazon Cognito enviroment variables + SOCIAL_AUTH_AMAZON_COGNITO_DOMAIN: + SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_ID: + SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_SECRET: command: -c supervisord/server.conf labels: - traefik.enable=true diff --git a/tests/cypress/integration/actions_users/issue_1810_login_logout.js b/tests/cypress/integration/actions_users/issue_1810_login_logout.js index b0b1d789..d470452c 100644 --- a/tests/cypress/integration/actions_users/issue_1810_login_logout.js +++ b/tests/cypress/integration/actions_users/issue_1810_login_logout.js @@ -103,7 +103,7 @@ context('When clicking on the Logout button, get the user session closed.', () = method: 'GET', url: '/api/auth/social/methods/', }).then((response) => { - socialAuthMethods = Object.keys(response.body); + socialAuthMethods = Object.keys(response.body).filter((item) => response.body[item].is_enabled); expect(socialAuthMethods).length.gt(0); cy.visit('auth/login'); diff --git a/tests/python/mock_oauth2/adapters.py b/tests/python/mock_oauth2/adapters.py new file mode 100644 index 00000000..053cda79 --- /dev/null +++ b/tests/python/mock_oauth2/adapters.py @@ -0,0 +1,15 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from allauth.socialaccount.providers.amazon_cognito.views import AmazonCognitoOAuth2Adapter +from django.conf import settings + + +class TestAmazonCognitoOAuth2Adapter(AmazonCognitoOAuth2Adapter): + @property + def profile_url(self): + return super().profile_url.lower() + + def get_callback_url(self, request, app): + return settings.AMAZON_COGNITO_REDIRECT_URI diff --git a/tests/python/mock_oauth2/config.json b/tests/python/mock_oauth2/config.json new file mode 100644 index 00000000..62e8322b --- /dev/null +++ b/tests/python/mock_oauth2/config.json @@ -0,0 +1,24 @@ +{ + "interactiveLogin": true, + "httpServer": "NettyWrapper", + "tokenCallbacks": [ + { + "issuerId": "oauth2", + "tokenExpiry": 120, + "requestMappings": [ + { + "requestParam": "client_id", + "match": "test-client", + "claims": { + "sub": "test-sub", + "aud": [ + "test-aud" + ], + "email": "admin@localhost.company", + "email_verified": true + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/python/mock_oauth2/docker-compose.yml b/tests/python/mock_oauth2/docker-compose.yml new file mode 100644 index 00000000..210a359c --- /dev/null +++ b/tests/python/mock_oauth2/docker-compose.yml @@ -0,0 +1,23 @@ +services: + mock_oauth2: + image: ghcr.io/navikt/mock-oauth2-server:0.5.3 + environment: + JSON_CONFIG_PATH: "/devel/config.json" + SERVER_PORT: 9999 + ports: + - 9999:9999 + volumes: + - ./tests/python/mock_oauth2:/devel + networks: + - cvat + + cvat_server: + environment: + USE_ALLAUTH_SOCIAL_ACCOUNTS: "True" + SOCIAL_AUTH_AMAZON_COGNITO_DOMAIN: "http://mock_oauth2:9999" + SOCIAL_AUTH_AMAZON_COGNITO_CLIENT_ID: "test-client" + DJANGO_SETTINGS_MODULE: mock_oauth2.settings + volumes: + - ./tests/python/mock_oauth2:/home/django/mock_oauth2:ro + depends_on: + - mock_oauth2 diff --git a/tests/python/mock_oauth2/settings.py b/tests/python/mock_oauth2/settings.py new file mode 100644 index 00000000..72b80ab5 --- /dev/null +++ b/tests/python/mock_oauth2/settings.py @@ -0,0 +1,10 @@ +# Copyright (C) 2023 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from cvat.settings.production import * + +ACCOUNT_EMAIL_REQUIRED = True + +if USE_ALLAUTH_SOCIAL_ACCOUNTS: + SOCIALACCOUNT_AMAZON_COGNITO_ADAPTER = "mock_oauth2.adapters.TestAmazonCognitoOAuth2Adapter" diff --git a/tests/python/rest_api/test_auth.py b/tests/python/rest_api/test_auth.py index 05460a8e..222d11e6 100644 --- a/tests/python/rest_api/test_auth.py +++ b/tests/python/rest_api/test_auth.py @@ -8,7 +8,7 @@ from http import HTTPStatus import pytest from cvat_sdk.api_client import ApiClient, Configuration, models -from shared.utils.config import BASE_URL, USER_PASS, make_api_client +from shared.utils.config import BASE_URL, IS_AMAZON_COGNITO_AUTH_ENABLED, USER_PASS, make_api_client @pytest.mark.usefixtures("restore_db_per_class") @@ -21,6 +21,22 @@ class TestBasicAuth: assert response.status == HTTPStatus.OK assert user.username == username + @pytest.mark.with_external_services + @pytest.mark.skipif( + not IS_AMAZON_COGNITO_AUTH_ENABLED, reason="Amazon Cognito authentication is disabled" + ) + def test_can_do_basic_cognito_token_auth(self): + config = Configuration(host=BASE_URL) + with ApiClient(config) as client: + social_login_request = models.SocialLoginSerializerExRequest() + social_login_request.code = "test-code" + (auth, _) = client.auth_api.create_amazon_cognito_login_token( + social_login_serializer_ex_request=social_login_request + ) + assert "sessionid" in client.cookies + assert "csrftoken" in client.cookies + assert auth.key + @pytest.mark.usefixtures("restore_db_per_function") class TestTokenAuth: diff --git a/tests/python/shared/fixtures/init.py b/tests/python/shared/fixtures/init.py index 77c7e702..f175f93e 100644 --- a/tests/python/shared/fixtures/init.py +++ b/tests/python/shared/fixtures/init.py @@ -29,15 +29,24 @@ CONTAINER_NAME_FILES = [ ) ] -DC_FILES = [ - CVAT_ROOT_DIR / dc_file - for dc_file in ( - "docker-compose.dev.yml", - "tests/docker-compose.file_share.yml", - "tests/docker-compose.minio.yml", - "tests/docker-compose.test_servers.yml", - ) -] + CONTAINER_NAME_FILES +# this files contain some configurations that override the default configuration of the main containers +DC_OVERRIDE_FILES = [ + CVAT_ROOT_DIR / "tests/python/mock_oauth2/docker-compose.yml", +] + +DC_FILES = ( + [ + CVAT_ROOT_DIR / dc_file + for dc_file in ( + "docker-compose.dev.yml", + "tests/docker-compose.file_share.yml", + "tests/docker-compose.minio.yml", + "tests/docker-compose.test_servers.yml", + ) + ] + + CONTAINER_NAME_FILES + + DC_OVERRIDE_FILES +) def pytest_addoption(parser): diff --git a/tests/python/shared/utils/config.py b/tests/python/shared/utils/config.py index f5a3206c..5b380ab8 100644 --- a/tests/python/shared/utils/config.py +++ b/tests/python/shared/utils/config.py @@ -20,6 +20,9 @@ MINIO_SECRET_KEY = "minio_secret_key" # nosec MINIO_ENDPOINT_URL = "http://localhost:9000" +IS_AMAZON_COGNITO_AUTH_ENABLED = True + + def _to_query_params(**kwargs): return "&".join([f"{k}={v}" for k, v in kwargs.items()])