Compare commits

..

27 Commits

Author SHA1 Message Date
4e9a466d64 re-add paramiko
This reverts commit eb6f515ee0e9ed5196565c79eea9dd5b5cdc7444.

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 18:10:30 +03:00
9bd8cfbac0 fix folder perms
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 18:10:30 +03:00
e18c2fe084 fix web issue
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 18:10:30 +03:00
205f11532f fix build
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 18:10:30 +03:00
bc6d66cd88 use open
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 18:10:30 +03:00
609e9a00b4 what a pointless warning
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 18:10:30 +03:00
d5708d22e0 fix error handling
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 18:10:30 +03:00
71ac1282f9 cleanup
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 18:10:30 +03:00
cf9d8f64a2 simplify config, adjust perms in dockerfile
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 18:10:30 +03:00
1cda01511b outposts: use native ssh client
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 18:10:30 +03:00
13591fc72c ci: use correct sha for pushing image
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2023-04-24 16:57:29 +02:00
b604ff5114 ci: build on branch commit instead of merge commit
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2023-04-24 16:57:29 +02:00
f72fa41a75 website/integrations: DokuWiki integration (#5208)
* website: adds dokuwiki integration

* Apply suggestions from code review

Signed-off-by: Jens L. <jens@beryju.org>

* removed patch note since patch is upstream now

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens L. <jens@beryju.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Christian Mellwig <mellwig.c@fug-elektronik.de>
Co-authored-by: Jens L <jens@beryju.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-04-24 12:36:00 +03:00
adf4191066 web: bump eslint from 8.38.0 to 8.39.0 in /web (#5356)
Bumps [eslint](https://github.com/eslint/eslint) from 8.38.0 to 8.39.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.38.0...v8.39.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-24 12:31:06 +03:00
d2de586cc9 website: bump prettier from 2.8.7 to 2.8.8 in /website (#5357)
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.7 to 2.8.8.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.7...2.8.8)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-24 11:30:10 +03:00
dad5021870 core: bump importlib-metadata from 6.5.0 to 6.6.0 (#5359)
Bumps [importlib-metadata](https://github.com/python/importlib_metadata) from 6.5.0 to 6.6.0.
- [Release notes](https://github.com/python/importlib_metadata/releases)
- [Changelog](https://github.com/python/importlib_metadata/blob/main/CHANGES.rst)
- [Commits](https://github.com/python/importlib_metadata/compare/v6.5.0...v6.6.0)

---
updated-dependencies:
- dependency-name: importlib-metadata
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-24 11:29:42 +03:00
ab3f993bb9 web: bump prettier from 2.8.7 to 2.8.8 in /web (#5358)
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.7 to 2.8.8.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.7...2.8.8)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-24 11:00:09 +03:00
158fe2f9bb web/admin: fix cert expiry coloring (#5354) 2023-04-23 19:16:50 +03:00
5970a6e2a2 events: always run policies for notification rules even if no group is selected (#5353)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-23 19:10:22 +03:00
5c8f024d12 website: add documentation for AUTHENTIK_REDIS__TLS (#5349)
* website: add documentation for AUTHENTIK_REDIS__TLS

Signed-off-by: Bardi Harborow <bardi@bardiharborow.com>

* add tls reqs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Bardi Harborow <bardi@bardiharborow.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-04-23 11:37:53 +03:00
428daa5323 website/docs: Update terminology.md (#5350)
Signed-off-by: Patrick Hofmann <patrick@ph89.de>
2023-04-23 11:32:01 +03:00
4001af4d35 core: bump sqlparse from 0.4.3 to 0.4.4 (#5347) 2023-04-22 02:25:42 +03:00
f1cec03dcf web/admin: remove grouping (#5343)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 21:31:23 +03:00
574ed72b95 website/integrations: Update Discord login docs (#5345)
* Added trailing slash to redirect URI

Signed-off-by: Lázaro Blanc <40198445+lazaroblanc@users.noreply.github.com>

* updated images and removed unused one

---------

Signed-off-by: Lázaro Blanc <40198445+lazaroblanc@users.noreply.github.com>
Co-authored-by: Lázaro Blanc <lazaroblanc@users.noreply.github.com>
2023-04-21 19:24:42 +03:00
480f5c2aac ci: add log grouping (#5342)
* ci: add log grouping

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* try to group structlog output

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* earlier hooks

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* hmm

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* disable beats integration for now

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* test container logs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove testing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 19:06:11 +03:00
d4e502fdf5 ci: bump setup-node version (#5340)
* ci: bump setup-node version

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* set skip-pkg-cache

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix failing codeQL

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix airgapped avatars

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 15:45:41 +03:00
05b2fb5ec1 root: Change docker-compose HTTP and HTTPS port variables (#5335)
* Clarify that COMPOSE_PORT_ changes exposed ports

Signed-off-by: Bojan Bogojevic <20166636+Bojan023@users.noreply.github.com>

* Change AUTHENTIK_PORT to COMPOSE_PORT 

Signed-off-by: Bojan Bogojevic <20166636+Bojan023@users.noreply.github.com>

* Change AUTHENTIK_PORT to COMPOSE_PORT 

Signed-off-by: Bojan Bogojevic <20166636+Bojan023@users.noreply.github.com>

* Add hint to Configuration for internal ports

Signed-off-by: Bojan Bogojevic <20166636+Bojan023@users.noreply.github.com>

* dont use different env syntaxes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add changelog entry

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Bojan Bogojevic <20166636+Bojan023@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-04-21 14:54:40 +03:00
99 changed files with 385 additions and 1632 deletions

View File

@ -51,12 +51,14 @@ runs:
version_family = ".".join(version.split(".")[:-1])
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
sha = os.environ["GITHUB_SHA"] if not "${{ github.event.pull_request.head.sha }}" else "${{ github.event.pull_request.head.sha }}"
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print("branchName=%s" % branch_name, file=_output)
print("branchNameContainer=%s" % safe_branch_name, file=_output)
print("timestamp=%s" % int(time()), file=_output)
print("sha=%s" % os.environ["GITHUB_SHA"], file=_output)
print("shortHash=%s" % os.environ["GITHUB_SHA"][:7], file=_output)
print("sha=%s" % sha, file=_output)
print("shortHash=%s" % sha[:7], file=_output)
print("shouldBuild=%s" % should_build, file=_output)
print("version=%s" % version, file=_output)
print("versionFamily=%s" % version_family, file=_output)

View File

@ -21,7 +21,7 @@ runs:
python-version: "3.11"
cache: "poetry"
- name: Setup node
uses: actions/setup-node@v3.1.0
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"

View File

@ -2,8 +2,7 @@ version: "3.7"
services:
postgresql:
container_name: postgres
image: library/postgres:${PSQL_TAG:-12}
image: docker.io/library/postgres:${PSQL_TAG:-12}
volumes:
- db-data:/var/lib/postgresql/data
environment:
@ -14,8 +13,7 @@ services:
- 5432:5432
restart: always
redis:
container_name: redis
image: library/redis
image: docker.io/library/redis
ports:
- 6379:6379
restart: always

View File

@ -187,6 +187,8 @@ jobs:
timeout-minutes: 120
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
@ -229,6 +231,8 @@ jobs:
timeout-minutes: 120
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx

View File

@ -30,6 +30,7 @@ jobs:
uses: golangci/golangci-lint-action@v3
with:
args: --timeout 5000s
skip-pkg-cache: true
test-unittest:
runs-on: ubuntu-latest
steps:
@ -63,6 +64,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
@ -110,6 +113,8 @@ jobs:
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-go@v4
with:
go-version-file: "go.mod"

View File

@ -4,7 +4,6 @@ on:
push:
branches: [main, "*", next, version*]
pull_request:
# The branches below must be a subset of the branches above
branches: [main]
schedule:
- cron: "30 6 * * 5"
@ -22,39 +21,16 @@ jobs:
fail-fast: false
matrix:
language: ["go", "javascript", "python"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@ -84,6 +84,8 @@ RUN apt-get update && \
apt-get install -y --no-install-recommends libxmlsec1-openssl libmaxminddb0 && \
# Required for bootstrap & healtcheck
apt-get install -y --no-install-recommends runit && \
# Required for outposts
apt-get install -y --no-install-recommends openssh-client && \
pip install --no-cache-dir -r /requirements.txt && \
apt-get remove --purge -y build-essential pkg-config libxmlsec1-dev && \
apt-get autoremove --purge -y && \
@ -91,8 +93,9 @@ RUN apt-get update && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
mkdir -p /certs /media /blueprints && \
mkdir -p /authentik/.ssh && \
chown authentik:authentik /certs /media /authentik/.ssh
chown authentik:authentik /certs /media && \
chmod g+w /etc/ssh/ssh_config.d/ && \
chgrp authentik /etc/ssh/ssh_config.d/
COPY ./authentik/ /authentik
COPY ./pyproject.toml /

View File

@ -18,7 +18,6 @@ from authentik.core.api.utils import PassiveSerializer
from authentik.lib.utils.reflection import get_env
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost
from authentik.tenants.utils import get_tenant
class RuntimeDict(TypedDict):
@ -78,7 +77,7 @@ class SystemSerializer(PassiveSerializer):
def get_tenant(self, request: Request) -> str:
"""Currently active tenant"""
return str(get_tenant(request))
return str(request._request.tenant)
def get_server_time(self, request: Request) -> datetime:
"""Current server time"""

View File

@ -33,7 +33,6 @@ from authentik.flows.api.flows import FlowViewSet
from authentik.flows.api.stages import StageViewSet
from authentik.flows.views.executor import FlowExecutorView
from authentik.flows.views.inspector import FlowInspectorView
from authentik.interfaces.api import InterfaceViewSet
from authentik.outposts.api.outposts import OutpostViewSet
from authentik.outposts.api.service_connections import (
DockerServiceConnectionViewSet,
@ -124,8 +123,6 @@ router.register("core/user_consent", UserConsentViewSet)
router.register("core/tokens", TokenViewSet)
router.register("core/tenants", TenantViewSet)
router.register("interfaces", InterfaceViewSet)
router.register("outposts/instances", OutpostViewSet)
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)

View File

@ -10,6 +10,7 @@ from django.db.models.functions import ExtractHour
from django.db.models.query import QuerySet
from django.db.transaction import atomic
from django.db.utils import IntegrityError
from django.urls import reverse_lazy
from django.utils.http import urlencode
from django.utils.text import slugify
from django.utils.timezone import now
@ -71,12 +72,10 @@ from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
from authentik.tenants.utils import get_tenant
from authentik.tenants.models import Tenant
LOGGER = get_logger()
@ -322,7 +321,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
"""Create a recovery link (when the current tenant has a recovery flow set),
that can either be shown to an admin or sent to the user directly"""
tenant = get_tenant(self.request)
tenant: Tenant = self.request._request.tenant
# Check that there is a recovery flow, if not return an error
flow = tenant.flow_recovery
if not flow:
@ -351,12 +350,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
)
querystring = urlencode({QS_KEY_TOKEN: token.key})
link = self.request.build_absolute_uri(
reverse_interface(
self.request,
InterfaceType.FLOW,
flow_slug=flow.slug,
),
+f"?{querystring}",
reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
+ f"?{querystring}"
)
return link, token

View File

@ -33,7 +33,6 @@ from authentik.lib.models import (
)
from authentik.lib.utils.http import get_client_ip
from authentik.policies.models import PolicyBindingModel
from authentik.tenants.utils import get_tenant
LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
@ -169,7 +168,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
including the users attributes"""
final_attributes = {}
if request and hasattr(request, "tenant"):
always_merger.merge(final_attributes, get_tenant(request).attributes)
always_merger.merge(final_attributes, request.tenant.attributes)
for group in self.ak_groups.all().order_by("name"):
always_merger.merge(final_attributes, group.attributes)
always_merger.merge(final_attributes, self.attributes)
@ -228,7 +227,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
except Exception as exc:
LOGGER.warning("Failed to get default locale", exc=exc)
if request:
return get_tenant(request).default_locale
return request.tenant.locale
return ""
@property

View File

@ -25,8 +25,7 @@ from authentik.flows.planner import (
)
from authentik.flows.stage import StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_keys
@ -227,7 +226,7 @@ class SourceFlowManager:
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:root-redirect"
NEXT_ARG_NAME, "authentik_core:if-user"
)
kwargs.update(
{
@ -254,9 +253,9 @@ class SourceFlowManager:
for stage in stages:
plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_to_default_interface(
self.request,
InterfaceType.FLOW,
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=flow.slug,
)
@ -300,9 +299,8 @@ class SourceFlowManager:
_("Successfully linked %(source)s!" % {"source": self.source.name}),
)
return redirect(
# Not ideal that we don't directly redirect to the configured user interface
reverse(
"authentik_core:root-redirect",
"authentik_core:if-user",
)
+ f"#/settings;page-{self.source.slug}"
)

View File

@ -59,6 +59,4 @@ class TestImpersonation(TestCase):
self.client.force_login(self.other_user)
response = self.client.get(reverse("authentik_core:impersonate-end"))
self.assertRedirects(
response, reverse("authentik_interfaces:if", kwargs={"if_name": "user"})
)
self.assertRedirects(response, reverse("authentik_core:if-user"))

View File

@ -3,30 +3,23 @@ from channels.auth import AuthMiddleware
from channels.sessions import CookieMiddleware
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse
from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import RedirectView
from authentik.core.views import apps, impersonate
from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.session import EndSessionView
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import RedirectToInterface
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.messages.consumer import MessageConsumer
def placeholder_view(request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Empty view used as placeholder
(Mounted to websocket endpoints and used by e2e tests)"""
return HttpResponse(status_code=200)
urlpatterns = [
path(
"",
login_required(RedirectToInterface.as_view(type=InterfaceType.USER)),
login_required(
RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True)
),
name="root-redirect",
),
path(
@ -47,16 +40,31 @@ urlpatterns = [
name="impersonate-end",
),
# Interfaces
path(
"if/admin/",
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")),
name="if-admin",
),
path(
"if/user/",
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")),
name="if-user",
),
path(
"if/flow/<slug:flow_slug>/",
ensure_csrf_cookie(FlowInterfaceView.as_view()),
name="if-flow",
),
path(
"if/session-end/<slug:application_slug>/",
ensure_csrf_cookie(EndSessionView.as_view()),
name="if-session-end",
),
# Fallback for WS
path("ws/outpost/<uuid:pk>/", placeholder_view),
path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")),
path(
"ws/client/",
placeholder_view,
InterfaceView.as_view(template_name="if/admin.html"),
),
]

View File

@ -20,13 +20,11 @@ from authentik.flows.views.executor import (
SESSION_KEY_PLAN,
ToDefaultFlow,
)
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.utils.urls import redirect_with_qs
from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS,
)
from authentik.tenants.utils import get_tenant
class RedirectToAppLaunch(View):
@ -61,7 +59,7 @@ class RedirectToAppLaunch(View):
raise Http404
plan.insert_stage(in_memory_stage(RedirectToAppStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug)
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
class RedirectToAppStage(ChallengeStageView):

View File

@ -35,7 +35,7 @@ class ImpersonateInitView(View):
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return redirect("authentik_core:root-redirect")
return redirect("authentik_core:if-user")
class ImpersonateEndView(View):
@ -48,7 +48,7 @@ class ImpersonateEndView(View):
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
):
LOGGER.debug("Can't end impersonation", user=request.user)
return redirect("authentik_core:root-redirect")
return redirect("authentik_core:if-user")
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]

View File

@ -0,0 +1,36 @@
"""Interface views"""
from json import dumps
from typing import Any
from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView
from rest_framework.request import Request
from authentik import get_build_hash
from authentik.admin.tasks import LOCAL_VERSION
from authentik.api.v3.config import ConfigView
from authentik.flows.models import Flow
from authentik.tenants.api import CurrentTenantSerializer
class InterfaceView(TemplateView):
"""Base interface view"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data)
kwargs["tenant_json"] = dumps(CurrentTenantSerializer(self.request.tenant).data)
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
kwargs["build"] = get_build_hash()
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)

View File

@ -41,7 +41,8 @@ from authentik.lib.utils.http import get_client_ip, get_http_session
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.models import PolicyBindingModel
from authentik.stages.email.utils import TemplateEmailMessage
from authentik.tenants.utils import get_fallback_tenant, get_tenant
from authentik.tenants.models import Tenant
from authentik.tenants.utils import DEFAULT_TENANT
LOGGER = get_logger()
if TYPE_CHECKING:
@ -56,7 +57,7 @@ def default_event_duration():
def default_tenant():
"""Get a default value for tenant"""
return sanitize_dict(model_to_dict(get_fallback_tenant()))
return sanitize_dict(model_to_dict(DEFAULT_TENANT))
class NotificationTransportError(SentryIgnoredException):
@ -226,7 +227,7 @@ class Event(SerializerModel, ExpiringModel):
wrapped = self.context["http_request"]["args"][QS_QUERY]
self.context["http_request"]["args"] = QueryDict(wrapped)
if hasattr(request, "tenant"):
tenant = get_tenant(request)
tenant: Tenant = request.tenant
# Because self.created only gets set on save, we can't use it's value here
# hence we set self.created to now and then use it
self.created = now()

View File

@ -57,10 +57,6 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
LOGGER.debug("e(trigger): attempting to prevent infinite loop", trigger=trigger)
return
if not trigger.group:
LOGGER.debug("e(trigger): trigger has no group", trigger=trigger)
return
LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
try:
user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
@ -77,6 +73,10 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
if not result.passing:
return
if not trigger.group:
LOGGER.debug("e(trigger): trigger has no group", trigger=trigger)
return
LOGGER.debug("e(trigger): event trigger matched", trigger=trigger)
# Create the notification objects
for transport in trigger.transports.all():

View File

@ -25,8 +25,6 @@ from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow
from authentik.flows.planner import CACHE_PREFIX, PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface
from authentik.lib.utils.file import (
FilePathSerializer,
FileUploadSerializer,
@ -296,11 +294,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
return Response(
{
"link": request._request.build_absolute_uri(
reverse_interface(
request,
InterfaceType.FLOW,
flow_slug=flow.slug,
),
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
)
}
)

View File

@ -7,8 +7,6 @@ from authentik.core.tests.utils import create_test_flow
from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.tests import reverse_interface
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider
@ -23,10 +21,7 @@ class TestHelperView(TestCase):
response = self.client.get(
reverse("authentik_flows:default-invalidation"),
)
expected_url = reverse_interface(
InterfaceType.FLOW,
flow_slug=flow.slug,
)
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url)
@ -77,9 +72,6 @@ class TestHelperView(TestCase):
response = self.client.get(
reverse("authentik_flows:default-invalidation"),
)
expected_url = reverse_interface(
InterfaceType.FLOW,
flow_slug=flow.slug,
)
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, expected_url)

View File

@ -53,14 +53,12 @@ from authentik.flows.planner import (
FlowPlanner,
)
from authentik.flows.stage import AccessDeniedChallengeView, StageView
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.reflection import all_subclasses, class_to_path
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
from authentik.policies.engine import PolicyEngine
from authentik.tenants.utils import get_tenant
from authentik.tenants.models import Tenant
LOGGER = get_logger()
# Argument used to redirect user after login
@ -481,7 +479,7 @@ class ToDefaultFlow(View):
def get_flow(self) -> Flow:
"""Get a flow for the selected designation"""
tenant = get_tenant(self.request)
tenant: Tenant = self.request.tenant
flow = None
# First, attempt to get default flow from tenant
if self.designation == FlowDesignation.AUTHENTICATION:
@ -514,7 +512,7 @@ class ToDefaultFlow(View):
flow_slug=flow.slug,
)
del self.request.session[SESSION_KEY_PLAN]
return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug)
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse:
@ -585,8 +583,8 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
LOGGER.warning("Flow not applicable to user")
raise Http404
request.session[SESSION_KEY_PLAN] = plan
return redirect_to_default_interface(
self.request,
InterfaceType.FLOW,
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=stage.configure_flow.slug,
)

View File

@ -1,28 +0,0 @@
"""interfaces API"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.interfaces.models import Interface
class InterfaceSerializer(ModelSerializer):
"""Interface serializer"""
class Meta:
model = Interface
fields = [
"interface_uuid",
"url_name",
"type",
"template",
]
class InterfaceViewSet(UsedByMixin, ModelViewSet):
"""Interface serializer"""
queryset = Interface.objects.all()
serializer_class = InterfaceSerializer
filterset_fields = ["url_name", "type", "template"]
search_fields = ["url_name", "type", "template"]

View File

@ -1,12 +0,0 @@
"""authentik interfaces app config"""
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikInterfacesConfig(ManagedAppConfig):
"""authentik interfaces app config"""
name = "authentik.interfaces"
label = "authentik_interfaces"
verbose_name = "authentik Interfaces"
mountpoint = "if/"
default = True

View File

@ -1,36 +0,0 @@
# Generated by Django 4.1.7 on 2023-02-16 11:01
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Interface",
fields=[
(
"interface_uuid",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
("url_name", models.SlugField(unique=True)),
(
"type",
models.TextField(
choices=[("user", "User"), ("admin", "Admin"), ("flow", "Flow")]
),
),
("template", models.TextField()),
],
options={
"abstract": False,
},
),
]

View File

@ -1,33 +0,0 @@
"""Interface models"""
from typing import Type
from uuid import uuid4
from django.db import models
from rest_framework.serializers import BaseSerializer
from authentik.lib.models import SerializerModel
class InterfaceType(models.TextChoices):
"""Interface types"""
USER = "user"
ADMIN = "admin"
FLOW = "flow"
class Interface(SerializerModel):
"""Interface"""
interface_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
url_name = models.SlugField(unique=True)
type = models.TextField(choices=InterfaceType.choices)
template = models.TextField()
@property
def serializer(self) -> Type[BaseSerializer]:
from authentik.interfaces.api import InterfaceSerializer
return InterfaceSerializer

View File

@ -1,12 +0,0 @@
"""Interface tests"""
from django.test import RequestFactory
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface as full_reverse_interface
def reverse_interface(interface_type: InterfaceType, **kwargs):
"""reverse_interface wrapper for tests"""
factory = RequestFactory()
request = factory.get("/")
return full_reverse_interface(request, interface_type, **kwargs)

View File

@ -1,14 +0,0 @@
"""Interface urls"""
from django.urls import path
from authentik.interfaces.views import InterfaceView
urlpatterns = [
path(
"<slug:if_name>/",
InterfaceView.as_view(),
kwargs={"flow_slug": None},
name="if",
),
path("<slug:if_name>/<slug:flow_slug>/", InterfaceView.as_view(), name="if"),
]

View File

@ -1,113 +0,0 @@
"""Interface views"""
from json import dumps
from typing import Any, Optional
from urllib.parse import urlencode
from django.http import Http404, HttpRequest, HttpResponse, QueryDict
from django.shortcuts import get_object_or_404, redirect
from django.template import Template, TemplateSyntaxError, engines
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import ensure_csrf_cookie
from rest_framework.request import Request
from structlog.stdlib import get_logger
from authentik import get_build_hash
from authentik.admin.tasks import LOCAL_VERSION
from authentik.api.v3.config import ConfigView
from authentik.flows.models import Flow
from authentik.interfaces.models import Interface, InterfaceType
from authentik.lib.utils.urls import reverse_with_qs
from authentik.tenants.api import CurrentTenantSerializer
from authentik.tenants.utils import get_tenant
LOGGER = get_logger()
def template_from_string(template_string: str) -> Template:
"""Render template from string"""
chain = []
engine_list = engines.all()
for engine in engine_list:
try:
return engine.from_string(template_string)
except TemplateSyntaxError as exc:
chain.append(exc)
raise TemplateSyntaxError(template_string, chain=chain)
def redirect_to_default_interface(request: HttpRequest, interface_type: InterfaceType, **kwargs):
"""Shortcut to inline redirect to default interface,
keeping GET parameters of the passed request"""
return RedirectToInterface.as_view(type=interface_type)(request, **kwargs)
def reverse_interface(
request: HttpRequest, interface_type: InterfaceType, query: Optional[QueryDict] = None, **kwargs
):
"""Reverse URL to configured default interface"""
tenant = get_tenant(request)
interface: Interface = None
if interface_type == InterfaceType.USER:
interface = tenant.interface_user
if interface_type == InterfaceType.ADMIN:
interface = tenant.interface_admin
if interface_type == InterfaceType.FLOW:
interface = tenant.interface_flow
if not interface:
LOGGER.warning("No interface found", type=interface_type, tenant=tenant)
raise Http404()
kwargs["if_name"] = interface.url_name
return reverse_with_qs(
"authentik_interfaces:if",
query=query or request.GET,
kwargs=kwargs,
)
class RedirectToInterface(View):
"""Redirect to tenant's configured view for specified type"""
type: Optional[InterfaceType] = None
def dispatch(self, request: HttpRequest, **kwargs: Any) -> HttpResponse:
target = reverse_interface(request, self.type, **kwargs)
if self.request.GET:
target += "?" + urlencode(self.request.GET.items())
return redirect(target)
@method_decorator(ensure_csrf_cookie, name="dispatch")
@method_decorator(cache_page(60 * 10), name="dispatch")
class InterfaceView(View):
"""General interface view"""
def get_context_data(self) -> dict[str, Any]:
"""Get template context"""
return {
"config_json": dumps(ConfigView(request=Request(self.request)).get_config().data),
"tenant_json": dumps(CurrentTenantSerializer(get_tenant(self.request)).data),
"version_family": f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}",
"version_subdomain": f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}",
"build": get_build_hash(),
}
def type_flow(self, context: dict[str, Any]):
"""Special handling for flow interfaces"""
if self.kwargs.get("flow_slug", None) is None:
raise Http404()
context["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
context["inspector"] = "inspector" in self.request.GET
def dispatch(self, request: HttpRequest, if_name: str, **kwargs: Any) -> HttpResponse:
context = self.get_context_data()
# TODO: Cache
interface: Interface = get_object_or_404(Interface, url_name=if_name)
if interface.type == InterfaceType.FLOW:
self.type_flow(context)
template = template_from_string(interface.template)
return TemplateResponse(request, template, context)

View File

@ -67,7 +67,7 @@ def sentry_init(**sentry_init_kwargs):
ArgvIntegration(),
StdlibIntegration(),
DjangoIntegration(transaction_style="function_name"),
CeleryIntegration(monitor_beat_tasks=True),
CeleryIntegration(),
RedisIntegration(),
ThreadingIntegration(propagate_hub=True),
SocketIntegration(),

View File

@ -1,4 +1,5 @@
"""Docker controller"""
from subprocess import SubprocessError # nosec
from time import sleep
from typing import Optional
from urllib.parse import urlparse
@ -9,7 +10,6 @@ from docker import DockerClient as UpstreamDockerClient
from docker.errors import DockerException, NotFound
from docker.models.containers import Container
from docker.utils.utils import kwargs_from_env
from paramiko.ssh_exception import SSHException
from structlog.stdlib import get_logger
from yaml import safe_dump
@ -58,8 +58,9 @@ class DockerClient(UpstreamDockerClient, BaseClient):
super().__init__(
base_url=connection.url,
tls=tls_config,
use_ssh_client=True,
)
except SSHException as exc:
except SubprocessError as exc:
if self.ssh:
self.ssh.cleanup()
raise ServiceConnectionInvalid(exc) from exc

View File

@ -7,8 +7,7 @@ from docker.errors import DockerException
from authentik.crypto.models import CertificateKeyPair
HEADER = "### Managed by authentik"
FOOTER = "### End Managed by authentik"
SSH_CONFIG_DIR = Path("/etc/ssh/ssh_config.d/")
def opener(path, flags):
@ -28,70 +27,54 @@ class DockerInlineSSH:
key_path: str
config_path: Path
header: str
def __init__(self, host: str, keypair: CertificateKeyPair) -> None:
self.host = host
self.keypair = keypair
self.config_path = Path("~/.ssh/config").expanduser()
if self.config_path.exists() and HEADER not in self.config_path.read_text(encoding="utf-8"):
# SSH Config file already exists and there's no header from us, meaning that it's
# been externally mapped into the container for more complex configs
raise SSHManagedExternallyException(
"SSH Config exists and does not contain authentik header"
)
self.config_path = SSH_CONFIG_DIR / Path(self.host + ".conf")
with open(self.config_path, "w", encoding="utf-8") as _config:
if not _config.writable():
# SSH Config file already exists and there's no header from us, meaning that it's
# been externally mapped into the container for more complex configs
raise SSHManagedExternallyException(
"SSH Config exists and does not contain authentik header"
)
if not self.keypair:
raise DockerException("keypair must be set for SSH connections")
self.header = f"{HEADER} - {self.host}\n"
def write_config(self, key_path: str) -> bool:
def write_config(self, key_path: str):
"""Update the local user's ssh config file"""
with open(self.config_path, "a+", encoding="utf-8") as ssh_config:
if self.header in ssh_config.readlines():
return False
with open(self.config_path, "w", encoding="utf-8") as ssh_config:
ssh_config.writelines(
[
self.header,
f"Host {self.host}\n",
f" IdentityFile {key_path}\n",
f" IdentityFile {str(key_path)}\n",
" StrictHostKeyChecking No\n",
" UserKnownHostsFile /dev/null\n",
f"{FOOTER}\n",
"\n",
]
)
return True
def write_key(self):
def write_key(self) -> Path:
"""Write keypair's private key to a temporary file"""
path = Path(gettempdir(), f"{self.keypair.pk}_private.pem")
with open(path, "w", encoding="utf8", opener=opener) as _file:
_file.write(self.keypair.key_data)
return str(path)
return path
def write(self):
"""Write keyfile and update ssh config"""
self.key_path = self.write_key()
was_written = self.write_config(self.key_path)
if not was_written:
try:
self.write_config(self.key_path)
except OSError:
self.cleanup()
def cleanup(self):
"""Cleanup when we're done"""
try:
os.unlink(self.key_path)
with open(self.config_path, "r", encoding="utf-8") as ssh_config:
start = 0
end = 0
lines = ssh_config.readlines()
for idx, line in enumerate(lines):
if line == self.header:
start = idx
if start != 0 and line == f"{FOOTER}\n":
end = idx
with open(self.config_path, "w+", encoding="utf-8") as ssh_config:
lines = lines[:start] + lines[end + 2 :]
ssh_config.writelines(lines)
os.unlink(self.config_path)
except OSError:
# If we fail deleting a file it doesn't matter that much
# since we're just in a container

View File

@ -12,7 +12,6 @@ from authentik.lib.utils.http import get_http_session
from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.tenants.utils import get_tenant
LOGGER = get_logger()
RE_LOWER = re.compile("[a-z]")
@ -144,8 +143,7 @@ class PasswordPolicy(Policy):
user_inputs.append(request.user.name)
user_inputs.append(request.user.email)
if request.http_request:
tenant = get_tenant(request.http_request)
user_inputs.append(tenant.branding_title)
user_inputs.append(request.http_request.tenant.branding_title)
# Only calculate result for the first 100 characters, as with over 100 char
# long passwords we can be reasonably sure that they'll surpass the score anyways
# See https://github.com/dropbox/zxcvbn#runtime-latency

View File

@ -39,9 +39,8 @@ class TesOAuth2DeviceInit(OAuthTestCase):
self.assertEqual(
res.url,
reverse(
"authentik_interfaces:if",
"authentik_core:if-flow",
kwargs={
"if_name": "flow",
"flow_slug": self.device_flow.slug,
},
),
@ -69,9 +68,8 @@ class TesOAuth2DeviceInit(OAuthTestCase):
self.assertEqual(
res.url,
reverse(
"authentik_interfaces:if",
"authentik_core:if-flow",
kwargs={
"if_name": "flow",
"flow_slug": self.provider.authorization_flow.slug,
},
)

View File

@ -29,9 +29,8 @@ from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.stage import StageView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.policies.types import PolicyRequest
from authentik.policies.views import PolicyAccessView, RequestValidationError
@ -405,9 +404,9 @@ class AuthorizationFlowInitView(PolicyAccessView):
plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_to_default_interface(
self.request,
InterfaceType.FLOW,
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=self.provider.authorization_flow.slug,
)

View File

@ -15,8 +15,7 @@ from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.utils.urls import redirect_with_qs
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.views.device_finish import (
PLAN_CONTEXT_DEVICE,
@ -27,7 +26,7 @@ from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS,
)
from authentik.tenants.utils import get_tenant
from authentik.tenants.models import Tenant
LOGGER = get_logger()
QS_KEY_CODE = "code" # nosec
@ -78,9 +77,9 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]:
return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_to_default_interface(
request,
InterfaceType.FLOW,
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=token.provider.authorization_flow.slug,
)
@ -89,7 +88,7 @@ class DeviceEntryView(View):
"""View used to initiate the device-code flow, url entered by endusers"""
def dispatch(self, request: HttpRequest) -> HttpResponse:
tenant = get_tenant(request)
tenant: Tenant = request.tenant
device_flow = tenant.flow_device_code
if not device_flow:
LOGGER.info("Tenant has no device code flow configured", tenant=tenant)
@ -111,9 +110,9 @@ class DeviceEntryView(View):
plan.append_stage(in_memory_stage(OAuthDeviceCodeStage))
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_to_default_interface(
self.request,
InterfaceType.FLOW,
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=device_flow.slug,
)

View File

@ -9,7 +9,6 @@ from django.views.decorators.csrf import csrf_exempt
from authentik.providers.oauth2.constants import SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER_EMAIL
from authentik.providers.oauth2.models import RefreshToken
from authentik.providers.oauth2.utils import protected_resource_view
from authentik.tenants.utils import get_tenant
@method_decorator(csrf_exempt, name="dispatch")
@ -77,7 +76,6 @@ class GitHubUserTeamsView(View):
def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse:
"""Emulate GitHub's /user/teams API Endpoint"""
user = token.user
tenant = get_tenant(request)
orgs_response = []
for org in user.ak_groups.all():
@ -99,7 +97,7 @@ class GitHubUserTeamsView(View):
"created_at": "",
"updated_at": "",
"organization": {
"login": slugify(tenant.branding_title),
"login": slugify(request.tenant.branding_title),
"id": 1,
"node_id": "",
"url": "",
@ -111,7 +109,7 @@ class GitHubUserTeamsView(View):
"public_members_url": "",
"avatar_url": "",
"description": "",
"name": tenant.branding_title,
"name": request.tenant.branding_title,
"company": "",
"blog": "",
"location": "",

View File

@ -15,8 +15,7 @@ from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion
@ -77,9 +76,9 @@ class SAMLSSOView(PolicyAccessView):
raise Http404
plan.append_stage(in_memory_stage(SAMLFlowFinalView))
request.session[SESSION_KEY_PLAN] = plan
return redirect_to_default_interface(
request,
InterfaceType.FLOW,
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=self.provider.authorization_flow.slug,
)

View File

@ -22,4 +22,4 @@ class UseTokenView(View):
login(request, token.user, backend=BACKEND_INBUILT)
token.delete()
messages.warning(request, _("Used recovery-link to authenticate."))
return redirect("authentik_core:root-redirect")
return redirect("authentik_core:if-user")

View File

@ -65,7 +65,6 @@ INSTALLED_APPS = [
"authentik.admin",
"authentik.api",
"authentik.crypto",
"authentik.interfaces",
"authentik.events",
"authentik.flows",
"authentik.lib",

View File

@ -32,8 +32,7 @@ from authentik.flows.planner import (
)
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import redirect_to_default_interface
from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
@ -73,7 +72,7 @@ class InitiateView(View):
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:root-redirect"
NEXT_ARG_NAME, "authentik_core:if-user"
)
kwargs.update(
{
@ -92,9 +91,9 @@ class InitiateView(View):
for stage in stages_to_append:
plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_to_default_interface(
self.request,
InterfaceType.FLOW,
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=source.pre_authentication_flow.slug,
)

View File

@ -17,7 +17,6 @@ from authentik.flows.challenge import (
from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER
from authentik.tenants.utils import get_tenant
SESSION_TOTP_DEVICE = "totp_device"
@ -58,7 +57,7 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
data={
"type": ChallengeTypes.NATIVE.value,
"config_url": device.config_url.replace(
OTP_TOTP_ISSUER, quote(get_tenant(self.request).branding_title)
OTP_TOTP_ISSUER, quote(self.request.tenant.branding_title)
),
}
)

View File

@ -33,7 +33,6 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
from authentik.tenants.utils import get_tenant
LOGGER = get_logger()
@ -188,7 +187,7 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
type=__(
"%(brand_name)s Login request"
% {
"brand_name": get_tenant(stage_view.request).branding_title,
"brand_name": stage_view.request.tenant.branding_title,
}
),
display_username=user.username,

View File

@ -19,7 +19,7 @@ from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, Duo
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.user_login.models import UserLoginStage
from authentik.tenants.utils import lookup_tenant_for_request
from authentik.tenants.utils import get_tenant_for_request
class AuthenticatorValidateStageDuoTests(FlowTestCase):
@ -36,7 +36,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
setattr(request, "tenant", lookup_tenant_for_request(request))
setattr(request, "tenant", get_tenant_for_request(request))
stage = AuthenticatorDuoStage.objects.create(
name=generate_id(),

View File

@ -29,7 +29,6 @@ from authentik.flows.challenge import (
from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
from authentik.tenants.utils import get_tenant
SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge"
@ -93,7 +92,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
rp_id=get_rp_id(self.request),
rp_name=get_tenant(self.request).branding_title,
rp_name=self.request.tenant.branding_title,
user_id=user.uid,
user_name=user.username,
user_display_name=user.name,

View File

@ -3,6 +3,7 @@ from datetime import timedelta
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.text import slugify
from django.utils.timezone import now
@ -15,8 +16,6 @@ from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -48,10 +47,9 @@ class EmailStageView(ChallengeStageView):
def get_full_url(self, **kwargs) -> str:
"""Get full URL to be used in template"""
base_url = reverse_interface(
self.request,
InterfaceType.FLOW,
flow_slug=self.executor.flow.slug,
base_url = reverse(
"authentik_core:if-flow",
kwargs={"flow_slug": self.executor.flow.slug},
)
relative_url = f"{base_url}?{urlencode(kwargs)}"
return self.request.build_absolute_uri(relative_url)

View File

@ -7,7 +7,6 @@ from django.core.mail.backends.locmem import EmailBackend
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker
@ -30,7 +29,6 @@ class TestEmailStageSending(APITestCase):
)
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
@apply_blueprint("system/interfaces.yaml")
def test_pending_user(self):
"""Test with pending user"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
@ -56,7 +54,6 @@ class TestEmailStageSending(APITestCase):
self.assertEqual(event.context["to_email"], [self.user.email])
self.assertEqual(event.context["from_email"], "system@authentik.local")
@apply_blueprint("system/interfaces.yaml")
def test_send_error(self):
"""Test error during sending (sending will be retried)"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])

View File

@ -7,7 +7,6 @@ from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
from django.urls import reverse
from django.utils.http import urlencode
from authentik.blueprints.tests import apply_blueprint
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
@ -75,7 +74,6 @@ class TestEmailStage(FlowTestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
@apply_blueprint("system/interfaces.yaml")
@patch(
"authentik.stages.email.models.EmailStage.backend_class",
PropertyMock(return_value=EmailBackend),
@ -125,7 +123,6 @@ class TestEmailStage(FlowTestCase):
with self.settings(EMAIL_HOST=host):
self.assertEqual(EmailStage(use_global_settings=True).backend.host, host)
@apply_blueprint("system/interfaces.yaml")
def test_token(self):
"""Test with token"""
# Make sure token exists

View File

@ -26,9 +26,8 @@ from authentik.flows.models import FlowDesignation
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_GET
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface
from authentik.lib.utils.http import get_client_ip
from authentik.lib.utils.urls import reverse_with_qs
from authentik.sources.oauth.types.apple import AppleLoginChallenge
from authentik.sources.plex.models import PlexAuthenticationChallenge
from authentik.stages.identification.models import IdentificationStage
@ -206,25 +205,22 @@ class IdentificationStageView(ChallengeStageView):
get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET)
# Check for related enrollment and recovery flow, add URL to view
if current_stage.enrollment_flow:
challenge.initial_data["enroll_url"] = reverse_interface(
self.request,
InterfaceType.FLOW,
challenge.initial_data["enroll_url"] = reverse_with_qs(
"authentik_core:if-flow",
query=get_qs,
flow_slug=current_stage.enrollment_flow.slug,
kwargs={"flow_slug": current_stage.enrollment_flow.slug},
)
if current_stage.recovery_flow:
challenge.initial_data["recovery_url"] = reverse_interface(
self.request,
InterfaceType.FLOW,
challenge.initial_data["recovery_url"] = reverse_with_qs(
"authentik_core:if-flow",
query=get_qs,
flow_slug=current_stage.recovery_flow.slug,
kwargs={"flow_slug": current_stage.recovery_flow.slug},
)
if current_stage.passwordless_flow:
challenge.initial_data["passwordless_url"] = reverse_interface(
self.request,
InterfaceType.FLOW,
challenge.initial_data["passwordless_url"] = reverse_with_qs(
"authentik_core:if-flow",
query=get_qs,
flow_slug=current_stage.passwordless_flow.slug,
kwargs={"flow_slug": current_stage.passwordless_flow.slug},
)
# Check all enabled source, add them if they have a UI Login button.

View File

@ -5,8 +5,6 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.tests import FlowTestCase
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.tests import reverse_interface
from authentik.sources.oauth.models import OAuthSource
from authentik.stages.identification.models import IdentificationStage, UserFields
from authentik.stages.password import BACKEND_INBUILT
@ -168,9 +166,9 @@ class TestIdentificationStage(FlowTestCase):
component="ak-stage-identification",
user_fields=["email"],
password_fields=False,
enroll_url=reverse_interface(
InterfaceType.FLOW,
flow_slug=flow.slug,
enroll_url=reverse(
"authentik_core:if-flow",
kwargs={"flow_slug": flow.slug},
),
show_source_labels=False,
primary_action="Log in",
@ -206,9 +204,9 @@ class TestIdentificationStage(FlowTestCase):
component="ak-stage-identification",
user_fields=["email"],
password_fields=False,
recovery_url=reverse_interface(
InterfaceType.FLOW,
flow_slug=flow.slug,
recovery_url=reverse(
"authentik_core:if-flow",
kwargs={"flow_slug": flow.slug},
),
show_source_labels=False,
primary_action="Log in",

View File

@ -5,6 +5,7 @@ from django.contrib.auth import _clean_credentials
from django.contrib.auth.backends import BaseBackend
from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, HttpResponse
from django.urls import reverse
from django.utils.translation import gettext as _
from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.fields import CharField
@ -22,8 +23,6 @@ from authentik.flows.challenge import (
from authentik.flows.models import Flow, FlowDesignation, Stage
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.interfaces.models import InterfaceType
from authentik.interfaces.views import reverse_interface
from authentik.lib.utils.reflection import path_to_class
from authentik.stages.password.models import PasswordStage
@ -96,12 +95,11 @@ class PasswordStageView(ChallengeStageView):
"type": ChallengeTypes.NATIVE.value,
}
)
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY).first()
if recovery_flow:
recover_url = reverse_interface(
self.request,
InterfaceType.FLOW,
flow_slug=recovery_flow.slug,
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
if recovery_flow.exists():
recover_url = reverse(
"authentik_core:if-flow",
kwargs={"flow_slug": recovery_flow.first().slug},
)
challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url)
return challenge

View File

@ -18,7 +18,6 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant
from authentik.tenants.utils import get_tenant
class FooterLinkSerializer(PassiveSerializer):
@ -55,9 +54,6 @@ class TenantSerializer(ModelSerializer):
"flow_unenrollment",
"flow_user_settings",
"flow_device_code",
"interface_admin",
"interface_user",
"interface_flow",
"event_retention",
"web_certificate",
"attributes",
@ -124,9 +120,6 @@ class TenantViewSet(UsedByMixin, ModelViewSet):
"flow_unenrollment",
"flow_user_settings",
"flow_device_code",
"interface_admin",
"interface_user",
"interface_flow",
"event_retention",
"web_certificate",
]
@ -140,4 +133,5 @@ class TenantViewSet(UsedByMixin, ModelViewSet):
@action(methods=["GET"], detail=False, permission_classes=[AllowAny])
def current(self, request: Request) -> Response:
"""Get current tenant"""
return Response(CurrentTenantSerializer(get_tenant(request)).data)
tenant: Tenant = request._request.tenant
return Response(CurrentTenantSerializer(tenant).data)

View File

@ -6,7 +6,7 @@ from django.http.response import HttpResponse
from django.utils.translation import activate
from sentry_sdk.api import set_tag
from authentik.tenants.utils import lookup_tenant_for_request
from authentik.tenants.utils import get_tenant_for_request
class TenantMiddleware:
@ -19,7 +19,7 @@ class TenantMiddleware:
def __call__(self, request: HttpRequest) -> HttpResponse:
if not hasattr(request, "tenant"):
tenant = lookup_tenant_for_request(request)
tenant = get_tenant_for_request(request)
setattr(request, "tenant", tenant)
set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
set_tag("authentik.tenant_domain", tenant.domain)

View File

@ -1,78 +0,0 @@
# Generated by Django 4.1.7 on 2023-02-21 14:18
import django.db.models.deletion
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_set_default(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Tenant = apps.get_model("authentik_tenants", "tenant")
Interface = apps.get_model("authentik_interfaces", "Interface")
db_alias = schema_editor.connection.alias
from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.tasks import blueprints_discovery
from authentik.interfaces.models import InterfaceType
# If we don't have any tenants yet, we don't need wait for the default interface blueprint
if not Tenant.objects.using(db_alias).exists():
return
interface_blueprint = BlueprintInstance.objects.filter(path="system/interfaces.yaml").first()
if not interface_blueprint:
blueprints_discovery.delay().get()
interface_blueprint = BlueprintInstance.objects.filter(
path="system/interfaces.yaml"
).first()
if not interface_blueprint:
raise ValueError("Failed to apply system/interfaces.yaml blueprint")
Importer(interface_blueprint.retrieve()).apply()
for tenant in Tenant.objects.using(db_alias).all():
tenant.interface_admin = Interface.objects.filter(type=InterfaceType.ADMIN).first()
tenant.interface_user = Interface.objects.filter(type=InterfaceType.USER).first()
tenant.interface_flow = Interface.objects.filter(type=InterfaceType.FLOW).first()
tenant.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_interfaces", "0001_initial"),
("authentik_tenants", "0004_tenant_flow_device_code"),
]
operations = [
migrations.AddField(
model_name="tenant",
name="interface_admin",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="tenant_admin",
to="authentik_interfaces.interface",
),
),
migrations.AddField(
model_name="tenant",
name="interface_flow",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="tenant_flow",
to="authentik_interfaces.interface",
),
),
migrations.AddField(
model_name="tenant",
name="interface_user",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="tenant_user",
to="authentik_interfaces.interface",
),
),
migrations.RunPython(migrate_set_default),
]

View File

@ -7,6 +7,7 @@ from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.lib.models import SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
@ -32,59 +33,22 @@ class Tenant(SerializerModel):
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
flow_authentication = models.ForeignKey(
"authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_authentication",
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_authentication"
)
flow_invalidation = models.ForeignKey(
"authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_invalidation",
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_invalidation"
)
flow_recovery = models.ForeignKey(
"authentik_flows.Flow", null=True, on_delete=models.SET_NULL, related_name="tenant_recovery"
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_recovery"
)
flow_unenrollment = models.ForeignKey(
"authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_unenrollment",
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_unenrollment"
)
flow_user_settings = models.ForeignKey(
"authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_user_settings",
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_user_settings"
)
flow_device_code = models.ForeignKey(
"authentik_flows.Flow",
null=True,
on_delete=models.SET_NULL,
related_name="tenant_device_code",
)
interface_flow = models.ForeignKey(
"authentik_interfaces.Interface",
default=None,
on_delete=models.SET_NULL,
null=True,
related_name="tenant_flow",
)
interface_user = models.ForeignKey(
"authentik_interfaces.Interface",
default=None,
on_delete=models.SET_NULL,
null=True,
related_name="tenant_user",
)
interface_admin = models.ForeignKey(
"authentik_interfaces.Interface",
default=None,
on_delete=models.SET_NULL,
null=True,
related_name="tenant_admin",
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_device_code"
)
event_retention = models.TextField(

View File

@ -75,7 +75,7 @@ class TestTenants(APITestCase):
)
factory = RequestFactory()
request = factory.get("/")
setattr(request, "tenant", tenant)
request.tenant = tenant
event = Event.new(action=EventAction.SYSTEM_EXCEPTION, message="test").from_http(request)
self.assertEqual(event.expires.day, (event.created + timedelta_from_string("weeks=3")).day)
self.assertEqual(

View File

@ -4,41 +4,17 @@ from typing import Any
from django.db.models import F, Q
from django.db.models import Value as V
from django.http.request import HttpRequest
from rest_framework.request import Request
from sentry_sdk.hub import Hub
from authentik import get_full_version
from authentik.interfaces.models import Interface, InterfaceType
from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant
_q_default = Q(default=True)
DEFAULT_TENANT = Tenant(domain="fallback")
def get_fallback_tenant():
"""Get fallback tenant"""
fallback_interface = Interface(
url_name="fallback",
type=InterfaceType.FLOW,
template="Fallback interface",
)
return Tenant(
domain="fallback",
interface_flow=fallback_interface,
interface_user=fallback_interface,
interface_admin=fallback_interface,
)
def get_tenant(request: HttpRequest | Request) -> "Tenant":
"""Get the request's tenant, falls back to a fallback tenant object"""
if isinstance(request, Request):
request = request._request
return getattr(request, "tenant", get_fallback_tenant())
def lookup_tenant_for_request(request: HttpRequest) -> "Tenant":
def get_tenant_for_request(request: HttpRequest) -> Tenant:
"""Get tenant object for current request"""
db_tenants = (
Tenant.objects.annotate(host_domain=V(request.get_host()))
@ -47,13 +23,13 @@ def lookup_tenant_for_request(request: HttpRequest) -> "Tenant":
)
tenants = list(db_tenants.all())
if len(tenants) < 1:
return get_fallback_tenant()
return DEFAULT_TENANT
return tenants[0]
def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects tenant object into every template"""
tenant = getattr(request, "tenant", get_fallback_tenant())
tenant = getattr(request, "tenant", DEFAULT_TENANT)
trace = ""
span = Hub.current.scope.span
if span:

View File

@ -2,11 +2,6 @@ metadata:
name: Default - Tenant
version: 1
entries:
- model: authentik_blueprints.metaapplyblueprint
attrs:
identifiers:
name: System - Interfaces
required: false
- model: authentik_blueprints.metaapplyblueprint
attrs:
identifiers:
@ -26,9 +21,6 @@ entries:
flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]]
flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]]
interface_admin: !Find [authentik_interfaces.Interface, [type, admin]]
interface_user: !Find [authentik_interfaces.Interface, [type, user]]
interface_flow: !Find [authentik_interfaces.Interface, [type, flow]]
identifiers:
domain: authentik-default
default: True

View File

@ -61,7 +61,6 @@
"authentik_events.notificationwebhookmapping",
"authentik_flows.flow",
"authentik_flows.flowstagebinding",
"authentik_interfaces.interface",
"authentik_outposts.dockerserviceconnection",
"authentik_outposts.kubernetesserviceconnection",
"authentik_outposts.outpost",

View File

@ -1,139 +0,0 @@
version: 1
metadata:
labels:
blueprints.goauthentik.io/system: "true"
name: System - Interfaces
entries:
- model: authentik_interfaces.interface
identifiers:
url_name: user
type: user
attrs:
template: |
{% extends "base/skeleton.html" %}
{% load static %}
{% load i18n %}
{% block head %}
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
<link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
{% include "base/header_js.html" %}
{% endblock %}
{% block body %}
<ak-message-container></ak-message-container>
<ak-interface-user>
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
<h1 class="pf-c-title pf-m-lg">
{% trans "Loading..." %}
</h1>
</div>
</div>
</section>
</ak-interface-user>
{% endblock %}
- model: authentik_interfaces.interface
identifiers:
url_name: admin
type: admin
attrs:
template: |
{% extends "base/skeleton.html" %}
{% load static %}
{% load i18n %}
{% block head %}
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
{% include "base/header_js.html" %}
{% endblock %}
{% block body %}
<ak-message-container></ak-message-container>
<ak-interface-admin>
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
<h1 class="pf-c-title pf-m-lg">
{% trans "Loading..." %}
</h1>
</div>
</div>
</section>
</ak-interface-admin>
{% endblock %}
- model: authentik_interfaces.interface
identifiers:
url_name: flow
type: flow
attrs:
template: |
{% extends "base/skeleton.html" %}
{% load static %}
{% load i18n %}
{% block head_before %}
{{ block.super }}
<link rel="prefetch" href="{{ flow.background_url }}" />
<link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
{% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: !navigator.webdriver };</script>
{% endif %}
{% include "base/header_js.html" %}
<script>
window.authentik.flow = {
"layout": "{{ flow.layout }}",
};
</script>
{% endblock %}
{% block head %}
<script src="{% static 'dist/flow/FlowInterface.js' %}?version={{ version }}" type="module"></script>
<style>
:root {
--ak-flow-background: url("{{ flow.background_url }}");
}
</style>
{% endblock %}
{% block body %}
<ak-message-container></ak-message-container>
<ak-flow-executor>
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state" style="height: 100vh;">
<div class="pf-c-empty-state__content">
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
<h1 class="pf-c-title pf-m-lg">
{% trans "Loading..." %}
</h1>
</div>
</div>
</section>
</ak-flow-executor>
{% endblock %}

View File

@ -1,5 +1,5 @@
---
version: '3.4'
version: "3.4"
services:
postgresql:
@ -14,9 +14,9 @@ services:
volumes:
- database:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${PG_PASS:?database password required}
- POSTGRES_USER=${PG_USER:-authentik}
- POSTGRES_DB=${PG_DB:-authentik}
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
POSTGRES_DB: ${PG_DB:-authentik}
env_file:
- .env
redis:
@ -47,8 +47,8 @@ services:
env_file:
- .env
ports:
- "${AUTHENTIK_PORT_HTTP:-9000}:9000"
- "${AUTHENTIK_PORT_HTTPS:-9443}:9443"
- "${COMPOSE_PORT_HTTP:-9000}:9000"
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.1}
restart: unless-stopped

View File

@ -30,7 +30,7 @@ type RedisConfig struct {
Password string `yaml:"password" env:"AUTHENTIK_REDIS__PASSWORD"`
TLS bool `yaml:"tls" env:"AUTHENTIK_REDIS__TLS"`
TLSReqs string `yaml:"tls_reqs" env:"AUTHENTIK_REDIS__TLS_REQS"`
DB int `yaml:"cache_db" env:"AUTHENTIK_REDIS__CACHE_DB"`
DB int `yaml:"cache_db" env:"AUTHENTIK_REDIS__DB"`
CacheTimeout int `yaml:"cache_timeout" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT"`
CacheTimeoutFlows int `yaml:"cache_timeout_flows" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_FLOWS"`
CacheTimeoutPolicies int `yaml:"cache_timeout_policies" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_POLICIES"`

17
poetry.lock generated
View File

@ -1676,14 +1676,14 @@ files = [
[[package]]
name = "importlib-metadata"
version = "6.5.0"
version = "6.6.0"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "importlib_metadata-6.5.0-py3-none-any.whl", hash = "sha256:03ba783c3a2c69d751b109fc0c94a62c51f581b3d6acf8ed1331b6d5729321ff"},
{file = "importlib_metadata-6.5.0.tar.gz", hash = "sha256:7a8bdf1bc3a726297f5cfbc999e6e7ff6b4fa41b26bba4afc580448624460045"},
{file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"},
{file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"},
]
[package.dependencies]
@ -3263,16 +3263,21 @@ files = [
[[package]]
name = "sqlparse"
version = "0.4.3"
version = "0.4.4"
description = "A non-validating SQL parser."
category = "main"
optional = false
python-versions = ">=3.5"
files = [
{file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"},
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
{file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
{file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
]
[package.extras]
dev = ["build", "flake8"]
doc = ["sphinx"]
test = ["pytest", "pytest-cov"]
[[package]]
name = "stevedore"
version = "4.1.1"

View File

@ -3676,24 +3676,6 @@ paths:
description: flow_user_settings
schema:
type: string
- name: interface_admin
required: false
in: query
description: interface_admin
schema:
type: string
- name: interface_flow
required: false
in: query
description: interface_flow
schema:
type: string
- name: interface_user
required: false
in: query
description: interface_user
schema:
type: string
- name: ordering
required: false
in: query
@ -7749,295 +7731,6 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/interfaces/:
get:
operationId: interfaces_list
description: Interface serializer
parameters:
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- name: search
required: false
in: query
description: A search term.
schema:
type: string
- in: query
name: template
schema:
type: string
- in: query
name: type
schema:
type: string
enum:
- admin
- flow
- user
description: |-
* `user` - User
* `admin` - Admin
* `flow` - Flow
* `user` - User
* `admin` - Admin
* `flow` - Flow
- in: query
name: url_name
schema:
type: string
tags:
- interfaces
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedInterfaceList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: interfaces_create
description: Interface serializer
tags:
- interfaces
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InterfaceRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/interfaces/{interface_uuid}/:
get:
operationId: interfaces_retrieve
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: interfaces_update
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InterfaceRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: interfaces_partial_update
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedInterfaceRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: interfaces_destroy
description: Interface serializer
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/interfaces/{interface_uuid}/used_by/:
get:
operationId: interfaces_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: interface_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this interface.
required: true
tags:
- interfaces
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/managed/blueprints/:
get:
operationId: managed_blueprints_list
@ -26614,7 +26307,6 @@ components:
- authentik.admin
- authentik.api
- authentik.crypto
- authentik.interfaces
- authentik.events
- authentik.flows
- authentik.lib
@ -26665,7 +26357,6 @@ components:
* `authentik.admin` - authentik Admin
* `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib
@ -29383,7 +29074,6 @@ components:
* `authentik.admin` - authentik Admin
* `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib
@ -29494,7 +29184,6 @@ components:
* `authentik.admin` - authentik Admin
* `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib
@ -30608,55 +30297,6 @@ components:
* `api` - Intent Api
* `recovery` - Intent Recovery
* `app_password` - Intent App Password
Interface:
type: object
description: Interface serializer
properties:
interface_uuid:
type: string
format: uuid
readOnly: true
url_name:
type: string
maxLength: 50
pattern: ^[-a-zA-Z0-9_]+$
type:
$ref: '#/components/schemas/InterfaceTypeEnum'
template:
type: string
required:
- interface_uuid
- template
- type
- url_name
InterfaceRequest:
type: object
description: Interface serializer
properties:
url_name:
type: string
minLength: 1
maxLength: 50
pattern: ^[-a-zA-Z0-9_]+$
type:
$ref: '#/components/schemas/InterfaceTypeEnum'
template:
type: string
minLength: 1
required:
- template
- type
- url_name
InterfaceTypeEnum:
enum:
- user
- admin
- flow
type: string
description: |-
* `user` - User
* `admin` - Admin
* `flow` - Flow
InvalidResponseActionEnum:
enum:
- retry
@ -33411,41 +33051,6 @@ components:
required:
- pagination
- results
PaginatedInterfaceList:
type: object
properties:
pagination:
type: object
properties:
next:
type: number
previous:
type: number
count:
type: number
current:
type: number
total_pages:
type: number
start_index:
type: number
end_index:
type: number
required:
- next
- previous
- count
- current
- total_pages
- start_index
- end_index
results:
type: array
items:
$ref: '#/components/schemas/Interface'
required:
- pagination
- results
PaginatedInvitationList:
type: object
properties:
@ -36265,7 +35870,6 @@ components:
* `authentik.admin` - authentik Admin
* `authentik.api` - authentik API
* `authentik.crypto` - authentik Crypto
* `authentik.interfaces` - authentik Interfaces
* `authentik.events` - authentik Events
* `authentik.flows` - authentik Flows
* `authentik.lib` - authentik lib
@ -36517,20 +36121,6 @@ components:
description: Specify which sources should be shown.
show_source_labels:
type: boolean
PatchedInterfaceRequest:
type: object
description: Interface serializer
properties:
url_name:
type: string
minLength: 1
maxLength: 50
pattern: ^[-a-zA-Z0-9_]+$
type:
$ref: '#/components/schemas/InterfaceTypeEnum'
template:
type: string
minLength: 1
PatchedInvitationRequest:
type: object
description: Invitation Serializer
@ -37815,18 +37405,6 @@ components:
type: string
format: uuid
nullable: true
interface_admin:
type: string
format: uuid
nullable: true
interface_user:
type: string
format: uuid
nullable: true
interface_flow:
type: string
format: uuid
nullable: true
event_retention:
type: string
minLength: 1
@ -40998,18 +40576,6 @@ components:
type: string
format: uuid
nullable: true
interface_admin:
type: string
format: uuid
nullable: true
interface_user:
type: string
format: uuid
nullable: true
interface_flow:
type: string
format: uuid
nullable: true
event_retention:
type: string
description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).'
@ -41068,18 +40634,6 @@ components:
type: string
format: uuid
nullable: true
interface_admin:
type: string
format: uuid
nullable: true
interface_user:
type: string
format: uuid
nullable: true
interface_flow:
type: string
format: uuid
nullable: true
event_retention:
type: string
minLength: 1

View File

@ -33,7 +33,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug))
self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug))
self.login()
# Get expected token
@ -57,7 +57,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
"""test TOTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug))
self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug))
self.login()
self.wait_for_url(self.if_user_url("/library"))
@ -103,7 +103,7 @@ class TestFlowsAuthenticator(SeleniumTestCase):
"""test Static OTP Setup stage"""
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
self.driver.get(self.url("authentik_interfaces:if", if_name="flow", flow_slug=flow.slug))
self.driver.get(self.url("authentik_core:if-flow", flow_slug=flow.slug))
self.login()
self.wait_for_url(self.if_user_url("/library"))

View File

@ -15,8 +15,7 @@ class TestFlowsLogin(SeleniumTestCase):
"""test default login flow"""
self.driver.get(
self.url(
"authentik_interfaces:if",
if_name="flow",
"authentik_core:if-flow",
flow_slug="default-authentication-flow",
)
)

View File

@ -35,8 +35,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
self.driver.get(
self.url(
"authentik_interfaces:if",
if_name="flow",
"authentik_core:if-flow",
flow_slug="default-authentication-flow",
)
)

View File

@ -3,6 +3,7 @@ import json
import os
from functools import lru_cache, wraps
from os import environ
from sys import stderr
from time import sleep
from typing import Any, Callable, Optional
@ -28,6 +29,7 @@ from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
RETRIES = int(environ.get("RETRIES", "3"))
IS_CI = "CI" in environ
def get_docker_tag() -> str:
@ -49,6 +51,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
user: User
def setUp(self):
if IS_CI:
print("::group::authentik Logs", file=stderr)
super().setUp()
# pylint: disable=invalid-name
self.maxDiff = None
@ -91,8 +95,12 @@ class SeleniumTestCase(StaticLiveServerTestCase):
def output_container_logs(self, container: Optional[Container] = None):
"""Output the container logs to our STDOUT"""
_container = container or self.container
if IS_CI:
print(f"::group::Container logs - {_container.image.tags[0]}")
for log in _container.logs().decode().split("\n"):
self.logger.info(log, source="container", container=_container.image.tags[0])
print(log)
if IS_CI:
print("::endgroup::")
def get_container_specs(self) -> Optional[dict[str, Any]]:
"""Optionally get container specs which will launched on setup, wait for the container to
@ -118,15 +126,19 @@ class SeleniumTestCase(StaticLiveServerTestCase):
raise ValueError(f"Webdriver failed after {RETRIES}.")
def tearDown(self):
self.logger.debug("--------browser logs")
super().tearDown()
if IS_CI:
print("::endgroup::", file=stderr)
if IS_CI:
print("::group::Browser logs")
for line in self.driver.get_log("browser"):
self.logger.debug(line["message"], source=line["source"], level=line["level"])
self.logger.debug("--------end browser logs")
print(line["message"])
if IS_CI:
print("::endgroup::")
if self.container:
self.output_container_logs()
self.container.kill()
self.driver.quit()
super().tearDown()
def wait_for_url(self, desired_url):
"""Wait until URL is `desired_url`."""

35
web/package-lock.json generated
View File

@ -54,7 +54,7 @@
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.30.1",
"country-flag-icons": "^1.5.7",
"eslint": "^8.38.0",
"eslint": "^8.39.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-lit": "^1.8.3",
@ -62,7 +62,7 @@
"lit": "^2.7.2",
"mermaid": "^10.1.0",
"moment": "^2.29.4",
"prettier": "^2.8.7",
"prettier": "^2.8.8",
"pyright": "^1.1.304",
"rapidoc": "^9.3.4",
"rollup": "^2.79.1",
@ -1982,9 +1982,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz",
"integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz",
"integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
@ -5255,14 +5255,14 @@
}
},
"node_modules/eslint": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz",
"integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz",
"integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.2",
"@eslint/js": "8.38.0",
"@eslint/js": "8.39.0",
"@humanwhocodes/config-array": "^0.11.8",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@ -5272,7 +5272,7 @@
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.1.1",
"eslint-scope": "^7.2.0",
"eslint-visitor-keys": "^3.4.0",
"espree": "^9.5.1",
"esquery": "^1.4.2",
@ -5425,15 +5425,18 @@
}
},
"node_modules/eslint/node_modules/eslint-scope": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
"integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
"integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/estraverse": {
@ -8117,9 +8120,9 @@
}
},
"node_modules/prettier": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"bin": {
"prettier": "bin-prettier.js"
},

View File

@ -98,7 +98,7 @@
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.30.1",
"country-flag-icons": "^1.5.7",
"eslint": "^8.38.0",
"eslint": "^8.39.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-lit": "^1.8.3",
@ -106,7 +106,7 @@
"lit": "^2.7.2",
"mermaid": "^10.1.0",
"moment": "^2.29.4",
"prettier": "^2.8.7",
"prettier": "^2.8.8",
"pyright": "^1.1.304",
"rapidoc": "^9.3.4",
"rollup": "^2.79.1",

View File

@ -299,9 +299,6 @@ export class AdminInterface extends Interface {
<ak-sidebar-item path="/core/tenants">
<span slot="label">${t`Tenants`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/interfaces">
<span slot="label">${t`Interfaces`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/crypto/certificates">
<span slot="label">${t`Certificates`}</span>
</ak-sidebar-item>

View File

@ -132,10 +132,6 @@ export const ROUTES: Route[] = [
await import("@goauthentik/admin/blueprints/BlueprintListPage");
return html`<ak-blueprint-list></ak-blueprint-list>`;
}),
new Route(new RegExp("^/interfaces$"), async () => {
await import("@goauthentik/admin/interfaces/InterfaceListPage");
return html`<ak-interface-list></ak-interface-list>`;
}),
new Route(new RegExp("^/debug$"), async () => {
await import("@goauthentik/admin/DebugPage");
return html`<ak-admin-debug-page></ak-admin-debug-page>`;

View File

@ -95,17 +95,25 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
if (item.managed && item.managed.startsWith("goauthentik.io/crypto/discovered")) {
managedSubText = t`Managed by authentik (Discovered)`;
}
let color = PFColor.Green;
if (item.certExpiry) {
const now = new Date();
const inAMonth = new Date();
inAMonth.setDate(inAMonth.getDate() + 30);
if (item.certExpiry <= inAMonth) {
color = PFColor.Orange;
}
if (item.certExpiry <= now) {
color = PFColor.Red;
}
}
return [
html`<div>${item.name}</div>
${item.managed ? html`<small>${managedSubText}</small>` : html``}`,
html`<ak-label color=${item.privateKeyAvailable ? PFColor.Green : PFColor.Grey}>
${item.privateKeyAvailable ? t`Yes (${item.privateKeyType?.toUpperCase()})` : t`No`}
</ak-label>`,
html`<ak-label
color=${item.certExpiry || new Date() > new Date() ? PFColor.Green : PFColor.Orange}
>
${item.certExpiry?.toLocaleString()}
</ak-label>`,
html`<ak-label color=${color}> ${item.certExpiry?.toLocaleString()} </ak-label>`,
html`<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update Certificate-Key Pair`} </span>

View File

@ -50,9 +50,6 @@ export class FlowListPage extends TablePage<Flow> {
groupBy(items: Flow[]): [string, Flow[]][] {
return groupBy(items, (flow) => {
if (!flow.designation) {
return "";
}
return DesignationToLabel(flow.designation);
});
}

View File

@ -1,90 +0,0 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { Interface, InterfaceTypeEnum, InterfacesApi } from "@goauthentik/api";
@customElement("ak-interface-form")
export class InterfaceForm extends ModelForm<Interface, string> {
loadInstance(pk: string): Promise<Interface> {
return new InterfacesApi(DEFAULT_CONFIG).interfacesRetrieve({
interfaceUuid: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated interface.`;
} else {
return t`Successfully created interface.`;
}
}
send = (data: Interface): Promise<Interface> => {
if (this.instance?.interfaceUuid) {
return new InterfacesApi(DEFAULT_CONFIG).interfacesUpdate({
interfaceUuid: this.instance.interfaceUuid,
interfaceRequest: data,
});
} else {
return new InterfacesApi(DEFAULT_CONFIG).interfacesCreate({
interfaceRequest: data,
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`URL Name`} ?required=${true} name="urlName">
<input
type="text"
value="${first(this.instance?.urlName, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Name used in the URL when accessing this interface.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Type`} ?required=${true} name="type">
<ak-radio
.options=${[
{
label: t`Enduser interface`,
value: InterfaceTypeEnum.User,
default: true,
},
{
label: t`Flow interface`,
value: InterfaceTypeEnum.Flow,
},
{
label: t`Admin interface`,
value: InterfaceTypeEnum.Admin,
},
]}
.value=${this.instance?.type}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${t`Configure how authentik will use this interface.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Template`} ?required=${true} name="template"
><ak-codemirror
mode="html"
value="${ifDefined(this.instance?.template)}"
></ak-codemirror>
</ak-form-element-horizontal>
</form>`;
}
}

View File

@ -1,101 +0,0 @@
import "@goauthentik/admin/interfaces/InterfaceForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Interface, InterfacesApi } from "@goauthentik/api";
@customElement("ak-interface-list")
export class InterfaceListPage extends TablePage<Interface> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return t`Interfaces`;
}
pageDescription(): string {
return t`Manage custom interfaces for authentik`;
}
pageIcon(): string {
return "fa fa-home";
}
checkbox = true;
@property()
order = "url_name";
async apiEndpoint(page: number): Promise<PaginatedResponse<Interface>> {
return new InterfacesApi(DEFAULT_CONFIG).interfacesList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [new TableColumn(t`URL Name`, "url_name"), new TableColumn(t`Actions`)];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${t`Interface(s)`}
.objects=${this.selectedElements}
.metadata=${(item: Interface) => {
return [{ key: t`Domain`, value: item.urlName }];
}}
.usedBy=${(item: Interface) => {
return new InterfacesApi(DEFAULT_CONFIG).interfacesUsedByList({
interfaceUuid: item.interfaceUuid,
});
}}
.delete=${(item: Interface) => {
return new InterfacesApi(DEFAULT_CONFIG).interfacesDestroy({
interfaceUuid: item.interfaceUuid,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete`}
</button>
</ak-forms-delete-bulk>`;
}
row(item: Interface): TemplateResult[] {
return [
html`${item.urlName}`,
html`<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update Interface`} </span>
<ak-interface-form slot="form" .instancePk=${item.interfaceUuid}>
</ak-interface-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<i class="fas fa-edit"></i>
</button>
</ak-forms-modal>`,
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit"> ${t`Create`} </span>
<span slot="header"> ${t`Create Interface`} </span>
<ak-interface-form slot="form"> </ak-interface-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
</ak-forms-modal>
`;
}
}

View File

@ -85,7 +85,7 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
html`<ak-label color=${item.local ? PFColor.Grey : PFColor.Green}>
${item.local ? t`Yes` : t`No`}
</ak-label>`,
html`${itemState.healthy
html`${itemState?.healthy
? html`<ak-label color=${PFColor.Green}>${ifDefined(itemState.version)}</ak-label>`
: html`<ak-label color=${PFColor.Red}>${t`Unhealthy`}</ak-label>`}`,
html` <ak-forms-modal>

View File

@ -8,7 +8,6 @@ import "@goauthentik/admin/policies/password/PasswordPolicyForm";
import "@goauthentik/admin/policies/reputation/ReputationPolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import { groupBy } from "@goauthentik/common/utils";
import { PFColor } from "@goauthentik/elements/Label";
import "@goauthentik/elements/forms/ConfirmationForm";
import "@goauthentik/elements/forms/DeleteBulkForm";
@ -63,10 +62,6 @@ export class PolicyListPage extends TablePage<Policy> {
];
}
groupBy(items: Policy[]): [string, Policy[]][] {
return groupBy(items, (policy) => policy.verboseNamePlural);
}
row(item: Policy): TemplateResult[] {
return [
html`<div>${item.name}</div>

View File

@ -7,7 +7,6 @@ import "@goauthentik/admin/property-mappings/PropertyMappingTestForm";
import "@goauthentik/admin/property-mappings/PropertyMappingWizard";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/forms/ProxyForm";
@ -57,10 +56,6 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
});
}
groupBy(items: PropertyMapping[]): [string, PropertyMapping[]][] {
return groupBy(items, (mapping) => mapping.verboseNamePlural);
}
columns(): TableColumn[] {
return [
new TableColumn(t`Name`, "name"),

View File

@ -21,7 +21,6 @@ import "@goauthentik/admin/stages/user_logout/UserLogoutStageForm";
import "@goauthentik/admin/stages/user_write/UserWriteStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/forms/ProxyForm";
@ -66,10 +65,6 @@ export class StageListPage extends TablePage<Stage> {
});
}
groupBy(items: Stage[]): [string, Stage[]][] {
return groupBy(items, (stage) => stage.verboseNamePlural);
}
columns(): TableColumn[] {
return [
new TableColumn(t`Name`, "name"),

View File

@ -23,10 +23,6 @@ import {
FlowsApi,
FlowsInstancesListDesignationEnum,
FlowsInstancesListRequest,
Interface,
InterfacesApi,
InterfacesListRequest,
InterfacesListTypeEnum,
Tenant,
} from "@goauthentik/api";
@ -372,107 +368,6 @@ export class TenantForm extends ModelForm<Tenant, string> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${t`Interfaces`} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`User Interface`} name="interfaceUser">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Interface[]> => {
const args: InterfacesListRequest = {
ordering: "url_name",
type: InterfacesListTypeEnum.User,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new InterfacesApi(
DEFAULT_CONFIG,
).interfacesList(args);
return flows.results;
}}
.renderElement=${(iface: Interface): string => {
return iface.urlName;
}}
.renderDescription=${(iface: Interface): TemplateResult => {
return html`${iface.type}`;
}}
.value=${(iface: Interface | undefined): string | undefined => {
return iface?.interfaceUuid;
}}
.selected=${(iface: Interface): boolean => {
return this.instance?.interfaceUser === iface.interfaceUuid;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">${t`.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Flow Interface`} name="interfaceFlow">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Interface[]> => {
const args: InterfacesListRequest = {
ordering: "url_name",
type: InterfacesListTypeEnum.Flow,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new InterfacesApi(
DEFAULT_CONFIG,
).interfacesList(args);
return flows.results;
}}
.renderElement=${(iface: Interface): string => {
return iface.urlName;
}}
.renderDescription=${(iface: Interface): TemplateResult => {
return html`${iface.type}`;
}}
.value=${(iface: Interface | undefined): string | undefined => {
return iface?.interfaceUuid;
}}
.selected=${(iface: Interface): boolean => {
return this.instance?.interfaceFlow === iface.interfaceUuid;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">${t`.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Admin Interface`} name="interfaceAdmin">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Interface[]> => {
const args: InterfacesListRequest = {
ordering: "url_name",
type: InterfacesListTypeEnum.Admin,
};
if (query !== undefined) {
args.search = query;
}
const flows = await new InterfacesApi(
DEFAULT_CONFIG,
).interfacesList(args);
return flows.results;
}}
.renderElement=${(iface: Interface): string => {
return iface.urlName;
}}
.renderDescription=${(iface: Interface): TemplateResult => {
return html`${iface.type}`;
}}
.value=${(iface: Interface | undefined): string | undefined => {
return iface?.interfaceUuid;
}}
.selected=${(iface: Interface): boolean => {
return this.instance?.interfaceAdmin === iface.interfaceUuid;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">${t`.`}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${t`Other global settings`} </span>
<div slot="body" class="pf-c-form">

View File

@ -8,7 +8,7 @@ slug: /terminology
graph LR
source_ldap((LDAP Source)) <-->|Synchronizes| datasource_ldap["FreeIPA/
Active Directory"]
datasource_oauth1(Twtitter) --> source_oauth((OAuth/SAML\nSource))
datasource_oauth1(Twitter) --> source_oauth((OAuth/SAML\nSource))
datasource_oauth2(GitHub) --> source_oauth((OAuth/SAML\nSource))
source_oauth --> authentik_db(authentik Database)
source_ldap --> authentik_db(authentik Database)

View File

@ -31,6 +31,10 @@ After you've created the policies to match the events you want, create a "Notifi
You have to select which group the generated notification should be sent to. If left empty, the rule will be disabled.
:::info
Before authentik 2023.5, when no group is selected, policies bound to the rule are not executed. Starting with authentik 2023.5, policies are executed even when no group is selected.
:::
You also have to select which transports should be used to send the notification.
A transport with the name "default-email-transport" is created by default. This transport will use the [global email configuration](../installation/docker-compose#email-configuration-optional-but-recommended).

View File

@ -17,7 +17,7 @@ To disable these outbound connections, set the following in your `.env` file:
AUTHENTIK_DISABLE_UPDATE_CHECK=true
AUTHENTIK_ERROR_REPORTING__ENABLED=false
AUTHENTIK_DISABLE_STARTUP_ANALYTICS=true
AUTHENTIK_AVATARS=none
AUTHENTIK_AVATARS=initials
```
For a Helm-based install, set the following in your values.yaml file:

View File

@ -43,6 +43,8 @@ kubectl exec -it deployment/authentik-worker -c authentik -- ak dump_config
- `AUTHENTIK_REDIS__HOST`: Hostname of your Redis Server
- `AUTHENTIK_REDIS__PORT`: Redis port, defaults to 6379
- `AUTHENTIK_REDIS__PASSWORD`: Password for your Redis Server
- `AUTHENTIK_REDIS__TLS`: Use TLS to connect to Redis, defaults to false
- `AUTHENTIK_REDIS__TLS_REQS`: Redis TLS requirements, defaults to "none"
- `AUTHENTIK_REDIS__DB`: Database, defaults to 0
- `AUTHENTIK_REDIS__CACHE_TIMEOUT`: Timeout for cached data until it expires in seconds, defaults to 300
- `AUTHENTIK_REDIS__CACHE_TIMEOUT_FLOWS`: Timeout for cached flow plans until they expire in seconds, defaults to 300

View File

@ -60,14 +60,14 @@ AUTHENTIK_EMAIL__FROM=authentik@localhost
## Configure for port 80/443
By default, authentik listens on port 9000 for HTTP and 9443 for HTTPS. To change the default and instead use ports 80 and 443, you can set the following variables in `.env`:
By default, authentik listens internally on port 9000 for HTTP and 9443 for HTTPS. To change the exposed ports to 80 and 443, you can set the following variables in `.env`:
```shell
AUTHENTIK_PORT_HTTP=80
AUTHENTIK_PORT_HTTPS=443
COMPOSE_PORT_HTTP=80
COMPOSE_PORT_HTTPS=443
```
Be sure to run `docker-compose up -d` to rebuild with the new port numbers.
See [Configuration](../installation/configuration) to change the internal ports. Be sure to run `docker-compose up -d` to rebuild with the new port numbers.
## Startup

View File

@ -11,6 +11,10 @@ slug: "/releases/2023.5"
Additionally, any custom fields based on user attributes will only be represented with their sanitized key, removing any slashes with dashes, and removing periods.
- Renamed docker-compose environment variables
To better distinguish settings that configure authentik itself and settings that configure docker-compose, the environment variables `AUTHENTIK_PORT_HTTP` and `AUTHENTIK_PORT_HTTPS` have been renamed to `COMPOSE_PORT_HTTP` and `COMPOSE_PORT_HTTPS` respectively.
## New features
## Upgrading

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1,68 @@
---
title: DokuWiki
---
<span class="badge badge--secondary">Support level: Community</span>
## What is Service Name
From https://en.wikipedia.org/wiki/DokuWiki
:::note
DokuWiki is a wiki application licensed under GPLv2 and written in the PHP programming language. It works on plain text files and thus does not need a database. Its syntax is similar to the one used by MediaWiki. It is often recommended as a more lightweight, easier to customize alternative to MediaWiki.
:::
## Preparation
The following placeholders will be used:
- `dokuwiki.company` is the FQDN of the DokiWiki install.
- `authentik.company` is the FQDN of the authentik install.
## Service Configuration
In DokuWiki, navigate to the _Extension Manager_ section in the _Administration_ interface and install
- https://www.dokuwiki.org/plugin:oauth
- https://www.dokuwiki.org/plugin:oauthgeneric
Navigate to _Configuration Settings_ section in the _Administration_ interface and change _Oauth_ and _Oauthgeneric_ options:
For _Oauth_:
- Check the _plugin»oauth»register-on-auth_ option
For _Oauthgeneric_:
- plugin»oauthgeneric»key: The Application UID
- plugin»oauthgeneric»secret: The Application Secret
- plugin»oauthgeneric»authurl: https://authentik.company/application/o/authorize/
- plugin»oauthgeneric»tokenurl: https://authentik.company/application/o/token/
- plugin»oauthgeneric»userurl: https://authentik.company/application/o/userinfo/
- plugin»oauthgeneric»authmethod: Bearer Header
- plugin»oauthgeneric»scopes: email, openid, profile
- plugin»oauthgeneric»needs-state: checked
- plugin»oauthgeneric»json-user: preferred_username
- plugin»oauthgeneric»json-name: name
- plugin»oauthgeneric»json-mail: email
- plugin»oauthgeneric»json-grps: groups
![](./dokuwiki_oauth_generic.png)
In the _Configuration Settings_ section in the _Administration_ interface navigate to _Authentication_ and activate _oauth_ in _Authentication backend_.
## authentik Configuration
### Provider
In authentik, under _Providers_, create an _OAuth2/OpenID Provider_ with these settings:
- Redirect URI: The _Callback URL / Redirect URI_ from _plugin»oauth»info_, usually `dokuwiki.company/doku.php`
- Signing Key: Select any available key
Note the _client ID_ and _client secret_, then save the provider. If you need to retrieve these values, you can do so by editing the provider.
### Application
In authentik, create an application which uses this provider. Optionally apply access restrictions to the application using policy bindings.
Set the Launch URL to the _Callback URL / Redirect URI_ (`dokuwiki.company/doku.php`).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

View File

@ -28,11 +28,11 @@ The following placeholders will be used:
5. **Click to Reveal** the Client Secret and _save it for later_
6. Click **Add Redirect** and add https://authentik.company/source/oauth/callback/discord
6. Click **Add Redirect** and add https://authentik.company/source/oauth/callback/discord/
Here is an example of a completed OAuth2 screen for Discord.
![](discord4.png)
![](discord3.png)
## authentik
@ -45,7 +45,7 @@ Here is an example of a completed OAuth2 screen for Discord.
Here is an example of a complete authentik Discord OAuth Source
![](discord5.png)
![](discord4.png)
Save, and you now have Discord as a source.

View File

@ -24,7 +24,7 @@
"react-toggle": "^4.1.3"
},
"devDependencies": {
"prettier": "2.8.7"
"prettier": "2.8.8"
}
},
"node_modules/@algolia/autocomplete-core": {
@ -9918,9 +9918,9 @@
}
},
"node_modules/prettier": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
@ -20601,9 +20601,9 @@
"integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
},
"prettier": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz",
"integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==",
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true
},
"pretty-error": {

View File

@ -43,6 +43,6 @@
]
},
"devDependencies": {
"prettier": "2.8.7"
"prettier": "2.8.8"
}
}

View File

@ -13,6 +13,7 @@ module.exports = {
label: "Chat, Communication & Collaboration",
items: [
"services/bookstack/index",
"services/dokuwiki/index",
"services/hedgedoc/index",
"services/kimai/index",
"services/mastodon/index",