Compare commits
40 Commits
main
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
8f207c7504 | |||
34d30bb549 | |||
b4f04881e0 | |||
5314485426 | |||
ad6b6e4576 | |||
fb9aa9d7f7 | |||
fe7662f80d | |||
d6904b6aa1 | |||
cd581efacd | |||
6c159d120b | |||
4ddd4e7f88 | |||
441912414f | |||
9e177ed5c0 | |||
881548176f | |||
56739d0dc4 | |||
b23972e9c9 | |||
0a9595089e | |||
72c22b5fab | |||
84cdbb0a03 | |||
9fc659f121 | |||
db6abf61b8 | |||
6426a1d177 | |||
9075270b01 | |||
d17a39a431 | |||
db1d091d2e | |||
f98204e78e | |||
3f663cab0f | |||
3fe129e107 | |||
f26d41aef9 | |||
5d8b5998ae | |||
7a5e136346 | |||
bfbab6357a | |||
5997b93f15 | |||
6cdae09dc0 | |||
ff0ef7a2b3 | |||
3986104a20 | |||
1aa60e7864 | |||
045578dd07 | |||
f23d70dc75 | |||
496f3426d9 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2024.6.0
|
current_version = 2024.6.3
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||||
|
38
.github/dependabot.yml
vendored
38
.github/dependabot.yml
vendored
@ -21,7 +21,10 @@ updates:
|
|||||||
labels:
|
labels:
|
||||||
- dependencies
|
- dependencies
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/web"
|
directories:
|
||||||
|
- "/web"
|
||||||
|
- "/tests/wdio"
|
||||||
|
- "/web/sfe"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "04:00"
|
time: "04:00"
|
||||||
@ -30,7 +33,6 @@ updates:
|
|||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "web:"
|
prefix: "web:"
|
||||||
# TODO: deduplicate these groups
|
|
||||||
groups:
|
groups:
|
||||||
sentry:
|
sentry:
|
||||||
patterns:
|
patterns:
|
||||||
@ -56,38 +58,6 @@ updates:
|
|||||||
patterns:
|
patterns:
|
||||||
- "@rollup/*"
|
- "@rollup/*"
|
||||||
- "rollup-*"
|
- "rollup-*"
|
||||||
- package-ecosystem: npm
|
|
||||||
directory: "/tests/wdio"
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
time: "04:00"
|
|
||||||
labels:
|
|
||||||
- dependencies
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
commit-message:
|
|
||||||
prefix: "web:"
|
|
||||||
# TODO: deduplicate these groups
|
|
||||||
groups:
|
|
||||||
sentry:
|
|
||||||
patterns:
|
|
||||||
- "@sentry/*"
|
|
||||||
- "@spotlightjs/*"
|
|
||||||
babel:
|
|
||||||
patterns:
|
|
||||||
- "@babel/*"
|
|
||||||
- "babel-*"
|
|
||||||
eslint:
|
|
||||||
patterns:
|
|
||||||
- "@typescript-eslint/*"
|
|
||||||
- "eslint"
|
|
||||||
- "eslint-*"
|
|
||||||
storybook:
|
|
||||||
patterns:
|
|
||||||
- "@storybook/*"
|
|
||||||
- "*storybook*"
|
|
||||||
esbuild:
|
|
||||||
patterns:
|
|
||||||
- "@esbuild/*"
|
|
||||||
wdio:
|
wdio:
|
||||||
patterns:
|
patterns:
|
||||||
- "@wdio/*"
|
- "@wdio/*"
|
||||||
|
7
.github/workflows/api-ts-publish.yml
vendored
7
.github/workflows/api-ts-publish.yml
vendored
@ -31,7 +31,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||||
- name: Upgrade /web
|
- name: Upgrade /web
|
||||||
working-directory: web/
|
working-directory: web
|
||||||
|
run: |
|
||||||
|
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||||
|
npm i @goauthentik/api@$VERSION
|
||||||
|
- name: Upgrade /web/sfe
|
||||||
|
working-directory: web/sfe
|
||||||
run: |
|
run: |
|
||||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||||
npm i @goauthentik/api@$VERSION
|
npm i @goauthentik/api@$VERSION
|
||||||
|
10
.github/workflows/ci-web.yml
vendored
10
.github/workflows/ci-web.yml
vendored
@ -20,6 +20,16 @@ jobs:
|
|||||||
project:
|
project:
|
||||||
- web
|
- web
|
||||||
- tests/wdio
|
- tests/wdio
|
||||||
|
include:
|
||||||
|
- command: tsc
|
||||||
|
project: web
|
||||||
|
extra_setup: |
|
||||||
|
cd sfe/ && npm ci
|
||||||
|
exclude:
|
||||||
|
- command: lint:lockfile
|
||||||
|
project: tests/wdio
|
||||||
|
- command: tsc
|
||||||
|
project: tests/wdio
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
|
@ -30,7 +30,12 @@ WORKDIR /work/web
|
|||||||
|
|
||||||
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
||||||
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
||||||
|
--mount=type=bind,target=/work/web/sfe/package.json,src=./web/sfe/package.json \
|
||||||
|
--mount=type=bind,target=/work/web/sfe/package-lock.json,src=./web/sfe/package-lock.json \
|
||||||
|
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
|
||||||
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
|
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
|
||||||
|
npm ci --include=dev && \
|
||||||
|
cd sfe && \
|
||||||
npm ci --include=dev
|
npm ci --include=dev
|
||||||
|
|
||||||
COPY ./package.json /work
|
COPY ./package.json /work
|
||||||
@ -38,7 +43,9 @@ COPY ./web /work/web/
|
|||||||
COPY ./website /work/website/
|
COPY ./website /work/website/
|
||||||
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build && \
|
||||||
|
cd sfe && \
|
||||||
|
npm run build
|
||||||
|
|
||||||
# Stage 3: Build go proxy
|
# Stage 3: Build go proxy
|
||||||
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.22-fips-bookworm AS go-builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.22-fips-bookworm AS go-builder
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2024.6.0"
|
__version__ = "2024.6.3"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ from authentik.tenants.utils import get_current_tenant
|
|||||||
class FooterLinkSerializer(PassiveSerializer):
|
class FooterLinkSerializer(PassiveSerializer):
|
||||||
"""Links returned in Config API"""
|
"""Links returned in Config API"""
|
||||||
|
|
||||||
href = CharField(read_only=True)
|
href = CharField(read_only=True, allow_null=True)
|
||||||
name = CharField(read_only=True)
|
name = CharField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,12 +7,13 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|||||||
|
|
||||||
|
|
||||||
def backport_is_backchannel(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def backport_is_backchannel(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
from authentik.providers.ldap.models import LDAPProvider
|
from authentik.providers.ldap.models import LDAPProvider
|
||||||
from authentik.providers.scim.models import SCIMProvider
|
from authentik.providers.scim.models import SCIMProvider
|
||||||
|
|
||||||
for model in [LDAPProvider, SCIMProvider]:
|
for model in [LDAPProvider, SCIMProvider]:
|
||||||
try:
|
try:
|
||||||
for obj in model.objects.only("is_backchannel"):
|
for obj in model.objects.using(db_alias).only("is_backchannel"):
|
||||||
obj.is_backchannel = True
|
obj.is_backchannel = True
|
||||||
obj.save()
|
obj.save()
|
||||||
except (DatabaseError, InternalError, ProgrammingError):
|
except (DatabaseError, InternalError, ProgrammingError):
|
||||||
|
@ -212,7 +212,7 @@ class SourceFlowManager:
|
|||||||
|
|
||||||
def _prepare_flow(
|
def _prepare_flow(
|
||||||
self,
|
self,
|
||||||
flow: Flow,
|
flow: Flow | None,
|
||||||
connection: UserSourceConnection,
|
connection: UserSourceConnection,
|
||||||
stages: list[StageView] | None = None,
|
stages: list[StageView] | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -309,7 +309,9 @@ class SourceFlowManager:
|
|||||||
# When request isn't authenticated we jump straight to auth
|
# When request isn't authenticated we jump straight to auth
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return self.handle_auth(connection)
|
return self.handle_auth(connection)
|
||||||
# Connection has already been saved
|
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||||
|
return self._prepare_flow(None, connection)
|
||||||
|
connection.save()
|
||||||
Event.new(
|
Event.new(
|
||||||
EventAction.SOURCE_LINKED,
|
EventAction.SOURCE_LINKED,
|
||||||
message="Linked Source",
|
message="Linked Source",
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
versionSubdomain: "{{ version_subdomain }}",
|
versionSubdomain: "{{ version_subdomain }}",
|
||||||
build: "{{ build }}",
|
build: "{{ build }}",
|
||||||
};
|
};
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
window.addEventListener("DOMContentLoaded", function () {
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("ak-message", {
|
new CustomEvent("ak-message", {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html lang="en">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
@ -71,9 +71,9 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li>
|
<li>
|
||||||
<a href="https://goauthentik.io?utm_source=authentik">
|
<span>
|
||||||
{% trans 'Powered by authentik' %}
|
{% trans 'Powered by authentik' %}
|
||||||
</a>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -17,11 +17,5 @@ def versioned_script(path: str) -> str:
|
|||||||
f'<script src="{static_loader(path.replace("%v", get_full_version()))}'
|
f'<script src="{static_loader(path.replace("%v", get_full_version()))}'
|
||||||
'" type="module"></script>'
|
'" type="module"></script>'
|
||||||
),
|
),
|
||||||
# Legacy method of loading scripts used as a fallback, without the version in the filename
|
|
||||||
# TODO: Remove after 2024.6 or later
|
|
||||||
(
|
|
||||||
f'<script src="{static_loader(path.replace("-%v", ""))}?'
|
|
||||||
f'version={get_full_version()}" type="module"></script>'
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
return mark_safe("".join(returned_lines)) # nosec
|
return mark_safe("".join(returned_lines)) # nosec
|
||||||
|
@ -20,8 +20,9 @@ from authentik.core.api.transactional_applications import TransactionalApplicati
|
|||||||
from authentik.core.api.users import UserViewSet
|
from authentik.core.api.users import UserViewSet
|
||||||
from authentik.core.views import apps
|
from authentik.core.views import apps
|
||||||
from authentik.core.views.debug import AccessDeniedView
|
from authentik.core.views.debug import AccessDeniedView
|
||||||
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
|
from authentik.core.views.interface import InterfaceView
|
||||||
from authentik.core.views.session import EndSessionView
|
from authentik.core.views.session import EndSessionView
|
||||||
|
from authentik.flows.views.interface import FlowInterfaceView
|
||||||
from authentik.root.asgi_middleware import SessionMiddleware
|
from authentik.root.asgi_middleware import SessionMiddleware
|
||||||
from authentik.root.messages.consumer import MessageConsumer
|
from authentik.root.messages.consumer import MessageConsumer
|
||||||
from authentik.root.middleware import ChannelsLoggingMiddleware
|
from authentik.root.middleware import ChannelsLoggingMiddleware
|
||||||
@ -53,6 +54,8 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"if/flow/<slug:flow_slug>/",
|
"if/flow/<slug:flow_slug>/",
|
||||||
|
# FIXME: move this url to the flows app...also will cause all
|
||||||
|
# of the reverse calls to be adjusted
|
||||||
ensure_csrf_cookie(FlowInterfaceView.as_view()),
|
ensure_csrf_cookie(FlowInterfaceView.as_view()),
|
||||||
name="if-flow",
|
name="if-flow",
|
||||||
),
|
),
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from json import dumps
|
from json import dumps
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
@ -11,7 +10,6 @@ from authentik import get_build_hash
|
|||||||
from authentik.admin.tasks import LOCAL_VERSION
|
from authentik.admin.tasks import LOCAL_VERSION
|
||||||
from authentik.api.v3.config import ConfigView
|
from authentik.api.v3.config import ConfigView
|
||||||
from authentik.brands.api import CurrentBrandSerializer
|
from authentik.brands.api import CurrentBrandSerializer
|
||||||
from authentik.flows.models import Flow
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceView(TemplateView):
|
class InterfaceView(TemplateView):
|
||||||
@ -25,14 +23,3 @@ class InterfaceView(TemplateView):
|
|||||||
kwargs["build"] = get_build_hash()
|
kwargs["build"] = get_build_hash()
|
||||||
kwargs["url_kwargs"] = self.kwargs
|
kwargs["url_kwargs"] = self.kwargs
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class FlowInterfaceView(InterfaceView):
|
|
||||||
"""Flow interface"""
|
|
||||||
|
|
||||||
template_name = "if/flow.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
|
||||||
kwargs["inspector"] = "inspector" in self.request.GET
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
@ -34,6 +34,12 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionTokenOwnerFilter(OwnerFilter):
|
||||||
|
"""Owner filter for connection tokens (checks session's user)"""
|
||||||
|
|
||||||
|
owner_key = "session__user"
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTokenViewSet(
|
class ConnectionTokenViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
mixins.UpdateModelMixin,
|
||||||
@ -50,4 +56,9 @@ class ConnectionTokenViewSet(
|
|||||||
search_fields = ["endpoint__name", "provider__name"]
|
search_fields = ["endpoint__name", "provider__name"]
|
||||||
ordering = ["endpoint__name", "provider__name"]
|
ordering = ["endpoint__name", "provider__name"]
|
||||||
permission_classes = [OwnerSuperuserPermissions]
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
filter_backends = [
|
||||||
|
ConnectionTokenOwnerFilter,
|
||||||
|
DjangoFilterBackend,
|
||||||
|
OrderingFilter,
|
||||||
|
SearchFilter,
|
||||||
|
]
|
||||||
|
@ -5,7 +5,6 @@ from channels.sessions import CookieMiddleware
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
|
||||||
from authentik.core.channels import TokenOutpostMiddleware
|
|
||||||
from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet
|
from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet
|
||||||
from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet
|
from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet
|
||||||
from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet
|
from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet
|
||||||
@ -13,6 +12,7 @@ from authentik.enterprise.providers.rac.api.providers import RACProviderViewSet
|
|||||||
from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer
|
from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer
|
||||||
from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer
|
from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer
|
||||||
from authentik.enterprise.providers.rac.views import RACInterface, RACStartView
|
from authentik.enterprise.providers.rac.views import RACInterface, RACStartView
|
||||||
|
from authentik.outposts.channels import TokenOutpostMiddleware
|
||||||
from authentik.root.asgi_middleware import SessionMiddleware
|
from authentik.root.asgi_middleware import SessionMiddleware
|
||||||
from authentik.root.middleware import ChannelsLoggingMiddleware
|
from authentik.root.middleware import ChannelsLoggingMiddleware
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ IGNORED_MODELS = tuple(
|
|||||||
|
|
||||||
_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None)
|
_CTX_OVERWRITE_USER = ContextVar[User | None]("authentik_events_log_overwrite_user", default=None)
|
||||||
_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False)
|
_CTX_IGNORE = ContextVar[bool]("authentik_events_log_ignore", default=False)
|
||||||
|
_CTX_REQUEST = ContextVar[HttpRequest | None]("authentik_events_log_request", default=None)
|
||||||
|
|
||||||
|
|
||||||
def should_log_model(model: Model) -> bool:
|
def should_log_model(model: Model) -> bool:
|
||||||
@ -149,11 +150,13 @@ class AuditMiddleware:
|
|||||||
m2m_changed.disconnect(dispatch_uid=request.request_id)
|
m2m_changed.disconnect(dispatch_uid=request.request_id)
|
||||||
|
|
||||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
_CTX_REQUEST.set(request)
|
||||||
self.connect(request)
|
self.connect(request)
|
||||||
|
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
|
|
||||||
self.disconnect(request)
|
self.disconnect(request)
|
||||||
|
_CTX_REQUEST.set(None)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def process_exception(self, request: HttpRequest, exception: Exception):
|
def process_exception(self, request: HttpRequest, exception: Exception):
|
||||||
@ -167,7 +170,7 @@ class AuditMiddleware:
|
|||||||
thread = EventNewThread(
|
thread = EventNewThread(
|
||||||
EventAction.SUSPICIOUS_REQUEST,
|
EventAction.SUSPICIOUS_REQUEST,
|
||||||
request,
|
request,
|
||||||
message=str(exception),
|
message=exception_to_string(exception),
|
||||||
)
|
)
|
||||||
thread.run()
|
thread.run()
|
||||||
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
||||||
@ -192,6 +195,8 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
if _CTX_IGNORE.get():
|
if _CTX_IGNORE.get():
|
||||||
return
|
return
|
||||||
|
if request.request_id != _CTX_REQUEST.get().request_id:
|
||||||
|
return
|
||||||
user = self.get_user(request)
|
user = self.get_user(request)
|
||||||
|
|
||||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||||
@ -205,6 +210,8 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
if _CTX_IGNORE.get():
|
if _CTX_IGNORE.get():
|
||||||
return
|
return
|
||||||
|
if request.request_id != _CTX_REQUEST.get().request_id:
|
||||||
|
return
|
||||||
user = self.get_user(request)
|
user = self.get_user(request)
|
||||||
|
|
||||||
EventNewThread(
|
EventNewThread(
|
||||||
@ -230,6 +237,8 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
if _CTX_IGNORE.get():
|
if _CTX_IGNORE.get():
|
||||||
return
|
return
|
||||||
|
if request.request_id != _CTX_REQUEST.get().request_id:
|
||||||
|
return
|
||||||
user = self.get_user(request)
|
user = self.get_user(request)
|
||||||
|
|
||||||
EventNewThread(
|
EventNewThread(
|
||||||
|
@ -238,6 +238,8 @@ class Event(SerializerModel, ExpiringModel):
|
|||||||
"args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
|
"args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
|
||||||
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
|
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
|
||||||
}
|
}
|
||||||
|
if hasattr(request, "request_id"):
|
||||||
|
self.context["http_request"]["request_id"] = request.request_id
|
||||||
# Special case for events created during flow execution
|
# Special case for events created during flow execution
|
||||||
# since they keep the http query within a wrapped query
|
# since they keep the http query within a wrapped query
|
||||||
if QS_QUERY in self.context["http_request"]["args"]:
|
if QS_QUERY in self.context["http_request"]["args"]:
|
||||||
|
@ -75,7 +75,10 @@ def on_login_failed(
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Failed Login, authentik custom event"""
|
"""Failed Login, authentik custom event"""
|
||||||
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(request)
|
user = User.objects.filter(username=credentials.get("username")).first()
|
||||||
|
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(
|
||||||
|
request, user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(invitation_used)
|
@receiver(invitation_used)
|
||||||
|
@ -21,7 +21,9 @@ def set_oobe_flow_authentication(apps: Apps, schema_editor: BaseDatabaseSchemaEd
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if users.exists():
|
if users.exists():
|
||||||
Flow.objects.filter(slug="initial-setup").update(authentication="require_superuser")
|
Flow.objects.using(db_alias).filter(slug="initial-setup").update(
|
||||||
|
authentication="require_superuser"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
54
authentik/flows/templates/if/flow-sfe.html
Normal file
54
authentik/flows/templates/if/flow-sfe.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load authentik_core %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
||||||
|
<link rel="icon" href="{{ brand.branding_favicon }}">
|
||||||
|
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
|
||||||
|
{% block head_before %}
|
||||||
|
{% endblock %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">
|
||||||
|
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||||
|
{% include "base/header_js.html" %}
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-image: url("{{ flow.background_url }}");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin {
|
||||||
|
max-width: 330px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin .form-floating:focus-within {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.brand-icon {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="d-flex align-items-center py-4 bg-body-tertiary">
|
||||||
|
<div class="card m-auto">
|
||||||
|
<main class="form-signin w-100 m-auto" id="flow-sfe-container">
|
||||||
|
</main>
|
||||||
|
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
|
||||||
|
</div>
|
||||||
|
<script src="{% static 'dist/sfe/index.js' %}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
41
authentik/flows/views/interface.py
Normal file
41
authentik/flows/views/interface.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""Interface views"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from ua_parser.user_agent_parser import Parse
|
||||||
|
|
||||||
|
from authentik.core.views.interface import InterfaceView
|
||||||
|
from authentik.flows.models import Flow
|
||||||
|
|
||||||
|
|
||||||
|
class FlowInterfaceView(InterfaceView):
|
||||||
|
"""Flow interface"""
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||||
|
kwargs["inspector"] = "inspector" in self.request.GET
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def compat_needs_sfe(self) -> bool:
|
||||||
|
"""Check if we need to use the simplified flow executor for compatibility"""
|
||||||
|
ua = Parse(self.request.META.get("HTTP_USER_AGENT", ""))
|
||||||
|
if ua["user_agent"]["family"] == "IE":
|
||||||
|
return True
|
||||||
|
# Only use SFE for Edge 18 and older, after Edge 18 MS switched to chromium which supports
|
||||||
|
# the default flow executor
|
||||||
|
if (
|
||||||
|
ua["user_agent"]["family"] == "Edge"
|
||||||
|
and int(ua["user_agent"]["major"]) <= 18 # noqa: PLR2004
|
||||||
|
): # noqa: PLR2004
|
||||||
|
return True
|
||||||
|
# https://github.com/AzureAD/microsoft-authentication-library-for-objc
|
||||||
|
# Used by Microsoft Teams/Office on macOS, and also uses a very outdated browser engine
|
||||||
|
if "PKeyAuth" in ua["string"]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_template_names(self) -> list[str]:
|
||||||
|
if self.compat_needs_sfe() or "sfe" in self.request.GET:
|
||||||
|
return ["if/flow-sfe.html"]
|
||||||
|
return ["if/flow.html"]
|
@ -2,6 +2,7 @@ from collections.abc import Callable
|
|||||||
|
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
|
from django.db.models.query import Q
|
||||||
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
||||||
|
|
||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
@ -34,7 +35,9 @@ def register_signals(
|
|||||||
|
|
||||||
def model_post_save(sender: type[Model], instance: User | Group, created: bool, **_):
|
def model_post_save(sender: type[Model], instance: User | Group, created: bool, **_):
|
||||||
"""Post save handler"""
|
"""Post save handler"""
|
||||||
if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
|
if not provider_type.objects.filter(
|
||||||
|
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
|
||||||
|
).exists():
|
||||||
return
|
return
|
||||||
task_sync_direct.delay(class_to_path(instance.__class__), instance.pk, Direction.add.value)
|
task_sync_direct.delay(class_to_path(instance.__class__), instance.pk, Direction.add.value)
|
||||||
|
|
||||||
@ -43,7 +46,9 @@ def register_signals(
|
|||||||
|
|
||||||
def model_pre_delete(sender: type[Model], instance: User | Group, **_):
|
def model_pre_delete(sender: type[Model], instance: User | Group, **_):
|
||||||
"""Pre-delete handler"""
|
"""Pre-delete handler"""
|
||||||
if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
|
if not provider_type.objects.filter(
|
||||||
|
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
|
||||||
|
).exists():
|
||||||
return
|
return
|
||||||
task_sync_direct.delay(
|
task_sync_direct.delay(
|
||||||
class_to_path(instance.__class__), instance.pk, Direction.remove.value
|
class_to_path(instance.__class__), instance.pk, Direction.remove.value
|
||||||
@ -58,7 +63,9 @@ def register_signals(
|
|||||||
"""Sync group membership"""
|
"""Sync group membership"""
|
||||||
if action not in ["post_add", "post_remove"]:
|
if action not in ["post_add", "post_remove"]:
|
||||||
return
|
return
|
||||||
if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
|
if not provider_type.objects.filter(
|
||||||
|
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
|
||||||
|
).exists():
|
||||||
return
|
return
|
||||||
# reverse: instance is a Group, pk_set is a list of user pks
|
# reverse: instance is a Group, pk_set is a list of user pks
|
||||||
# non-reverse: instance is a User, pk_set is a list of groups
|
# non-reverse: instance is a User, pk_set is a list of groups
|
||||||
|
@ -5,6 +5,7 @@ from celery.exceptions import Retry
|
|||||||
from celery.result import allow_join_result
|
from celery.result import allow_join_result
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Model, QuerySet
|
from django.db.models import Model, QuerySet
|
||||||
|
from django.db.models.query import Q
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from structlog.stdlib import BoundLogger, get_logger
|
from structlog.stdlib import BoundLogger, get_logger
|
||||||
@ -37,7 +38,9 @@ class SyncTasks:
|
|||||||
self._provider_model = provider_model
|
self._provider_model = provider_model
|
||||||
|
|
||||||
def sync_all(self, single_sync: Callable[[int], None]):
|
def sync_all(self, single_sync: Callable[[int], None]):
|
||||||
for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
|
for provider in self._provider_model.objects.filter(
|
||||||
|
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
|
||||||
|
):
|
||||||
self.trigger_single_task(provider, single_sync)
|
self.trigger_single_task(provider, single_sync)
|
||||||
|
|
||||||
def trigger_single_task(self, provider: OutgoingSyncProvider, sync_task: Callable[[int], None]):
|
def trigger_single_task(self, provider: OutgoingSyncProvider, sync_task: Callable[[int], None]):
|
||||||
@ -62,7 +65,8 @@ class SyncTasks:
|
|||||||
provider_pk=provider_pk,
|
provider_pk=provider_pk,
|
||||||
)
|
)
|
||||||
provider = self._provider_model.objects.filter(
|
provider = self._provider_model.objects.filter(
|
||||||
pk=provider_pk, backchannel_application__isnull=False
|
Q(backchannel_application__isnull=False) | Q(application__isnull=False),
|
||||||
|
pk=provider_pk,
|
||||||
).first()
|
).first()
|
||||||
if not provider:
|
if not provider:
|
||||||
return
|
return
|
||||||
@ -204,7 +208,9 @@ class SyncTasks:
|
|||||||
if not instance:
|
if not instance:
|
||||||
return
|
return
|
||||||
operation = Direction(raw_op)
|
operation = Direction(raw_op)
|
||||||
for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
|
for provider in self._provider_model.objects.filter(
|
||||||
|
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
|
||||||
|
):
|
||||||
client = provider.client_for_model(instance.__class__)
|
client = provider.client_for_model(instance.__class__)
|
||||||
# Check if the object is allowed within the provider's restrictions
|
# Check if the object is allowed within the provider's restrictions
|
||||||
queryset = provider.get_object_qs(instance.__class__)
|
queryset = provider.get_object_qs(instance.__class__)
|
||||||
@ -223,6 +229,8 @@ class SyncTasks:
|
|||||||
client.delete(instance)
|
client.delete(instance)
|
||||||
except TransientSyncException as exc:
|
except TransientSyncException as exc:
|
||||||
raise Retry() from exc
|
raise Retry() from exc
|
||||||
|
except SkipObjectException:
|
||||||
|
continue
|
||||||
except StopSync as exc:
|
except StopSync as exc:
|
||||||
self.logger.warning(exc, provider_pk=provider.pk)
|
self.logger.warning(exc, provider_pk=provider.pk)
|
||||||
|
|
||||||
@ -233,7 +241,9 @@ class SyncTasks:
|
|||||||
group = Group.objects.filter(pk=group_pk).first()
|
group = Group.objects.filter(pk=group_pk).first()
|
||||||
if not group:
|
if not group:
|
||||||
return
|
return
|
||||||
for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
|
for provider in self._provider_model.objects.filter(
|
||||||
|
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
|
||||||
|
):
|
||||||
# Check if the object is allowed within the provider's restrictions
|
# Check if the object is allowed within the provider's restrictions
|
||||||
queryset: QuerySet = provider.get_object_qs(Group)
|
queryset: QuerySet = provider.get_object_qs(Group)
|
||||||
# The queryset we get from the provider must include the instance we've got given
|
# The queryset we get from the provider must include the instance we've got given
|
||||||
@ -251,5 +261,7 @@ class SyncTasks:
|
|||||||
client.update_group(group, operation, pk_set)
|
client.update_group(group, operation, pk_set)
|
||||||
except TransientSyncException as exc:
|
except TransientSyncException as exc:
|
||||||
raise Retry() from exc
|
raise Retry() from exc
|
||||||
|
except SkipObjectException:
|
||||||
|
continue
|
||||||
except StopSync as exc:
|
except StopSync as exc:
|
||||||
self.logger.warning(exc, provider_pk=provider.pk)
|
self.logger.warning(exc, provider_pk=provider.pk)
|
||||||
|
@ -20,6 +20,7 @@ from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSeri
|
|||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.enterprise.license import LicenseKey
|
from authentik.enterprise.license import LicenseKey
|
||||||
from authentik.enterprise.providers.rac.models import RACProvider
|
from authentik.enterprise.providers.rac.models import RACProvider
|
||||||
|
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
||||||
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
|
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
|
from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
|
||||||
from authentik.outposts.models import (
|
from authentik.outposts.models import (
|
||||||
@ -49,6 +50,10 @@ class OutpostSerializer(ModelSerializer):
|
|||||||
service_connection_obj = ServiceConnectionSerializer(
|
service_connection_obj = ServiceConnectionSerializer(
|
||||||
source="service_connection", read_only=True
|
source="service_connection", read_only=True
|
||||||
)
|
)
|
||||||
|
refresh_interval_s = SerializerMethodField()
|
||||||
|
|
||||||
|
def get_refresh_interval_s(self, obj: Outpost) -> int:
|
||||||
|
return int(timedelta_from_string(obj.config.refresh_interval).total_seconds())
|
||||||
|
|
||||||
def validate_name(self, name: str) -> str:
|
def validate_name(self, name: str) -> str:
|
||||||
"""Validate name (especially for embedded outpost)"""
|
"""Validate name (especially for embedded outpost)"""
|
||||||
@ -84,7 +89,8 @@ class OutpostSerializer(ModelSerializer):
|
|||||||
def validate_config(self, config) -> dict:
|
def validate_config(self, config) -> dict:
|
||||||
"""Check that the config has all required fields"""
|
"""Check that the config has all required fields"""
|
||||||
try:
|
try:
|
||||||
from_dict(OutpostConfig, config)
|
parsed = from_dict(OutpostConfig, config)
|
||||||
|
timedelta_string_validator(parsed.refresh_interval)
|
||||||
except DaciteError as exc:
|
except DaciteError as exc:
|
||||||
raise ValidationError(f"Failed to validate config: {str(exc)}") from exc
|
raise ValidationError(f"Failed to validate config: {str(exc)}") from exc
|
||||||
return config
|
return config
|
||||||
@ -99,6 +105,7 @@ class OutpostSerializer(ModelSerializer):
|
|||||||
"providers_obj",
|
"providers_obj",
|
||||||
"service_connection",
|
"service_connection",
|
||||||
"service_connection_obj",
|
"service_connection_obj",
|
||||||
|
"refresh_interval_s",
|
||||||
"token_identifier",
|
"token_identifier",
|
||||||
"config",
|
"config",
|
||||||
"managed",
|
"managed",
|
||||||
|
@ -13,16 +13,17 @@ import authentik.outposts.models
|
|||||||
|
|
||||||
|
|
||||||
def fix_missing_token_identifier(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def fix_missing_token_identifier(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
User = apps.get_model("authentik_core", "User")
|
User = apps.get_model("authentik_core", "User")
|
||||||
Token = apps.get_model("authentik_core", "Token")
|
Token = apps.get_model("authentik_core", "Token")
|
||||||
from authentik.outposts.models import Outpost
|
from authentik.outposts.models import Outpost
|
||||||
|
|
||||||
for outpost in Outpost.objects.using(schema_editor.connection.alias).all().only("pk"):
|
for outpost in Outpost.objects.using(db_alias).all().only("pk"):
|
||||||
user_identifier = outpost.user_identifier
|
user_identifier = outpost.user_identifier
|
||||||
users = User.objects.filter(username=user_identifier)
|
users = User.objects.using(db_alias).filter(username=user_identifier)
|
||||||
if not users.exists():
|
if not users.exists():
|
||||||
continue
|
continue
|
||||||
tokens = Token.objects.filter(user=users.first())
|
tokens = Token.objects.using(db_alias).filter(user=users.first())
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if token.identifier != outpost.token_identifier:
|
if token.identifier != outpost.token_identifier:
|
||||||
token.identifier = outpost.token_identifier
|
token.identifier = outpost.token_identifier
|
||||||
@ -37,8 +38,8 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE
|
|||||||
"authentik_outposts", "KubernetesServiceConnection"
|
"authentik_outposts", "KubernetesServiceConnection"
|
||||||
)
|
)
|
||||||
|
|
||||||
docker = DockerServiceConnection.objects.filter(local=True).first()
|
docker = DockerServiceConnection.objects.using(db_alias).filter(local=True).first()
|
||||||
k8s = KubernetesServiceConnection.objects.filter(local=True).first()
|
k8s = KubernetesServiceConnection.objects.using(db_alias).filter(local=True).first()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for outpost in Outpost.objects.using(db_alias).all().exclude(deployment_type="custom"):
|
for outpost in Outpost.objects.using(db_alias).all().exclude(deployment_type="custom"):
|
||||||
@ -54,21 +55,21 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE
|
|||||||
|
|
||||||
|
|
||||||
def remove_pb_prefix_users(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def remove_pb_prefix_users(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
User = apps.get_model("authentik_core", "User")
|
User = apps.get_model("authentik_core", "User")
|
||||||
Outpost = apps.get_model("authentik_outposts", "Outpost")
|
Outpost = apps.get_model("authentik_outposts", "Outpost")
|
||||||
|
|
||||||
for outpost in Outpost.objects.using(alias).all():
|
for outpost in Outpost.objects.using(db_alias).all():
|
||||||
matching = User.objects.using(alias).filter(username=f"pb-outpost-{outpost.uuid.hex}")
|
matching = User.objects.using(db_alias).filter(username=f"pb-outpost-{outpost.uuid.hex}")
|
||||||
if matching.exists():
|
if matching.exists():
|
||||||
matching.delete()
|
matching.delete()
|
||||||
|
|
||||||
|
|
||||||
def update_config_prefix(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def update_config_prefix(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
Outpost = apps.get_model("authentik_outposts", "Outpost")
|
Outpost = apps.get_model("authentik_outposts", "Outpost")
|
||||||
|
|
||||||
for outpost in Outpost.objects.using(alias).all():
|
for outpost in Outpost.objects.using(db_alias).all():
|
||||||
config = outpost._config
|
config = outpost._config
|
||||||
for key in list(config):
|
for key in list(config):
|
||||||
if "passbook" in key:
|
if "passbook" in key:
|
||||||
|
@ -61,6 +61,7 @@ class OutpostConfig:
|
|||||||
|
|
||||||
log_level: str = CONFIG.get("log_level")
|
log_level: str = CONFIG.get("log_level")
|
||||||
object_naming_template: str = field(default="ak-outpost-%(name)s")
|
object_naming_template: str = field(default="ak-outpost-%(name)s")
|
||||||
|
refresh_interval: str = "minutes=5"
|
||||||
|
|
||||||
container_image: str | None = field(default=None)
|
container_image: str | None = field(default=None)
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
|
||||||
from channels.exceptions import DenyConnection
|
|
||||||
from channels.routing import URLRouter
|
from channels.routing import URLRouter
|
||||||
from channels.testing import WebsocketCommunicator
|
from channels.testing import WebsocketCommunicator
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
@ -37,9 +36,8 @@ class TestOutpostWS(TransactionTestCase):
|
|||||||
communicator = WebsocketCommunicator(
|
communicator = WebsocketCommunicator(
|
||||||
URLRouter(websocket.websocket_urlpatterns), f"/ws/outpost/{self.outpost.pk}/"
|
URLRouter(websocket.websocket_urlpatterns), f"/ws/outpost/{self.outpost.pk}/"
|
||||||
)
|
)
|
||||||
with self.assertRaises(DenyConnection):
|
connected, _ = await communicator.connect()
|
||||||
connected, _ = await communicator.connect()
|
self.assertFalse(connected)
|
||||||
self.assertFalse(connected)
|
|
||||||
|
|
||||||
async def test_auth_valid(self):
|
async def test_auth_valid(self):
|
||||||
"""Test auth with token"""
|
"""Test auth with token"""
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.core.channels import TokenOutpostMiddleware
|
|
||||||
from authentik.outposts.api.outposts import OutpostViewSet
|
from authentik.outposts.api.outposts import OutpostViewSet
|
||||||
from authentik.outposts.api.service_connections import (
|
from authentik.outposts.api.service_connections import (
|
||||||
DockerServiceConnectionViewSet,
|
DockerServiceConnectionViewSet,
|
||||||
KubernetesServiceConnectionViewSet,
|
KubernetesServiceConnectionViewSet,
|
||||||
ServiceConnectionViewSet,
|
ServiceConnectionViewSet,
|
||||||
)
|
)
|
||||||
|
from authentik.outposts.channels import TokenOutpostMiddleware
|
||||||
from authentik.outposts.consumer import OutpostConsumer
|
from authentik.outposts.consumer import OutpostConsumer
|
||||||
from authentik.root.middleware import ChannelsLoggingMiddleware
|
from authentik.root.middleware import ChannelsLoggingMiddleware
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""Reputation policy API Views"""
|
"""Reputation policy API Views"""
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_filters.filters import BaseInFilter, CharFilter
|
||||||
|
from django_filters.filterset import FilterSet
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||||
@ -11,6 +13,10 @@ from authentik.policies.api.policies import PolicySerializer
|
|||||||
from authentik.policies.reputation.models import Reputation, ReputationPolicy
|
from authentik.policies.reputation.models import Reputation, ReputationPolicy
|
||||||
|
|
||||||
|
|
||||||
|
class CharInFilter(BaseInFilter, CharFilter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ReputationPolicySerializer(PolicySerializer):
|
class ReputationPolicySerializer(PolicySerializer):
|
||||||
"""Reputation Policy Serializer"""
|
"""Reputation Policy Serializer"""
|
||||||
|
|
||||||
@ -38,6 +44,16 @@ class ReputationPolicyViewSet(UsedByMixin, ModelViewSet):
|
|||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
class ReputationFilter(FilterSet):
|
||||||
|
"""Filter for reputation"""
|
||||||
|
|
||||||
|
identifier_in = CharInFilter(field_name="identifier", lookup_expr="in")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reputation
|
||||||
|
fields = ["identifier", "ip", "score"]
|
||||||
|
|
||||||
|
|
||||||
class ReputationSerializer(ModelSerializer):
|
class ReputationSerializer(ModelSerializer):
|
||||||
"""Reputation Serializer"""
|
"""Reputation Serializer"""
|
||||||
|
|
||||||
@ -66,5 +82,5 @@ class ReputationViewSet(
|
|||||||
queryset = Reputation.objects.all()
|
queryset = Reputation.objects.all()
|
||||||
serializer_class = ReputationSerializer
|
serializer_class = ReputationSerializer
|
||||||
search_fields = ["identifier", "ip", "score"]
|
search_fields = ["identifier", "ip", "score"]
|
||||||
filterset_fields = ["identifier", "ip", "score"]
|
filterset_class = ReputationFilter
|
||||||
ordering = ["ip"]
|
ordering = ["ip"]
|
||||||
|
@ -268,7 +268,7 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
|
|||||||
except ValueError as exc: # pragma: no cover
|
except ValueError as exc: # pragma: no cover
|
||||||
LOGGER.warning(str(exc))
|
LOGGER.warning(str(exc))
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("Failed to import Metadata: {messages}".format_map({"message": str(exc)})),
|
_("Failed to import Metadata: {messages}".format_map({"messages": str(exc)})),
|
||||||
) from None
|
) from None
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
|
@ -89,6 +89,6 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
|
|||||||
return ServiceProviderConfiguration.model_validate(
|
return ServiceProviderConfiguration.model_validate(
|
||||||
self._request("GET", "/ServiceProviderConfig")
|
self._request("GET", "/ServiceProviderConfig")
|
||||||
)
|
)
|
||||||
except (ValidationError, SCIMRequestException) as exc:
|
except (ValidationError, SCIMRequestException, NotFoundSyncException) as exc:
|
||||||
self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
|
self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
|
||||||
return default_config
|
return default_config
|
||||||
|
@ -274,9 +274,13 @@ class ChannelsLoggingMiddleware:
|
|||||||
self.log(scope)
|
self.log(scope)
|
||||||
try:
|
try:
|
||||||
return await self.inner(scope, receive, send)
|
return await self.inner(scope, receive, send)
|
||||||
|
except DenyConnection:
|
||||||
|
return await send({"type": "websocket.close"})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
if settings.DEBUG:
|
||||||
|
raise exc
|
||||||
LOGGER.warning("Exception in ASGI application", exc=exc)
|
LOGGER.warning("Exception in ASGI application", exc=exc)
|
||||||
raise DenyConnection() from None
|
return await send({"type": "websocket.close"})
|
||||||
|
|
||||||
def log(self, scope: dict, **kwargs):
|
def log(self, scope: dict, **kwargs):
|
||||||
"""Log request"""
|
"""Log request"""
|
||||||
|
@ -31,9 +31,9 @@ def set_default_group_mappings(apps: Apps, schema_editor):
|
|||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
for source in LDAPSource.objects.using(db_alias).all():
|
for source in LDAPSource.objects.using(db_alias).all():
|
||||||
if source.property_mappings_group.exists():
|
if source.property_mappings_group.using(db_alias).exists():
|
||||||
continue
|
continue
|
||||||
source.property_mappings_group.set(
|
source.property_mappings_group.using(db_alias).set(
|
||||||
LDAPPropertyMapping.objects.using(db_alias).filter(
|
LDAPPropertyMapping.objects.using(db_alias).filter(
|
||||||
managed="goauthentik.io/sources/ldap/default-name"
|
managed="goauthentik.io/sources/ldap/default-name"
|
||||||
)
|
)
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from facebook import GraphAPI
|
|
||||||
|
|
||||||
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
|
|
||||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||||
@ -19,19 +16,9 @@ class FacebookOAuthRedirect(OAuthRedirect):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FacebookOAuth2Client(OAuth2Client):
|
|
||||||
"""Facebook OAuth2 Client"""
|
|
||||||
|
|
||||||
def get_profile_info(self, token: dict[str, str]) -> dict[str, Any] | None:
|
|
||||||
api = GraphAPI(access_token=token["access_token"])
|
|
||||||
return api.get_object("me", fields="id,name,email")
|
|
||||||
|
|
||||||
|
|
||||||
class FacebookOAuth2Callback(OAuthCallback):
|
class FacebookOAuth2Callback(OAuthCallback):
|
||||||
"""Facebook OAuth2 Callback"""
|
"""Facebook OAuth2 Callback"""
|
||||||
|
|
||||||
client_class = FacebookOAuth2Client
|
|
||||||
|
|
||||||
def get_user_enroll_context(
|
def get_user_enroll_context(
|
||||||
self,
|
self,
|
||||||
info: dict[str, Any],
|
info: dict[str, Any],
|
||||||
|
@ -10,6 +10,8 @@ from authentik.sources.saml.processors import constants
|
|||||||
|
|
||||||
|
|
||||||
def update_algorithms(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def update_algorithms(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
SAMLSource = apps.get_model("authentik_sources_saml", "SAMLSource")
|
SAMLSource = apps.get_model("authentik_sources_saml", "SAMLSource")
|
||||||
signature_translation_map = {
|
signature_translation_map = {
|
||||||
"rsa-sha1": constants.RSA_SHA1,
|
"rsa-sha1": constants.RSA_SHA1,
|
||||||
@ -22,7 +24,7 @@ def update_algorithms(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
"sha256": constants.SHA256,
|
"sha256": constants.SHA256,
|
||||||
}
|
}
|
||||||
|
|
||||||
for source in SAMLSource.objects.all():
|
for source in SAMLSource.objects.using(db_alias).all():
|
||||||
source.signature_algorithm = signature_translation_map.get(
|
source.signature_algorithm = signature_translation_map.get(
|
||||||
source.signature_algorithm, constants.RSA_SHA256
|
source.signature_algorithm, constants.RSA_SHA256
|
||||||
)
|
)
|
||||||
|
@ -10,6 +10,7 @@ from django.core.cache import cache
|
|||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from lxml import etree # nosec
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
@ -240,7 +241,7 @@ class ResponseProcessor:
|
|||||||
name_id.text,
|
name_id.text,
|
||||||
delete_none_values(self.get_attributes()),
|
delete_none_values(self.get_attributes()),
|
||||||
)
|
)
|
||||||
flow_manager.policy_context["saml_response"] = self._root
|
flow_manager.policy_context["saml_response"] = etree.tostring(self._root)
|
||||||
return flow_manager
|
return flow_manager
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
"""SCIM Source"""
|
"""SCIM Source"""
|
||||||
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -19,8 +17,6 @@ class SCIMSource(Source):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def service_account_identifier(self) -> str:
|
def service_account_identifier(self) -> str:
|
||||||
if not self.pk:
|
|
||||||
self.pk = uuid4()
|
|
||||||
return f"ak-source-scim-{self.pk}"
|
return f"ak-source-scim-{self.pk}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1,41 +1,44 @@
|
|||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import pre_delete, pre_save
|
from django.db.models.signals import post_delete, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from authentik.core.models import USER_PATH_SYSTEM_PREFIX, Token, TokenIntents, User, UserTypes
|
from authentik.core.models import USER_PATH_SYSTEM_PREFIX, Token, TokenIntents, User, UserTypes
|
||||||
|
from authentik.events.middleware import audit_ignore
|
||||||
from authentik.sources.scim.models import SCIMSource
|
from authentik.sources.scim.models import SCIMSource
|
||||||
|
|
||||||
USER_PATH_SOURCE_SCIM = USER_PATH_SYSTEM_PREFIX + "/sources/scim"
|
USER_PATH_SOURCE_SCIM = USER_PATH_SYSTEM_PREFIX + "/sources/scim"
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=SCIMSource)
|
@receiver(post_save, sender=SCIMSource)
|
||||||
def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_):
|
def scim_source_post_save(sender: type[Model], instance: SCIMSource, created: bool, **_):
|
||||||
"""Create service account before source is saved"""
|
"""Create service account before source is saved"""
|
||||||
# .service_account_identifier will auto-assign a primary key uuid to the source
|
|
||||||
# if none is set yet, just so we can get the identifier before we save
|
|
||||||
identifier = instance.service_account_identifier
|
identifier = instance.service_account_identifier
|
||||||
user = User.objects.create(
|
user, _ = User.objects.update_or_create(
|
||||||
username=identifier,
|
username=identifier,
|
||||||
name=f"SCIM Source {instance.name} Service-Account",
|
defaults={
|
||||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
"name": f"SCIM Source {instance.name} Service-Account",
|
||||||
path=USER_PATH_SOURCE_SCIM,
|
"type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
||||||
|
"path": USER_PATH_SOURCE_SCIM,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
token = Token.objects.create(
|
token, token_created = Token.objects.update_or_create(
|
||||||
user=user,
|
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
intent=TokenIntents.INTENT_API,
|
defaults={
|
||||||
expiring=False,
|
"user": user,
|
||||||
managed=f"goauthentik.io/sources/scim/{instance.pk}",
|
"intent": TokenIntents.INTENT_API,
|
||||||
|
"expiring": False,
|
||||||
|
"managed": f"goauthentik.io/sources/scim/{instance.pk}",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
instance.token = token
|
if created or token_created:
|
||||||
|
with audit_ignore():
|
||||||
|
instance.token = token
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=SCIMSource)
|
@receiver(post_delete, sender=SCIMSource)
|
||||||
def scim_source_pre_delete(sender: type[Model], instance: SCIMSource, **_):
|
def scim_source_post_delete(sender: type[Model], instance: SCIMSource, **_):
|
||||||
"""Delete SCIM Source service account before deleting source"""
|
"""Delete SCIM Source service account after deleting source"""
|
||||||
Token.objects.filter(
|
|
||||||
identifier=instance.service_account_identifier, intent=TokenIntents.INTENT_API
|
|
||||||
).delete()
|
|
||||||
User.objects.filter(
|
User.objects.filter(
|
||||||
username=instance.service_account_identifier, type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
username=instance.service_account_identifier, type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||||
).delete()
|
).delete()
|
||||||
|
@ -13,7 +13,7 @@ def migrate_configuration_stage(apps: Apps, schema_editor: BaseDatabaseSchemaEdi
|
|||||||
|
|
||||||
for stage in AuthenticatorValidateStage.objects.using(db_alias).all():
|
for stage in AuthenticatorValidateStage.objects.using(db_alias).all():
|
||||||
if stage.configuration_stage:
|
if stage.configuration_stage:
|
||||||
stage.configuration_stages.set([stage.configuration_stage])
|
stage.configuration_stages.using(db_alias).set([stage.configuration_stage])
|
||||||
stage.save()
|
stage.save()
|
||||||
|
|
||||||
|
|
||||||
|
@ -325,7 +325,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
serializer = SelectableStageSerializer(
|
serializer = SelectableStageSerializer(
|
||||||
data={
|
data={
|
||||||
"pk": stage.pk,
|
"pk": stage.pk,
|
||||||
"name": stage.friendly_name or stage.name,
|
"name": getattr(stage, "friendly_name", stage.name),
|
||||||
"verbose_name": str(stage._meta.verbose_name)
|
"verbose_name": str(stage._meta.verbose_name)
|
||||||
.replace("Setup Stage", "")
|
.replace("Setup Stage", "")
|
||||||
.strip(),
|
.strip(),
|
||||||
|
@ -8,6 +8,7 @@ from django.urls import reverse
|
|||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from authentik.brands.utils import get_brand_for_request
|
from authentik.brands.utils import get_brand_for_request
|
||||||
|
from authentik.core.middleware import RESPONSE_HEADER_ID
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
@ -186,6 +187,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": f"/api/v3/flows/executor/{flow.slug}/",
|
"path": f"/api/v3/flows/executor/{flow.slug}/",
|
||||||
"user_agent": "",
|
"user_agent": "",
|
||||||
|
"request_id": response[RESPONSE_HEADER_ID],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -120,7 +120,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
Powered by <a href="https://goauthentik.io?utm_source=authentik&utm_medium=email">authentik</a>.
|
Powered by <a rel="noopener noreferrer" target="_blank" href="https://goauthentik.io?utm_source=authentik&utm_medium=email">authentik</a>.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -13,9 +13,9 @@ def assign_sources(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
IdentificationStage = apps.get_model("authentik_stages_identification", "identificationstage")
|
IdentificationStage = apps.get_model("authentik_stages_identification", "identificationstage")
|
||||||
Source = apps.get_model("authentik_core", "source")
|
Source = apps.get_model("authentik_core", "source")
|
||||||
|
|
||||||
sources = Source.objects.all()
|
sources = Source.objects.using(db_alias).all()
|
||||||
for stage in IdentificationStage.objects.all().using(db_alias):
|
for stage in IdentificationStage.objects.using(db_alias).all():
|
||||||
stage.sources.set(sources)
|
stage.sources.using(db_alias).set(sources)
|
||||||
stage.save()
|
stage.save()
|
||||||
|
|
||||||
|
|
||||||
@ -144,7 +144,7 @@ class Migration(migrations.Migration):
|
|||||||
default=None,
|
default=None,
|
||||||
help_text=(
|
help_text=(
|
||||||
"When set, shows a password field, instead of showing the password field as"
|
"When set, shows a password field, instead of showing the password field as"
|
||||||
" seaprate step."
|
" separate step."
|
||||||
),
|
),
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
@ -108,7 +108,7 @@ class PromptViewSet(UsedByMixin, ModelViewSet):
|
|||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"non_field_errors": [
|
"non_field_errors": [
|
||||||
exception_to_string(exc),
|
exception_to_string(exc.exc),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
status=400,
|
status=400,
|
||||||
|
@ -12,7 +12,7 @@ def set_generated_name(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
|
|
||||||
for prompt in Prompt.objects.using(db_alias).all():
|
for prompt in Prompt.objects.using(db_alias).all():
|
||||||
name = prompt.field_key
|
name = prompt.field_key
|
||||||
stage = prompt.promptstage_set.order_by("name").first()
|
stage = prompt.promptstage_set.using(db_alias).order_by("name").first()
|
||||||
if stage:
|
if stage:
|
||||||
name += "_" + stage.name
|
name += "_" + stage.name
|
||||||
else:
|
else:
|
||||||
|
@ -170,7 +170,7 @@ class Prompt(SerializerModel):
|
|||||||
try:
|
try:
|
||||||
raw_choices = evaluator.evaluate(self.placeholder)
|
raw_choices = evaluator.evaluate(self.placeholder)
|
||||||
except Exception as exc: # pylint:disable=broad-except
|
except Exception as exc: # pylint:disable=broad-except
|
||||||
wrapped = PropertyMappingExpressionException(str(exc))
|
wrapped = PropertyMappingExpressionException(exc, None)
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"failed to evaluate prompt choices",
|
"failed to evaluate prompt choices",
|
||||||
exc=wrapped,
|
exc=wrapped,
|
||||||
@ -208,7 +208,7 @@ class Prompt(SerializerModel):
|
|||||||
try:
|
try:
|
||||||
return evaluator.evaluate(self.placeholder)
|
return evaluator.evaluate(self.placeholder)
|
||||||
except Exception as exc: # pylint:disable=broad-except
|
except Exception as exc: # pylint:disable=broad-except
|
||||||
wrapped = PropertyMappingExpressionException(str(exc), None)
|
wrapped = PropertyMappingExpressionException(exc, None)
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"failed to evaluate prompt placeholder",
|
"failed to evaluate prompt placeholder",
|
||||||
exc=wrapped,
|
exc=wrapped,
|
||||||
@ -237,7 +237,7 @@ class Prompt(SerializerModel):
|
|||||||
try:
|
try:
|
||||||
value = evaluator.evaluate(self.initial_value)
|
value = evaluator.evaluate(self.initial_value)
|
||||||
except Exception as exc: # pylint:disable=broad-except
|
except Exception as exc: # pylint:disable=broad-except
|
||||||
wrapped = PropertyMappingExpressionException(str(exc))
|
wrapped = PropertyMappingExpressionException(exc, None)
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"failed to evaluate prompt initial value",
|
"failed to evaluate prompt initial value",
|
||||||
exc=wrapped,
|
exc=wrapped,
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
"""Sessions bound to ASN/Network and GeoIP/Continent/etc"""
|
"""Sessions bound to ASN/Network and GeoIP/Continent/etc"""
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.middleware import AuthenticationMiddleware
|
from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||||
from django.contrib.auth.signals import user_logged_out
|
from django.contrib.auth.signals import user_logged_out
|
||||||
|
from django.contrib.auth.views import redirect_to_login
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from django.shortcuts import redirect
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import AuthenticatedSession
|
from authentik.core.models import AuthenticatedSession
|
||||||
@ -87,7 +86,7 @@ class BoundSessionMiddleware(SessionMiddleware):
|
|||||||
AuthenticationMiddleware(lambda request: request).process_request(request)
|
AuthenticationMiddleware(lambda request: request).process_request(request)
|
||||||
logout_extra(request, exc)
|
logout_extra(request, exc)
|
||||||
request.session.clear()
|
request.session.clear()
|
||||||
return redirect(settings.LOGIN_URL)
|
return redirect_to_login(request.get_full_path())
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def recheck_session(self, request: HttpRequest):
|
def recheck_session(self, request: HttpRequest):
|
||||||
|
@ -6,6 +6,7 @@ from django.contrib.auth import update_session_auth_hash
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.utils import IntegrityError, InternalError
|
from django.db.utils import IntegrityError, InternalError
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
@ -118,6 +119,14 @@ class UserWriteStageView(StageView):
|
|||||||
UserWriteStageView.write_attribute(user, key, value)
|
UserWriteStageView.write_attribute(user, key, value)
|
||||||
# User has this key already
|
# User has this key already
|
||||||
elif hasattr(user, key):
|
elif hasattr(user, key):
|
||||||
|
if isinstance(user, SimpleLazyObject):
|
||||||
|
user._setup()
|
||||||
|
user = user._wrapped
|
||||||
|
attr = getattr(type(user), key)
|
||||||
|
if isinstance(attr, property):
|
||||||
|
if not attr.fset:
|
||||||
|
self.logger.info("discarding key", key=key)
|
||||||
|
continue
|
||||||
setattr(user, key, value)
|
setattr(user, key, value)
|
||||||
# If none of the cases above matched, we have an attribute that the user doesn't have,
|
# If none of the cases above matched, we have an attribute that the user doesn't have,
|
||||||
# has no setter for, is not a nested attributes value and as such is invalid
|
# has no setter for, is not a nested attributes value and as such is invalid
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"$schema": "http://json-schema.org/draft-07/schema",
|
"$schema": "http://json-schema.org/draft-07/schema",
|
||||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "authentik 2024.6.0 Blueprint schema",
|
"title": "authentik 2024.6.3 Blueprint schema",
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
"entries"
|
"entries"
|
||||||
|
@ -31,7 +31,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.3}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -52,7 +52,7 @@ services:
|
|||||||
- postgresql
|
- postgresql
|
||||||
- redis
|
- redis
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
|
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.3}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
2
go.mod
2
go.mod
@ -28,7 +28,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/wwt/guac v1.3.2
|
github.com/wwt/guac v1.3.2
|
||||||
goauthentik.io/api/v3 v3.2024042.11
|
goauthentik.io/api/v3 v3.2024060.5
|
||||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||||
golang.org/x/oauth2 v0.21.0
|
golang.org/x/oauth2 v0.21.0
|
||||||
golang.org/x/sync v0.7.0
|
golang.org/x/sync v0.7.0
|
||||||
|
4
go.sum
4
go.sum
@ -294,8 +294,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
|||||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||||
goauthentik.io/api/v3 v3.2024042.11 h1:cGgUz1E8rlMphGvv04VI7i+MgT8eidZbxTpza5zd96I=
|
goauthentik.io/api/v3 v3.2024060.5 h1:AjvPUZoObk7a86ZZaz2tmruteY+1vAEfVzIOzQpWSXM=
|
||||||
goauthentik.io/api/v3 v3.2024042.11/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
goauthentik.io/api/v3 v3.2024060.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2024.6.0"
|
const VERSION = "2024.6.3"
|
||||||
|
@ -183,7 +183,19 @@ func (ac *APIController) startWSHealth() {
|
|||||||
|
|
||||||
func (ac *APIController) startIntervalUpdater() {
|
func (ac *APIController) startIntervalUpdater() {
|
||||||
logger := ac.logger.WithField("loop", "interval-updater")
|
logger := ac.logger.WithField("loop", "interval-updater")
|
||||||
ticker := time.NewTicker(5 * time.Minute)
|
getInterval := func() time.Duration {
|
||||||
|
// Ensure timer interval is not negative or 0
|
||||||
|
// for 0 we assume migration or unconfigured, so default to 5 minutes
|
||||||
|
if ac.Outpost.RefreshIntervalS <= 0 {
|
||||||
|
return 5 * time.Minute
|
||||||
|
}
|
||||||
|
// Clamp interval to be at least 30 seconds
|
||||||
|
if ac.Outpost.RefreshIntervalS < 30 {
|
||||||
|
return 30 * time.Second
|
||||||
|
}
|
||||||
|
return time.Duration(ac.Outpost.RefreshIntervalS) * time.Second
|
||||||
|
}
|
||||||
|
ticker := time.NewTicker(getInterval())
|
||||||
for ; true; <-ticker.C {
|
for ; true; <-ticker.C {
|
||||||
logger.Debug("Running interval update")
|
logger.Debug("Running interval update")
|
||||||
err := ac.OnRefresh()
|
err := ac.OnRefresh()
|
||||||
@ -198,6 +210,7 @@ func (ac *APIController) startIntervalUpdater() {
|
|||||||
"build": constants.BUILD("tagged"),
|
"build": constants.BUILD("tagged"),
|
||||||
}).SetToCurrentTime()
|
}).SetToCurrentTime()
|
||||||
}
|
}
|
||||||
|
ticker.Reset(getInterval())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,9 +48,9 @@
|
|||||||
<footer class="pf-c-login__footer">
|
<footer class="pf-c-login__footer">
|
||||||
<ul class="pf-c-list pf-m-inline">
|
<ul class="pf-c-list pf-m-inline">
|
||||||
<li>
|
<li>
|
||||||
<a href="https://goauthentik.io?utm_source=authentik_outpost&utm_campaign=proxy_error">
|
<span>
|
||||||
Powered by authentik
|
Powered by authentik
|
||||||
</a>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from lifecycle.migrate import BaseMigration
|
from lifecycle.migrate import BaseMigration
|
||||||
|
|
||||||
MEDIA_ROOT = Path(__file__).parent.parent.parent / "media"
|
MEDIA_ROOT = Path(__file__).parent.parent.parent / "media"
|
||||||
@ -9,7 +10,9 @@ TENANT_MEDIA_ROOT = MEDIA_ROOT / "public"
|
|||||||
|
|
||||||
class Migration(BaseMigration):
|
class Migration(BaseMigration):
|
||||||
def needs_migration(self) -> bool:
|
def needs_migration(self) -> bool:
|
||||||
return not TENANT_MEDIA_ROOT.exists()
|
return (
|
||||||
|
not TENANT_MEDIA_ROOT.exists() and CONFIG.get("storage.media.backend", "file") != "s3"
|
||||||
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
TENANT_MEDIA_ROOT.mkdir(parents=True)
|
TENANT_MEDIA_ROOT.mkdir(parents=True)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/authentik",
|
"name": "@goauthentik/authentik",
|
||||||
"version": "2024.6.0",
|
"version": "2024.6.3",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
29
poetry.lock
generated
29
poetry.lock
generated
@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohttp"
|
name = "aiohttp"
|
||||||
@ -1513,20 +1513,6 @@ files = [
|
|||||||
dnspython = ">=2.0.0"
|
dnspython = ">=2.0.0"
|
||||||
idna = ">=2.0.0"
|
idna = ">=2.0.0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "facebook-sdk"
|
|
||||||
version = "3.1.0"
|
|
||||||
description = "This client library is designed to support the Facebook Graph API and the official Facebook JavaScript SDK, which is the canonical way to implement Facebook authentication."
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "facebook-sdk-3.1.0.tar.gz", hash = "sha256:cabcd2e69ea3d9f042919c99b353df7aa1e2be86d040121f6e9f5e63c1cf0f8d"},
|
|
||||||
{file = "facebook_sdk-3.1.0-py2.py3-none-any.whl", hash = "sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
requests = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fido2"
|
name = "fido2"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@ -2954,9 +2940,14 @@ version = "0.0.14"
|
|||||||
description = "Python module for oci specifications"
|
description = "Python module for oci specifications"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = []
|
||||||
{file = "opencontainers-0.0.14.tar.gz", hash = "sha256:fde3b8099b56b5c956415df8933e2227e1914e805a277b844f2f9e52341738f2"},
|
develop = false
|
||||||
]
|
|
||||||
|
[package.source]
|
||||||
|
type = "git"
|
||||||
|
url = "https://github.com/vsoch/oci-python"
|
||||||
|
reference = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf"
|
||||||
|
resolved_reference = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opentelemetry-api"
|
name = "opentelemetry-api"
|
||||||
@ -5350,4 +5341,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "~3.12"
|
python-versions = "~3.12"
|
||||||
content-hash = "f960013b56683ab42d82f8b49b2822dffc76046e3d22695ebb737b405a98dbaf"
|
content-hash = "055376879ff784080ab95c02eaa012fb1dad1213b1faa0dd1d61b0b812859b6d"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "authentik"
|
name = "authentik"
|
||||||
version = "2024.6.0"
|
version = "2024.6.3"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||||
|
|
||||||
@ -110,7 +110,6 @@ docker = "*"
|
|||||||
drf-spectacular = "*"
|
drf-spectacular = "*"
|
||||||
dumb-init = "*"
|
dumb-init = "*"
|
||||||
duo-client = "*"
|
duo-client = "*"
|
||||||
facebook-sdk = "*"
|
|
||||||
fido2 = "*"
|
fido2 = "*"
|
||||||
flower = "*"
|
flower = "*"
|
||||||
geoip2 = "*"
|
geoip2 = "*"
|
||||||
@ -121,7 +120,7 @@ kubernetes = "*"
|
|||||||
ldap3 = "*"
|
ldap3 = "*"
|
||||||
lxml = "*"
|
lxml = "*"
|
||||||
msgraph-sdk = "*"
|
msgraph-sdk = "*"
|
||||||
opencontainers = { extras = ["reggie"], version = "*" }
|
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = ["reggie"] }
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
paramiko = "*"
|
paramiko = "*"
|
||||||
psycopg = { extras = ["c"], version = "*" }
|
psycopg = { extras = ["c"], version = "*" }
|
||||||
|
16
schema.yml
16
schema.yml
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: authentik
|
title: authentik
|
||||||
version: 2024.6.0
|
version: 2024.6.3
|
||||||
description: Making authentication simple.
|
description: Making authentication simple.
|
||||||
contact:
|
contact:
|
||||||
email: hello@goauthentik.io
|
email: hello@goauthentik.io
|
||||||
@ -13080,6 +13080,15 @@ paths:
|
|||||||
name: identifier
|
name: identifier
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: identifier_in
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Multiple values may be separated by commas.
|
||||||
|
explode: false
|
||||||
|
style: form
|
||||||
- in: query
|
- in: query
|
||||||
name: ip
|
name: ip
|
||||||
schema:
|
schema:
|
||||||
@ -36625,6 +36634,7 @@ components:
|
|||||||
href:
|
href:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
nullable: true
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
@ -39488,6 +39498,9 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/ServiceConnection'
|
- $ref: '#/components/schemas/ServiceConnection'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
refresh_interval_s:
|
||||||
|
type: integer
|
||||||
|
readOnly: true
|
||||||
token_identifier:
|
token_identifier:
|
||||||
type: string
|
type: string
|
||||||
description: Get Token identifier
|
description: Get Token identifier
|
||||||
@ -39509,6 +39522,7 @@ components:
|
|||||||
- pk
|
- pk
|
||||||
- providers
|
- providers
|
||||||
- providers_obj
|
- providers_obj
|
||||||
|
- refresh_interval_s
|
||||||
- service_connection_obj
|
- service_connection_obj
|
||||||
- token_identifier
|
- token_identifier
|
||||||
- type
|
- type
|
||||||
|
23
tests/e2e/test-saml-idp/saml20-sp-remote.php
Normal file
23
tests/e2e/test-saml-idp/saml20-sp-remote.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* SAML 2.0 remote SP metadata for SimpleSAMLphp.
|
||||||
|
*
|
||||||
|
* See: https://simplesamlphp.org/docs/stable/simplesamlphp-reference-sp-remote
|
||||||
|
*/
|
||||||
|
|
||||||
|
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array(
|
||||||
|
'AssertionConsumerService' => getenv('SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE'),
|
||||||
|
'SingleLogoutService' => getenv('SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (null != getenv('SIMPLESAMLPHP_SP_NAME_ID_FORMAT')) {
|
||||||
|
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array_merge($metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')], array('NameIDFormat' => getenv('SIMPLESAMLPHP_SP_NAME_ID_FORMAT')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null != getenv('SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE')) {
|
||||||
|
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array_merge($metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')], array('simplesaml.nameidattribute' => getenv('SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null != getenv('SIMPLESAMLPHP_SP_SIGN_ASSERTION')) {
|
||||||
|
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array_merge($metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')], array('saml20.sign.assertion' => ('true' == getenv('SIMPLESAMLPHP_SP_SIGN_ASSERTION'))));
|
||||||
|
}
|
@ -5,7 +5,6 @@ from time import sleep
|
|||||||
|
|
||||||
from docker.client import DockerClient, from_env
|
from docker.client import DockerClient, from_env
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
from guardian.shortcuts import get_anonymous_user
|
|
||||||
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
||||||
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
|
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
|
||||||
|
|
||||||
@ -180,15 +179,13 @@ class TestProviderLDAP(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
with self.assertRaises(LDAPInvalidCredentialsResult):
|
with self.assertRaises(LDAPInvalidCredentialsResult):
|
||||||
_connection.bind()
|
_connection.bind()
|
||||||
anon = get_anonymous_user()
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
Event.objects.filter(
|
Event.objects.filter(
|
||||||
action=EventAction.LOGIN_FAILED,
|
action=EventAction.LOGIN_FAILED,
|
||||||
user={
|
user={
|
||||||
"pk": anon.pk,
|
"pk": self.user.pk,
|
||||||
"email": anon.email,
|
"email": self.user.email,
|
||||||
"username": anon.username,
|
"username": self.user.username,
|
||||||
"is_anonymous": True,
|
|
||||||
},
|
},
|
||||||
).exists(),
|
).exists(),
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""test OAuth Source"""
|
"""test OAuth Source"""
|
||||||
|
|
||||||
|
from json import loads
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -194,3 +195,41 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||||||
self.driver.get(self.if_user_url("/settings"))
|
self.driver.get(self.if_user_url("/settings"))
|
||||||
|
|
||||||
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
|
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
|
||||||
|
|
||||||
|
@retry()
|
||||||
|
@apply_blueprint(
|
||||||
|
"default/flow-default-authentication-flow.yaml",
|
||||||
|
"default/flow-default-invalidation-flow.yaml",
|
||||||
|
)
|
||||||
|
@apply_blueprint(
|
||||||
|
"default/flow-default-source-authentication.yaml",
|
||||||
|
"default/flow-default-source-enrollment.yaml",
|
||||||
|
"default/flow-default-source-pre-authentication.yaml",
|
||||||
|
)
|
||||||
|
def test_oauth_link(self):
|
||||||
|
"""test OAuth Source link OIDC"""
|
||||||
|
self.create_objects()
|
||||||
|
self.driver.get(self.live_server_url)
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
self.driver.get(
|
||||||
|
self.url("authentik_sources_oauth:oauth-client-login", source_slug=self.slug)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now we should be at the IDP, wait for the login field
|
||||||
|
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||||
|
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||||
|
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||||
|
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
|
# Wait until we're logged in
|
||||||
|
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]")))
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||||
|
|
||||||
|
self.driver.get(self.url("authentik_api:usersourceconnection-list") + "?format=json")
|
||||||
|
body_json = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||||
|
results = body_json["results"]
|
||||||
|
self.assertEqual(len(results), 1)
|
||||||
|
connection = results[0]
|
||||||
|
self.assertEqual(connection["source"]["slug"], self.slug)
|
||||||
|
self.assertEqual(connection["user"], self.user.pk)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""test SAML Source"""
|
"""test SAML Source"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -88,8 +89,20 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
interval=5 * 1_000 * 1_000_000,
|
interval=5 * 1_000 * 1_000_000,
|
||||||
start_period=1 * 1_000 * 1_000_000,
|
start_period=1 * 1_000 * 1_000_000,
|
||||||
),
|
),
|
||||||
|
"volumes": {
|
||||||
|
str(
|
||||||
|
(Path(__file__).parent / Path("test-saml-idp/saml20-sp-remote.php")).absolute()
|
||||||
|
): {
|
||||||
|
"bind": "/var/www/simplesamlphp/metadata/saml20-sp-remote.php",
|
||||||
|
"mode": "ro",
|
||||||
|
}
|
||||||
|
},
|
||||||
"environment": {
|
"environment": {
|
||||||
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
|
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
|
||||||
|
"SIMPLESAMLPHP_SP_NAME_ID_FORMAT": (
|
||||||
|
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
||||||
|
),
|
||||||
|
"SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE": "email",
|
||||||
"SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": (
|
"SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": (
|
||||||
self.url("authentik_sources_saml:acs", source_slug=self.slug)
|
self.url("authentik_sources_saml:acs", source_slug=self.slug)
|
||||||
),
|
),
|
||||||
@ -318,3 +331,109 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
.exclude(pk=self.user.pk)
|
.exclude(pk=self.user.pk)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
|
@apply_blueprint(
|
||||||
|
"default/flow-default-authentication-flow.yaml",
|
||||||
|
"default/flow-default-invalidation-flow.yaml",
|
||||||
|
)
|
||||||
|
@apply_blueprint(
|
||||||
|
"default/flow-default-source-authentication.yaml",
|
||||||
|
"default/flow-default-source-enrollment.yaml",
|
||||||
|
"default/flow-default-source-pre-authentication.yaml",
|
||||||
|
)
|
||||||
|
def test_idp_post_auto_enroll_auth(self):
|
||||||
|
"""test SAML Source With post binding (auto redirect)"""
|
||||||
|
# Bootstrap all needed objects
|
||||||
|
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||||
|
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||||
|
pre_authentication_flow = Flow.objects.get(slug="default-source-pre-authentication")
|
||||||
|
keypair = CertificateKeyPair.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
certificate_data=IDP_CERT,
|
||||||
|
key_data=IDP_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
|
source = SAMLSource.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
slug=self.slug,
|
||||||
|
authentication_flow=authentication_flow,
|
||||||
|
enrollment_flow=enrollment_flow,
|
||||||
|
pre_authentication_flow=pre_authentication_flow,
|
||||||
|
issuer="entity-id",
|
||||||
|
sso_url=f"http://{self.host}:8080/simplesaml/saml2/idp/SSOService.php",
|
||||||
|
binding_type=SAMLBindingTypes.POST_AUTO,
|
||||||
|
signing_kp=keypair,
|
||||||
|
)
|
||||||
|
ident_stage = IdentificationStage.objects.first()
|
||||||
|
ident_stage.sources.set([source])
|
||||||
|
ident_stage.save()
|
||||||
|
|
||||||
|
self.driver.get(self.live_server_url)
|
||||||
|
|
||||||
|
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||||
|
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
|
||||||
|
wait = WebDriverWait(identification_stage, self.wait_timeout)
|
||||||
|
|
||||||
|
wait.until(
|
||||||
|
ec.presence_of_element_located(
|
||||||
|
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
identification_stage.find_element(
|
||||||
|
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||||
|
).click()
|
||||||
|
|
||||||
|
# Now we should be at the IDP, wait for the username field
|
||||||
|
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||||
|
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||||
|
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||||
|
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
|
# Wait until we're logged in
|
||||||
|
self.wait_for_url(self.if_user_url("/library"))
|
||||||
|
self.driver.get(self.if_user_url("/settings"))
|
||||||
|
|
||||||
|
self.assert_user(
|
||||||
|
User.objects.exclude(username="akadmin")
|
||||||
|
.exclude(username__startswith="ak-outpost")
|
||||||
|
.exclude_anonymous()
|
||||||
|
.exclude(pk=self.user.pk)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clear all cookies and log in again
|
||||||
|
self.driver.delete_all_cookies()
|
||||||
|
self.driver.get(self.live_server_url)
|
||||||
|
|
||||||
|
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||||
|
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
|
||||||
|
wait = WebDriverWait(identification_stage, self.wait_timeout)
|
||||||
|
|
||||||
|
wait.until(
|
||||||
|
ec.presence_of_element_located(
|
||||||
|
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
identification_stage.find_element(
|
||||||
|
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||||
|
).click()
|
||||||
|
|
||||||
|
# Now we should be at the IDP, wait for the username field
|
||||||
|
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||||
|
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||||
|
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||||
|
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
|
# Wait until we're logged in
|
||||||
|
self.wait_for_url(self.if_user_url("/library"))
|
||||||
|
self.driver.get(self.if_user_url("/settings"))
|
||||||
|
|
||||||
|
# sleep(999999)
|
||||||
|
self.assert_user(
|
||||||
|
User.objects.exclude(username="akadmin")
|
||||||
|
.exclude(username__startswith="ak-outpost")
|
||||||
|
.exclude_anonymous()
|
||||||
|
.exclude(pk=self.user.pk)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
7823
web/package-lock.json
generated
7823
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@
|
|||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@formatjs/intl-listformat": "^7.5.7",
|
"@formatjs/intl-listformat": "^7.5.7",
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
"@goauthentik/api": "^2024.4.2-1718378698",
|
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lit/localize": "^0.12.1",
|
"@lit/localize": "^0.12.1",
|
||||||
"@lit/reactive-element": "^2.0.4",
|
"@lit/reactive-element": "^2.0.4",
|
||||||
|
529
web/sfe/index.ts
Normal file
529
web/sfe/index.ts
Normal file
@ -0,0 +1,529 @@
|
|||||||
|
import { fromByteArray } from "base64-js";
|
||||||
|
import "formdata-polyfill";
|
||||||
|
import $ from "jquery";
|
||||||
|
import "weakmap-polyfill";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type AuthenticatorValidationChallenge,
|
||||||
|
type AutosubmitChallenge,
|
||||||
|
type ChallengeTypes,
|
||||||
|
ChallengeTypesFromJSON,
|
||||||
|
type ContextualFlowInfo,
|
||||||
|
type DeviceChallenge,
|
||||||
|
type ErrorDetail,
|
||||||
|
type IdentificationChallenge,
|
||||||
|
type PasswordChallenge,
|
||||||
|
type RedirectChallenge,
|
||||||
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
interface GlobalAuthentik {
|
||||||
|
brand: {
|
||||||
|
branding_logo: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ak(): GlobalAuthentik {
|
||||||
|
return (
|
||||||
|
window as unknown as {
|
||||||
|
authentik: GlobalAuthentik;
|
||||||
|
}
|
||||||
|
).authentik;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleFlowExecutor {
|
||||||
|
challenge?: ChallengeTypes;
|
||||||
|
flowSlug: string;
|
||||||
|
container: HTMLDivElement;
|
||||||
|
|
||||||
|
constructor(container: HTMLDivElement) {
|
||||||
|
this.flowSlug = window.location.pathname.split("/")[3];
|
||||||
|
this.container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
get apiURL() {
|
||||||
|
return `/api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
url: this.apiURL,
|
||||||
|
success: (data) => {
|
||||||
|
this.challenge = ChallengeTypesFromJSON(data);
|
||||||
|
this.renderChallenge();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(data: { [key: string]: unknown } | FormData) {
|
||||||
|
$("button[type=submit]").addClass("disabled")
|
||||||
|
.html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
||||||
|
<span role="status">Loading...</span>`);
|
||||||
|
let finalData: { [key: string]: unknown } = {};
|
||||||
|
if (data instanceof FormData) {
|
||||||
|
finalData = {};
|
||||||
|
data.forEach((value, key) => {
|
||||||
|
finalData[key] = value;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
finalData = data;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: this.apiURL,
|
||||||
|
data: JSON.stringify(finalData),
|
||||||
|
success: (data) => {
|
||||||
|
this.challenge = ChallengeTypesFromJSON(data);
|
||||||
|
this.renderChallenge();
|
||||||
|
},
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: "json",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChallenge() {
|
||||||
|
switch (this.challenge?.component) {
|
||||||
|
case "ak-stage-identification":
|
||||||
|
new IdentificationStage(this, this.challenge).render();
|
||||||
|
return;
|
||||||
|
case "ak-stage-password":
|
||||||
|
new PasswordStage(this, this.challenge).render();
|
||||||
|
return;
|
||||||
|
case "xak-flow-redirect":
|
||||||
|
new RedirectStage(this, this.challenge).render();
|
||||||
|
return;
|
||||||
|
case "ak-stage-autosubmit":
|
||||||
|
new AutosubmitStage(this, this.challenge).render();
|
||||||
|
return;
|
||||||
|
case "ak-stage-authenticator-validate":
|
||||||
|
new AuthenticatorValidateStage(this, this.challenge).render();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
this.container.innerText = "Unsupported stage: " + this.challenge?.component;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowInfoChallenge {
|
||||||
|
flowInfo?: ContextualFlowInfo;
|
||||||
|
responseErrors?: {
|
||||||
|
[key: string]: Array<ErrorDetail>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Stage<T extends FlowInfoChallenge> {
|
||||||
|
constructor(
|
||||||
|
public executor: SimpleFlowExecutor,
|
||||||
|
public challenge: T,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
error(fieldName: string) {
|
||||||
|
if (!this.challenge.responseErrors) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.challenge.responseErrors[fieldName] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInputError(fieldName: string) {
|
||||||
|
return `${this.error(fieldName)
|
||||||
|
.map((error) => {
|
||||||
|
return `<div class="invalid-feedback">
|
||||||
|
${error.string}
|
||||||
|
</div>`;
|
||||||
|
})
|
||||||
|
.join("")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNonFieldErrors() {
|
||||||
|
return `${this.error("non_field_errors")
|
||||||
|
.map((error) => {
|
||||||
|
return `<div class="alert alert-danger" role="alert">
|
||||||
|
${error.string}
|
||||||
|
</div>`;
|
||||||
|
})
|
||||||
|
.join("")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html(html: string) {
|
||||||
|
this.executor.container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
throw new Error("Abstract method");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IdentificationStage extends Stage<IdentificationChallenge> {
|
||||||
|
render() {
|
||||||
|
this.html(`
|
||||||
|
<form id="ident-form">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
${
|
||||||
|
this.challenge.applicationPre
|
||||||
|
? `<p>
|
||||||
|
Login to continue to ${this.challenge.applicationPre}.
|
||||||
|
</p>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="form-label-group my-3 has-validation">
|
||||||
|
<input type="text" autofocus class="form-control" name="uid_field" placeholder="Email / Username">
|
||||||
|
</div>
|
||||||
|
${
|
||||||
|
this.challenge.passwordFields
|
||||||
|
? `<div class="form-label-group my-3 has-validation">
|
||||||
|
<input type="password" class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
|
||||||
|
${this.renderInputError("password")}
|
||||||
|
</div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${this.renderNonFieldErrors()}
|
||||||
|
<button class="btn btn-primary w-100 py-2" type="submit">${this.challenge.primaryAction}</button>
|
||||||
|
</form>`);
|
||||||
|
$("#ident-form input[name=uid_field]").trigger("focus");
|
||||||
|
$("#ident-form").on("submit", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const data = new FormData(ev.target as HTMLFormElement);
|
||||||
|
this.executor.submit(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordStage extends Stage<PasswordChallenge> {
|
||||||
|
render() {
|
||||||
|
this.html(`
|
||||||
|
<form id="password-form">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
<div class="form-label-group my-3 has-validation">
|
||||||
|
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
|
||||||
|
${this.renderInputError("password")}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
|
||||||
|
</form>`);
|
||||||
|
$("#password-form input").trigger("focus");
|
||||||
|
$("#password-form").on("submit", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const data = new FormData(ev.target as HTMLFormElement);
|
||||||
|
this.executor.submit(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RedirectStage extends Stage<RedirectChallenge> {
|
||||||
|
render() {
|
||||||
|
window.location.assign(this.challenge.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutosubmitStage extends Stage<AutosubmitChallenge> {
|
||||||
|
render() {
|
||||||
|
this.html(`
|
||||||
|
<form id="autosubmit-form" action="${this.challenge.url}" method="POST">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
${Object.entries(this.challenge.attrs).map(([key, value]) => {
|
||||||
|
return `<input
|
||||||
|
type="hidden"
|
||||||
|
name="${key}"
|
||||||
|
value="${value}"
|
||||||
|
/>`;
|
||||||
|
})}
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>`);
|
||||||
|
$("#autosubmit-form").submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Assertion {
|
||||||
|
id: string;
|
||||||
|
rawId: string;
|
||||||
|
type: string;
|
||||||
|
registrationClientExtensions: string;
|
||||||
|
response: {
|
||||||
|
clientDataJSON: string;
|
||||||
|
attestationObject: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthAssertion {
|
||||||
|
id: string;
|
||||||
|
rawId: string;
|
||||||
|
type: string;
|
||||||
|
assertionClientExtensions: string;
|
||||||
|
response: {
|
||||||
|
clientDataJSON: string;
|
||||||
|
authenticatorData: string;
|
||||||
|
signature: string;
|
||||||
|
userHandle: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
|
||||||
|
deviceChallenge?: DeviceChallenge;
|
||||||
|
|
||||||
|
b64enc(buf: Uint8Array): string {
|
||||||
|
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
b64RawEnc(buf: Uint8Array): string {
|
||||||
|
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
u8arr(input: string): Uint8Array {
|
||||||
|
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
|
||||||
|
c.charCodeAt(0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkWebAuthnSupport(): boolean {
|
||||||
|
if ("credentials" in navigator) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
|
||||||
|
console.warn("WebAuthn requires this page to be accessed via HTTPS.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.warn("WebAuthn not supported by browser.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms items in the credentialCreateOptions generated on the server
|
||||||
|
* into byte arrays expected by the navigator.credentials.create() call
|
||||||
|
*/
|
||||||
|
transformCredentialCreateOptions(
|
||||||
|
credentialCreateOptions: PublicKeyCredentialCreationOptions,
|
||||||
|
userId: string,
|
||||||
|
): PublicKeyCredentialCreationOptions {
|
||||||
|
const user = credentialCreateOptions.user;
|
||||||
|
// Because json can't contain raw bytes, the server base64-encodes the User ID
|
||||||
|
// So to get the base64 encoded byte array, we first need to convert it to a regular
|
||||||
|
// string, then a byte array, re-encode it and wrap that in an array.
|
||||||
|
const stringId = decodeURIComponent(window.atob(userId));
|
||||||
|
user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
|
||||||
|
const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
|
||||||
|
|
||||||
|
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
|
||||||
|
challenge,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
|
return transformedCredentialCreateOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the binary data in the credential into base64 strings
|
||||||
|
* for posting to the server.
|
||||||
|
* @param {PublicKeyCredential} newAssertion
|
||||||
|
*/
|
||||||
|
transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
|
||||||
|
const attObj = new Uint8Array(
|
||||||
|
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
|
||||||
|
);
|
||||||
|
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
|
||||||
|
const rawId = new Uint8Array(newAssertion.rawId);
|
||||||
|
|
||||||
|
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||||
|
return {
|
||||||
|
id: newAssertion.id,
|
||||||
|
rawId: this.b64enc(rawId),
|
||||||
|
type: newAssertion.type,
|
||||||
|
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||||
|
response: {
|
||||||
|
clientDataJSON: this.b64enc(clientDataJSON),
|
||||||
|
attestationObject: this.b64enc(attObj),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
transformCredentialRequestOptions(
|
||||||
|
credentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
): PublicKeyCredentialRequestOptions {
|
||||||
|
const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
|
||||||
|
|
||||||
|
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
|
||||||
|
(credentialDescriptor) => {
|
||||||
|
const id = this.u8arr(credentialDescriptor.id.toString());
|
||||||
|
return Object.assign({}, credentialDescriptor, { id });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
|
||||||
|
challenge,
|
||||||
|
allowCredentials,
|
||||||
|
});
|
||||||
|
|
||||||
|
return transformedCredentialRequestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the binary data in the assertion into strings for posting to the server.
|
||||||
|
* @param {PublicKeyCredential} newAssertion
|
||||||
|
*/
|
||||||
|
transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
|
||||||
|
const response = newAssertion.response as AuthenticatorAssertionResponse;
|
||||||
|
const authData = new Uint8Array(response.authenticatorData);
|
||||||
|
const clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||||
|
const rawId = new Uint8Array(newAssertion.rawId);
|
||||||
|
const sig = new Uint8Array(response.signature);
|
||||||
|
const assertionClientExtensions = newAssertion.getClientExtensionResults();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: newAssertion.id,
|
||||||
|
rawId: this.b64enc(rawId),
|
||||||
|
type: newAssertion.type,
|
||||||
|
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||||
|
|
||||||
|
response: {
|
||||||
|
clientDataJSON: this.b64RawEnc(clientDataJSON),
|
||||||
|
signature: this.b64RawEnc(sig),
|
||||||
|
authenticatorData: this.b64RawEnc(authData),
|
||||||
|
userHandle: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.deviceChallenge) {
|
||||||
|
return this.renderChallengePicker();
|
||||||
|
}
|
||||||
|
switch (this.deviceChallenge.deviceClass) {
|
||||||
|
case "static":
|
||||||
|
case "totp":
|
||||||
|
this.renderCodeInput();
|
||||||
|
break;
|
||||||
|
case "webauthn":
|
||||||
|
this.renderWebauthn();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChallengePicker() {
|
||||||
|
const challenges = this.challenge.deviceChallenges.filter((challenge) => {
|
||||||
|
if (challenge.deviceClass === "webauthn") {
|
||||||
|
if (!this.checkWebAuthnSupport()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return challenge;
|
||||||
|
});
|
||||||
|
this.html(`<form id="picker-form">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
${
|
||||||
|
challenges.length > 0
|
||||||
|
? "<p>Select an authentication method.</p>"
|
||||||
|
: `
|
||||||
|
<p>No compatible authentication method available</p>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
${challenges
|
||||||
|
.map((challenge) => {
|
||||||
|
let label = undefined;
|
||||||
|
switch (challenge.deviceClass) {
|
||||||
|
case "static":
|
||||||
|
label = "Recovery keys";
|
||||||
|
break;
|
||||||
|
case "totp":
|
||||||
|
label = "Traditional authenticator";
|
||||||
|
break;
|
||||||
|
case "webauthn":
|
||||||
|
label = "Security key";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!label) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `<div class="form-label-group my-3 has-validation">
|
||||||
|
<button id="${challenge.deviceClass}-${challenge.deviceUid}" class="btn btn-secondary w-100 py-2" type="button">
|
||||||
|
${label}
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
})
|
||||||
|
.join("")}
|
||||||
|
</form>`);
|
||||||
|
this.challenge.deviceChallenges.forEach((challenge) => {
|
||||||
|
$(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
|
this.deviceChallenge = challenge;
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCodeInput() {
|
||||||
|
this.html(`
|
||||||
|
<form id="totp-form">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
<div class="form-label-group my-3 has-validation">
|
||||||
|
<input type="text" autofocus class="form-control ${this.error("code").length > 0 ? "is-invalid" : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
|
||||||
|
${this.renderInputError("code")}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
|
||||||
|
</form>`);
|
||||||
|
$("#totp-form input").trigger("focus");
|
||||||
|
$("#totp-form").on("submit", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const data = new FormData(ev.target as HTMLFormElement);
|
||||||
|
this.executor.submit(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWebauthn() {
|
||||||
|
this.html(`
|
||||||
|
<form id="totp-form">
|
||||||
|
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
|
||||||
|
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
navigator.credentials
|
||||||
|
.get({
|
||||||
|
publicKey: this.transformCredentialRequestOptions(
|
||||||
|
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.then((assertion) => {
|
||||||
|
if (!assertion) {
|
||||||
|
throw new Error("No assertion");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// we now have an authentication assertion! encode the byte arrays contained
|
||||||
|
// in the assertion data as strings for posting to the server
|
||||||
|
const transformedAssertionForServer = this.transformAssertionForServer(
|
||||||
|
assertion as PublicKeyCredential,
|
||||||
|
);
|
||||||
|
|
||||||
|
// post the assertion to the server for verification.
|
||||||
|
this.executor.submit({
|
||||||
|
webauthn: transformedAssertionForServer,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Error when validating assertion on server: ${err}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn(error);
|
||||||
|
this.deviceChallenge = undefined;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sfe = new SimpleFlowExecutor($("#flow-sfe-container")[0] as HTMLDivElement);
|
||||||
|
sfe.start();
|
3057
web/sfe/package-lock.json
generated
Normal file
3057
web/sfe/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
web/sfe/package.json
Normal file
28
web/sfe/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@goauthentik/web-sfe",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@goauthentik/api": "^2024.6.0-1719577139",
|
||||||
|
"base64-js": "^1.5.1",
|
||||||
|
"bootstrap": "^4.6.1",
|
||||||
|
"formdata-polyfill": "^4.0.10",
|
||||||
|
"jquery": "^3.7.1",
|
||||||
|
"weakmap-polyfill": "^2.0.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c rollup.config.js --bundleConfigAsCjs",
|
||||||
|
"watch": "rollup -w -c rollup.config.js --bundleConfigAsCjs"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^26.0.1",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
|
"@rollup/plugin-swc": "^0.3.1",
|
||||||
|
"@swc/cli": "^0.3.14",
|
||||||
|
"@swc/core": "^1.6.6",
|
||||||
|
"@types/jquery": "^3.5.30",
|
||||||
|
"rollup": "^4.18.0",
|
||||||
|
"rollup-plugin-copy": "^3.5.0"
|
||||||
|
}
|
||||||
|
}
|
40
web/sfe/rollup.config.js
Normal file
40
web/sfe/rollup.config.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import resolve from "@rollup/plugin-node-resolve";
|
||||||
|
import swc from "@rollup/plugin-swc";
|
||||||
|
import copy from "rollup-plugin-copy";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: "index.ts",
|
||||||
|
output: {
|
||||||
|
dir: "../dist/sfe",
|
||||||
|
format: "cjs",
|
||||||
|
},
|
||||||
|
context: "window",
|
||||||
|
plugins: [
|
||||||
|
copy({
|
||||||
|
targets: [
|
||||||
|
{ src: "node_modules/bootstrap/dist/css/bootstrap.min.css", dest: "../dist/sfe" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
resolve({ browser: true }),
|
||||||
|
commonjs(),
|
||||||
|
swc({
|
||||||
|
swc: {
|
||||||
|
jsc: {
|
||||||
|
loose: false,
|
||||||
|
externalHelpers: false,
|
||||||
|
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
|
||||||
|
keepClassNames: false,
|
||||||
|
},
|
||||||
|
minify: false,
|
||||||
|
env: {
|
||||||
|
targets: {
|
||||||
|
edge: "17",
|
||||||
|
ie: "11",
|
||||||
|
},
|
||||||
|
mode: "entry",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
7
web/sfe/tsconfig.json
Normal file
7
web/sfe/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["jquery"],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["DOM", "ES2015", "ES2017"]
|
||||||
|
},
|
||||||
|
}
|
@ -208,7 +208,14 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
|||||||
|
|
||||||
return html`<li>
|
return html`<li>
|
||||||
${ex(
|
${ex(
|
||||||
() => html`<a href="${url}" class="pf-u-mb-xl" target="_blank">${content}</a>`,
|
() =>
|
||||||
|
html`<a
|
||||||
|
href="${url}"
|
||||||
|
class="pf-u-mb-xl"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>${content}</a
|
||||||
|
>`,
|
||||||
() => html`<a href="${url}" class="pf-u-mb-xl" )>${content}</a>`,
|
() => html`<a href="${url}" class="pf-u-mb-xl" )>${content}</a>`,
|
||||||
)}
|
)}
|
||||||
</li>`;
|
</li>`;
|
||||||
|
@ -56,6 +56,6 @@ export class VersionStatusCard extends AdminStatusCard<Version> {
|
|||||||
text = this.value.buildHash?.substring(0, 7);
|
text = this.value.buildHash?.substring(0, 7);
|
||||||
link = `https://github.com/goauthentik/authentik/commit/${this.value.buildHash}`;
|
link = `https://github.com/goauthentik/authentik/commit/${this.value.buildHash}`;
|
||||||
}
|
}
|
||||||
return html`<a href=${link} target="_blank">${text}</a>`;
|
return html`<a rel="noopener noreferrer" href=${link} target="_blank">${text}</a>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
this.centerText = outposts.pagination.count.toString();
|
this.centerText = outposts.pagination.count.toString();
|
||||||
|
outpostStats.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
return outpostStats;
|
return outpostStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ const doGroupBy = (items: Provider[]) => groupBy(items, (item) => item.verboseNa
|
|||||||
async function fetch(query?: string) {
|
async function fetch(query?: string) {
|
||||||
const args: ProvidersAllListRequest = {
|
const args: ProvidersAllListRequest = {
|
||||||
ordering: "name",
|
ordering: "name",
|
||||||
backchannel: false,
|
|
||||||
};
|
};
|
||||||
if (query !== undefined) {
|
if (query !== undefined) {
|
||||||
args.search = query;
|
args.search = query;
|
||||||
|
@ -157,6 +157,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
|
|||||||
${msg("See more about OCI support here:")}
|
${msg("See more about OCI support here:")}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="${docLink(
|
href="${docLink(
|
||||||
"/developer-docs/blueprints/?utm_source=authentik#storage---oci",
|
"/developer-docs/blueprints/?utm_source=authentik#storage---oci",
|
||||||
)}"
|
)}"
|
||||||
|
@ -23,6 +23,7 @@ export class OutpostDeploymentModal extends ModalButton {
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="${docLink("/docs/outposts?utm_source=authentik#deploy")}"
|
href="${docLink("/docs/outposts?utm_source=authentik#deploy")}"
|
||||||
|
rel="noopener noreferrer"
|
||||||
>${msg("View deployment documentation")}</a
|
>${msg("View deployment documentation")}</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
@ -210,9 +210,11 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
See
|
<a
|
||||||
<a target="_blank" href="${docLink("/docs/outposts?utm_source=authentik")}"
|
target="_blank"
|
||||||
>documentation</a
|
rel="noopener noreferrer"
|
||||||
|
href="${docLink("/docs/outposts?utm_source=authentik")}"
|
||||||
|
>${msg("See documentation")}</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
@ -245,6 +247,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
|||||||
${msg("See more here:")}
|
${msg("See more here:")}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="${docLink(
|
href="${docLink(
|
||||||
"/docs/outposts?utm_source=authentik#configuration",
|
"/docs/outposts?utm_source=authentik#configuration",
|
||||||
)}"
|
)}"
|
||||||
|
@ -85,6 +85,7 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
|
|||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${msg("Expression using Python.")}
|
${msg("Expression using Python.")}
|
||||||
<a
|
<a
|
||||||
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="${docLink("/docs/policies/expression?utm_source=authentik")}"
|
href="${docLink("/docs/policies/expression?utm_source=authentik")}"
|
||||||
>
|
>
|
||||||
|
@ -62,6 +62,7 @@ export class PropertyMappingGoogleWorkspaceForm extends BasePropertyMappingForm<
|
|||||||
${msg("Expression using Python.")}
|
${msg("Expression using Python.")}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
||||||
>
|
>
|
||||||
${msg("See documentation for a list of all variables.")}
|
${msg("See documentation for a list of all variables.")}
|
||||||
|
@ -71,6 +71,7 @@ export class PropertyMappingLDAPForm extends BasePropertyMappingForm<LDAPPropert
|
|||||||
${msg("Expression using Python.")}
|
${msg("Expression using Python.")}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
||||||
>
|
>
|
||||||
${msg("See documentation for a list of all variables.")}
|
${msg("See documentation for a list of all variables.")}
|
||||||
|
@ -62,6 +62,7 @@ export class PropertyMappingMicrosoftEntraForm extends BasePropertyMappingForm<M
|
|||||||
${msg("Expression using Python.")}
|
${msg("Expression using Python.")}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
||||||
>
|
>
|
||||||
${msg("See documentation for a list of all variables.")}
|
${msg("See documentation for a list of all variables.")}
|
||||||
|
@ -62,6 +62,7 @@ export class PropertyMappingNotification extends ModelForm<NotificationWebhookMa
|
|||||||
${msg("Expression using Python.")}
|
${msg("Expression using Python.")}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
||||||
>
|
>
|
||||||
${msg("See documentation for a list of all variables.")}
|
${msg("See documentation for a list of all variables.")}
|
||||||
|
@ -160,6 +160,7 @@ export class PropertyMappingLDAPForm extends ModelForm<RACPropertyMapping, strin
|
|||||||
${msg("Expression using Python.")}
|
${msg("Expression using Python.")}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="${docLink(
|
href="${docLink(
|
||||||
"/docs/property-mappings/expression?utm_source=authentik",
|
"/docs/property-mappings/expression?utm_source=authentik",
|
||||||
)}"
|
)}"
|
||||||
|
@ -83,6 +83,7 @@ export class PropertyMappingSAMLForm extends BasePropertyMappingForm<SAMLPropert
|
|||||||
${msg("Expression using Python.")}
|
${msg("Expression using Python.")}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
||||||
>
|
>
|
||||||
${msg("See documentation for a list of all variables.")}
|
${msg("See documentation for a list of all variables.")}
|
||||||
|
@ -56,6 +56,7 @@ export class PropertyMappingSCIMForm extends BasePropertyMappingForm<SCIMMapping
|
|||||||
${msg("Expression using Python.")}
|
${msg("Expression using Python.")}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
||||||
>
|
>
|
||||||
${msg("See documentation for a list of all variables.")}
|
${msg("See documentation for a list of all variables.")}
|
||||||
|
@ -83,6 +83,7 @@ export class PropertyMappingScopeForm extends BasePropertyMappingForm<ScopeMappi
|
|||||||
${msg("Expression using Python.")}
|
${msg("Expression using Python.")}
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
|
||||||
>
|
>
|
||||||
${msg("See documentation for a list of all variables.")}
|
${msg("See documentation for a list of all variables.")}
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
digestAlgorithmOptions,
|
||||||
|
signatureAlgorithmOptions,
|
||||||
|
} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions";
|
||||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||||
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||||
@ -14,7 +18,6 @@ import { customElement } from "lit/decorators.js";
|
|||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DigestAlgorithmEnum,
|
|
||||||
FlowsInstancesListDesignationEnum,
|
FlowsInstancesListDesignationEnum,
|
||||||
PaginatedSAMLPropertyMappingList,
|
PaginatedSAMLPropertyMappingList,
|
||||||
PropertymappingsApi,
|
PropertymappingsApi,
|
||||||
@ -22,7 +25,6 @@ import {
|
|||||||
ProvidersApi,
|
ProvidersApi,
|
||||||
SAMLPropertyMapping,
|
SAMLPropertyMapping,
|
||||||
SAMLProvider,
|
SAMLProvider,
|
||||||
SignatureAlgorithmEnum,
|
|
||||||
SpBindingEnum,
|
SpBindingEnum,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@ -333,25 +335,7 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
|
|||||||
name="digestAlgorithm"
|
name="digestAlgorithm"
|
||||||
>
|
>
|
||||||
<ak-radio
|
<ak-radio
|
||||||
.options=${[
|
.options=${digestAlgorithmOptions}
|
||||||
{
|
|
||||||
label: "SHA1",
|
|
||||||
value: DigestAlgorithmEnum._200009Xmldsigsha1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "SHA256",
|
|
||||||
value: DigestAlgorithmEnum._200104Xmlencsha256,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "SHA384",
|
|
||||||
value: DigestAlgorithmEnum._200104XmldsigMoresha384,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "SHA512",
|
|
||||||
value: DigestAlgorithmEnum._200104Xmlencsha512,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
.value=${this.instance?.digestAlgorithm}
|
.value=${this.instance?.digestAlgorithm}
|
||||||
>
|
>
|
||||||
</ak-radio>
|
</ak-radio>
|
||||||
@ -362,29 +346,7 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
|
|||||||
name="signatureAlgorithm"
|
name="signatureAlgorithm"
|
||||||
>
|
>
|
||||||
<ak-radio
|
<ak-radio
|
||||||
.options=${[
|
.options=${signatureAlgorithmOptions}
|
||||||
{
|
|
||||||
label: "RSA-SHA1",
|
|
||||||
value: SignatureAlgorithmEnum._200009XmldsigrsaSha1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "RSA-SHA256",
|
|
||||||
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha256,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "RSA-SHA384",
|
|
||||||
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha384,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "RSA-SHA512",
|
|
||||||
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha512,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "DSA-SHA1",
|
|
||||||
value: SignatureAlgorithmEnum._200009XmldsigdsaSha1,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
.value=${this.instance?.signatureAlgorithm}
|
.value=${this.instance?.signatureAlgorithm}
|
||||||
>
|
>
|
||||||
</ak-radio>
|
</ak-radio>
|
||||||
|
@ -36,11 +36,13 @@ import "@goauthentik/elements/oauth/UserRefreshTokenList";
|
|||||||
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
import "@goauthentik/elements/rbac/ObjectPermissionsPage";
|
||||||
import "@goauthentik/elements/user/SessionList";
|
import "@goauthentik/elements/user/SessionList";
|
||||||
import "@goauthentik/elements/user/UserConsentList";
|
import "@goauthentik/elements/user/UserConsentList";
|
||||||
|
import "@goauthentik/elements/user/UserReputationList";
|
||||||
import "@goauthentik/elements/user/sources/SourceSettings";
|
import "@goauthentik/elements/user/sources/SourceSettings";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { TemplateResult, css, html, nothing } from "lit";
|
import { TemplateResult, css, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
@ -274,6 +276,21 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section
|
||||||
|
slot="page-reputation"
|
||||||
|
data-tab-title="${msg("Reputation scores")}"
|
||||||
|
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||||
|
>
|
||||||
|
<div class="pf-c-card">
|
||||||
|
<div class="pf-c-card__body">
|
||||||
|
<ak-user-reputation-list
|
||||||
|
targetUsername=${user.username}
|
||||||
|
targetEmail=${ifDefined(user.email)}
|
||||||
|
>
|
||||||
|
</ak-user-reputation-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<section
|
<section
|
||||||
slot="page-consent"
|
slot="page-consent"
|
||||||
data-tab-title="${msg("Explicit Consent")}"
|
data-tab-title="${msg("Explicit Consent")}"
|
||||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||||||
export const ERROR_CLASS = "pf-m-danger";
|
export const ERROR_CLASS = "pf-m-danger";
|
||||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||||
export const CURRENT_CLASS = "pf-m-current";
|
export const CURRENT_CLASS = "pf-m-current";
|
||||||
export const VERSION = "2024.6.0";
|
export const VERSION = "2024.6.3";
|
||||||
export const TITLE_DEFAULT = "authentik";
|
export const TITLE_DEFAULT = "authentik";
|
||||||
export const ROUTE_SEPARATOR = ";";
|
export const ROUTE_SEPARATOR = ";";
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
|
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
|
||||||
|
import { globalAK } from "@goauthentik/common/global";
|
||||||
import { UIConfig } from "@goauthentik/common/ui/config";
|
import { UIConfig } from "@goauthentik/common/ui/config";
|
||||||
import { adaptCSS } from "@goauthentik/common/utils";
|
import { adaptCSS } from "@goauthentik/common/utils";
|
||||||
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
|
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
|
||||||
@ -16,6 +17,7 @@ type AkInterface = HTMLElement & {
|
|||||||
brand?: CurrentBrand;
|
brand?: CurrentBrand;
|
||||||
uiConfig?: UIConfig;
|
uiConfig?: UIConfig;
|
||||||
config?: Config;
|
config?: Config;
|
||||||
|
get activeTheme(): UiThemeEnum | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rootInterface = <T extends AkInterface>(): T | undefined =>
|
export const rootInterface = <T extends AkInterface>(): T | undefined =>
|
||||||
@ -41,7 +43,11 @@ function fetchCustomCSS(): Promise<string[]> {
|
|||||||
return css;
|
return css;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
|
export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)";
|
||||||
|
|
||||||
|
// Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the
|
||||||
|
// when changing themes we might not remove the correct css stylesheet instance.
|
||||||
|
const _darkTheme = ensureCSSStyleSheet(ThemeDark);
|
||||||
|
|
||||||
@localized()
|
@localized()
|
||||||
export class AKElement extends LitElement {
|
export class AKElement extends LitElement {
|
||||||
@ -90,12 +96,7 @@ export class AKElement extends LitElement {
|
|||||||
async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
|
async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
|
||||||
// Early activate theme based on media query to prevent light flash
|
// Early activate theme based on media query to prevent light flash
|
||||||
// when dark is preferred
|
// when dark is preferred
|
||||||
this._activateTheme(
|
this._applyTheme(root, globalAK().brand.uiTheme);
|
||||||
root,
|
|
||||||
window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches
|
|
||||||
? UiThemeEnum.Light
|
|
||||||
: UiThemeEnum.Dark,
|
|
||||||
);
|
|
||||||
this._applyTheme(root, await this.getTheme());
|
this._applyTheme(root, await this.getTheme());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,8 +126,9 @@ export class AKElement extends LitElement {
|
|||||||
ev?.matches || this._mediaMatcher?.matches
|
ev?.matches || this._mediaMatcher?.matches
|
||||||
? UiThemeEnum.Light
|
? UiThemeEnum.Light
|
||||||
: UiThemeEnum.Dark;
|
: UiThemeEnum.Dark;
|
||||||
this._activateTheme(root, theme);
|
this._activateTheme(theme, root);
|
||||||
};
|
};
|
||||||
|
this._mediaMatcherHandler(undefined);
|
||||||
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
|
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -136,17 +138,21 @@ export class AKElement extends LitElement {
|
|||||||
this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler);
|
this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler);
|
||||||
this._mediaMatcher = undefined;
|
this._mediaMatcher = undefined;
|
||||||
}
|
}
|
||||||
this._activateTheme(root, theme);
|
this._activateTheme(theme, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
|
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
|
||||||
if (theme === UiThemeEnum.Dark) {
|
if (theme === UiThemeEnum.Dark) {
|
||||||
return ThemeDark;
|
return _darkTheme;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
_activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum) {
|
/**
|
||||||
|
* Directly activate a given theme, accepts multiple document/ShadowDOMs to apply the stylesheet
|
||||||
|
* to. The stylesheets are applied to each DOM in order. Does nothing if the given theme is already active.
|
||||||
|
*/
|
||||||
|
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]) {
|
||||||
if (theme === this._activeTheme) {
|
if (theme === this._activeTheme) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -161,12 +167,19 @@ export class AKElement extends LitElement {
|
|||||||
this.setAttribute("theme", theme);
|
this.setAttribute("theme", theme);
|
||||||
const stylesheet = AKElement.themeToStylesheet(theme);
|
const stylesheet = AKElement.themeToStylesheet(theme);
|
||||||
const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme);
|
const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme);
|
||||||
if (stylesheet) {
|
roots.forEach((root) => {
|
||||||
root.adoptedStyleSheets = [...root.adoptedStyleSheets, ensureCSSStyleSheet(stylesheet)];
|
if (stylesheet) {
|
||||||
}
|
root.adoptedStyleSheets = [
|
||||||
if (oldStylesheet) {
|
...root.adoptedStyleSheets,
|
||||||
root.adoptedStyleSheets = root.adoptedStyleSheets.filter((v) => v !== oldStylesheet);
|
ensureCSSStyleSheet(stylesheet),
|
||||||
}
|
];
|
||||||
|
}
|
||||||
|
if (oldStylesheet) {
|
||||||
|
root.adoptedStyleSheets = root.adoptedStyleSheets.filter(
|
||||||
|
(v) => v !== oldStylesheet,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
this._activeTheme = theme;
|
this._activeTheme = theme;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|||||||
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
|
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
|
||||||
import { UiThemeEnum } from "@goauthentik/api";
|
import { UiThemeEnum } from "@goauthentik/api";
|
||||||
|
|
||||||
import { AKElement } from "../Base";
|
import { AKElement, rootInterface } from "../Base";
|
||||||
import { BrandContextController } from "./BrandContextController";
|
import { BrandContextController } from "./BrandContextController";
|
||||||
import { ConfigContextController } from "./ConfigContextController";
|
import { ConfigContextController } from "./ConfigContextController";
|
||||||
import { EnterpriseContextController } from "./EnterpriseContextController";
|
import { EnterpriseContextController } from "./EnterpriseContextController";
|
||||||
@ -50,9 +50,19 @@ export class Interface extends AKElement implements AkInterface {
|
|||||||
this.dataset.akInterfaceRoot = "true";
|
this.dataset.akInterfaceRoot = "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
_activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum): void {
|
_activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]): void {
|
||||||
super._activateTheme(root, theme);
|
if (theme === this._activeTheme) {
|
||||||
super._activateTheme(document as unknown as DocumentOrShadowRoot, theme);
|
return;
|
||||||
|
}
|
||||||
|
console.debug(
|
||||||
|
`authentik/interface[${rootInterface()?.tagName.toLowerCase()}]: Enabling theme ${theme}`,
|
||||||
|
);
|
||||||
|
// Special case for root interfaces, as they need to modify the global document CSS too
|
||||||
|
// Instead of calling ._activateTheme() twice, we insert the root document in the call
|
||||||
|
// since multiple calls to ._activateTheme() would not do anything after the first call
|
||||||
|
// as the theme is already enabled.
|
||||||
|
roots.unshift(document as unknown as DocumentOrShadowRoot);
|
||||||
|
super._activateTheme(theme, ...roots);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTheme(): Promise<UiThemeEnum> {
|
async getTheme(): Promise<UiThemeEnum> {
|
||||||
|
@ -78,7 +78,7 @@ export class Markdown extends AKElement {
|
|||||||
const pathName = path.replace(".md", "");
|
const pathName = path.replace(".md", "");
|
||||||
const link = `docs/${baseName}${pathName}`;
|
const link = `docs/${baseName}${pathName}`;
|
||||||
const url = new URL(link, baseUrl).toString();
|
const url = new URL(link, baseUrl).toString();
|
||||||
return `href="${url}" _target="blank"`;
|
return `href="${url}" _target="blank" rel="noopener noreferrer"`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
||||||
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { customEvent } from "@goauthentik/elements/utils/customEvents";
|
import { customEvent } from "@goauthentik/elements/utils/customEvents";
|
||||||
|
|
||||||
import { LitElement, html } from "lit";
|
import { html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
import { WithBrandConfig } from "../Interface/brandProvider";
|
import { WithBrandConfig } from "../Interface/brandProvider";
|
||||||
@ -9,8 +10,6 @@ import { initializeLocalization } from "./configureLocale";
|
|||||||
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
||||||
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
|
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
|
||||||
|
|
||||||
const LocaleContextBase = WithBrandConfig(LitElement);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component to manage your locale settings.
|
* A component to manage your locale settings.
|
||||||
*
|
*
|
||||||
@ -25,7 +24,7 @@ const LocaleContextBase = WithBrandConfig(LitElement);
|
|||||||
* @fires ak-locale-change - When a valid locale has been swapped in
|
* @fires ak-locale-change - When a valid locale has been swapped in
|
||||||
*/
|
*/
|
||||||
@customElement("ak-locale-context")
|
@customElement("ak-locale-context")
|
||||||
export class LocaleContext extends LocaleContextBase {
|
export class LocaleContext extends WithBrandConfig(AKElement) {
|
||||||
/// @attribute The text representation of the current locale */
|
/// @attribute The text representation of the current locale */
|
||||||
@property({ attribute: true, type: String })
|
@property({ attribute: true, type: String })
|
||||||
locale = DEFAULT_LOCALE;
|
locale = DEFAULT_LOCALE;
|
||||||
@ -78,7 +77,7 @@ export class LocaleContext extends LocaleContextBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
locale.locale().then(() => {
|
locale.locale().then(() => {
|
||||||
console.debug(`Setting Locale to ... ${locale.label()} (${locale.code})`);
|
console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`);
|
||||||
this.setLocale(locale.code).then(() => {
|
this.setLocale(locale.code).then(() => {
|
||||||
window.setTimeout(this.notifyApplication, 0);
|
window.setTimeout(this.notifyApplication, 0);
|
||||||
});
|
});
|
||||||
|
@ -66,15 +66,15 @@ export class UserOAuthAccessTokenList extends Table<TokenModel> {
|
|||||||
renderToolbarSelected(): TemplateResult {
|
renderToolbarSelected(): TemplateResult {
|
||||||
const disabled = this.selectedElements.length < 1;
|
const disabled = this.selectedElements.length < 1;
|
||||||
return html`<ak-forms-delete-bulk
|
return html`<ak-forms-delete-bulk
|
||||||
objectLabel=${msg("Refresh Tokens(s)")}
|
objectLabel=${msg("Access Tokens(s)")}
|
||||||
.objects=${this.selectedElements}
|
.objects=${this.selectedElements}
|
||||||
.usedBy=${(item: ExpiringBaseGrantModel) => {
|
.usedBy=${(item: ExpiringBaseGrantModel) => {
|
||||||
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensUsedByList({
|
return new Oauth2Api(DEFAULT_CONFIG).oauth2AccessTokensUsedByList({
|
||||||
id: item.pk,
|
id: item.pk,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
.delete=${(item: ExpiringBaseGrantModel) => {
|
.delete=${(item: ExpiringBaseGrantModel) => {
|
||||||
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensDestroy({
|
return new Oauth2Api(DEFAULT_CONFIG).oauth2AccessTokensDestroy({
|
||||||
id: item.pk,
|
id: item.pk,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
|
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||||
|
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||||
|
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
import { customElement } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
@ -84,7 +85,7 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
|
|||||||
<a href="#/" class="pf-c-page__header-brand-link">
|
<a href="#/" class="pf-c-page__header-brand-link">
|
||||||
<div class="pf-c-brand ak-brand">
|
<div class="pf-c-brand ak-brand">
|
||||||
<img
|
<img
|
||||||
src=${this.brand?.brandingLogo ?? DefaultBrand.brandingLogo}
|
src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
|
||||||
alt="authentik Logo"
|
alt="authentik Logo"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user