tenants api: add recovery group and token creation endpoints

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
Marc 'risson' Schmitt 2023-12-27 13:54:28 +01:00
parent fe38310d97
commit e8747a5a30
No known key found for this signature in database
GPG Key ID: 9C3FA22FABF1AA8D
5 changed files with 231 additions and 30 deletions

35
authentik/recovery/lib.py Normal file
View File

@ -0,0 +1,35 @@
from datetime import datetime
from django.urls import reverse
from django.utils.text import slugify
from django.utils.timezone import now
from rest_framework.request import Request
from authentik.core.models import Group, Token, TokenIntents, User
def create_admin_group(user: User) -> Group:
"""Create admin group and add user to it."""
group, _ = Group.objects.update_or_create(
name="authentik Admins",
defaults={
"is_superuser": True,
},
)
group.users.add(user)
return group
def create_recovery_token(user: User, expiry: datetime, generated_from: str) -> (Token, str):
"""Create recovery token and associated link"""
_now = now()
token = Token.objects.create(
expires=expiry,
user=user,
intent=TokenIntents.INTENT_RECOVERY,
description=f"Recovery Token generated by {generated_from} on {_now}",
identifier=slugify(f"ak-recovery-{user}-{_now}"),
)
url = reverse("authentik_recovery:use-token", kwargs={"key": str(token.key)})
return token, url

View File

@ -1,7 +1,8 @@
"""authentik recovery create_admin_group""" """authentik recovery create_admin_group"""
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from authentik.core.models import Group, User from authentik.core.models import User
from authentik.recovery.lib import create_admin_group
from authentik.tenants.management import TenantCommand from authentik.tenants.management import TenantCommand
@ -20,11 +21,5 @@ class Command(TenantCommand):
if not user: if not user:
self.stderr.write(f"User '{username}' not found.") self.stderr.write(f"User '{username}' not found.")
return return
group, _ = Group.objects.update_or_create( group = create_admin_group(user)
name="authentik Admins", self.stdout.write(f"User '{username}' successfully added to the group '{group.name}'.")
defaults={
"is_superuser": True,
},
)
group.users.add(user)
self.stdout.write(f"User '{username}' successfully added to the group 'authentik Admins'.")

View File

@ -2,12 +2,11 @@
from datetime import timedelta from datetime import timedelta
from getpass import getuser from getpass import getuser
from django.urls import reverse
from django.utils.text import slugify
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from authentik.core.models import Token, TokenIntents, User from authentik.core.models import User
from authentik.recovery.lib import create_recovery_token
from authentik.tenants.management import TenantCommand from authentik.tenants.management import TenantCommand
@ -25,28 +24,16 @@ class Command(TenantCommand):
) )
parser.add_argument("user", action="store", help="Which user the Token gives access to.") parser.add_argument("user", action="store", help="Which user the Token gives access to.")
def get_url(self, token: Token) -> str:
"""Get full recovery link"""
return reverse("authentik_recovery:use-token", kwargs={"key": str(token.key)})
def handle_per_tenant(self, *args, **options): def handle_per_tenant(self, *args, **options):
"""Create Token used to recover access""" """Create Token used to recover access"""
duration = int(options.get("duration", 1)) duration = int(options.get("duration", 1))
_now = now() expiry = now() + timedelta(days=duration * 365.2425)
expiry = _now + timedelta(days=duration * 365.2425) user = User.objects.filter(username=options.get("user")).first()
users = User.objects.filter(username=options.get("user")) if not user:
if not users.exists():
self.stderr.write(f"User '{options.get('user')}' not found.") self.stderr.write(f"User '{options.get('user')}' not found.")
return return
user = users.first() _, url = create_recovery_token(user, expiry, getuser())
token = Token.objects.create(
expires=expiry,
user=user,
intent=TokenIntents.INTENT_RECOVERY,
description=f"Recovery Token generated by {getuser()} on {_now}",
identifier=slugify(f"ak-recovery-{user}-{_now}"),
)
self.stdout.write( self.stdout.write(
f"Store this link safely, as it will allow anyone to access authentik as {user}." f"Store this link safely, as it will allow anyone to access authentik as {user}."
) )
self.stdout.write(self.get_url(token)) self.stdout.write(url)

View File

