Compare commits
	
		
			33 Commits
		
	
	
		
			website/do
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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.2
 | 
				
			||||||
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.2"
 | 
				
			||||||
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)
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -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,7 +36,6 @@ 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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"
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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.2 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.2}
 | 
				
			||||||
    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.2}
 | 
				
			||||||
    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.2"
 | 
				
			||||||
 | 
				
			|||||||
@ -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.2",
 | 
				
			||||||
    "private": true
 | 
					    "private": true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
[tool.poetry]
 | 
					[tool.poetry]
 | 
				
			||||||
name = "authentik"
 | 
					name = "authentik"
 | 
				
			||||||
version = "2024.6.0"
 | 
					version = "2024.6.2"
 | 
				
			||||||
description = ""
 | 
					description = ""
 | 
				
			||||||
authors = ["authentik Team <hello@goauthentik.io>"]
 | 
					authors = ["authentik Team <hello@goauthentik.io>"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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.2
 | 
				
			||||||
  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()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7817
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7817
									
								
								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.2";
 | 
				
			||||||
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());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -127,6 +128,7 @@ export class AKElement extends LitElement {
 | 
				
			|||||||
                            : UiThemeEnum.Dark;
 | 
					                            : UiThemeEnum.Dark;
 | 
				
			||||||
                    this._activateTheme(root, theme);
 | 
					                    this._activateTheme(root, theme);
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
 | 
					                this._mediaMatcherHandler(undefined);
 | 
				
			||||||
                this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
 | 
					                this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
@ -141,7 +143,7 @@ export class AKElement extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    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;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -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";
 | 
				
			||||||
@ -51,8 +51,14 @@ export class Interface extends AKElement implements AkInterface {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum): void {
 | 
					    _activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum): void {
 | 
				
			||||||
        super._activateTheme(root, theme);
 | 
					        if (theme === this._activeTheme) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        console.debug(
 | 
				
			||||||
 | 
					            `authentik/interface[${rootInterface()?.tagName.toLowerCase()}]: Enabling theme ${theme}`,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        super._activateTheme(document as unknown as DocumentOrShadowRoot, theme);
 | 
					        super._activateTheme(document as unknown as DocumentOrShadowRoot, theme);
 | 
				
			||||||
 | 
					        super._activateTheme(root, theme);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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"
 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										83
									
								
								web/src/elements/user/UserReputationList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								web/src/elements/user/UserReputationList.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
				
			||||||
 | 
					import { uiConfig } from "@goauthentik/common/ui/config";
 | 
				
			||||||
 | 
					import { getRelativeTime } from "@goauthentik/common/utils";
 | 
				
			||||||
 | 
					import "@goauthentik/elements/forms/DeleteBulkForm";
 | 
				
			||||||
 | 
					import { PaginatedResponse } from "@goauthentik/elements/table/Table";
 | 
				
			||||||
 | 
					import { Table, TableColumn } from "@goauthentik/elements/table/Table";
 | 
				
			||||||
 | 
					import getUnicodeFlagIcon from "country-flag-icons/unicode";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { msg } from "@lit/localize";
 | 
				
			||||||
 | 
					import { TemplateResult, html } from "lit";
 | 
				
			||||||
 | 
					import { customElement, property } from "lit/decorators.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { PoliciesApi, Reputation } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@customElement("ak-user-reputation-list")
 | 
				
			||||||
 | 
					export class UserReputationList extends Table<Reputation> {
 | 
				
			||||||
 | 
					    @property()
 | 
				
			||||||
 | 
					    targetUsername!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property()
 | 
				
			||||||
 | 
					    targetEmail!: string | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async apiEndpoint(page: number): Promise<PaginatedResponse<Reputation>> {
 | 
				
			||||||
 | 
					        const identifiers = [this.targetUsername];
 | 
				
			||||||
 | 
					        if (this.targetEmail !== undefined) {
 | 
				
			||||||
 | 
					            identifiers.push(this.targetEmail);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresList({
 | 
				
			||||||
 | 
					            identifierIn: identifiers,
 | 
				
			||||||
 | 
					            ordering: this.order,
 | 
				
			||||||
 | 
					            page: page,
 | 
				
			||||||
 | 
					            pageSize: (await uiConfig()).pagination.perPage,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    checkbox = true;
 | 
				
			||||||
 | 
					    clearOnRefresh = true;
 | 
				
			||||||
 | 
					    order = "identifier";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    columns(): TableColumn[] {
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            new TableColumn(msg("Identifier"), "identifier"),
 | 
				
			||||||
 | 
					            new TableColumn(msg("IP"), "ip"),
 | 
				
			||||||
 | 
					            new TableColumn(msg("Score"), "score"),
 | 
				
			||||||
 | 
					            new TableColumn(msg("Updated"), "updated"),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    renderToolbarSelected(): TemplateResult {
 | 
				
			||||||
 | 
					        const disabled = this.selectedElements.length < 1;
 | 
				
			||||||
 | 
					        return html`<ak-forms-delete-bulk
 | 
				
			||||||
 | 
					            objectLabel=${msg("Reputation score(s)")}
 | 
				
			||||||
 | 
					            .objects=${this.selectedElements}
 | 
				
			||||||
 | 
					            .usedBy=${(item: Reputation) => {
 | 
				
			||||||
 | 
					                return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresUsedByList({
 | 
				
			||||||
 | 
					                    reputationUuid: item.pk || "",
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            .delete=${(item: Reputation) => {
 | 
				
			||||||
 | 
					                return new PoliciesApi(DEFAULT_CONFIG).policiesReputationScoresDestroy({
 | 
				
			||||||
 | 
					                    reputationUuid: item.pk || "",
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
 | 
				
			||||||
 | 
					                ${msg("Delete")}
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					        </ak-forms-delete-bulk>`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    row(item: Reputation): TemplateResult[] {
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            html`${item.identifier}`,
 | 
				
			||||||
 | 
					            html`${item.ipGeoData?.country
 | 
				
			||||||
 | 
					                ? html` ${getUnicodeFlagIcon(item.ipGeoData.country)} `
 | 
				
			||||||
 | 
					                : html``}
 | 
				
			||||||
 | 
					            ${item.ip}`,
 | 
				
			||||||
 | 
					            html`${item.score}`,
 | 
				
			||||||
 | 
					            html`<div>${getRelativeTime(item.updated)}</div>
 | 
				
			||||||
 | 
					                <small>${item.updated.toLocaleString()}</small>`,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								web/src/elements/utils/images.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/src/elements/utils/images.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import { QUERY_MEDIA_COLOR_LIGHT, rootInterface } from "@goauthentik/elements/Base";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { UiThemeEnum } from "@goauthentik/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function themeImage(rawPath: string) {
 | 
				
			||||||
 | 
					    let enabledTheme = rootInterface()?.activeTheme;
 | 
				
			||||||
 | 
					    if (!enabledTheme || enabledTheme === UiThemeEnum.Automatic) {
 | 
				
			||||||
 | 
					        enabledTheme = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches
 | 
				
			||||||
 | 
					            ? UiThemeEnum.Light
 | 
				
			||||||
 | 
					            : UiThemeEnum.Dark;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return rawPath.replace("%(theme)s", enabledTheme);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -11,6 +11,7 @@ import { WebsocketClient } from "@goauthentik/common/ws";
 | 
				
			|||||||
import { Interface } from "@goauthentik/elements/Interface";
 | 
					import { Interface } from "@goauthentik/elements/Interface";
 | 
				
			||||||
import "@goauthentik/elements/LoadingOverlay";
 | 
					import "@goauthentik/elements/LoadingOverlay";
 | 
				
			||||||
import "@goauthentik/elements/ak-locale-context";
 | 
					import "@goauthentik/elements/ak-locale-context";
 | 
				
			||||||
 | 
					import { themeImage } from "@goauthentik/elements/utils/images";
 | 
				
			||||||
import "@goauthentik/flow/sources/apple/AppleLoginInit";
 | 
					import "@goauthentik/flow/sources/apple/AppleLoginInit";
 | 
				
			||||||
import "@goauthentik/flow/sources/plex/PlexLoginInit";
 | 
					import "@goauthentik/flow/sources/plex/PlexLoginInit";
 | 
				
			||||||
import "@goauthentik/flow/stages/FlowErrorStage";
 | 
					import "@goauthentik/flow/stages/FlowErrorStage";
 | 
				
			||||||
@ -442,7 +443,9 @@ export class FlowExecutor extends Interface implements StageHost {
 | 
				
			|||||||
    renderChallengeWrapper(): TemplateResult {
 | 
					    renderChallengeWrapper(): TemplateResult {
 | 
				
			||||||
        const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
 | 
					        const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
 | 
				
			||||||
            <img
 | 
					            <img
 | 
				
			||||||
                src="${first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, "")}"
 | 
					                src="${themeImage(
 | 
				
			||||||
 | 
					                    first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, ""),
 | 
				
			||||||
 | 
					                )}"
 | 
				
			||||||
                alt="authentik Logo"
 | 
					                alt="authentik Logo"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
        </div>`;
 | 
					        </div>`;
 | 
				
			||||||
@ -503,28 +506,18 @@ export class FlowExecutor extends Interface implements StageHost {
 | 
				
			|||||||
                                        <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">
 | 
				
			||||||
                                                ${this.brand?.uiFooterLinks?.map((link) => {
 | 
					                                                ${this.brand?.uiFooterLinks?.map((link) => {
 | 
				
			||||||
 | 
					                                                    if (link.href) {
 | 
				
			||||||
                                                        return html`<li>
 | 
					                                                        return html`<li>
 | 
				
			||||||
                                                        <a href="${link.href || ""}"
 | 
					                                                            <a href="${link.href}">${link.name}</a>
 | 
				
			||||||
                                                            >${link.name}</a
 | 
					                                                        </li>`;
 | 
				
			||||||
                                                        >
 | 
					                                                    }
 | 
				
			||||||
 | 
					                                                    return html`<li>
 | 
				
			||||||
 | 
					                                                        <span>${link.name}</span>
 | 
				
			||||||
                                                    </li>`;
 | 
					                                                    </li>`;
 | 
				
			||||||
                                                })}
 | 
					                                                })}
 | 
				
			||||||
                                                <li>
 | 
					                                                <li>
 | 
				
			||||||
                                                    <a
 | 
					                                                    <span>${msg("Powered by authentik")}</span>
 | 
				
			||||||
                                                        href="https://goauthentik.io?utm_source=authentik&utm_medium=flow"
 | 
					 | 
				
			||||||
                                                        >${msg("Powered by authentik")}</a
 | 
					 | 
				
			||||||
                                                    >
 | 
					 | 
				
			||||||
                                                </li>
 | 
					                                                </li>
 | 
				
			||||||
                                                ${this.flowInfo?.background?.startsWith("/static")
 | 
					 | 
				
			||||||
                                                    ? html`
 | 
					 | 
				
			||||||
                                                          <li>
 | 
					 | 
				
			||||||
                                                              <a
 | 
					 | 
				
			||||||
                                                                  href="https://unsplash.com/@benjaminpunzalan"
 | 
					 | 
				
			||||||
                                                                  >${msg("Background image")}</a
 | 
					 | 
				
			||||||
                                                              >
 | 
					 | 
				
			||||||
                                                          </li>
 | 
					 | 
				
			||||||
                                                      `
 | 
					 | 
				
			||||||
                                                    : html``}
 | 
					 | 
				
			||||||
                                            </ul>
 | 
					                                            </ul>
 | 
				
			||||||
                                        </footer>
 | 
					                                        </footer>
 | 
				
			||||||
                                    </div>
 | 
					                                    </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -51,11 +51,6 @@ export class AutosubmitStage extends BaseStage<
 | 
				
			|||||||
                        />`;
 | 
					                        />`;
 | 
				
			||||||
                    })}
 | 
					                    })}
 | 
				
			||||||
                    <ak-empty-state ?loading="${true}"> </ak-empty-state>
 | 
					                    <ak-empty-state ?loading="${true}"> </ak-empty-state>
 | 
				
			||||||
                    <div class="pf-c-form__group pf-m-action">
 | 
					 | 
				
			||||||
                        <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
					 | 
				
			||||||
                            ${msg("Continue")}
 | 
					 | 
				
			||||||
                        </button>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                </form>
 | 
					                </form>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <footer class="pf-c-login__main-footer">
 | 
					            <footer class="pf-c-login__main-footer">
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import { first, getCookie } from "@goauthentik/common/utils";
 | 
				
			|||||||
import { Interface } from "@goauthentik/elements/Interface";
 | 
					import { Interface } from "@goauthentik/elements/Interface";
 | 
				
			||||||
import "@goauthentik/elements/ak-locale-context";
 | 
					import "@goauthentik/elements/ak-locale-context";
 | 
				
			||||||
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
 | 
					import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
 | 
				
			||||||
 | 
					import { themeImage } from "@goauthentik/elements/utils/images";
 | 
				
			||||||
import "rapidoc";
 | 
					import "rapidoc";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CSSResult, TemplateResult, css, html } from "lit";
 | 
					import { CSSResult, TemplateResult, css, html } from "lit";
 | 
				
			||||||
@ -103,7 +104,9 @@ export class APIBrowser extends Interface {
 | 
				
			|||||||
                        <img
 | 
					                        <img
 | 
				
			||||||
                            alt="authentik Logo"
 | 
					                            alt="authentik Logo"
 | 
				
			||||||
                            class="logo"
 | 
					                            class="logo"
 | 
				
			||||||
                            src="${first(this.brand?.brandingLogo, DefaultBrand.brandingLogo)}"
 | 
					                            src="${themeImage(
 | 
				
			||||||
 | 
					                                first(this.brand?.brandingLogo, DefaultBrand.brandingLogo),
 | 
				
			||||||
 | 
					                            )}"
 | 
				
			||||||
                        />
 | 
					                        />
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </rapi-doc>
 | 
					                </rapi-doc>
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user