Merge branch 'master' into new-forms

This commit is contained in:
Jens Langhammer 2021-03-29 15:37:12 +02:00
commit 464a56ad52
14 changed files with 219 additions and 28 deletions

View 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

View file

@ -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,

View file

@ -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})

View file

@ -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"))

View file

@ -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()

View file

@ -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"),
]

View 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",
},
),
]

View file

@ -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"),
]

View file

@ -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"},

View 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",
},
),
]

View file

@ -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"),
]

View file

@ -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:

View 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"
```

View file

@ -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",