Merge branch 'master' into new-forms
This commit is contained in:
commit
464a56ad52
28
authentik/api/decorators.py
Normal file
28
authentik/api/decorators.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
"""API Decorators"""
|
||||
from functools import wraps
|
||||
from typing import Callable
|
||||
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
|
||||
def permission_required(perm: str, *other_perms: str):
|
||||
"""Check permissions for a single custom action"""
|
||||
|
||||
def wrapper_outter(func: Callable):
|
||||
"""Check permissions for a single custom action"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response:
|
||||
obj = self.get_object()
|
||||
if not request.user.has_perm(perm, obj):
|
||||
return self.permission_denied(request)
|
||||
for other_perm in other_perms:
|
||||
if not request.user.has_perm(other_perm):
|
||||
return self.permission_denied(request)
|
||||
return func(self, request, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return wrapper_outter
|
|
@ -1,12 +1,9 @@
|
|||
"""Application API Views"""
|
||||
from django.core.cache import cache
|
||||
from django.db.models import QuerySet
|
||||
from django.http.response import Http404
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
|
@ -15,6 +12,7 @@ from rest_framework_guardian.filters import ObjectPermissionsFilter
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.models import Application
|
||||
from authentik.events.models import EventAction
|
||||
|
@ -110,16 +108,15 @@ class ApplicationViewSet(ModelViewSet):
|
|||
serializer = self.get_serializer(allowed_applications, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
@permission_required(
|
||||
"authentik_core.view_application", "authentik_events.view_event"
|
||||
)
|
||||
@swagger_auto_schema(responses={200: CoordinateSerializer(many=True)})
|
||||
@action(detail=True)
|
||||
# pylint: disable=unused-argument
|
||||
def metrics(self, request: Request, slug: str):
|
||||
"""Metrics for application logins"""
|
||||
app = get_object_or_404(
|
||||
get_objects_for_user(request.user, "authentik_core.view_application"),
|
||||
slug=slug,
|
||||
)
|
||||
if not request.user.has_perm("authentik_events.view_event"):
|
||||
raise Http404
|
||||
app = self.get_object()
|
||||
return Response(
|
||||
get_events_per_1h(
|
||||
action=EventAction.AUTHORIZE_APPLICATION,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
"""User API Views"""
|
||||
from django.db.models.base import Model
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.http import urlencode
|
||||
from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method
|
||||
from guardian.utils import get_anonymous_user
|
||||
from rest_framework.decorators import action
|
||||
|
@ -10,11 +12,12 @@ from rest_framework.serializers import BooleanField, ModelSerializer, Serializer
|
|||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.middleware import (
|
||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||
SESSION_IMPERSONATE_USER,
|
||||
)
|
||||
from authentik.core.models import User
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.events.models import EventAction
|
||||
|
||||
|
||||
|
@ -54,6 +57,18 @@ class SessionUserSerializer(Serializer):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
class UserRecoverySerializer(Serializer):
|
||||
"""Recovery link for a user to reset their password"""
|
||||
|
||||
link = CharField()
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class UserMetricsSerializer(Serializer):
|
||||
"""User Metrics"""
|
||||
|
||||
|
@ -116,6 +131,7 @@ class UserViewSet(ModelViewSet):
|
|||
serializer.is_valid()
|
||||
return Response(serializer.data)
|
||||
|
||||
@permission_required("authentik_core.view_user", "authentik_events.view_event")
|
||||
@swagger_auto_schema(responses={200: UserMetricsSerializer(many=False)})
|
||||
@action(detail=False)
|
||||
def metrics(self, request: Request) -> Response:
|
||||
|
@ -123,3 +139,23 @@ class UserViewSet(ModelViewSet):
|
|||
serializer = UserMetricsSerializer(True)
|
||||
serializer.context["request"] = request
|
||||
return Response(serializer.data)
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@swagger_auto_schema(
|
||||
responses={"200": UserRecoverySerializer(many=False)},
|
||||
)
|
||||
@action(detail=True)
|
||||
# pylint: disable=invalid-name, unused-argument
|
||||
def recovery(self, request: Request, pk: int) -> Response:
|
||||
"""Create a temporary link that a user can use to recover their accounts"""
|
||||
user: User = self.get_object()
|
||||
token, __ = Token.objects.get_or_create(
|
||||
identifier=f"{user.uid}-password-reset",
|
||||
user=user,
|
||||
intent=TokenIntents.INTENT_RECOVERY,
|
||||
)
|
||||
querystring = urlencode({"token": token.key})
|
||||
link = request.build_absolute_uri(
|
||||
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
|
||||
)
|
||||
return Response({"link": link})
|
||||
|
|
|
@ -3,6 +3,7 @@ import django_filters
|
|||
from django.db.models.aggregates import Count
|
||||
from django.db.models.fields.json import KeyTextTransform
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, DictField, IntegerField
|
||||
from rest_framework.request import Request
|
||||
|
@ -132,7 +133,8 @@ class EventViewSet(ReadOnlyModelViewSet):
|
|||
filtered_action = request.query_params.get("action", EventAction.LOGIN)
|
||||
top_n = request.query_params.get("top_n", 15)
|
||||
return Response(
|
||||
Event.objects.filter(action=filtered_action)
|
||||
get_objects_for_user(request.user, "authentik_events.view_event")
|
||||
.filter(action=filtered_action)
|
||||
.exclude(context__authorized_application=None)
|
||||
.annotate(application=KeyTextTransform("authorized_application", "context"))
|
||||
.annotate(user_pk=KeyTextTransform("pk", "user"))
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""NotificationTransport API Views"""
|
||||
from django.http.response import Http404
|
||||
from drf_yasg2.utils import no_body, swagger_auto_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
|
@ -9,6 +8,7 @@ from rest_framework.response import Response
|
|||
from rest_framework.serializers import ModelSerializer, Serializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.events.models import (
|
||||
Notification,
|
||||
NotificationSeverity,
|
||||
|
@ -57,18 +57,17 @@ class NotificationTransportViewSet(ModelViewSet):
|
|||
queryset = NotificationTransport.objects.all()
|
||||
serializer_class = NotificationTransportSerializer
|
||||
|
||||
@permission_required("authentik_events.change_notificationtransport")
|
||||
@swagger_auto_schema(
|
||||
responses={200: NotificationTransportTestSerializer(many=False)},
|
||||
request_body=no_body,
|
||||
)
|
||||
@action(detail=True, methods=["post"])
|
||||
# pylint: disable=invalid-name
|
||||
# pylint: disable=invalid-name, unused-argument
|
||||
def test(self, request: Request, pk=None) -> Response:
|
||||
"""Send example notification using selected transport. Requires
|
||||
Modify permissions."""
|
||||
transports = get_objects_for_user(
|
||||
request.user, "authentik_events.change_notificationtransport"
|
||||
).filter(pk=pk)
|
||||
transports = self.get_object()
|
||||
if not transports.exists():
|
||||
raise Http404
|
||||
transport: NotificationTransport = transports.first()
|
||||
|
|
|
@ -3,13 +3,11 @@ from dataclasses import dataclass
|
|||
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from django.http.response import HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http.response import JsonResponse
|
||||
from drf_yasg2 import openapi
|
||||
from drf_yasg2.utils import no_body, swagger_auto_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import (
|
||||
|
@ -21,6 +19,7 @@ from rest_framework.serializers import (
|
|||
from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.utils import CacheSerializer
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.planner import cache_key
|
||||
|
@ -89,12 +88,14 @@ class FlowViewSet(ModelViewSet):
|
|||
search_fields = ["name", "slug", "designation", "title"]
|
||||
filterset_fields = ["flow_uuid", "name", "slug", "designation"]
|
||||
|
||||
@permission_required("authentik_flows.view_flow_cache")
|
||||
@swagger_auto_schema(responses={200: CacheSerializer(many=False)})
|
||||
@action(detail=False)
|
||||
def cache_info(self, request: Request) -> Response:
|
||||
"""Info about cached flows"""
|
||||
return Response(data={"count": len(cache.keys("flow_*"))})
|
||||
|
||||
@permission_required("authentik_flows.clear_flow_cache")
|
||||
@swagger_auto_schema(
|
||||
request_body=no_body,
|
||||
responses={204: "Successfully cleared cache", 400: "Bad request"},
|
||||
|
@ -102,13 +103,12 @@ class FlowViewSet(ModelViewSet):
|
|||
@action(detail=False, methods=["POST"])
|
||||
def cache_clear(self, request: Request) -> Response:
|
||||
"""Clear flow cache"""
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseBadRequest()
|
||||
keys = cache.keys("flow_*")
|
||||
cache.delete_many(keys)
|
||||
LOGGER.debug("Cleared flow cache", keys=len(keys))
|
||||
return Response(status=204)
|
||||
|
||||
@permission_required("authentik_flows.export_flow")
|
||||
@swagger_auto_schema(
|
||||
responses={
|
||||
"200": openapi.Response(
|
||||
|
@ -121,8 +121,6 @@ class FlowViewSet(ModelViewSet):
|
|||
def export(self, request: Request, slug: str) -> Response:
|
||||
"""Export flow to .akflow file"""
|
||||
flow = self.get_object()
|
||||
if not request.user.has_perm("authentik_flows.export_flow", flow):
|
||||
raise PermissionDenied()
|
||||
exporter = FlowExporter(flow)
|
||||
response = JsonResponse(exporter.export(), encoder=DataclassEncoder, safe=False)
|
||||
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.akflow"'
|
||||
|
@ -130,13 +128,10 @@ class FlowViewSet(ModelViewSet):
|
|||
|
||||
@swagger_auto_schema(responses={200: FlowDiagramSerializer()})
|
||||
@action(detail=True, methods=["get"])
|
||||
# pylint: disable=unused-argument
|
||||
def diagram(self, request: Request, slug: str) -> Response:
|
||||
"""Return diagram for flow with slug `slug`, in the format used by flowchart.js"""
|
||||
flow = get_object_or_404(
|
||||
get_objects_for_user(request.user, "authentik_flows.view_flow").filter(
|
||||
slug=slug
|
||||
)
|
||||
)
|
||||
flow = self.get_object()
|
||||
header = [
|
||||
DiagramElement("st", "start", "Start"),
|
||||
]
|
||||
|
|
25
authentik/flows/migrations/0017_auto_20210329_1334.py
Normal file
25
authentik/flows/migrations/0017_auto_20210329_1334.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1.7 on 2021-03-29 13:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0016_auto_20201202_1307"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="flow",
|
||||
options={
|
||||
"permissions": [
|
||||
("export_flow", "Can export a Flow"),
|
||||
("view_flow_cache", "View Flow's cache metrics"),
|
||||
("clear_flow_cache", "Clear Flow's cache metrics"),
|
||||
],
|
||||
"verbose_name": "Flow",
|
||||
"verbose_name_plural": "Flows",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -158,6 +158,8 @@ class Flow(SerializerModel, PolicyBindingModel):
|
|||
|
||||
permissions = [
|
||||
("export_flow", "Can export a Flow"),
|
||||
("view_flow_cache", "View Flow's cache metrics"),
|
||||
("clear_flow_cache", "Clear Flow's cache metrics"),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ from rest_framework.serializers import (
|
|||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.applications import user_app_cache_key
|
||||
from authentik.core.api.groups import GroupSerializer
|
||||
from authentik.core.api.utils import (
|
||||
|
@ -143,12 +144,14 @@ class PolicyViewSet(
|
|||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
@permission_required("authentik_policies.view_policy_cache")
|
||||
@swagger_auto_schema(responses={200: CacheSerializer(many=False)})
|
||||
@action(detail=False)
|
||||
def cache_info(self, request: Request) -> Response:
|
||||
"""Info about cached policies"""
|
||||
return Response(data={"count": len(cache.keys("policy_*"))})
|
||||
|
||||
@permission_required("authentik_policies.clear_policy_cache")
|
||||
@swagger_auto_schema(
|
||||
request_body=no_body,
|
||||
responses={204: "Successfully cleared cache", 400: "Bad request"},
|
||||
|
|
25
authentik/policies/migrations/0006_auto_20210329_1334.py
Normal file
25
authentik/policies/migrations/0006_auto_20210329_1334.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1.7 on 2021-03-29 13:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies", "0005_binding_group"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="policy",
|
||||
options={
|
||||
"base_manager_name": "objects",
|
||||
"permissions": [
|
||||
("view_policy_cache", "View Policy's cache metrics"),
|
||||
("clear_policy_cache", "Clear Policy's cache metrics"),
|
||||
],
|
||||
"verbose_name": "Policy",
|
||||
"verbose_name_plural": "Policies",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -149,3 +149,8 @@ class Policy(SerializerModel, CreatedUpdatedModel):
|
|||
|
||||
verbose_name = _("Policy")
|
||||
verbose_name_plural = _("Policies")
|
||||
|
||||
permissions = [
|
||||
("view_policy_cache", "View Policy's cache metrics"),
|
||||
("clear_policy_cache", "Clear Policy's cache metrics"),
|
||||
]
|
||||
|
|
28
swagger.yaml
28
swagger.yaml
|
@ -1726,6 +1726,24 @@ paths:
|
|||
description: A unique integer value identifying this User.
|
||||
required: true
|
||||
type: integer
|
||||
/core/users/{id}/recovery/:
|
||||
get:
|
||||
operationId: core_users_recovery
|
||||
description: Create a temporary link that a user can use to recover their accounts
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: Recovery link for a user to reset their password
|
||||
schema:
|
||||
$ref: '#/definitions/UserRecovery'
|
||||
tags:
|
||||
- core
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: A unique integer value identifying this User.
|
||||
required: true
|
||||
type: integer
|
||||
/crypto/certificatekeypairs/:
|
||||
get:
|
||||
operationId: crypto_certificatekeypairs_list
|
||||
|
@ -11120,6 +11138,16 @@ definitions:
|
|||
items:
|
||||
$ref: '#/definitions/Coordinate'
|
||||
readOnly: true
|
||||
UserRecovery:
|
||||
description: Recovery link for a user to reset their password
|
||||
required:
|
||||
- link
|
||||
type: object
|
||||
properties:
|
||||
link:
|
||||
title: Link
|
||||
type: string
|
||||
minLength: 1
|
||||
CertificateKeyPair:
|
||||
description: CertificateKeyPair Serializer
|
||||
required:
|
||||
|
|
45
website/docs/integrations/services/minio/index.md
Normal file
45
website/docs/integrations/services/minio/index.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: MinIO
|
||||
---
|
||||
|
||||
## What is MinIO
|
||||
|
||||
From https://en.wikipedia.org/wiki/MinIO
|
||||
|
||||
:::note
|
||||
MinIO is an Amazon S3 compatible object storage suite capable of handling structured and unstructured data including log files, artifacts, backups, container images, photos and videos. The current maximum supported object size is 5TB.
|
||||
:::
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `minio.company` is the FQDN of the MinIO install.
|
||||
- `authentik.company` is the FQDN of the authentik install.
|
||||
|
||||
Under _Property Mappings_, create a _Scope Mapping_. Give it a name like "OIDC-Scope-minio". Set the scope name to `minio` and the expression to the following
|
||||
|
||||
```python
|
||||
return {
|
||||
"policy": "readwrite",
|
||||
}
|
||||
```
|
||||
|
||||
Create an application in authentik. Create an _OAuth2/OpenID Provider_ with the following parameters:
|
||||
|
||||
- Client Type: `Public`
|
||||
- JWT Algorithm: `RS256`
|
||||
- Scopes: OpenID, Email, Profile and the scope you created above
|
||||
- RSA Key: Select any available key
|
||||
- Redirect URIs: `https://minio.company/minio/login/openid`
|
||||
|
||||
Note the Client ID and Client Secret values. Create an application, using the provider you've created above. Note the slug of the application you've created.
|
||||
|
||||
## MinIO
|
||||
|
||||
```
|
||||
~ mc admin config set myminio identity_openid \
|
||||
config_url="https://id.beryju.org/application/o/<applicaiton-slug>/.well-known/openid-configuration" \
|
||||
client_id="<client id from above>" \
|
||||
scopes="openid,profile,email,minio"
|
||||
```
|
|
@ -117,6 +117,7 @@ module.exports = {
|
|||
"integrations/services/grafana/index",
|
||||
"integrations/services/harbor/index",
|
||||
"integrations/services/home-assistant/index",
|
||||
"integrations/services/minio/index",
|
||||
"integrations/services/nextcloud/index",
|
||||
"integrations/services/rancher/index",
|
||||
"integrations/services/sentry/index",
|
||||
|
|
Reference in a new issue