tenants api: add recovery group and token creation endpoints
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
parent
fe38310d97
commit
e8747a5a30
|
@ -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
|
|
@ -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'.")
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
103
schema.yml
103
schema.yml
|
@ -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
|
||||||
|
|
Reference in New Issue