From 3f9e95e6b94b7bba3c63cae0f186553da0f525a3 Mon Sep 17 00:00:00 2001 From: Michael Wedl Date: Tue, 19 Mar 2024 14:13:56 +0100 Subject: [PATCH] APIToken restrictions for community --- api/src/reportcreator_api/conf/settings.py | 4 ++-- .../reportcreator_api/tests/test_license.py | 13 +++++++++++++ api/src/reportcreator_api/users/querysets.py | 13 +++++++------ api/src/reportcreator_api/users/signals.py | 19 ++++++++++++++++++- frontend/src/components/S/DatePicker.vue | 2 ++ frontend/src/pages/users/self/apitokens.vue | 11 ++++++++++- 6 files changed, 52 insertions(+), 10 deletions(-) diff --git a/api/src/reportcreator_api/conf/settings.py b/api/src/reportcreator_api/conf/settings.py index 4043db4dd..53d511a03 100644 --- a/api/src/reportcreator_api/conf/settings.py +++ b/api/src/reportcreator_api/conf/settings.py @@ -30,7 +30,7 @@ SECRET_KEY = config('SECRET_KEY', default='django-insecure-ygvn9(x==kcv#r%pccf4rlzyz7_1v1b83$19&b2lsj6uz$mbro') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False # config('DEBUG', cast=bool, default=False) +DEBUG = config('DEBUG', cast=bool, default=False) ALLOWED_HOSTS = ['*'] APPEND_SLASH = True @@ -581,7 +581,7 @@ def __bool__(self): NOTIFICATION_IMPORT_URL = config('NOTIFICATION_IMPORT_URL', default='https://cloud.sysreptor.com/api/v1/notifications/') # License -LICENSE = config('LICENSE', default=None) +LICENSE = None # TODO: config('LICENSE', default=None) LICENSE_VALIDATION_KEYS = [ {'id': 'amber', 'algorithm': 'ed25519', 'key': 'MCowBQYDK2VwAyEAkqCS3lZbrzh+2mKTYymqPHtKBrh8glFxnj9OcoQR9xQ='}, {'id': 'silver', 'algorithm': 'ed25519', 'key': 'MCowBQYDK2VwAyEAwu/cl0CZSSBFOzFSz/hhUQQjHIKiT4RS3ekPevSKn7w='}, diff --git a/api/src/reportcreator_api/tests/test_license.py b/api/src/reportcreator_api/tests/test_license.py index c827e7079..7ddfc210f 100644 --- a/api/src/reportcreator_api/tests/test_license.py +++ b/api/src/reportcreator_api/tests/test_license.py @@ -15,6 +15,7 @@ from rest_framework import status from django.utils.crypto import get_random_string +from reportcreator_api.users.models import APIToken from reportcreator_api.utils import license from reportcreator_api.tests.mock import create_project, create_project_type, create_public_key, create_template, create_user, api_client @@ -167,6 +168,18 @@ def test_user_count_limit(self): with pytest.raises(license.LicenseError): self.user_regular.is_active = True self.user_regular.save() + + def test_apitoken_limit(self): + APIToken.objects.create(user=self.user_regular) + + with pytest.raises(license.LicenseLimitExceededError): + APIToken.objects.create(user=self.user_regular) + + def test_apitoken_no_expiry(self): + session = self.client.session + session.setdefault('authentication_info', {})['reauth_time'] = timezone.now().isoformat() + session.save() + assert_api_license_error(self.client.post(reverse('apitoken-list', kwargs={'pentestuser_pk': 'self'}), data={'name': 'test', 'expire_date': timezone.now().date().isoformat()})) @pytest.mark.django_db diff --git a/api/src/reportcreator_api/users/querysets.py b/api/src/reportcreator_api/users/querysets.py index a11c5e5f2..ebeda11e6 100644 --- a/api/src/reportcreator_api/users/querysets.py +++ b/api/src/reportcreator_api/users/querysets.py @@ -1,15 +1,12 @@ -import secrets -from typing import Any import pyotp -from fido2.server import Fido2Server, AttestedCredentialData -from fido2.webauthn import PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, UserVerificationRequirement, AuthenticatorAttachment, \ - AttestationObject, CollectedClientData +from fido2.server import AttestedCredentialData +from fido2.webauthn import PublicKeyCredentialUserEntity, UserVerificationRequirement, AuthenticatorAttachment from fido2.utils import websafe_encode, websafe_decode -from django.conf import settings from django.db import models from django.contrib.sessions.base_session import BaseSessionManager from django.contrib.auth.models import UserManager from django.utils.crypto import get_random_string +from django.utils import timezone class SessionQueryset(models.QuerySet): @@ -90,6 +87,10 @@ def only_permitted(self, user): if user.is_admin or user.is_user_manager: return self return self.filter(user=user) + + def only_active(self): + return self \ + .filter(models.Q(expire_date=None) | models.Q(expire_date__lte=timezone.now().date())) class APITokenManager(models.Manager.from_queryset(APITokenQuerySet)): diff --git a/api/src/reportcreator_api/users/signals.py b/api/src/reportcreator_api/users/signals.py index b8b113f27..a058079b6 100644 --- a/api/src/reportcreator_api/users/signals.py +++ b/api/src/reportcreator_api/users/signals.py @@ -2,7 +2,7 @@ from django.dispatch import receiver from django.conf import settings -from reportcreator_api.users.models import PentestUser +from reportcreator_api.users.models import APIToken, PentestUser from reportcreator_api.utils import license @@ -41,3 +41,20 @@ def user_count_license_check(sender, instance, *args, **kwargs): if current_user_count + 1 > max_users: raise license.LicenseError(f'License limit exceeded. Your license allows max. {max_users} users. Please deactivate some users or extend your license.') + +@receiver(signals.pre_save, sender=APIToken) +def api_token_license_limit(sender, instance, *args, **kwargs): + if license.is_professional(): + return + + current_apitoken_count = APIToken.objects \ + .filter(user=instance.user) \ + .only_active() \ + .count() + if current_apitoken_count >= 1: + raise license.LicenseLimitExceededError( + f'Community Edition allows max. 1 active API token per user. ' + 'Please delete some tokens or upgrade to Professional.') + + if instance.expire_date: + raise license.LicenseError('API token expiration is not supported in Community edition.') diff --git a/frontend/src/components/S/DatePicker.vue b/frontend/src/components/S/DatePicker.vue index cca462193..5cb520517 100644 --- a/frontend/src/components/S/DatePicker.vue +++ b/frontend/src/components/S/DatePicker.vue @@ -11,6 +11,8 @@ @click:clear="emits('update:modelValue', null)" v-bind="$attrs" > + + + :disabled="!apiSettings.isProfessionalLicense" + > + + + + + {{ setupWizard.errors.detail }} + @@ -108,6 +115,8 @@ import { formatISO9075 } from 'date-fns'; const auth = useAuth(); +const apiSettings = useApiSettings(); + const apiTokens = await useAsyncDataE(async () => { try { return await $fetch('/api/v1/pentestusers/self/apitokens/', { method: 'GET' });