@ -1,20 +1,30 @@
"""Serializer for tenants models""" """Serializer for tenants models"""
from datetime import timedelta
from hmac import compare_digest from hmac import compare_digest
from django.http import HttpResponseNotFound from django.http import HttpResponseNotFound
from django.http.request import urljoin
from django.utils.timezone import now
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import permissions from rest_framework import permissions
from rest_framework.authentication import get_authorization_header from rest_framework.authentication import get_authorization_header
from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.generics import RetrieveUpdateAPIView from rest_framework.generics import RetrieveUpdateAPIView
from rest_framework.permissions import SAFE_METHODS from rest_framework.permissions import SAFE_METHODS
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.serializers import ModelSerializer from rest_framework.response import Response
from rest_framework.serializers import DateTimeField, ModelSerializer
from rest_framework.views import View from rest_framework.views import View
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authentication import validate_auth from authentik.api.authentication import validate_auth
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.rbac.permissions import HasPermission from authentik.rbac.permissions import HasPermission
from authentik.recovery.lib import create_admin_group, create_recovery_token
from authentik.tenants.models import Domain, Tenant from authentik.tenants.models import Domain, Tenant
@ -44,6 +54,23 @@ class TenantSerializer(ModelSerializer):
] ]
class TenantAdminGroupRequestSerializer(PassiveSerializer):
"""Tenant admin group creation request serializer"""
user = CharField()
class TenantRecoveryKeyRequestSerializer(PassiveSerializer):
"""Tenant recovery key creation request serializer"""
user = CharField()
duration_days = IntegerField(initial=365)
class TenantRecoveryKeyResponseSerializer(PassiveSerializer):
"""Tenant recovery key creation response serializer"""
expiry = DateTimeField()
url = CharField()
class TenantViewSet(ModelViewSet): class TenantViewSet(ModelViewSet):
"""Tenant Viewset""" """Tenant Viewset"""
@ -65,6 +92,60 @@ class TenantViewSet(ModelViewSet):
return HttpResponseNotFound() return HttpResponseNotFound()
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@extend_schema(
request=TenantAdminGroupRequestSerializer(),
responses={
204: OpenApiResponse(description="Group created successfully."),
400: OpenApiResponse(description="Bad request"),
404: OpenApiResponse(description="User not found"),
},
)
@action(detail=True, pagination_class=None, methods=["POST"])
def create_admin_group(self, request: Request, pk: str) -> Response:
"""Create admin group and add user to it."""
tenant = self.get_object()
with tenant:
data = TenantAdminGroupRequestSerializer(data=request.data)
if not data.is_valid():
return Response(data.errors, status=400)
user = User.objects.filter(username=data.validated_data.get("user")).first()
if not user:
return Response(status=404)
_ = create_admin_group(user)
return Response(status=200)
@extend_schema(
request=TenantRecoveryKeyRequestSerializer(),
responses={
200: TenantRecoveryKeyResponseSerializer(),
400: OpenApiResponse(description="Bad request"),
404: OpenApiResponse(description="User not found"),
},
)
@action(detail=True, pagination_class=None, methods=["POST"])
def create_recovery_key(self, request: Request, pk: str) -> Response:
"""Create recovery key for user."""
tenant = self.get_object()
with tenant:
data = TenantRecoveryKeyRequestSerializer(data=request.data)
if not data.is_valid():
return Response(data.errors, status=400)
user = User.objects.filter(username=data.validated_data.get("user")).first()
if not user:
return Response(status=404)
expiry = now() + timedelta(days=data.validated_data.get("duration_days"))
token, url = create_recovery_token(user, expiry, "tenants API")
domain = tenant.get_primary_domain()
host = domain.domain if domain else request.get_host()
url = urljoin(f"{request.scheme}://{host}", url)
serializer = TenantRecoveryKeyResponseSerializer({"expiry": token.expires, "url": url})
return Response(serializer.data)
class DomainSerializer(ModelSerializer): class DomainSerializer(ModelSerializer):
"""Domain Serializer""" """Domain Serializer"""

View File

@ -28371,6 +28371,76 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/tenants/tenants/{tenant_uuid}/create_admin_group/:
post:
operationId: tenants_tenants_create_admin_group_create
description: Create admin group and add user to it.
parameters:
- in: path
name: tenant_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Tenant.
required: true
tags:
- tenants
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TenantAdminGroupRequestRequest'
required: true
responses:
'204':
description: Group created successfully.
'400':
description: Bad request
'404':
description: User not found
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/tenants/tenants/{tenant_uuid}/create_recovery_key/:
post:
operationId: tenants_tenants_create_recovery_key_create
description: Create recovery key for user.
parameters:
- in: path
name: tenant_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Tenant.
required: true
tags:
- tenants
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/TenantRecoveryKeyRequestRequest'
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/TenantRecoveryKeyResponse'
description: ''
'400':
description: Bad request
'404':
description: User not found
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
components: components:
schemas: schemas:
AccessDeniedChallenge: AccessDeniedChallenge:
@ -42275,6 +42345,39 @@ components:
- name - name
- schema_name - schema_name
- tenant_uuid - tenant_uuid
TenantAdminGroupRequestRequest:
type: object
description: Tenant admin group creation request serializer
properties:
user:
type: string
minLength: 1
required:
- user
TenantRecoveryKeyRequestRequest:
type: object
description: Tenant recovery key creation request serializer
properties:
user:
type: string
minLength: 1
duration_days:
type: integer
required:
- duration_days
- user
TenantRecoveryKeyResponse:
type: object
description: Tenant recovery key creation response serializer
properties:
expiry:
type: string
format: date-time
url:
type: string
required:
- expiry
- url
TenantRequest: TenantRequest:
type: object type: object
description: Tenant Serializer description: Tenant Serializer