Compare commits
34 Commits
version/20
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
| 5725e54334 | |||
| c20856ca17 | |||
| 9c73e9cf4e | |||
| b10c3db13d | |||
| fe290aa214 | |||
| a2e69bd250 | |||
| d2a35eb8de | |||
| 3437d8b4b0 | |||
| b862bf4284 | |||
| de22a367b1 | |||
| 17ab895652 | |||
| a4d5815e1b | |||
| e81d3dad3e | |||
| 5aabaebd96 | |||
| 7b60bca297 | |||
| a07d7456c8 | |||
| f33369bf0c | |||
| 1abcff39c7 | |||
| c1caf84d92 | |||
| 86c069fe64 | |||
| ce0140ef67 | |||
| bba43c5109 | |||
| d99a415502 | |||
| bd48955f39 | |||
| 53adcd9157 | |||
| c5a2bb8914 | |||
| 7da90ff7e4 | |||
| 61b5714652 | |||
| d2df426489 | |||
| e6c75ed173 | |||
| a353c6956e | |||
| a367d8515f | |||
| 2b7a22a29a | |||
| e6712a50d2 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.2.2-stable
|
||||
current_version = 2021.2.6-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/authentik:2021.2.2-stable
|
||||
-t beryju/authentik:2021.2.6-stable
|
||||
-t beryju/authentik:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik:2021.2.2-stable
|
||||
run: docker push beryju/authentik:2021.2.6-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik:latest
|
||||
build-proxy:
|
||||
@ -48,11 +48,11 @@ jobs:
|
||||
cd outpost/
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/authentik-proxy:2021.2.2-stable \
|
||||
-t beryju/authentik-proxy:2021.2.6-stable \
|
||||
-t beryju/authentik-proxy:latest \
|
||||
-f proxy.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik-proxy:2021.2.2-stable
|
||||
run: docker push beryju/authentik-proxy:2021.2.6-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik-proxy:latest
|
||||
build-static:
|
||||
@ -69,11 +69,11 @@ jobs:
|
||||
cd web/
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/authentik-static:2021.2.2-stable \
|
||||
-t beryju/authentik-static:2021.2.6-stable \
|
||||
-t beryju/authentik-static:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik-static:2021.2.2-stable
|
||||
run: docker push beryju/authentik-static:2021.2.6-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik-static:latest
|
||||
test-release:
|
||||
@ -107,5 +107,5 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 2021.2.2-stable
|
||||
tagName: 2021.2.6-stable
|
||||
environment: beryjuorg-prod
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.2.2-stable"
|
||||
__version__ = "2021.2.6-stable"
|
||||
|
||||
@ -41,6 +41,9 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</ak-dropdown>
|
||||
<button role="ak-refresh" class="pf-c-button pf-m-primary">
|
||||
{% trans 'Refresh' %}
|
||||
</button>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
|
||||
@ -27,6 +27,7 @@ class ApplicationCreateView(
|
||||
form_class = ApplicationForm
|
||||
permission_required = "authentik_core.add_application"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/create.html"
|
||||
success_message = _("Successfully created Application")
|
||||
|
||||
@ -44,6 +45,7 @@ class ApplicationUpdateView(
|
||||
form_class = ApplicationForm
|
||||
permission_required = "authentik_core.change_application"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/update.html"
|
||||
success_message = _("Successfully updated Application")
|
||||
|
||||
@ -56,5 +58,6 @@ class ApplicationDeleteView(
|
||||
model = Application
|
||||
permission_required = "authentik_core.delete_application"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/delete.html"
|
||||
success_message = _("Successfully deleted Application")
|
||||
|
||||
@ -27,6 +27,7 @@ class NotificationRuleCreateView(
|
||||
form_class = NotificationRuleForm
|
||||
permission_required = "authentik_events.add_NotificationRule"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/create.html"
|
||||
success_message = _("Successfully created Notification Rule")
|
||||
|
||||
@ -44,6 +45,7 @@ class NotificationRuleUpdateView(
|
||||
form_class = NotificationRuleForm
|
||||
permission_required = "authentik_events.change_NotificationRule"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/update.html"
|
||||
success_message = _("Successfully updated Notification Rule")
|
||||
|
||||
@ -56,5 +58,6 @@ class NotificationRuleDeleteView(
|
||||
model = NotificationRule
|
||||
permission_required = "authentik_events.delete_NotificationRule"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/delete.html"
|
||||
success_message = _("Successfully deleted Notification Rule")
|
||||
|
||||
@ -26,7 +26,7 @@ class NotificationTransportCreateView(
|
||||
model = NotificationTransport
|
||||
form_class = NotificationTransportForm
|
||||
permission_required = "authentik_events.add_notificationtransport"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/create.html"
|
||||
success_message = _("Successfully created Notification Transport")
|
||||
|
||||
@ -43,7 +43,7 @@ class NotificationTransportUpdateView(
|
||||
model = NotificationTransport
|
||||
form_class = NotificationTransportForm
|
||||
permission_required = "authentik_events.change_notificationtransport"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/update.html"
|
||||
success_message = _("Successfully updated Notification Transport")
|
||||
|
||||
@ -55,6 +55,6 @@ class NotificationTransportDeleteView(
|
||||
|
||||
model = NotificationTransport
|
||||
permission_required = "authentik_events.delete_notificationtransport"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/delete.html"
|
||||
success_message = _("Successfully deleted Notification Transport")
|
||||
|
||||
@ -29,7 +29,7 @@ class OutpostCreateView(
|
||||
model = Outpost
|
||||
form_class = OutpostForm
|
||||
permission_required = "authentik_outposts.add_outpost"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/create.html"
|
||||
success_message = _("Successfully created Outpost")
|
||||
|
||||
@ -53,7 +53,7 @@ class OutpostUpdateView(
|
||||
model = Outpost
|
||||
form_class = OutpostForm
|
||||
permission_required = "authentik_outposts.change_outpost"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/update.html"
|
||||
success_message = _("Successfully updated Outpost")
|
||||
|
||||
@ -63,6 +63,6 @@ class OutpostDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessa
|
||||
|
||||
model = Outpost
|
||||
permission_required = "authentik_outposts.delete_outpost"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/delete.html"
|
||||
success_message = _("Successfully deleted Outpost")
|
||||
|
||||
@ -18,7 +18,7 @@ class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
||||
"""View to clear Policy cache"""
|
||||
|
||||
form_class = PolicyCacheClearForm
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/form_non_model.html"
|
||||
success_message = _("Successfully cleared Policy cache")
|
||||
|
||||
@ -36,7 +36,7 @@ class FlowCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
||||
"""View to clear Flow cache"""
|
||||
|
||||
form_class = FlowCacheClearForm
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/form_non_model.html"
|
||||
success_message = _("Successfully cleared Flow cache")
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ class PropertyMappingCreateView(
|
||||
|
||||
model = PropertyMapping
|
||||
permission_required = "authentik_core.add_propertymapping"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/create.html"
|
||||
success_message = _("Successfully created Property Mapping")
|
||||
|
||||
@ -50,7 +50,7 @@ class PropertyMappingUpdateView(
|
||||
|
||||
model = PropertyMapping
|
||||
permission_required = "authentik_core.change_propertymapping"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/update.html"
|
||||
success_message = _("Successfully updated Property Mapping")
|
||||
|
||||
@ -62,7 +62,7 @@ class PropertyMappingDeleteView(
|
||||
|
||||
model = PropertyMapping
|
||||
permission_required = "authentik_core.delete_propertymapping"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/delete.html"
|
||||
success_message = _("Successfully deleted Property Mapping")
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ class ProviderCreateView(
|
||||
|
||||
model = Provider
|
||||
permission_required = "authentik_core.add_provider"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/create.html"
|
||||
success_message = _("Successfully created Provider")
|
||||
|
||||
@ -43,7 +43,7 @@ class ProviderUpdateView(
|
||||
|
||||
model = Provider
|
||||
permission_required = "authentik_core.change_provider"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/update.html"
|
||||
success_message = _("Successfully updated Provider")
|
||||
|
||||
@ -55,6 +55,6 @@ class ProviderDeleteView(
|
||||
|
||||
model = Provider
|
||||
permission_required = "authentik_core.delete_provider"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/delete.html"
|
||||
success_message = _("Successfully deleted Provider")
|
||||
|
||||
@ -28,6 +28,7 @@ class SourceCreateView(
|
||||
model = Source
|
||||
permission_required = "authentik_core.add_source"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/create.html"
|
||||
success_message = _("Successfully created Source")
|
||||
|
||||
@ -44,6 +45,7 @@ class SourceUpdateView(
|
||||
model = Source
|
||||
permission_required = "authentik_core.change_source"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/update.html"
|
||||
success_message = _("Successfully updated Source")
|
||||
|
||||
@ -54,5 +56,6 @@ class SourceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessag
|
||||
model = Source
|
||||
permission_required = "authentik_core.delete_source"
|
||||
|
||||
success_url = "/"
|
||||
template_name = "generic/delete.html"
|
||||
success_message = _("Successfully deleted Source")
|
||||
|
||||
@ -1,10 +1,18 @@
|
||||
"""Provider API Views"""
|
||||
from django.shortcuts import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.models import Provider
|
||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
@ -51,3 +59,26 @@ class ProviderViewSet(ModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
return Provider.objects.select_subclasses()
|
||||
|
||||
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False)
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable provider types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
data.append(
|
||||
{
|
||||
"name": verbose_name(subclass),
|
||||
"description": subclass.__doc__,
|
||||
"link": reverse("authentik_admin:provider-create")
|
||||
+ f"?type={subclass.__name__}",
|
||||
}
|
||||
)
|
||||
data.append(
|
||||
{
|
||||
"name": _("SAML Provider from Metadata"),
|
||||
"description": _("Create a SAML Provider by importing its Metadata."),
|
||||
"link": reverse("authentik_admin:provider-saml-from-metadata"),
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
"""Source API Views"""
|
||||
from django.shortcuts import reverse
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.models import Source
|
||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
@ -40,3 +47,19 @@ class SourceViewSet(ReadOnlyModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
return Source.objects.select_subclasses()
|
||||
|
||||
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False)
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable source types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
data.append(
|
||||
{
|
||||
"name": verbose_name(subclass),
|
||||
"description": subclass.__doc__,
|
||||
"link": reverse("authentik_admin:source-create")
|
||||
+ f"?type={subclass.__name__}",
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
@ -43,12 +43,12 @@ class TokenViewSet(ModelViewSet):
|
||||
|
||||
@swagger_auto_schema(responses={200: TokenViewSerializer(many=False)})
|
||||
@action(detail=True)
|
||||
# pylint: disable=unused-argument
|
||||
def view_key(self, request: Request, identifier: str) -> Response:
|
||||
"""Return token key and log access"""
|
||||
tokens = Token.filter_not_expired(identifier=identifier)
|
||||
if not tokens.exists():
|
||||
token: Token = self.get_object()
|
||||
if token.is_expired:
|
||||
raise Http404
|
||||
token = tokens.first()
|
||||
Event.new(EventAction.SECRET_VIEW, secret=token).from_http( # noqa # nosec
|
||||
request
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""API Utilities"""
|
||||
from django.db.models import Model
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.serializers import Serializer, SerializerMethodField
|
||||
|
||||
|
||||
@ -22,3 +23,17 @@ class MetaNameSerializer(Serializer):
|
||||
def get_verbose_name_plural(self, obj: Model) -> str:
|
||||
"""Return object's plural verbose_name"""
|
||||
return obj._meta.verbose_name_plural
|
||||
|
||||
|
||||
class TypeCreateSerializer(Serializer):
|
||||
"""Types of an object that can be created"""
|
||||
|
||||
name = CharField(read_only=True)
|
||||
description = CharField(read_only=True)
|
||||
link = CharField(read_only=True)
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
@ -9,6 +9,7 @@ from django.http import HttpRequest, HttpResponse
|
||||
SESSION_IMPERSONATE_USER = "authentik_impersonate_user"
|
||||
SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
|
||||
LOCAL = local()
|
||||
RESPONSE_HEADER_ID = "X-authentik-id"
|
||||
|
||||
|
||||
class ImpersonateMiddleware:
|
||||
@ -43,7 +44,7 @@ class RequestIDMiddleware:
|
||||
setattr(request, "request_id", request_id)
|
||||
LOCAL.authentik = {"request_id": request_id}
|
||||
response = self.get_response(request)
|
||||
response["X-authentik-id"] = request.request_id
|
||||
response[RESPONSE_HEADER_ID] = request.request_id
|
||||
del LOCAL.authentik["request_id"]
|
||||
return response
|
||||
|
||||
|
||||
@ -41,8 +41,8 @@
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% user_sources as user_sources_loc %}
|
||||
{% for source, source_link in user_sources_loc.item %}
|
||||
<section slot="page-{{ source.pk }}" data-tab-title="{{ source|verbose_name }}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
{% for source, source_link in user_sources_loc.items %}
|
||||
<section slot="page-{{ source.pk }}" data-tab-title="{{ source.name }}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-u-w-75">
|
||||
<ak-site-shell url="{{ source_link }}">
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from structlog import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import (
|
||||
Event,
|
||||
Notification,
|
||||
@ -29,7 +30,11 @@ def event_notification_handler(event_uuid: str):
|
||||
@CELERY_APP.task()
|
||||
def event_trigger_handler(event_uuid: str, trigger_name: str):
|
||||
"""Check if policies attached to NotificationRule match event"""
|
||||
event: Event = Event.objects.get(event_uuid=event_uuid)
|
||||
events = Event.objects.filter(event_uuid=event_uuid)
|
||||
if not events.exists():
|
||||
LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid)
|
||||
return
|
||||
event: Event = events.first()
|
||||
trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name)
|
||||
|
||||
if "policy_uuid" in event.context:
|
||||
@ -53,7 +58,8 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
|
||||
return
|
||||
|
||||
LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
|
||||
policy_engine = PolicyEngine(trigger, get_anonymous_user())
|
||||
user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
|
||||
policy_engine = PolicyEngine(trigger, user)
|
||||
policy_engine.mode = PolicyEngineMode.MODE_OR
|
||||
policy_engine.empty_result = False
|
||||
policy_engine.use_cache = False
|
||||
|
||||
@ -24,6 +24,6 @@ def pbflow_tester(file_name: str) -> Callable:
|
||||
return tester
|
||||
|
||||
|
||||
for flow_file in glob("website/static/flows/*.pbflow"):
|
||||
for flow_file in glob("website/static/flows/*.akflow"):
|
||||
method_name = Path(flow_file).stem.replace("-", "_").replace(".", "_")
|
||||
setattr(TestTransferDocs, f"test_flow_{method_name}", pbflow_tester(flow_file))
|
||||
|
||||
@ -152,7 +152,13 @@ class FlowImporter:
|
||||
entries = deepcopy(self.__import.entries)
|
||||
for entry in entries:
|
||||
model_app_label, model_name = entry.model.split(".")
|
||||
model: SerializerModel = apps.get_model(model_app_label, model_name)
|
||||
try:
|
||||
model: SerializerModel = apps.get_model(model_app_label, model_name)
|
||||
except LookupError:
|
||||
self.logger.error(
|
||||
"app or model does not exist", app=model_app_label, model=model_name
|
||||
)
|
||||
return False
|
||||
# Validate each single entry
|
||||
try:
|
||||
serializer = self._validate_single(entry)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
from django import forms
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.policies.forms import GENERAL_FIELDS
|
||||
from authentik.policies.group_membership.models import GroupMembershipPolicy
|
||||
|
||||
@ -9,6 +10,8 @@ from authentik.policies.group_membership.models import GroupMembershipPolicy
|
||||
class GroupMembershipPolicyForm(forms.ModelForm):
|
||||
"""GroupMembershipPolicy Form"""
|
||||
|
||||
group = forms.ModelChoiceField(queryset=Group.objects.all().order_by("name"))
|
||||
|
||||
class Meta:
|
||||
|
||||
model = GroupMembershipPolicy
|
||||
|
||||
@ -4,6 +4,7 @@ import binascii
|
||||
import json
|
||||
import time
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
from urllib.parse import urlparse
|
||||
@ -480,10 +481,14 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
|
||||
now + timedelta_from_string(self.provider.token_validity).seconds
|
||||
)
|
||||
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
|
||||
auth_event = Event.objects.filter(
|
||||
auth_events = Event.objects.filter(
|
||||
action=EventAction.LOGIN, user=get_user(user)
|
||||
).latest("created")
|
||||
auth_time = int(dateformat.format(auth_event.created, "U"))
|
||||
).order_by("-created")
|
||||
# Fallback in case we can't find any login events
|
||||
auth_time = datetime.now()
|
||||
if auth_events.exists():
|
||||
auth_time = auth_events.first().created
|
||||
auth_time = int(dateformat.format(auth_time, "U"))
|
||||
|
||||
token = IDToken(
|
||||
iss=self.provider.get_issuer(request),
|
||||
|
||||
@ -18,6 +18,8 @@ from django.core.asgi import get_asgi_application
|
||||
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.middleware import RESPONSE_HEADER_ID
|
||||
|
||||
# DJANGO_SETTINGS_MODULE is set in gunicorn.conf.py
|
||||
|
||||
defuse_stdlib()
|
||||
@ -67,6 +69,7 @@ class ASGILogger:
|
||||
status_code: int
|
||||
start: float
|
||||
content_length: int
|
||||
request_id: str
|
||||
|
||||
def __init__(self, app: ASGIApp):
|
||||
self.app = app
|
||||
@ -75,23 +78,29 @@ class ASGILogger:
|
||||
self.scope = scope
|
||||
self.content_length = 0
|
||||
self.headers = dict(scope.get("headers", []))
|
||||
self.request_id = ""
|
||||
|
||||
async def send_hooked(message: Message) -> None:
|
||||
"""Hooked send method, which records status code and content-length, and for the final
|
||||
requests logs it"""
|
||||
headers = dict(message.get("headers", []))
|
||||
|
||||
if "status" in message:
|
||||
self.status_code = message["status"]
|
||||
|
||||
if b"Content-Length" in headers:
|
||||
self.content_length += int(headers.get(b"Content-Length", b"0"))
|
||||
|
||||
if message["type"] == "http.response.start":
|
||||
response_headers = dict(message["headers"])
|
||||
self.request_id = response_headers.get(
|
||||
RESPONSE_HEADER_ID.encode(), b""
|
||||
).decode()
|
||||
|
||||
if message["type"] == "http.response.body" and not message.get(
|
||||
"more_body", None
|
||||
"more_body", True
|
||||
):
|
||||
runtime = int((time() - self.start) * 1000)
|
||||
self.log(runtime)
|
||||
self.log(runtime, request_id=self.request_id)
|
||||
await send(message)
|
||||
|
||||
self.start = time()
|
||||
@ -111,7 +120,7 @@ class ASGILogger:
|
||||
# Check if header has multiple values, and use the first one
|
||||
return client_ip.split(", ")[0]
|
||||
|
||||
def log(self, runtime: float):
|
||||
def log(self, runtime: float, **kwargs):
|
||||
"""Outpot access logs in a structured format"""
|
||||
host = self._get_ip()
|
||||
query_string = ""
|
||||
@ -125,6 +134,7 @@ class ASGILogger:
|
||||
status=self.status_code,
|
||||
size=self.content_length / 1000 if self.content_length > 0 else 0,
|
||||
runtime=runtime,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""Source API Views"""
|
||||
from datetime import datetime
|
||||
from time import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.models.base import Model
|
||||
@ -68,7 +69,7 @@ class LDAPSourceViewSet(ModelViewSet):
|
||||
def sync_status(self, request: Request, slug: str) -> Response:
|
||||
"""Get source's sync status"""
|
||||
source = self.get_object()
|
||||
last_sync = cache.get(source.state_cache_prefix("last_sync"), None)
|
||||
last_sync = cache.get(source.state_cache_prefix("last_sync"), time())
|
||||
return Response(
|
||||
LDAPSourceSyncStatusSerializer(
|
||||
{"last_sync": datetime.fromtimestamp(last_sync)}
|
||||
|
||||
@ -77,7 +77,8 @@ class LDAPPasswordChanger:
|
||||
"""Change user's password"""
|
||||
user_dn = user.attributes.get(LDAP_DISTINGUISHED_NAME, None)
|
||||
if not user_dn:
|
||||
raise AttributeError(f"User has no {LDAP_DISTINGUISHED_NAME} set.")
|
||||
LOGGER.info(f"User has no {LDAP_DISTINGUISHED_NAME} set.")
|
||||
return
|
||||
self._source.connection.extend.microsoft.modify_password(user_dn, password)
|
||||
|
||||
def _ad_check_password_existing(self, password: str, user_dn: str) -> bool:
|
||||
|
||||
@ -9,13 +9,13 @@
|
||||
<div class="pf-c-card__body">
|
||||
{% if connections.exists %}
|
||||
<p>{% trans 'Connected.' %}</p>
|
||||
<a class="pf-c-button pf-m-danger"
|
||||
<a class="pf-c-button pf-m-danger ak-root-link"
|
||||
href="{% url 'authentik_sources_oauth:oauth-client-disconnect' source_slug=source.slug %}">
|
||||
{% trans 'Disconnect' %}
|
||||
</a>
|
||||
{% else %}
|
||||
<p>Not connected.</p>
|
||||
<a class="pf-c-button pf-m-primary"
|
||||
<a class="pf-c-button pf-m-primary ak-root-link"
|
||||
href="{% url 'authentik_sources_oauth:oauth-client-login' source_slug=source.slug %}">
|
||||
{% trans 'Connect' %}
|
||||
</a>
|
||||
|
||||
@ -19,7 +19,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: beryju/authentik:${AUTHENTIK_TAG:-2021.2.2-stable}
|
||||
image: beryju/authentik:${AUTHENTIK_TAG:-2021.2.6-stable}
|
||||
command: server
|
||||
environment:
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
@ -45,7 +45,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
worker:
|
||||
image: beryju/authentik:${AUTHENTIK_TAG:-2021.2.2-stable}
|
||||
image: beryju/authentik:${AUTHENTIK_TAG:-2021.2.6-stable}
|
||||
command: worker
|
||||
networks:
|
||||
- internal
|
||||
@ -62,7 +62,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
image: beryju/authentik-static:${AUTHENTIK_TAG:-2021.2.2-stable}
|
||||
image: beryju/authentik-static:${AUTHENTIK_TAG:-2021.2.6-stable}
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
|
||||
@ -4,7 +4,7 @@ name: authentik
|
||||
home: https://goauthentik.io
|
||||
sources:
|
||||
- https://github.com/BeryJu/authentik
|
||||
version: "2021.2.2-stable"
|
||||
version: "2021.2.6-stable"
|
||||
icon: https://raw.githubusercontent.com/BeryJu/authentik/master/web/icons/icon.svg
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|-----------------------------------|-------------------------|-------------|
|
||||
| image.name | beryju/authentik | Image used to run the authentik server and worker |
|
||||
| image.name_static | beryju/authentik-static | Image used to run the authentik static server (CSS and JS Files) |
|
||||
| image.tag | 2021.2.2-stable | Image tag |
|
||||
| image.tag | 2021.2.6-stable | Image tag |
|
||||
| image.pullPolicy | IfNotPresent | Image Pull Policy used for all deployments |
|
||||
| serverReplicas | 1 | Replicas for the Server deployment |
|
||||
| workerReplicas | 1 | Replicas for the Worker deployment |
|
||||
|
||||
@ -99,10 +99,12 @@ spec:
|
||||
httpGet:
|
||||
path: /-/health/live/
|
||||
port: http
|
||||
initialDelaySeconds: 15
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /-/health/ready/
|
||||
port: http
|
||||
initialDelaySeconds: 15
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
|
||||
@ -5,7 +5,7 @@ image:
|
||||
name: beryju/authentik
|
||||
name_static: beryju/authentik-static
|
||||
name_outposts: beryju/authentik # Prefix used for Outpost deployments, Outpost type and version is appended
|
||||
tag: 2021.2.2-stable
|
||||
tag: 2021.2.6-stable
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
serverReplicas: 1
|
||||
|
||||
@ -49,12 +49,14 @@ func NewAPIController(pbURL url.URL, token string) *APIController {
|
||||
// create the API client, with the transport
|
||||
apiClient := client.New(transport, strfmt.Default)
|
||||
|
||||
log := log.WithField("logger", "authentik.outpost.ak-api-controller")
|
||||
|
||||
// Because we don't know the outpost UUID, we simply do a list and pick the first
|
||||
// The service account this token belongs to should only have access to a single outpost
|
||||
outposts, err := apiClient.Outposts.OutpostsOutpostsList(outposts.NewOutpostsOutpostsListParams(), auth)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.WithError(err).Panic("Failed to fetch configuration")
|
||||
}
|
||||
outpost := outposts.Payload.Results[0]
|
||||
doGlobalSetup(outpost.Config.(map[string]interface{}))
|
||||
@ -64,7 +66,7 @@ func NewAPIController(pbURL url.URL, token string) *APIController {
|
||||
Auth: auth,
|
||||
token: token,
|
||||
|
||||
logger: log.WithField("component", "ak-api-controller"),
|
||||
logger: log,
|
||||
|
||||
reloadOffset: time.Duration(rand.Intn(10)) * time.Second,
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ func (ac *APIController) initWS(pbURL url.URL, outpostUUID strfmt.UUID) {
|
||||
}
|
||||
ws.Dial(fmt.Sprintf(pathTemplate, scheme, pbURL.Host, outpostUUID.String()), header)
|
||||
|
||||
ac.logger.WithField("component", "ak-ws").WithField("outpost", outpostUUID.String()).Debug("connecting to authentik")
|
||||
ac.logger.WithField("logger", "authentik.outpost.ak-ws").WithField("outpost", outpostUUID.String()).Debug("connecting to authentik")
|
||||
|
||||
ac.wsConn = ws
|
||||
// Send hello message with our version
|
||||
@ -52,7 +52,7 @@ func (ac *APIController) initWS(pbURL url.URL, outpostUUID strfmt.UUID) {
|
||||
}
|
||||
err := ws.WriteJSON(msg)
|
||||
if err != nil {
|
||||
ac.logger.WithField("component", "ak-ws").WithError(err).Warning("Failed to hello to authentik")
|
||||
ac.logger.WithField("logger", "authentik.outpost.ak-ws").WithError(err).Warning("Failed to hello to authentik")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,12 @@ import (
|
||||
)
|
||||
|
||||
func doGlobalSetup(config map[string]interface{}) {
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
log.SetFormatter(&log.JSONFormatter{
|
||||
FieldMap: log.FieldMap{
|
||||
log.FieldKeyMsg: "event",
|
||||
log.FieldKeyTime: "timestamp",
|
||||
},
|
||||
})
|
||||
switch config[ConfigLogLevel].(string) {
|
||||
case "debug":
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
@ -31,7 +31,7 @@ func (s *Server) bundleProviders(providers []*models.ProxyOutpostConfig) []*prov
|
||||
bundles[idx] = &providerBundle{
|
||||
s: s,
|
||||
Host: externalHost.Host,
|
||||
log: log.WithField("component", "proxy-bundle").WithField("provider", provider.Name),
|
||||
log: log.WithField("logger", "authentik.outpost.proxy-bundle").WithField("provider", provider.Name),
|
||||
}
|
||||
bundles[idx].Build(provider)
|
||||
}
|
||||
|
||||
@ -129,7 +129,7 @@ func (pb *providerBundle) Build(provider *models.ProxyOutpostConfig) {
|
||||
log.Printf("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
oauthproxy, err := NewOAuthProxy(opts)
|
||||
oauthproxy, err := NewOAuthProxy(opts, provider)
|
||||
if err != nil {
|
||||
log.Errorf("ERROR: Failed to initialise OAuth2 Proxy: %v", err)
|
||||
os.Exit(1)
|
||||
|
||||
@ -95,7 +95,7 @@ type loggingHandler struct {
|
||||
func LoggingHandler(h http.Handler) http.Handler {
|
||||
return loggingHandler{
|
||||
handler: h,
|
||||
logger: log.WithField("component", "proxy-http-server"),
|
||||
logger: log.WithField("logger", "authentik.outpost.proxy-http-server"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,19 +104,17 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
url := *req.URL
|
||||
responseLogger := &responseLogger{w: w}
|
||||
h.handler.ServeHTTP(responseLogger, req)
|
||||
duration := float64(time.Since(t)) / float64(time.Second)
|
||||
duration := float64(time.Since(t)) / float64(time.Millisecond)
|
||||
h.logger.WithFields(log.Fields{
|
||||
"Client": req.RemoteAddr,
|
||||
"Host": req.Host,
|
||||
"Protocol": req.Proto,
|
||||
"RequestDuration": fmt.Sprintf("%0.3f", duration),
|
||||
"RequestMethod": req.Method,
|
||||
"ResponseSize": responseLogger.Size(),
|
||||
"StatusCode": responseLogger.Status(),
|
||||
"Timestamp": t,
|
||||
"Upstream": responseLogger.upstream,
|
||||
"UserAgent": req.UserAgent(),
|
||||
"Username": responseLogger.authInfo,
|
||||
"host": req.RemoteAddr,
|
||||
"vhost": req.Host,
|
||||
"request_protocol": req.Proto,
|
||||
"runtime": fmt.Sprintf("%0.3f", duration),
|
||||
"method": req.Method,
|
||||
"size": responseLogger.Size(),
|
||||
"status": responseLogger.Status(),
|
||||
"upstream": responseLogger.upstream,
|
||||
"request_useragent": req.UserAgent(),
|
||||
"request_username": responseLogger.authInfo,
|
||||
}).Info(url.RequestURI())
|
||||
// logger.PrintReq(responseLogger.authInfo, responseLogger.upstream, req, url, t, , )
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/pkg/upstream"
|
||||
"github.com/oauth2-proxy/oauth2-proxy/providers"
|
||||
"goauthentik.io/outpost/pkg/models"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -92,8 +93,8 @@ type OAuthProxy struct {
|
||||
}
|
||||
|
||||
// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
|
||||
func NewOAuthProxy(opts *options.Options) (*OAuthProxy, error) {
|
||||
logger := log.WithField("component", "proxy").WithField("client-id", opts.ClientID)
|
||||
func NewOAuthProxy(opts *options.Options, provider *models.ProxyOutpostConfig) (*OAuthProxy, error) {
|
||||
logger := log.WithField("logger", "authentik.outpost.proxy").WithField("provider", provider.Name)
|
||||
sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error initialising session store: %v", err)
|
||||
@ -434,6 +435,7 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
|
||||
authVal := b64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
req.Header["Authorization"] = []string{fmt.Sprintf("Basic %s", authVal)}
|
||||
}
|
||||
rw.Header().Set("GAP-Auth", session.PreferredUsername)
|
||||
// Check if user has additional headers set that we should sent
|
||||
if additionalHeaders, ok := userAttributes["additionalHeaders"].(map[string]string); ok {
|
||||
if additionalHeaders == nil {
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -30,7 +31,7 @@ func NewServer(ac *ak.APIController) *Server {
|
||||
}
|
||||
return &Server{
|
||||
Handlers: make(map[string]*providerBundle),
|
||||
logger: log.WithField("component", "proxy-http-server"),
|
||||
logger: log.WithField("logger", "authentik.outpost.proxy-http-server"),
|
||||
defaultCert: defaultCert,
|
||||
ak: ac,
|
||||
}
|
||||
@ -50,12 +51,15 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
s.logger.WithField("host", r.Host).Debug("Host header does not match any we know of")
|
||||
s.logger.Printf("%v+\n", s.Handlers)
|
||||
w.WriteHeader(400)
|
||||
// Get a list of all host keys we know
|
||||
hostKeys := make([]string, 0, len(s.Handlers))
|
||||
for k := range s.Handlers {
|
||||
hostKeys = append(hostKeys, k)
|
||||
}
|
||||
s.logger.WithField("host", r.Host).WithField("known-hosts", strings.Join(hostKeys, ", ")).Debug("Host header does not match any we know of")
|
||||
w.WriteHeader(404)
|
||||
return
|
||||
}
|
||||
s.logger.WithField("host", r.Host).Debug("passing request from host head")
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
package pkg
|
||||
|
||||
const VERSION = "2021.2.2-stable"
|
||||
const VERSION = "2021.2.6-stable"
|
||||
|
||||
96
swagger.yaml
96
swagger.yaml
@ -4312,6 +4312,47 @@ paths:
|
||||
tags:
|
||||
- providers
|
||||
parameters: []
|
||||
/providers/all/types/:
|
||||
get:
|
||||
operationId: providers_all_types
|
||||
description: Get all creatable provider types
|
||||
parameters:
|
||||
- name: application__isnull
|
||||
in: query
|
||||
description: ''
|
||||
required: false
|
||||
type: string
|
||||
- name: ordering
|
||||
in: query
|
||||
description: Which field to use when ordering the results.
|
||||
required: false
|
||||
type: string
|
||||
- name: search
|
||||
in: query
|
||||
description: A search term.
|
||||
required: false
|
||||
type: string
|
||||
- name: page
|
||||
in: query
|
||||
description: A page number within the paginated result set.
|
||||
required: false
|
||||
type: integer
|
||||
- name: page_size
|
||||
in: query
|
||||
description: Number of results to return per page.
|
||||
required: false
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Types of an object that can be created
|
||||
schema:
|
||||
description: ''
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/TypeCreate'
|
||||
tags:
|
||||
- providers
|
||||
parameters: []
|
||||
/providers/all/{id}/:
|
||||
get:
|
||||
operationId: providers_all_read
|
||||
@ -4868,6 +4909,42 @@ paths:
|
||||
tags:
|
||||
- sources
|
||||
parameters: []
|
||||
/sources/all/types/:
|
||||
get:
|
||||
operationId: sources_all_types
|
||||
description: Get all creatable source types
|
||||
parameters:
|
||||
- name: ordering
|
||||
in: query
|
||||
description: Which field to use when ordering the results.
|
||||
required: false
|
||||
type: string
|
||||
- name: search
|
||||
in: query
|
||||
description: A search term.
|
||||
required: false
|
||||
type: string
|
||||
- name: page
|
||||
in: query
|
||||
description: A page number within the paginated result set.
|
||||
required: false
|
||||
type: integer
|
||||
- name: page_size
|
||||
in: query
|
||||
description: Number of results to return per page.
|
||||
required: false
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Types of an object that can be created
|
||||
schema:
|
||||
description: ''
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/TypeCreate'
|
||||
tags:
|
||||
- sources
|
||||
parameters: []
|
||||
/sources/all/{slug}/:
|
||||
get:
|
||||
operationId: sources_all_read
|
||||
@ -9005,6 +9082,25 @@ definitions:
|
||||
title: Verbose name plural
|
||||
type: string
|
||||
readOnly: true
|
||||
TypeCreate:
|
||||
description: Types of an object that can be created
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
readOnly: true
|
||||
minLength: 1
|
||||
description:
|
||||
title: Description
|
||||
type: string
|
||||
readOnly: true
|
||||
minLength: 1
|
||||
link:
|
||||
title: Link
|
||||
type: string
|
||||
readOnly: true
|
||||
minLength: 1
|
||||
OAuth2Provider:
|
||||
description: OAuth2Provider Serializer
|
||||
required:
|
||||
|
||||
39
web/package-lock.json
generated
39
web/package-lock.json
generated
@ -245,6 +245,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/integrations": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.2.0.tgz",
|
||||
"integrity": "sha512-gvAhP61qs2fog2xCTDs94ZT8cZbWEjFZmOWfT1VXlZDIVopIporj5Qe6IgrLTiCWL61Yko5h5nFnPZ4mpjf+0w==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.2.0",
|
||||
"@sentry/utils": "6.2.0",
|
||||
"localforage": "^1.8.1",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.1.0.tgz",
|
||||
@ -1756,6 +1774,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
|
||||
},
|
||||
"immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@ -2010,6 +2033,14 @@
|
||||
"type-check": "~0.4.0"
|
||||
}
|
||||
},
|
||||
"lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
|
||||
"requires": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"lit-analyzer": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lit-analyzer/-/lit-analyzer-1.2.1.tgz",
|
||||
@ -2191,6 +2222,14 @@
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.3.0.tgz",
|
||||
"integrity": "sha512-0Q1bwmaFH9O14vycPHw8C/IeHMk/uSDldVLIefu/kfbTBGIc44KGH6A8p1bDfxUfHdc8q6Ct7kQklWoHgr4t1Q=="
|
||||
},
|
||||
"localforage": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz",
|
||||
"integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==",
|
||||
"requires": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
|
||||
@ -14,7 +14,8 @@
|
||||
"@patternfly/patternfly": "^4.80.3",
|
||||
"@sentry/browser": "^6.1.0",
|
||||
"@sentry/tracing": "^6.1.0",
|
||||
"@types/chart.js": "^2.9.30",
|
||||
"@sentry/integrations": "^6.2.0",
|
||||
"@types/chart.js": "^2.9.31",
|
||||
"@types/codemirror": "0.0.108",
|
||||
"chart.js": "^2.9.4",
|
||||
"codemirror": "^5.59.2",
|
||||
|
||||
@ -3,6 +3,7 @@ import * as Sentry from "@sentry/browser";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
import { VERSION } from "../constants";
|
||||
import { SentryIgnoredError } from "../common/errors";
|
||||
import { CaptureConsole as CaptureConsoleIntegration } from "@sentry/integrations";
|
||||
|
||||
export class Config {
|
||||
branding_logo: string;
|
||||
@ -22,7 +23,10 @@ export class Config {
|
||||
Sentry.init({
|
||||
dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
|
||||
release: `authentik@${VERSION}`,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
integrations: [
|
||||
new Integrations.BrowserTracing(),
|
||||
new CaptureConsoleIntegration(),
|
||||
],
|
||||
tracesSampleRate: 0.6,
|
||||
environment: config.error_reporting_environment,
|
||||
beforeSend(event: Sentry.Event, hint: Sentry.EventHint) {
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import { BaseInheritanceModel, DefaultClient, AKResponse, QueryArguments } from "./Client";
|
||||
|
||||
export interface TypeCreate {
|
||||
name: string;
|
||||
description: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export class Provider implements BaseInheritanceModel {
|
||||
pk: number;
|
||||
name: string;
|
||||
@ -24,6 +30,10 @@ export class Provider implements BaseInheritanceModel {
|
||||
return DefaultClient.fetch<AKResponse<Provider>>(["providers", "all"], filter);
|
||||
}
|
||||
|
||||
static getTypes(): Promise<TypeCreate[]> {
|
||||
return DefaultClient.fetch<TypeCreate[]>(["providers", "all", "types"]);
|
||||
}
|
||||
|
||||
static adminUrl(rest: string): string {
|
||||
return `/administration/providers/${rest}`;
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { BaseInheritanceModel, DefaultClient, AKResponse, QueryArguments } from "./Client";
|
||||
import { TypeCreate } from "./Providers";
|
||||
|
||||
export class Source implements BaseInheritanceModel {
|
||||
pk: string;
|
||||
@ -23,6 +24,10 @@ export class Source implements BaseInheritanceModel {
|
||||
return DefaultClient.fetch<AKResponse<Source>>(["sources", "all"], filter);
|
||||
}
|
||||
|
||||
static getTypes(): Promise<TypeCreate[]> {
|
||||
return DefaultClient.fetch<TypeCreate[]>(["sources", "all", "types"]);
|
||||
}
|
||||
|
||||
static adminUrl(rest: string): string {
|
||||
return `/administration/sources/${rest}`;
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { css, CSSResult } from "lit-element";
|
||||
// @ts-ignore
|
||||
import PF from "@patternfly/patternfly/patternfly.css";
|
||||
// @ts-ignore
|
||||
@ -6,10 +7,31 @@ import PFAddons from "@patternfly/patternfly/patternfly-addons.css";
|
||||
import FA from "@fortawesome/fontawesome-free/css/fontawesome.css";
|
||||
// @ts-ignore
|
||||
import AKGlobal from "../authentik.css";
|
||||
import { CSSResult } from "lit-element";
|
||||
// @ts-ignore
|
||||
import CodeMirrorStyle from "codemirror/lib/codemirror.css";
|
||||
// @ts-ignore
|
||||
import CodeMirrorTheme from "codemirror/theme/monokai.css";
|
||||
|
||||
export const COMMON_STYLES: CSSResult[] = [PF, PFAddons, FA, AKGlobal, CodeMirrorStyle, CodeMirrorTheme];
|
||||
export const ColorStyles = css`
|
||||
.pf-m-success {
|
||||
color: var(--pf-global--success-color--100);
|
||||
}
|
||||
.pf-c-button.pf-m-success {
|
||||
color: var(--pf-c-button--m-primary--Color);
|
||||
background-color: var(--pf-global--success-color--100);
|
||||
}
|
||||
.pf-m-warning {
|
||||
color: var(--pf-global--warning-color--100);
|
||||
}
|
||||
.pf-c-button.pf-m-warning {
|
||||
color: var(--pf-c-button--m-primary--Color);
|
||||
background-color: var(--pf-global--warning-color--100);
|
||||
}
|
||||
.pf-m-danger {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
.pf-c-button.pf-m-danger {
|
||||
color: var(--pf-c-button--m-primary--Color);
|
||||
background-color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
`;
|
||||
export const COMMON_STYLES: CSSResult[] = [PF, PFAddons, FA, AKGlobal, CodeMirrorStyle, CodeMirrorTheme, ColorStyles];
|
||||
|
||||
@ -1,31 +1,6 @@
|
||||
import { css } from "lit-element";
|
||||
|
||||
export const PRIMARY_CLASS = "pf-m-primary";
|
||||
export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const ColorStyles = css`
|
||||
.pf-m-success {
|
||||
color: var(--pf-global--success-color--100);
|
||||
}
|
||||
.pf-c-button.pf-m-success {
|
||||
color: var(--pf-c-button--m-primary--Color);
|
||||
background-color: var(--pf-global--success-color--100);
|
||||
}
|
||||
.pf-m-warning {
|
||||
color: var(--pf-global--warning-color--100);
|
||||
}
|
||||
.pf-c-button.pf-m-warning {
|
||||
color: var(--pf-c-button--m-primary--Color);
|
||||
background-color: var(--pf-global--warning-color--100);
|
||||
}
|
||||
.pf-m-danger {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
.pf-c-button.pf-m-danger {
|
||||
color: var(--pf-c-button--m-primary--Color);
|
||||
background-color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
`;
|
||||
export const VERSION = "2021.2.2-stable";
|
||||
export const VERSION = "2021.2.6-stable";
|
||||
|
||||
@ -5,7 +5,8 @@ import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css";
|
||||
import ButtonStyle from "@patternfly/patternfly/components/Button/button.css";
|
||||
// @ts-ignore
|
||||
import SpinnerStyle from "@patternfly/patternfly/components/Spinner/spinner.css";
|
||||
import { ColorStyles, PRIMARY_CLASS, PROGRESS_CLASS } from "../../constants";
|
||||
import { PRIMARY_CLASS, PROGRESS_CLASS } from "../../constants";
|
||||
import { ColorStyles } from "../../common/styles";
|
||||
|
||||
@customElement("ak-spinner-button")
|
||||
export class SpinnerButton extends LitElement {
|
||||
|
||||
@ -4,7 +4,8 @@ import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css";
|
||||
// @ts-ignore
|
||||
import ButtonStyle from "@patternfly/patternfly/components/Button/button.css";
|
||||
import { tokenByIdentifier } from "../../api/Tokens";
|
||||
import { ColorStyles, ERROR_CLASS, PRIMARY_CLASS, SUCCESS_CLASS } from "../../constants";
|
||||
import { ERROR_CLASS, PRIMARY_CLASS, SUCCESS_CLASS } from "../../constants";
|
||||
import { ColorStyles } from "../../common/styles";
|
||||
|
||||
@customElement("ak-token-copy-button")
|
||||
export class TokenCopyButton extends LitElement {
|
||||
|
||||
@ -2,7 +2,6 @@ import { gettext } from "django";
|
||||
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { COMMON_STYLES } from "../../common/styles";
|
||||
import { ColorStyles } from "../../constants";
|
||||
|
||||
@customElement("ak-aggregate-card")
|
||||
export class AggregateCard extends LitElement {
|
||||
@ -25,7 +24,7 @@ export class AggregateCard extends LitElement {
|
||||
text-align: center;
|
||||
color: var(--pf-global--Color--100);
|
||||
}
|
||||
`, ColorStyles]);
|
||||
`]);
|
||||
}
|
||||
|
||||
renderInner(): TemplateResult {
|
||||
|
||||
@ -41,13 +41,13 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
||||
html`
|
||||
<ak-modal-button href="${PolicyBinding.adminUrl(`${item.pk}/update/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
Edit
|
||||
${gettext("Edit")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="${PolicyBinding.adminUrl(`${item.pk}/delete/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
Delete
|
||||
${gettext("Delete")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
|
||||
@ -3,7 +3,6 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu
|
||||
import CodeMirrorStyle from "codemirror/lib/codemirror.css";
|
||||
// @ts-ignore
|
||||
import CodeMirrorTheme from "codemirror/theme/monokai.css";
|
||||
import { ColorStyles } from "../../constants";
|
||||
import { COMMON_STYLES } from "../../common/styles";
|
||||
import { Route } from "./Route";
|
||||
import { ROUTES } from "../../routes";
|
||||
@ -23,7 +22,6 @@ export class RouterOutlet extends LitElement {
|
||||
return [
|
||||
CodeMirrorStyle,
|
||||
CodeMirrorTheme,
|
||||
ColorStyles,
|
||||
css`
|
||||
:host {
|
||||
height: 100vh;
|
||||
|
||||
@ -55,7 +55,12 @@ export class LibraryPage extends LitElement {
|
||||
apps?: AKResponse<Application>;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return COMMON_STYLES;
|
||||
return COMMON_STYLES.concat(css`
|
||||
:host,
|
||||
main {
|
||||
height: 100%;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
|
||||
@ -62,13 +62,13 @@ export class ApplicationListPage extends TablePage<Application> {
|
||||
html`
|
||||
<ak-modal-button href="${Application.adminUrl(`${item.pk}/update/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
Edit
|
||||
${gettext("Edit")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="${Application.adminUrl(`${item.pk}/delete/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
Delete
|
||||
${gettext("Delete")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
|
||||
@ -42,13 +42,13 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
||||
html`
|
||||
<ak-modal-button href="${FlowStageBinding.adminUrl(`${item.pk}/update/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
Edit
|
||||
${gettext("Edit")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="${FlowStageBinding.adminUrl(`${item.pk}/delete/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
Delete
|
||||
${gettext("Delete")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
|
||||
@ -89,10 +89,14 @@ export class SiteShell extends LitElement {
|
||||
if (a.href === "") {
|
||||
return;
|
||||
}
|
||||
if (a.href.startsWith("#")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = new URL(a.href);
|
||||
const qs = url.search || "";
|
||||
a.href = `#${url.pathname}${qs}`;
|
||||
const hash = (url.hash || "#").substring(2, Infinity);
|
||||
a.href = `#${url.pathname}${qs}${hash}`;
|
||||
} catch (e) {
|
||||
console.debug(`authentik/site-shell: error ${e}`);
|
||||
a.href = `#${a.href}`;
|
||||
|
||||
@ -52,13 +52,13 @@ export class OutpostListPage extends TablePage<Outpost> {
|
||||
})}</ul>`,
|
||||
html`<ak-outpost-health outpostId=${item.pk}></ak-outpost-health>`,
|
||||
html`
|
||||
<ak-modal-button href="${Outpost.adminUrl(`${item.pk}/update`)}">
|
||||
<ak-modal-button href="${Outpost.adminUrl(`${item.pk}/update/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
${gettext("Edit")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="${Outpost.adminUrl(`${item.pk}/delete`)}">
|
||||
<ak-modal-button href="${Outpost.adminUrl(`${item.pk}/delete/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
${gettext("Delete")}
|
||||
</ak-spinner-button>
|
||||
|
||||
@ -8,6 +8,7 @@ import "../../elements/buttons/ModalButton";
|
||||
import "../../elements/buttons/SpinnerButton";
|
||||
import "../../elements/buttons/Dropdown";
|
||||
import { TableColumn } from "../../elements/table/Table";
|
||||
import { until } from "lit-html/directives/until";
|
||||
|
||||
@customElement("ak-provider-list")
|
||||
export class ProviderListPage extends TablePage<Provider> {
|
||||
@ -81,46 +82,18 @@ export class ProviderListPage extends TablePage<Provider> {
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
<li>
|
||||
<ak-modal-button href="${Provider.adminUrl("/create/?type=OAuth2Provider")}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">${gettext("OAuth2/OpenID Provider")}<br>
|
||||
<small>
|
||||
${gettext("OAuth2 Provider for generic OAuth and OpenID Connect Applications.")}
|
||||
</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>
|
||||
<li>
|
||||
<ak-modal-button href="${Provider.adminUrl("/create/?type=ProxyProvider")}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">${gettext("Proxy Provider")}<br>
|
||||
<small>
|
||||
${gettext("Protect applications that don't support any of the other Protocols by using a Reverse-Proxy.")}
|
||||
</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>
|
||||
<li>
|
||||
<ak-modal-button href="${Provider.adminUrl("/create/?type=SAMLProvider")}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">${gettext("SAML Provider")}<br>
|
||||
<small>
|
||||
${gettext("SAML 2.0 Endpoint for applications which support SAML.")}
|
||||
</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>
|
||||
<li>
|
||||
<ak-modal-button href="${Provider.adminUrl("/create/saml/from-metadata/")}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">${gettext("SAML Provider from Metadata")}<br>
|
||||
<small>
|
||||
${gettext("Create a SAML Provider by importing its Metadata.")}
|
||||
</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>
|
||||
${until(Provider.getTypes().then((types) => {
|
||||
return types.map((type) => {
|
||||
return html`<li>
|
||||
<ak-modal-button href="${type.link}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">${type.name}<br>
|
||||
<small>${type.description}</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>`;
|
||||
});
|
||||
}), html`<ak-spinner></ak-spinner>`)}
|
||||
</ul>
|
||||
</ak-dropdown>
|
||||
${super.renderToolbar()}`;
|
||||
|
||||
@ -7,6 +7,8 @@ import { TablePage } from "../../elements/table/TablePage";
|
||||
|
||||
import "../../elements/buttons/ModalButton";
|
||||
import "../../elements/buttons/SpinnerButton";
|
||||
import "../../elements/buttons/Dropdown";
|
||||
import { until } from "lit-html/directives/until";
|
||||
|
||||
@customElement("ak-source-list")
|
||||
export class SourceListPage extends TablePage<Source> {
|
||||
@ -52,13 +54,13 @@ export class SourceListPage extends TablePage<Source> {
|
||||
html`
|
||||
<ak-modal-button href="${Source.adminUrl(`${item.pk}/update/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
||||
Edit
|
||||
${gettext("Edit")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-modal-button href="${Source.adminUrl(`${item.pk}/delete/`)}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
Delete
|
||||
${gettext("Delete")}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
@ -66,4 +68,29 @@ export class SourceListPage extends TablePage<Source> {
|
||||
];
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`
|
||||
<ak-dropdown class="pf-c-dropdown">
|
||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
||||
<span class="pf-c-dropdown__toggle-text">${gettext("Create")}</span>
|
||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="pf-c-dropdown__menu" hidden>
|
||||
${until(Source.getTypes().then((types) => {
|
||||
return types.map((type) => {
|
||||
return html`<li>
|
||||
<ak-modal-button href="${type.link}">
|
||||
<button slot="trigger" class="pf-c-dropdown__menu-item">${type.name}<br>
|
||||
<small>${type.description}</small>
|
||||
</button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</li>`;
|
||||
});
|
||||
}), html`<ak-spinner></ak-spinner>`)}
|
||||
</ul>
|
||||
</ak-dropdown>
|
||||
${super.renderToolbar()}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ Download the latest `docker-compose.yml` from [here](https://raw.githubuserconte
|
||||
|
||||
To optionally enable error-reporting, run `echo AUTHENTIK_ERROR_REPORTING__ENABLED=true >> .env`
|
||||
|
||||
To optionally deploy a different version run `echo AUTHENTIK_TAG=2021.2.2-stable >> .env`
|
||||
To optionally deploy a different version run `echo AUTHENTIK_TAG=2021.2.6-stable >> .env`
|
||||
|
||||
If this is a fresh authentik install run the following commands to generate a password:
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ image:
|
||||
name: beryju/authentik
|
||||
name_static: beryju/authentik-static
|
||||
name_outposts: beryju/authentik # Prefix used for Outpost deployments, Outpost type and version is appended
|
||||
tag: 2021.2.2-stable
|
||||
tag: 2021.2.6-stable
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
||||
@ -57,6 +57,26 @@ title: Release 2021.1.2
|
||||
- providers/oauth2: add unofficial groups attribute to default profile claim
|
||||
- web: fix sidebar being active when stage prompts is selected
|
||||
|
||||
## Fixed in 2021.2.2-stable
|
||||
|
||||
- crypto: move certificate and key data to separate api calls to create events
|
||||
- events: rename context.token to context.secret
|
||||
- events: rename token_view to secret_view
|
||||
- lib: fix stacktrace for general expressions
|
||||
- outposts: fix ProxyProvider update not triggering outpost update
|
||||
- policies: skip cache on debug request
|
||||
- providers/proxy: fix certificates without key being selectable
|
||||
- root: log runtime in milliseconds
|
||||
- sources/*: switch API to use slug in URL
|
||||
- sources/ldap: add API for sync status
|
||||
- sources/oauth: add callback URL to api
|
||||
- web: fix ModalButton working in global scope, causing issues on 2nd use
|
||||
|
||||
## Fixed in 2021.2.3-stable
|
||||
|
||||
- core: fix tokens using wrong lookup
|
||||
- web: fix missing source create button
|
||||
|
||||
## Upgrading
|
||||
|
||||
This release does not introduce any new requirements.
|
||||
|
||||
14
website/docs/releases/next.md
Normal file
14
website/docs/releases/next.md
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Next release
|
||||
---
|
||||
|
||||
## Headline Changes
|
||||
|
||||
- Simplify role-based access
|
||||
|
||||
Instead of having to create a Group Membership policy for every group you want to use, you can now select a Group and even a User directly in a binding.
|
||||
|
||||
When a group is selected, the binding behaves the same as if a Group Membership policy exists.
|
||||
|
||||
When a user is selected, the binding checks the user of the request, and denies the request when the user doesn't match.
|
||||
|
||||
Reference in New Issue
Block a user