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"""
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
@ -20,11 +21,5 @@ class Command(TenantCommand):
if not user:
self.stderr.write(f"User '{username}' not found.")
return
group, _ = Group.objects.update_or_create(
name="authentik Admins",
defaults={
"is_superuser": True,
},
)
group.users.add(user)
self.stdout.write(f"User '{username}' successfully added to the group 'authentik Admins'.")
group = create_admin_group(user)
self.stdout.write(f"User '{username}' successfully added to the group '{group.name}'.")

View File

@ -2,12 +2,11 @@
from datetime import timedelta
from getpass import getuser
from django.urls import reverse
from django.utils.text import slugify
from django.utils.timezone import now
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
@ -25,28 +24,16 @@ class Command(TenantCommand):
)
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):
"""Create Token used to recover access"""
duration = int(options.get("duration", 1))
_now = now()
expiry = _now + timedelta(days=duration * 365.2425)
users = User.objects.filter(username=options.get("user"))
if not users.exists():
expiry = now() + timedelta(days=duration * 365.2425)
user = User.objects.filter(username=options.get("user")).first()
if not user:
self.stderr.write(f"User '{options.get('user')}' not found.")
return
user = users.first()
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}"),
)
_, url = create_recovery_token(user, expiry, getuser())
self.stdout.write(
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"""
from datetime import timedelta
from hmac import compare_digest
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.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.generics import RetrieveUpdateAPIView
from rest_framework.permissions import SAFE_METHODS
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.viewsets import ModelViewSet
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.rbac.permissions import HasPermission
from authentik.recovery.lib import create_admin_group, create_recovery_token
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):
"""Tenant Viewset"""
@ -65,6 +92,60 @@ class TenantViewSet(ModelViewSet):
return HttpResponseNotFound()
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):
"""Domain Serializer"""

View File

@ -28371,6 +28371,76 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
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:
schemas:
AccessDeniedChallenge:
@ -42275,6 +42345,39 @@ components:
- name
- schema_name
- 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:
type: object
description: Tenant Serializer