Compare commits

...

39 Commits

Author SHA1 Message Date
ad7ad1fa78 release: 2024.8.2 2024-09-16 14:13:04 +02:00
c70e609e50 website/docs: prepare release notes for 2024.8.2 (#11394)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	website/docs/releases/2024/v2024.8.md
2024-09-16 14:12:28 +02:00
5f08485fff web: revert lockfile lint, re-add integrity (#11380)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/package-lock.json
2024-09-14 23:16:56 +02:00
3a2ed11821 providers/proxy: fix URL path getting lost when partial URL is given to rd= (cherry-pick #11354) (#11355)
providers/proxy: fix URL path getting lost when partial URL is given to rd= (#11354)

* providers/proxy: fix URL path getting lost when partial URL is given to rd=



* better fallback + tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-12 18:58:47 +02:00
ee04f39e28 enterprise: fix API mixin license validity check (cherry-pick #11331) (#11342)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
fix API mixin license validity check (#11331)
2024-09-11 13:22:01 +00:00
2c6aa72f3c sources/ldap: fix missing search attribute (cherry-pick #11125) (#11340)
sources/ldap: fix missing search attribute (#11125)

* unrelated



* sources/ldap: fix ldap sync not requesting uniqueness attribute



* check object_uniqueness_field for none



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-11 14:03:12 +02:00
bd0afef790 enterprise: show specific error if Install ID is invalid in license (cherry-pick #11317) (#11319)
enterprise: show specific error if Install ID is invalid in license (#11317)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-10 19:38:45 +02:00
fc11cc0a1a core: fix permission check for scoped impersonation (cherry-pick #11315) (#11316)
core: fix permission check for scoped impersonation (#11315)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-10 14:19:30 +02:00
fb78303e8f web/admin: fix notification property mapping forms (cherry-pick #11298) (#11300)
web/admin: fix notification property mapping forms (#11298)

* fix incorrect base class



* fix doclink url

closes #11276



* fix sidebar order in website



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-09 19:27:29 +02:00
2ea04440db events: optimise marking events as seen (cherry-pick #11297) (#11299)
events: optimise marking events as seen (#11297)

* events: optimise marking events as seen



* add tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-09 19:26:43 +02:00
96e1636be3 core: ensure all providers have correct priority (cherry-pick #11280) (#11281)
core: ensure all providers have correct priority (#11280)

follow up to #11267 which broke SAML lookup

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-08 16:09:09 +02:00
c546451a73 root: fix ensure `outpost_connection_discovery runs on worker startup (cherry-pick #11260) (#11270)
root: fix ensure `outpost_connection_discovery runs on worker startup (#11260)

* root: fix ensure outpost_connection_discovery runs on worker startup

Make outpost_connection_discovery a startup task for default_tenant to ensure it's ran during worker startup. Without this waiting for the 8 hour schedule to fire is required.

fixes: https://github.com/goauthentik/authentik/issues/10933



* format



---------

Signed-off-by: Anthony Rabbito <arabbito@coreweave.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Anthony Rabbito <hello@anthonyrabbito.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-09-07 21:54:30 +02:00
61778053b4 core: ensure proxy provider is correctly looked up (cherry-pick #11267) (#11269)
core: ensure proxy provider is correctly looked up (#11267)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-07 21:53:30 +02:00
f5580d311d release: 2024.8.1 2024-09-07 16:14:54 +02:00
99d292bce0 web/users: show - if device was registered before we started saving the time (cherry-pick #11256) (#11257)
web/users: show - if device was registered before we started saving the time (#11256)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-06 21:13:03 +02:00
b2801641bc internal: fix go paginator not setting page correctly (cherry-pick #11253) (#11255)
internal: fix go paginator not setting page correctly (#11253)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-06 18:46:18 +02:00
bfaa1046b2 core: fix missing argument name escaping for property mapping (cherry-pick #11231) (#11252)
core: fix missing argument name escaping for property mapping (#11231)

* escape property mapping args



* improve display of error



* fix error handling, missing dry_run argument



* use different sanitisation



* update docs



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-06 16:47:27 +02:00
95c30400cc providers/ldap: rework search_group migration to work with read replicas (cherry-pick #11228) (#11229)
providers/ldap: rework search_group migration to work with read replicas (#11228)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-05 15:57:01 +02:00
e77480ee1d web/admin: improve error handling (cherry-pick #11212) (#11219)
web/admin: improve error handling (#11212)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-05 13:48:28 +02:00
905800e535 providers/ldap: fix incorrect permission check for search access (cherry-pick #11217) (#11218)
providers/ldap: fix incorrect permission check for search access (#11217)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-05 01:30:48 +02:00
fadeaef4c6 web/admin: fix missing Sync object button SCIM Provider (cherry-pick #11211) (#11213)
web/admin: fix missing Sync object button SCIM Provider (#11211)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-04 21:34:34 +02:00
437efda649 website/docs: add note about terraform provider (cherry-pick #11206) (#11208)
website/docs: add note about terraform provider (#11206)

* website/docs: add note about terraform provider



* Update website/docs/releases/2024/v2024.8.md



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-09-04 19:50:00 +02:00
dd75d5f54b web/admin: fix misc dual select on different forms (#11203)
* fix prompt stage

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

* fix identification stage

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

* fix OAuth JWKS sources

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

* fix oauth provider default scopes

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

* fix outpost form

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

* fix webauthn

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

* fix transport form

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts
#	web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts
2024-09-04 13:46:45 +02:00
392a2e582e core: bump cryptography from 43.0.0 to 43.0.1 (cherry-pick #11185) (#11202)
core: bump cryptography from 43.0.0 to 43.0.1 (#11185)

Bumps [cryptography](https://github.com/pyca/cryptography) from 43.0.0 to 43.0.1.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/43.0.0...43.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-04 12:27:54 +02:00
a1da183721 root: backport s3 storage changes (cherry-pick #11181) (#11183)
root: backport s3 storage changes (#11181)

re-add _strip_signing_parameters
removed in https://github.com/jschneier/django-storages/pull/1402
could probably be re-factored to use the same approach that PR uses

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-03 22:08:55 +02:00
feea2df0b1 core: fix change_user_type always requiring usernames (cherry-pick #11177) (#11178)
core: fix change_user_type always requiring usernames (#11177)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-03 19:09:53 +02:00
b47acd8c76 web/admin: fix error in Outpost creation form (cherry-pick #11173) (#11175)
web/admin: fix error in Outpost creation form (#11173)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-03 18:26:37 +02:00
6fd87d9ced providers/ldap: fix migration assuming search group is set (cherry-pick #11170) (#11172)
providers/ldap: fix migration assuming search group is set (#11170)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-09-03 16:27:06 +02:00
acbb065808 website/docs: update release notes (#11151)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	website/docs/releases/2024/v2024.8.md
2024-09-03 14:05:18 +02:00
2fb097061d release: 2024.8.0 2024-09-02 14:14:03 +02:00
8962d17e03 web: fix dual-select with dynamic selection (cherry-pick #11133) (#11134)
web: fix dual-select with dynamic selection (#11133)

* web: fix dual-select with dynamic selection

For dynamic selection, the property name is `.selector` to message that it's a function the
API layer uses to select the elements.

A few bits of lint picked.

* web: added comment to clarify what the fallback selector does

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2024-08-30 19:07:36 +02:00
8326e1490c ci: fix failing release attestation (cherry-pick #11107) (#11120)
ci: fix failing release attestation (#11107)

* ci: fix failing release attestation



* fix



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-29 13:29:47 +02:00
091e4d3e4c enterprise: fix incorrect comparison for latest validity date (cherry-pick #11109) (#11110)
enterprise: fix incorrect comparison for latest validity date (#11109)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-29 01:58:56 +02:00
6ee77edcbb website/docs: 2024.8 release notes: reword group sync disable and fix typo (cherry-pick #11103) (#11108)
website/docs: 2024.8 release notes: reword group sync disable and fix… (#11103)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-08-29 01:34:33 +02:00
763e2288bf release: 2024.8.0-rc2 2024-08-28 20:22:52 +02:00
9cdb177ca7 website/docs: a couple of minor rewrite things (#11099)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	website/docs/releases/2024/v2024.8.md
2024-08-28 20:22:21 +02:00
6070508058 providers/oauth2: audit_ignore last_login change for generated service account (cherry-pick #11085) (#11086)
providers/oauth2: audit_ignore last_login change for generated service account (#11085)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-27 14:32:17 +02:00
ec13a5d84d release: 2024.8.0-rc1 2024-08-26 16:34:53 +02:00
057de82b01 schemas: fix XML Schema loading...for some reason?
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-08-26 16:34:47 +02:00
86 changed files with 10092 additions and 3872 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2024.6.4 current_version = 2024.8.2
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -29,9 +29,9 @@ outputs:
imageTags: imageTags:
description: "Docker image tags" description: "Docker image tags"
value: ${{ steps.ev.outputs.imageTags }} value: ${{ steps.ev.outputs.imageTags }}
imageNames: attestImageNames:
description: "Docker image names" description: "Docker image names used for attestation"
value: ${{ steps.ev.outputs.imageNames }} value: ${{ steps.ev.outputs.attestImageNames }}
imageMainTag: imageMainTag:
description: "Docker image main tag" description: "Docker image main tag"
value: ${{ steps.ev.outputs.imageMainTag }} value: ${{ steps.ev.outputs.imageMainTag }}

View File

@ -51,15 +51,24 @@ else:
] ]
image_main_tag = image_tags[0].split(":")[-1] image_main_tag = image_tags[0].split(":")[-1]
image_tags_rendered = ",".join(image_tags)
image_names_rendered = ",".join(set(name.split(":")[0] for name in image_tags))
def get_attest_image_names(image_with_tags: list[str]):
"""Attestation only for GHCR"""
image_tags = []
for image_name in set(name.split(":")[0] for name in image_with_tags):
if not image_name.startswith("ghcr.io"):
continue
image_tags.append(image_name)
return ",".join(set(image_tags))
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output: with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"shouldBuild={should_build}", file=_output) print(f"shouldBuild={should_build}", file=_output)
print(f"sha={sha}", file=_output) print(f"sha={sha}", file=_output)
print(f"version={version}", file=_output) print(f"version={version}", file=_output)
print(f"prerelease={prerelease}", file=_output) print(f"prerelease={prerelease}", file=_output)
print(f"imageTags={image_tags_rendered}", file=_output) print(f"imageTags={','.join(image_tags)}", file=_output)
print(f"imageNames={image_names_rendered}", file=_output) print(f"attestImageNames={get_attest_image_names(image_tags)}", file=_output)
print(f"imageMainTag={image_main_tag}", file=_output) print(f"imageMainTag={image_main_tag}", file=_output)
print(f"imageMainName={image_tags[0]}", file=_output) print(f"imageMainName={image_tags[0]}", file=_output)

View File

@ -261,7 +261,7 @@ jobs:
id: attest id: attest
if: ${{ steps.ev.outputs.shouldBuild == 'true' }} if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
with: with:
subject-name: ${{ steps.ev.outputs.imageNames }} subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true push-to-registry: true
pr-comment: pr-comment:

View File

@ -115,7 +115,7 @@ jobs:
id: attest id: attest
if: ${{ steps.ev.outputs.shouldBuild == 'true' }} if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
with: with:
subject-name: ${{ steps.ev.outputs.imageNames }} subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true push-to-registry: true
build-binary: build-binary:

View File

@ -58,7 +58,7 @@ jobs:
- uses: actions/attest-build-provenance@v1 - uses: actions/attest-build-provenance@v1
id: attest id: attest
with: with:
subject-name: ${{ steps.ev.outputs.imageNames }} subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true push-to-registry: true
build-outpost: build-outpost:
@ -122,7 +122,7 @@ jobs:
- uses: actions/attest-build-provenance@v1 - uses: actions/attest-build-provenance@v1
id: attest id: attest
with: with:
subject-name: ${{ steps.ev.outputs.imageNames }} subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true push-to-registry: true
build-outpost-binary: build-outpost-binary:

View File

@ -205,7 +205,7 @@ gen: gen-build gen-client-ts
web-build: web-install ## Build the Authentik UI web-build: web-install ## Build the Authentik UI
cd web && npm run build cd web && npm run build
web: web-lint-fix web-lint web-check-compile web-test ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it web: web-lint-fix web-lint web-check-compile ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
web-install: ## Install the necessary libraries to build the Authentik UI web-install: ## Install the necessary libraries to build the Authentik UI
cd web && npm ci cd web && npm ci

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2024.6.4" __version__ = "2024.8.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -30,8 +30,10 @@ from authentik.core.api.utils import (
PassiveSerializer, PassiveSerializer,
) )
from authentik.core.expression.evaluator import PropertyMappingEvaluator from authentik.core.expression.evaluator import PropertyMappingEvaluator
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import Group, PropertyMapping, User from authentik.core.models import Group, PropertyMapping, User
from authentik.events.utils import sanitize_item from authentik.events.utils import sanitize_item
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.api.exec import PolicyTestSerializer from authentik.policies.api.exec import PolicyTestSerializer
from authentik.rbac.decorators import permission_required from authentik.rbac.decorators import permission_required
@ -162,12 +164,15 @@ class PropertyMappingViewSet(
response_data = {"successful": True, "result": ""} response_data = {"successful": True, "result": ""}
try: try:
result = mapping.evaluate(**context) result = mapping.evaluate(dry_run=True, **context)
response_data["result"] = dumps( response_data["result"] = dumps(
sanitize_item(result), indent=(4 if format_result else None) sanitize_item(result), indent=(4 if format_result else None)
) )
except PropertyMappingExpressionException as exc:
response_data["result"] = exception_to_string(exc.exc)
response_data["successful"] = False
except Exception as exc: except Exception as exc:
response_data["result"] = str(exc) response_data["result"] = exception_to_string(exc)
response_data["successful"] = False response_data["successful"] = False
response = PropertyMappingTestResultSerializer(response_data) response = PropertyMappingTestResultSerializer(response_data)
return Response(response.data) return Response(response.data)

View File

@ -678,10 +678,10 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not request.tenant.impersonation: if not request.tenant.impersonation:
LOGGER.debug("User attempted to impersonate", user=request.user) LOGGER.debug("User attempted to impersonate", user=request.user)
return Response(status=401) return Response(status=401)
if not request.user.has_perm("impersonate"): user_to_be = self.get_object()
if not request.user.has_perm("impersonate", user_to_be):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user) LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return Response(status=401) return Response(status=401)
user_to_be = self.get_object()
if user_to_be.pk == self.request.user.pk: if user_to_be.pk == self.request.user.pk:
LOGGER.debug("User attempted to impersonate themselves", user=request.user) LOGGER.debug("User attempted to impersonate themselves", user=request.user)
return Response(status=401) return Response(status=401)

View File

@ -9,10 +9,11 @@ class Command(TenantCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument("--type", type=str, required=True) parser.add_argument("--type", type=str, required=True)
parser.add_argument("--all", action="store_true") parser.add_argument("--all", action="store_true", default=False)
parser.add_argument("usernames", nargs="+", type=str) parser.add_argument("usernames", nargs="*", type=str)
def handle_per_tenant(self, **options): def handle_per_tenant(self, **options):
print(options)
new_type = UserTypes(options["type"]) new_type = UserTypes(options["type"])
qs = ( qs = (
User.objects.exclude_anonymous() User.objects.exclude_anonymous()
@ -22,6 +23,9 @@ class Command(TenantCommand):
if options["usernames"] and options["all"]: if options["usernames"] and options["all"]:
self.stderr.write("--all and usernames specified, only one can be specified") self.stderr.write("--all and usernames specified, only one can be specified")
return return
if not options["usernames"] and not options["all"]:
self.stderr.write("--all or usernames must be specified")
return
if options["usernames"] and not options["all"]: if options["usernames"] and not options["all"]:
qs = qs.filter(username__in=options["usernames"]) qs = qs.filter(username__in=options["usernames"])
updated = qs.update(type=new_type) updated = qs.update(type=new_type)

View File

@ -466,8 +466,6 @@ class ApplicationQuerySet(QuerySet):
def with_provider(self) -> "QuerySet[Application]": def with_provider(self) -> "QuerySet[Application]":
qs = self.select_related("provider") qs = self.select_related("provider")
for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider): for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider):
if LOOKUP_SEP in subclass:
continue
qs = qs.select_related(f"provider__{subclass}") qs = qs.select_related(f"provider__{subclass}")
return qs return qs
@ -545,15 +543,24 @@ class Application(SerializerModel, PolicyBindingModel):
if not self.provider: if not self.provider:
return None return None
for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider): candidates = []
# We don't care about recursion, skip nested models base_class = Provider
if LOOKUP_SEP in subclass: for subclass in base_class.objects.get_queryset()._get_subclasses_recurse(base_class):
parent = self.provider
for level in subclass.split(LOOKUP_SEP):
try:
parent = getattr(parent, level)
except AttributeError:
break
if parent in candidates:
continue continue
try: idx = subclass.count(LOOKUP_SEP)
return getattr(self.provider, subclass) if type(parent) is not base_class:
except AttributeError: idx += 1
pass candidates.insert(idx, parent)
return None if not candidates:
return None
return candidates[-1]
def __str__(self): def __str__(self):
return str(self.name) return str(self.name)
@ -901,7 +908,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
except ControlFlowException as exc: except ControlFlowException as exc:
raise exc raise exc
except Exception as exc: except Exception as exc:
raise PropertyMappingExpressionException(self, exc) from exc raise PropertyMappingExpressionException(exc, self) from exc
def __str__(self): def __str__(self):
return f"Property Mapping {self.name}" return f"Property Mapping {self.name}"

View File

@ -9,9 +9,12 @@ from rest_framework.test import APITestCase
from authentik.core.models import Application from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import OAuth2Provider from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.proxy.models import ProxyProvider
from authentik.providers.saml.models import SAMLProvider
class TestApplicationsAPI(APITestCase): class TestApplicationsAPI(APITestCase):
@ -222,3 +225,31 @@ class TestApplicationsAPI(APITestCase):
], ],
}, },
) )
def test_get_provider(self):
"""Ensure that proxy providers (at the time of writing that is the only provider
that inherits from another proxy type (OAuth) instead of inheriting from the root
provider class) is correctly looked up and selected from the database"""
slug = generate_id()
provider = ProxyProvider.objects.create(name=generate_id())
Application.objects.create(
name=generate_id(),
slug=slug,
provider=provider,
)
self.assertEqual(Application.objects.get(slug=slug).get_provider(), provider)
self.assertEqual(
Application.objects.with_provider().get(slug=slug).get_provider(), provider
)
slug = generate_id()
provider = SAMLProvider.objects.create(name=generate_id())
Application.objects.create(
name=generate_id(),
slug=slug,
provider=provider,
)
self.assertEqual(Application.objects.get(slug=slug).get_provider(), provider)
self.assertEqual(
Application.objects.with_provider().get(slug=slug).get_provider(), provider
)

View File

@ -3,10 +3,10 @@
from json import loads from json import loads
from django.urls import reverse from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.core.models import User from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.core.tests.utils import create_test_admin_user
from authentik.tenants.utils import get_current_tenant from authentik.tenants.utils import get_current_tenant
@ -15,7 +15,7 @@ class TestImpersonation(APITestCase):
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()
self.other_user = User.objects.create(username="to-impersonate") self.other_user = create_test_user()
self.user = create_test_admin_user() self.user = create_test_admin_user()
def test_impersonate_simple(self): def test_impersonate_simple(self):
@ -44,6 +44,26 @@ class TestImpersonation(APITestCase):
self.assertEqual(response_body["user"]["username"], self.user.username) self.assertEqual(response_body["user"]["username"], self.user.username)
self.assertNotIn("original", response_body) self.assertNotIn("original", response_body)
def test_impersonate_scoped(self):
"""Test impersonation with scoped permissions"""
new_user = create_test_user()
assign_perm("authentik_core.impersonate", new_user, self.other_user)
assign_perm("authentik_core.view_user", new_user, self.other_user)
self.client.force_login(new_user)
response = self.client.post(
reverse(
"authentik_api:user-impersonate",
kwargs={"pk": self.other_user.pk},
)
)
self.assertEqual(response.status_code, 201)
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())
self.assertEqual(response_body["user"]["username"], self.other_user.username)
self.assertEqual(response_body["original"]["username"], new_user.username)
def test_impersonate_denied(self): def test_impersonate_denied(self):
"""test impersonation without permissions""" """test impersonation without permissions"""
self.client.force_login(self.other_user) self.client.force_login(self.other_user)

View File

@ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import User, UserTypes from authentik.core.models import User, UserTypes
from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer
from authentik.enterprise.models import License, LicenseUsageStatus from authentik.enterprise.models import License
from authentik.rbac.decorators import permission_required from authentik.rbac.decorators import permission_required
from authentik.tenants.utils import get_unique_identifier from authentik.tenants.utils import get_unique_identifier
@ -29,7 +29,7 @@ class EnterpriseRequiredMixin:
def validate(self, attrs: dict) -> dict: def validate(self, attrs: dict) -> dict:
"""Check that a valid license exists""" """Check that a valid license exists"""
if LicenseKey.cached_summary().status != LicenseUsageStatus.UNLICENSED: if not LicenseKey.cached_summary().status.is_valid:
raise ValidationError(_("Enterprise is required to create/update this object.")) raise ValidationError(_("Enterprise is required to create/update this object."))
return super().validate(attrs) return super().validate(attrs)

View File

@ -25,4 +25,4 @@ class AuthentikEnterpriseConfig(EnterpriseConfig):
"""Actual enterprise check, cached""" """Actual enterprise check, cached"""
from authentik.enterprise.license import LicenseKey from authentik.enterprise.license import LicenseKey
return LicenseKey.cached_summary().status return LicenseKey.cached_summary().status.is_valid

View File

@ -117,10 +117,13 @@ class LicenseKey:
our_cert.public_key(), our_cert.public_key(),
algorithms=["ES512"], algorithms=["ES512"],
audience=get_license_aud(), audience=get_license_aud(),
options={"verify_exp": check_expiry}, options={"verify_exp": check_expiry, "verify_signature": check_expiry},
), ),
) )
except PyJWTError: except PyJWTError:
unverified = decode(jwt, options={"verify_signature": False})
if unverified["aud"] != get_license_aud():
raise ValidationError("Invalid Install ID in license") from None
raise ValidationError("Unable to verify license") from None raise ValidationError("Unable to verify license") from None
return body return body
@ -134,7 +137,7 @@ class LicenseKey:
exp_ts = int(mktime(lic.expiry.timetuple())) exp_ts = int(mktime(lic.expiry.timetuple()))
if total.exp == 0: if total.exp == 0:
total.exp = exp_ts total.exp = exp_ts
total.exp = min(total.exp, exp_ts) total.exp = max(total.exp, exp_ts)
total.license_flags.extend(lic.status.license_flags) total.license_flags.extend(lic.status.license_flags)
return total return total

View File

@ -3,7 +3,7 @@
from datetime import datetime from datetime import datetime
from django.core.cache import cache from django.core.cache import cache
from django.db.models.signals import post_save, pre_save from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import get_current_timezone from django.utils.timezone import get_current_timezone
@ -27,3 +27,9 @@ def post_save_license(sender: type[License], instance: License, **_):
"""Trigger license usage calculation when license is saved""" """Trigger license usage calculation when license is saved"""
cache.delete(CACHE_KEY_ENTERPRISE_LICENSE) cache.delete(CACHE_KEY_ENTERPRISE_LICENSE)
enterprise_update_usage.delay() enterprise_update_usage.delay()
@receiver(post_delete, sender=License)
def post_delete_license(sender: type[License], instance: License, **_):
"""Clear license cache when license is deleted"""
cache.delete(CACHE_KEY_ENTERPRISE_LICENSE)

View File

@ -69,8 +69,5 @@ class NotificationViewSet(
@action(detail=False, methods=["post"]) @action(detail=False, methods=["post"])
def mark_all_seen(self, request: Request) -> Response: def mark_all_seen(self, request: Request) -> Response:
"""Mark all the user's notifications as seen""" """Mark all the user's notifications as seen"""
notifications = Notification.objects.filter(user=request.user) Notification.objects.filter(user=request.user, seen=False).update(seen=True)
for notification in notifications:
notification.seen = True
Notification.objects.bulk_update(notifications, ["seen"])
return Response({}, status=204) return Response({}, status=204)

View File

@ -2,7 +2,8 @@
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from django.test import TestCase from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.events.models import ( from authentik.events.models import (
@ -10,6 +11,7 @@ from authentik.events.models import (
EventAction, EventAction,
Notification, Notification,
NotificationRule, NotificationRule,
NotificationSeverity,
NotificationTransport, NotificationTransport,
NotificationWebhookMapping, NotificationWebhookMapping,
TransportMode, TransportMode,
@ -20,7 +22,7 @@ from authentik.policies.exceptions import PolicyException
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
class TestEventsNotifications(TestCase): class TestEventsNotifications(APITestCase):
"""Test Event Notifications""" """Test Event Notifications"""
def setUp(self) -> None: def setUp(self) -> None:
@ -131,3 +133,15 @@ class TestEventsNotifications(TestCase):
Notification.objects.all().delete() Notification.objects.all().delete()
Event.new(EventAction.CUSTOM_PREFIX).save() Event.new(EventAction.CUSTOM_PREFIX).save()
self.assertEqual(Notification.objects.first().body, "foo") self.assertEqual(Notification.objects.first().body, "foo")
def test_api_mark_all_seen(self):
"""Test mark_all_seen"""
self.client.force_login(self.user)
Notification.objects.create(
severity=NotificationSeverity.NOTICE, body="foo", user=self.user, seen=False
)
response = self.client.post(reverse("authentik_api:notification-mark-all-seen"))
self.assertEqual(response.status_code, 204)
self.assertFalse(Notification.objects.filter(body="foo", seen=False).exists())

View File

@ -2,7 +2,6 @@
import re import re
import socket import socket
from collections.abc import Iterable
from ipaddress import ip_address, ip_network from ipaddress import ip_address, ip_network
from textwrap import indent from textwrap import indent
from types import CodeType from types import CodeType
@ -28,6 +27,12 @@ from authentik.stages.authenticator import devices_for_user
LOGGER = get_logger() LOGGER = get_logger()
ARG_SANITIZE = re.compile(r"[:.-]")
def sanitize_arg(arg_name: str) -> str:
return re.sub(ARG_SANITIZE, "_", arg_name)
class BaseEvaluator: class BaseEvaluator:
"""Validate and evaluate python-based expressions""" """Validate and evaluate python-based expressions"""
@ -177,9 +182,9 @@ class BaseEvaluator:
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None) proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
return proc.profiling_wrapper() return proc.profiling_wrapper()
def wrap_expression(self, expression: str, params: Iterable[str]) -> str: def wrap_expression(self, expression: str) -> str:
"""Wrap expression in a function, call it, and save the result as `result`""" """Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(params) handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())
full_expression = "" full_expression = ""
full_expression += f"def handler({handler_signature}):\n" full_expression += f"def handler({handler_signature}):\n"
full_expression += indent(expression, " ") full_expression += indent(expression, " ")
@ -188,8 +193,8 @@ class BaseEvaluator:
def compile(self, expression: str) -> CodeType: def compile(self, expression: str) -> CodeType:
"""Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect.""" """Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect."""
param_keys = self._context.keys() expression = self.wrap_expression(expression)
return compile(self.wrap_expression(expression, param_keys), self._filename, "exec") return compile(expression, self._filename, "exec")
def evaluate(self, expression_source: str) -> Any: def evaluate(self, expression_source: str) -> Any:
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised. """Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
@ -205,7 +210,7 @@ class BaseEvaluator:
self.handle_error(exc, expression_source) self.handle_error(exc, expression_source)
raise exc raise exc
try: try:
_locals = self._context _locals = {sanitize_arg(x): y for x, y in self._context.items()}
# Yes this is an exec, yes it is potentially bad. Since we limit what variables are # Yes this is an exec, yes it is potentially bad. Since we limit what variables are
# available here, and these policies can only be edited by admins, this is a risk # available here, and these policies can only be edited by admins, this is a risk
# we're willing to take. # we're willing to take.

View File

@ -4,13 +4,13 @@ from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db import migrations from django.db import migrations
from django.contrib.auth.management import create_permissions
def migrate_search_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): def migrate_search_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from guardian.shortcuts import assign_perm
from authentik.core.models import User from authentik.core.models import User
from django.apps import apps as real_apps from django.apps import apps as real_apps
from django.contrib.auth.management import create_permissions
from guardian.shortcuts import UserObjectPermission
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
@ -20,14 +20,25 @@ def migrate_search_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
create_permissions(real_apps.get_app_config("authentik_providers_ldap"), using=db_alias) create_permissions(real_apps.get_app_config("authentik_providers_ldap"), using=db_alias)
LDAPProvider = apps.get_model("authentik_providers_ldap", "ldapprovider") LDAPProvider = apps.get_model("authentik_providers_ldap", "ldapprovider")
Permission = apps.get_model("auth", "Permission")
UserObjectPermission = apps.get_model("guardian", "UserObjectPermission")
ContentType = apps.get_model("contenttypes", "ContentType")
new_prem = Permission.objects.using(db_alias).get(codename="search_full_directory")
ct = ContentType.objects.using(db_alias).get(
app_label="authentik_providers_ldap",
model="ldapprovider",
)
for provider in LDAPProvider.objects.using(db_alias).all(): for provider in LDAPProvider.objects.using(db_alias).all():
for user_pk in ( if not provider.search_group:
provider.search_group.users.using(db_alias).all().values_list("pk", flat=True) continue
): for user in provider.search_group.users.using(db_alias).all():
# We need the correct user model instance to assign the permission UserObjectPermission.objects.using(db_alias).create(
assign_perm( user=user,
"search_full_directory", User.objects.using(db_alias).get(pk=user_pk), provider permission=new_prem,
object_pk=provider.pk,
content_type=ct,
) )
@ -35,6 +46,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
("authentik_providers_ldap", "0003_ldapprovider_mfa_support_and_more"), ("authentik_providers_ldap", "0003_ldapprovider_mfa_support_and_more"),
("guardian", "0002_generic_permissions_index"),
] ]
operations = [ operations = [

View File

@ -433,20 +433,21 @@ class TokenParams:
app = Application.objects.filter(provider=self.provider).first() app = Application.objects.filter(provider=self.provider).first()
if not app or not app.provider: if not app or not app.provider:
raise TokenError("invalid_grant") raise TokenError("invalid_grant")
self.user, _ = User.objects.update_or_create( with audit_ignore():
# trim username to ensure the entire username is max 150 chars self.user, _ = User.objects.update_or_create(
# (22 chars being the length of the "template") # trim username to ensure the entire username is max 150 chars
username=f"ak-{self.provider.name[:150-22]}-client_credentials", # (22 chars being the length of the "template")
defaults={ username=f"ak-{self.provider.name[:150-22]}-client_credentials",
"attributes": { defaults={
USER_ATTRIBUTE_GENERATED: True, "attributes": {
USER_ATTRIBUTE_GENERATED: True,
},
"last_login": timezone.now(),
"name": f"Autogenerated user from application {app.name} (client credentials)",
"path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
"type": UserTypes.SERVICE_ACCOUNT,
}, },
"last_login": timezone.now(), )
"name": f"Autogenerated user from application {app.name} (client credentials)",
"path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
"type": UserTypes.SERVICE_ACCOUNT,
},
)
self.__check_policy_access(app, request) self.__check_policy_access(app, request)
Event.new( Event.new(

View File

@ -164,7 +164,7 @@ class SAMLProvider(Provider):
) )
sign_assertion = models.BooleanField(default=True) sign_assertion = models.BooleanField(default=True)
sign_response = models.BooleanField(default=True) sign_response = models.BooleanField(default=False)
@property @property
def launch_url(self) -> str | None: def launch_url(self) -> str | None:

View File

@ -54,7 +54,11 @@ class TestServiceProviderMetadataParser(TestCase):
request = self.factory.get("/") request = self.factory.get("/")
metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor()) metadata = lxml_from_string(MetadataProcessor(provider, request).build_entity_descriptor())
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd")) # nosec schema = etree.XMLSchema(
etree.parse(
source="schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()
) # nosec
)
self.assertTrue(schema.validate(metadata)) self.assertTrue(schema.validate(metadata))
def test_schema_want_authn_requests_signed(self): def test_schema_want_authn_requests_signed(self):

View File

@ -47,7 +47,9 @@ class TestSchema(TestCase):
metadata = lxml_from_string(request) metadata = lxml_from_string(request)
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd")) # nosec schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
)
self.assertTrue(schema.validate(metadata)) self.assertTrue(schema.validate(metadata))
def test_response_schema(self): def test_response_schema(self):
@ -68,5 +70,7 @@ class TestSchema(TestCase):
metadata = lxml_from_string(response) metadata = lxml_from_string(response)
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-protocol-2.0.xsd")) # nosec schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
)
self.assertTrue(schema.validate(metadata)) self.assertTrue(schema.validate(metadata))

View File

@ -87,7 +87,11 @@ def task_error_hook(task_id: str, exception: Exception, traceback, *args, **kwar
def _get_startup_tasks_default_tenant() -> list[Callable]: def _get_startup_tasks_default_tenant() -> list[Callable]:
"""Get all tasks to be run on startup for the default tenant""" """Get all tasks to be run on startup for the default tenant"""
return [] from authentik.outposts.tasks import outpost_connection_discovery
return [
outpost_connection_discovery,
]
def _get_startup_tasks_all_tenants() -> list[Callable]: def _get_startup_tasks_all_tenants() -> list[Callable]:

View File

@ -1,6 +1,7 @@
"""authentik storage backends""" """authentik storage backends"""
import os import os
from urllib.parse import parse_qsl, urlsplit
from django.conf import settings from django.conf import settings
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
@ -110,3 +111,34 @@ class S3Storage(BaseS3Storage):
if self.querystring_auth: if self.querystring_auth:
return url return url
return self._strip_signing_parameters(url) return self._strip_signing_parameters(url)
def _strip_signing_parameters(self, url):
# Boto3 does not currently support generating URLs that are unsigned. Instead
# we take the signed URLs and strip any querystring params related to signing
# and expiration.
# Note that this may end up with URLs that are still invalid, especially if
# params are passed in that only work with signed URLs, e.g. response header
# params.
# The code attempts to strip all query parameters that match names of known
# parameters from v2 and v4 signatures, regardless of the actual signature
# version used.
split_url = urlsplit(url)
qs = parse_qsl(split_url.query, keep_blank_values=True)
blacklist = {
"x-amz-algorithm",
"x-amz-credential",
"x-amz-date",
"x-amz-expires",
"x-amz-signedheaders",
"x-amz-signature",
"x-amz-security-token",
"awsaccesskeyid",
"expires",
"signature",
}
filtered_qs = ((key, val) for key, val in qs if key.lower() not in blacklist)
# Note: Parameters that did not have a value in the original query string will
# have an '=' sign appended to it, e.g ?foo&bar becomes ?foo=&bar=
joined_qs = ("=".join(keyval) for keyval in filtered_qs)
split_url = split_url._replace(query="&".join(joined_qs))
return split_url.geturl()

View File

@ -38,7 +38,11 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
search_base=self.base_dn_groups, search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter, search_filter=self._source.group_object_filter,
search_scope=SUBTREE, search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES], attributes=[
ALL_ATTRIBUTES,
ALL_OPERATIONAL_ATTRIBUTES,
self._source.object_uniqueness_field,
],
**kwargs, **kwargs,
) )
@ -53,9 +57,9 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
continue continue
attributes = group.get("attributes", {}) attributes = group.get("attributes", {})
group_dn = flatten(flatten(group.get("entryDN", group.get("dn")))) group_dn = flatten(flatten(group.get("entryDN", group.get("dn"))))
if self._source.object_uniqueness_field not in attributes: if not attributes.get(self._source.object_uniqueness_field):
self.message( self.message(
f"Cannot find uniqueness field in attributes: '{group_dn}'", f"Uniqueness field not found/not set in attributes: '{group_dn}'",
attributes=attributes.keys(), attributes=attributes.keys(),
dn=group_dn, dn=group_dn,
) )

View File

@ -40,7 +40,11 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
search_base=self.base_dn_users, search_base=self.base_dn_users,
search_filter=self._source.user_object_filter, search_filter=self._source.user_object_filter,
search_scope=SUBTREE, search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES], attributes=[
ALL_ATTRIBUTES,
ALL_OPERATIONAL_ATTRIBUTES,
self._source.object_uniqueness_field,
],
**kwargs, **kwargs,
) )
@ -55,9 +59,9 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
continue continue
attributes = user.get("attributes", {}) attributes = user.get("attributes", {})
user_dn = flatten(user.get("entryDN", user.get("dn"))) user_dn = flatten(user.get("entryDN", user.get("dn")))
if self._source.object_uniqueness_field not in attributes: if not attributes.get(self._source.object_uniqueness_field):
self.message( self.message(
f"Cannot find uniqueness field in attributes: '{user_dn}'", f"Uniqueness field not found/not set in attributes: '{user_dn}'",
attributes=attributes.keys(), attributes=attributes.keys(),
dn=user_dn, dn=user_dn,
) )

View File

@ -30,7 +30,9 @@ class TestMetadataProcessor(TestCase):
xml = MetadataProcessor(self.source, request).build_entity_descriptor() xml = MetadataProcessor(self.source, request).build_entity_descriptor()
metadata = lxml_from_string(xml) metadata = lxml_from_string(xml)
schema = etree.XMLSchema(etree.parse("schemas/saml-schema-metadata-2.0.xsd")) # nosec schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-metadata-2.0.xsd", parser=etree.XMLParser()) # nosec
)
self.assertTrue(schema.validate(metadata)) self.assertTrue(schema.validate(metadata))
def test_metadata_consistent(self): def test_metadata_consistent(self):

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json", "$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object", "type": "object",
"title": "authentik 2024.6.4 Blueprint schema", "title": "authentik 2024.8.2 Blueprint schema",
"required": [ "required": [
"version", "version",
"entries" "entries"

View File

@ -31,7 +31,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.4} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.2}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -52,7 +52,7 @@ services:
- postgresql - postgresql
- redis - redis
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.4} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.8.2}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion()) return fmt.Sprintf("authentik@%s", FullVersion())
} }
const VERSION = "2024.6.4" const VERSION = "2024.8.2"

View File

@ -35,10 +35,11 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
req PaginatorRequest[Treq, Tres], req PaginatorRequest[Treq, Tres],
opts PaginatorOptions, opts PaginatorOptions,
) ([]Tobj, error) { ) ([]Tobj, error) {
var bfreq, cfreq interface{}
fetchOffset := func(page int32) (Tres, error) { fetchOffset := func(page int32) (Tres, error) {
req.Page(page) bfreq = req.Page(page)
req.PageSize(int32(opts.PageSize)) cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
res, _, err := req.Execute() res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
if err != nil { if err != nil {
opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page") opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
} }

View File

@ -0,0 +1,26 @@
package ak
// func Test_PaginatorCompile(t *testing.T) {
// req := api.ApiCoreUsersListRequest{}
// Paginator(req, PaginatorOptions{
// PageSize: 100,
// })
// }
// func Test_PaginatorCompileExplicit(t *testing.T) {
// req := api.ApiCoreUsersListRequest{}
// Paginator[
// api.User,
// api.ApiCoreUsersListRequest,
// *api.PaginatedUserList,
// ](req, PaginatorOptions{
// PageSize: 100,
// })
// }
// func Test_PaginatorCompileOther(t *testing.T) {
// req := api.ApiOutpostsProxyListRequest{}
// Paginator(req, PaginatorOptions{
// PageSize: 100,
// })
// }

View File

@ -96,7 +96,7 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
return ldap.LDAPResultOperationsError, nil return ldap.LDAPResultOperationsError, nil
} }
flags.UserPk = userInfo.User.Pk flags.UserPk = userInfo.User.Pk
flags.CanSearch = access.HasSearchPermission != nil flags.CanSearch = access.GetHasSearchPermission()
db.si.SetFlags(req.BindDN, &flags) db.si.SetFlags(req.BindDN, &flags)
if flags.CanSearch { if flags.CanSearch {
req.Log().Debug("Allowed access to search") req.Log().Debug("Allowed access to search")

View File

@ -193,7 +193,17 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server) (*A
}) })
mux.HandleFunc("/outpost.goauthentik.io/start", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/outpost.goauthentik.io/start", func(w http.ResponseWriter, r *http.Request) {
a.handleAuthStart(w, r, "") fwd := ""
// This should only really be hit for nginx forward_auth
// as for that the auth start redirect URL is generated by the
// reverse proxy, and as such we won't have a request we just
// denied to reference for final URL
rd, ok := a.checkRedirectParam(r)
if ok {
a.log.WithField("rd", rd).Trace("Setting redirect")
fwd = rd
}
a.handleAuthStart(w, r, fwd)
}) })
mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleAuthCallback) mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleAuthCallback)
mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut) mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut)

View File

@ -15,36 +15,6 @@ const (
LogoutSignature = "X-authentik-logout" LogoutSignature = "X-authentik-logout"
) )
func (a *Application) checkRedirectParam(r *http.Request) (string, bool) {
rd := r.URL.Query().Get(redirectParam)
if rd == "" {
return "", false
}
u, err := url.Parse(rd)
if err != nil {
a.log.WithError(err).Warning("Failed to parse redirect URL")
return "", false
}
// Check to make sure we only redirect to allowed places
if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE {
ext, err := url.Parse(a.proxyConfig.ExternalHost)
if err != nil {
return "", false
}
ext.Scheme = ""
if !strings.Contains(u.String(), ext.String()) {
a.log.WithField("url", u.String()).WithField("ext", ext.String()).Warning("redirect URI did not contain external host")
return "", false
}
} else {
if !strings.HasSuffix(u.Host, *a.proxyConfig.CookieDomain) {
a.log.WithField("host", u.Host).WithField("dom", *a.proxyConfig.CookieDomain).Warning("redirect URI Host was not included in cookie domain")
return "", false
}
}
return u.String(), true
}
func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, fwd string) { func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, fwd string) {
state, err := a.createState(r, fwd) state, err := a.createState(r, fwd)
if err != nil { if err != nil {

View File

@ -5,10 +5,13 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"goauthentik.io/api/v3"
) )
type OAuthState struct { type OAuthState struct {
@ -27,6 +30,44 @@ func (oas *OAuthState) GetAudience() (jwt.ClaimStrings, error) { return ni
var base32RawStdEncoding = base32.StdEncoding.WithPadding(base32.NoPadding) var base32RawStdEncoding = base32.StdEncoding.WithPadding(base32.NoPadding)
// Validate that the given redirect parameter (?rd=...) is valid and can be used
// For proxy/forward_single this checks that if the `rd` param has a Hostname (and is a full URL)
// the hostname matches what's configured, or no hostname must be given
// For forward_domain this checks if the domain of the URL in `rd` ends with the configured domain
func (a *Application) checkRedirectParam(r *http.Request) (string, bool) {
rd := r.URL.Query().Get(redirectParam)
if rd == "" {
return "", false
}
u, err := url.Parse(rd)
if err != nil {
a.log.WithError(err).Warning("Failed to parse redirect URL")
return "", false
}
// Check to make sure we only redirect to allowed places
if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE {
ext, err := url.Parse(a.proxyConfig.ExternalHost)
if err != nil {
return "", false
}
// Either hostname needs to match the configured domain, or host name must be empty for just a path
if u.Host == "" {
u.Host = ext.Host
u.Scheme = ext.Scheme
}
if u.Host != ext.Host {
a.log.WithField("url", u.String()).WithField("ext", ext.String()).Warning("redirect URI did not contain external host")
return "", false
}
} else {
if !strings.HasSuffix(u.Host, *a.proxyConfig.CookieDomain) {
a.log.WithField("host", u.Host).WithField("dom", *a.proxyConfig.CookieDomain).Warning("redirect URI Host was not included in cookie domain")
return "", false
}
}
return u.String(), true
}
func (a *Application) createState(r *http.Request, fwd string) (string, error) { func (a *Application) createState(r *http.Request, fwd string) (string, error) {
s, _ := a.sessions.Get(r, a.SessionName()) s, _ := a.sessions.Get(r, a.SessionName())
if s.ID == "" { if s.ID == "" {
@ -39,17 +80,6 @@ func (a *Application) createState(r *http.Request, fwd string) (string, error) {
SessionID: s.ID, SessionID: s.ID,
Redirect: fwd, Redirect: fwd,
} }
if fwd == "" {
// This should only really be hit for nginx forward_auth
// as for that the auth start redirect URL is generated by the
// reverse proxy, and as such we won't have a request we just
// denied to reference for final URL
rd, ok := a.checkRedirectParam(r)
if ok {
a.log.WithField("rd", rd).Trace("Setting redirect")
st.Redirect = rd
}
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, st) token := jwt.NewWithClaims(jwt.SigningMethodHS256, st)
tokenString, err := token.SignedString([]byte(a.proxyConfig.GetCookieSecret())) tokenString, err := token.SignedString([]byte(a.proxyConfig.GetCookieSecret()))
if err != nil { if err != nil {

View File

@ -8,25 +8,45 @@ import (
"goauthentik.io/api/v3" "goauthentik.io/api/v3"
) )
func TestCheckRedirectParam(t *testing.T) { func TestCheckRedirectParam_None(t *testing.T) {
a := newTestApplication() a := newTestApplication()
// Test no rd param
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start", nil) req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start", nil)
rd, ok := a.checkRedirectParam(req) rd, ok := a.checkRedirectParam(req)
assert.Equal(t, false, ok) assert.Equal(t, false, ok)
assert.Equal(t, "", rd) assert.Equal(t, "", rd)
}
req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://google.com", nil) func TestCheckRedirectParam_Invalid(t *testing.T) {
a := newTestApplication()
// Test invalid rd param
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://google.com", nil)
rd, ok = a.checkRedirectParam(req) rd, ok := a.checkRedirectParam(req)
assert.Equal(t, false, ok) assert.Equal(t, false, ok)
assert.Equal(t, "", rd) assert.Equal(t, "", rd)
}
req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test?foo", nil) func TestCheckRedirectParam_ValidFull(t *testing.T) {
a := newTestApplication()
// Test valid full rd param
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test?foo", nil)
rd, ok = a.checkRedirectParam(req) rd, ok := a.checkRedirectParam(req)
assert.Equal(t, true, ok)
assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd)
}
func TestCheckRedirectParam_ValidPartial(t *testing.T) {
a := newTestApplication()
// Test valid partial rd param
req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=/test?foo", nil)
rd, ok := a.checkRedirectParam(req)
assert.Equal(t, true, ok) assert.Equal(t, true, ok)
assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd) assert.Equal(t, "https://ext.t.goauthentik.io/test?foo", rd)

View File

@ -1,5 +1,5 @@
{ {
"name": "@goauthentik/authentik", "name": "@goauthentik/authentik",
"version": "2024.6.4", "version": "2024.8.2",
"private": true "private": true
} }

58
poetry.lock generated
View File

@ -1053,38 +1053,38 @@ toml = ["tomli"]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "43.0.0" version = "43.0.1"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"},
{file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"},
{file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"},
{file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"},
{file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"},
{file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"},
{file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"},
] ]
[package.dependencies] [package.dependencies]
@ -1097,7 +1097,7 @@ nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"] pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"] sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"] ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"] test-randomorder = ["pytest-randomly"]
[[package]] [[package]]

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "authentik" name = "authentik"
version = "2024.6.4" version = "2024.8.2"
description = "" description = ""
authors = ["authentik Team <hello@goauthentik.io>"] authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2024.6.4 version: 2024.8.2
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io

View File

@ -11,6 +11,7 @@ from ldap3.core.exceptions import LDAPInvalidCredentialsResult
from authentik.blueprints.tests import apply_blueprint, reconcile_app from authentik.blueprints.tests import apply_blueprint, reconcile_app
from authentik.core.models import Application, User from authentik.core.models import Application, User
from authentik.core.tests.utils import create_test_user
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
@ -331,6 +332,83 @@ class TestProviderLDAP(SeleniumTestCase):
] ]
self.assert_list_dict_equal(expected, response) self.assert_list_dict_equal(expected, response)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@reconcile_app("authentik_tenants")
@reconcile_app("authentik_outposts")
def test_ldap_bind_search_no_perms(self):
"""Test simple bind + search"""
user = create_test_user()
self._prepare()
server = Server("ldap://localhost:3389", get_info=ALL)
_connection = Connection(
server,
raise_exceptions=True,
user=f"cn={user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
password=user.username,
)
_connection.bind()
self.assertTrue(
Event.objects.filter(
action=EventAction.LOGIN,
user={
"pk": user.pk,
"email": user.email,
"username": user.username,
},
)
)
_connection.search(
"ou=Users,DC=ldaP,dc=goauthentik,dc=io",
"(objectClass=user)",
search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
)
response: list = _connection.response
# Remove raw_attributes to make checking easier
for obj in response:
del obj["raw_attributes"]
del obj["raw_dn"]
obj["attributes"] = dict(obj["attributes"])
expected = [
{
"dn": f"cn={user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": {
"cn": user.username,
"sAMAccountName": user.username,
"uid": user.uid,
"name": user.name,
"displayName": user.name,
"sn": user.name,
"mail": user.email,
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"user",
"posixAccount",
"goauthentik.io/ldap/user",
],
"uidNumber": 2000 + user.pk,
"gidNumber": 2000 + user.pk,
"memberOf": [
f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
for group in user.ak_groups.all()
],
"homeDirectory": f"/home/{user.username}",
"ak-active": True,
"ak-superuser": False,
},
"type": "searchResEntry",
},
]
self.assert_list_dict_equal(expected, response)
def assert_list_dict_equal(self, expected: list[dict], actual: list[dict], match_key="dn"): def assert_list_dict_equal(self, expected: list[dict], actual: list[dict], match_key="dn"):
"""Assert a list of dictionaries is identical, ignoring the ordering of items""" """Assert a list of dictionaries is identical, ignoring the ordering of items"""
self.assertEqual(len(expected), len(actual)) self.assertEqual(len(expected), len(actual))

8813
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -91,7 +91,6 @@
"glob": "^11.0.0", "glob": "^11.0.0",
"globals": "^15.9.0", "globals": "^15.9.0",
"lit-analyzer": "^2.0.3", "lit-analyzer": "^2.0.3",
"lockfile-lint": "^4.14.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"pseudolocale": "^2.1.0", "pseudolocale": "^2.1.0",
@ -257,7 +256,9 @@
] ]
}, },
"lint:lockfile": { "lint:lockfile": {
"command": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https" "__comment": "The lockfile-lint package does not have an option to ensure resolved hashes are set everywhere",
"shell": true,
"command": "[ -z \"$(jq -r '.packages | to_entries[] | select((.key | startswith(\"node_modules\")) and (.value | has(\"resolved\") | not)) | .key' < package-lock.json)\" ]"
}, },
"lint:lockfiles": { "lint:lockfiles": {
"dependencies": [ "dependencies": [

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { parseAPIError } from "@goauthentik/common/errors";
import "@goauthentik/components/ak-radio-input"; import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input"; import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
@ -24,7 +25,6 @@ import {
type TransactionApplicationRequest, type TransactionApplicationRequest,
type TransactionApplicationResponse, type TransactionApplicationResponse,
ValidationError, ValidationError,
ValidationErrorFromJSON,
} from "@goauthentik/api"; } from "@goauthentik/api";
import BasePanel from "../BasePanel"; import BasePanel from "../BasePanel";
@ -59,7 +59,7 @@ const runningState: State = {
}; };
const errorState: State = { const errorState: State = {
state: "error", state: "error",
label: msg("Authentik was unable to save this application:"), label: msg("authentik was unable to save this application:"),
icon: ["fa-times-circle", "pf-m-danger"], icon: ["fa-times-circle", "pf-m-danger"],
}; };
@ -133,9 +133,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
}) })
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch(async (resolution: any) => { .catch(async (resolution: any) => {
const errors = (this.errors = ValidationErrorFromJSON( const errors = await parseAPIError(resolution);
await resolution.response.json(),
));
this.dispatchWizardUpdate({ this.dispatchWizardUpdate({
update: { update: {
...this.wizard, ...this.wizard,

View File

@ -11,7 +11,10 @@ import {
redirectUriHelp, redirectUriHelp,
subjectModeOptions, subjectModeOptions,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; } from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import { oauth2SourcesProvider } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js"; import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-number-input"; import "@goauthentik/components/ak-number-input";
@ -263,12 +266,12 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
name="jwksSources" name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []} .errorMessages=${errors?.jwksSources ?? []}
> >
<ak-dual-select-provider <ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider} .provider=${oauth2SourcesProvider}
.selected=${provider?.jwksSources} .selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")} available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")} selected-label=${msg("Selected Sources")}
></ak-dual-select-provider> ></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.", "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",

View File

@ -1,5 +1,8 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title"; import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import { oauth2SourcesProvider } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js"; import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { import {
makeProxyPropertyMappingsSelector, makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider, proxyPropertyMappingsProvider,
@ -11,7 +14,6 @@ import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input"; import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/components/ak-toggle-group"; import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
@ -228,12 +230,12 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
name="jwksSources" name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []} .errorMessages=${errors?.jwksSources ?? []}
> >
<ak-dual-select-provider <ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider} .provider=${oauth2SourcesProvider}
.selected=${this.instance?.jwksSources} .selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")} available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")} selected-label=${msg("Selected Sources")}
></ak-dual-select-provider> ></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.", "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",

View File

@ -1,5 +1,7 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { severityToLabel } from "@goauthentik/common/labels"; import { severityToLabel } from "@goauthentik/common/labels";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio"; import "@goauthentik/elements/forms/Radio";
@ -16,6 +18,7 @@ import {
EventsApi, EventsApi,
Group, Group,
NotificationRule, NotificationRule,
NotificationTransport,
PaginatedNotificationTransportList, PaginatedNotificationTransportList,
SeverityEnum, SeverityEnum,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -34,6 +37,13 @@ async function eventTransportsProvider(page = 1, search = "") {
}; };
} }
export function makeTransportSelector(instanceTransports: string[] | undefined) {
const localTransports = instanceTransports ? new Set(instanceTransports) : undefined;
return localTransports
? ([pk, _]: DualSelectPair) => localTransports.has(pk)
: ([_0, _1, _2, stage]: DualSelectPair<NotificationTransport>) => stage !== undefined;
}
@customElement("ak-event-rule-form") @customElement("ak-event-rule-form")
export class RuleForm extends ModelForm<NotificationRule, string> { export class RuleForm extends ModelForm<NotificationRule, string> {
eventTransports?: PaginatedNotificationTransportList; eventTransports?: PaginatedNotificationTransportList;
@ -114,12 +124,12 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
?required=${true} ?required=${true}
name="transports" name="transports"
> >
<ak-dual-select-provider <ak-dual-select-dynamic-selected
.provider=${eventTransportsProvider} .provider=${eventTransportsProvider}
.selected=${this.instance?.transports} .selector=${makeTransportSelector(this.instance?.transports)}
available-label="${msg("Available Transports")}" available-label="${msg("Available Transports")}"
selected-label="${msg("Selected Transports")}" selected-label="${msg("Selected Transports")}"
></ak-dual-select-provider> ></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(
"Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.", "Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",

View File

@ -97,7 +97,8 @@ export class OutpostForm extends ModelForm<Outpost, string> {
embedded = false; embedded = false;
@state() @state()
providers?: DataProvider; providers: DataProvider = providerProvider(this.type);
defaultConfig?: OutpostDefaultConfig; defaultConfig?: OutpostDefaultConfig;
async loadInstance(pk: string): Promise<Outpost> { async loadInstance(pk: string): Promise<Outpost> {
@ -113,6 +114,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
this.defaultConfig = await new OutpostsApi( this.defaultConfig = await new OutpostsApi(
DEFAULT_CONFIG, DEFAULT_CONFIG,
).outpostsInstancesDefaultSettingsRetrieve(); ).outpostsInstancesDefaultSettingsRetrieve();
this.providers = providerProvider(this.type);
} }
getSuccessMessage(): string { getSuccessMessage(): string {

View File

@ -8,7 +8,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
interface PropertyMapping { interface PropertyMapping {
name: string; name: string;
expression: string; expression?: string;
} }
export abstract class BasePropertyMappingForm<T extends PropertyMapping> extends ModelForm< export abstract class BasePropertyMappingForm<T extends PropertyMapping> extends ModelForm<

View File

@ -1,14 +1,14 @@
import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { NotificationWebhookMapping, PropertymappingsApi } from "@goauthentik/api"; import { NotificationWebhookMapping, PropertymappingsApi } from "@goauthentik/api";
@customElement("ak-property-mapping-notification-form") @customElement("ak-property-mapping-notification-form")
export class PropertyMappingNotification extends ModelForm<NotificationWebhookMapping, string> { export class PropertyMappingNotification extends BasePropertyMappingForm<NotificationWebhookMapping> {
loadInstance(pk: string): Promise<NotificationWebhookMapping> { loadInstance(pk: string): Promise<NotificationWebhookMapping> {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsNotificationRetrieve({ return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsNotificationRetrieve({
pmUuid: pk, pmUuid: pk,

View File

@ -1,10 +1,10 @@
import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global"; import { docLink } from "@goauthentik/common/global";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio"; import "@goauthentik/elements/forms/Radio";
import type { RadioOption } from "@goauthentik/elements/forms/Radio"; import type { RadioOption } from "@goauthentik/elements/forms/Radio";
@ -33,21 +33,13 @@ export const staticSettingOptions: RadioOption<string | undefined>[] = [
]; ];
@customElement("ak-property-mapping-provider-rac-form") @customElement("ak-property-mapping-provider-rac-form")
export class PropertyMappingProviderRACForm extends ModelForm<RACPropertyMapping, string> { export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACPropertyMapping> {
loadInstance(pk: string): Promise<RACPropertyMapping> { loadInstance(pk: string): Promise<RACPropertyMapping> {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsProviderRacRetrieve({ return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsProviderRacRetrieve({
pmUuid: pk, pmUuid: pk,
}); });
} }
getSuccessMessage(): string {
if (this.instance) {
return msg("Successfully updated mapping.");
} else {
return msg("Successfully created mapping.");
}
}
async send(data: RACPropertyMapping): Promise<RACPropertyMapping> { async send(data: RACPropertyMapping): Promise<RACPropertyMapping> {
if (this.instance) { if (this.instance) {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsProviderRacUpdate({ return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsProviderRacUpdate({

View File

@ -10,7 +10,7 @@ import { LDAPSourcePropertyMapping, PropertymappingsApi } from "@goauthentik/api
@customElement("ak-property-mapping-source-ldap-form") @customElement("ak-property-mapping-source-ldap-form")
export class PropertyMappingSourceLDAPForm extends BasePropertyMappingForm<LDAPSourcePropertyMapping> { export class PropertyMappingSourceLDAPForm extends BasePropertyMappingForm<LDAPSourcePropertyMapping> {
docLink(): string { docLink(): string {
return "/docs/sources/property-mappings/expression?utm_source=authentik"; return "/docs/sources/property-mappings/expressions?utm_source=authentik";
} }
loadInstance(pk: string): Promise<LDAPSourcePropertyMapping> { loadInstance(pk: string): Promise<LDAPSourcePropertyMapping> {

View File

@ -10,7 +10,7 @@ import { OAuthSourcePropertyMapping, PropertymappingsApi } from "@goauthentik/ap
@customElement("ak-property-mapping-source-oauth-form") @customElement("ak-property-mapping-source-oauth-form")
export class PropertyMappingSourceOAuthForm extends BasePropertyMappingForm<OAuthSourcePropertyMapping> { export class PropertyMappingSourceOAuthForm extends BasePropertyMappingForm<OAuthSourcePropertyMapping> {
docLink(): string { docLink(): string {
return "/docs/sources/property-mappings/expression?utm_source=authentik"; return "/docs/sources/property-mappings/expressions?utm_source=authentik";
} }
loadInstance(pk: string): Promise<OAuthSourcePropertyMapping> { loadInstance(pk: string): Promise<OAuthSourcePropertyMapping> {

View File

@ -10,7 +10,7 @@ import { PlexSourcePropertyMapping, PropertymappingsApi } from "@goauthentik/api
@customElement("ak-property-mapping-source-plex-form") @customElement("ak-property-mapping-source-plex-form")
export class PropertyMappingSourcePlexForm extends BasePropertyMappingForm<PlexSourcePropertyMapping> { export class PropertyMappingSourcePlexForm extends BasePropertyMappingForm<PlexSourcePropertyMapping> {
docLink(): string { docLink(): string {
return "/docs/sources/property-mappings/expression?utm_source=authentik"; return "/docs/sources/property-mappings/expressions?utm_source=authentik";
} }
loadInstance(pk: string): Promise<PlexSourcePropertyMapping> { loadInstance(pk: string): Promise<PlexSourcePropertyMapping> {

View File

@ -10,7 +10,7 @@ import { PropertymappingsApi, SAMLSourcePropertyMapping } from "@goauthentik/api
@customElement("ak-property-mapping-source-saml-form") @customElement("ak-property-mapping-source-saml-form")
export class PropertyMappingSourceSAMLForm extends BasePropertyMappingForm<SAMLSourcePropertyMapping> { export class PropertyMappingSourceSAMLForm extends BasePropertyMappingForm<SAMLSourcePropertyMapping> {
docLink(): string { docLink(): string {
return "/docs/sources/property-mappings/expression?utm_source=authentik"; return "/docs/sources/property-mappings/expressions?utm_source=authentik";
} }
loadInstance(pk: string): Promise<SAMLSourcePropertyMapping> { loadInstance(pk: string): Promise<SAMLSourcePropertyMapping> {

View File

@ -10,7 +10,7 @@ import { PropertymappingsApi, SCIMSourcePropertyMapping } from "@goauthentik/api
@customElement("ak-property-mapping-source-scim-form") @customElement("ak-property-mapping-source-scim-form")
export class PropertyMappingSourceSCIMForm extends BasePropertyMappingForm<SCIMSourcePropertyMapping> { export class PropertyMappingSourceSCIMForm extends BasePropertyMappingForm<SCIMSourcePropertyMapping> {
docLink(): string { docLink(): string {
return "/docs/sources/property-mappings/expression?utm_source=authentik"; return "/docs/sources/property-mappings/expressions?utm_source=authentik";
} }
loadInstance(pk: string): Promise<SCIMSourcePropertyMapping> { loadInstance(pk: string): Promise<SCIMSourcePropertyMapping> {

View File

@ -61,7 +61,9 @@ export class PolicyTestForm extends Form<PropertyMappingTestRequest> {
</ak-codemirror>` </ak-codemirror>`
: html` <div class="pf-c-form__group-label"> : html` <div class="pf-c-form__group-label">
<div class="c-form__horizontal-group"> <div class="c-form__horizontal-group">
<span class="pf-c-form__label-text">${this.result?.result}</span> <span class="pf-c-form__label-text">
<pre>${this.result?.result}</pre>
</span>
</div> </div>
</div>`} </div>`}
</ak-form-element-horizontal>`; </ak-form-element-horizontal>`;

View File

@ -27,7 +27,7 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
renderToolbar(): TemplateResult { renderToolbar(): TemplateResult {
return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}> return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Sync")}</span> <span slot="submit">${msg("Sync")}</span>
<span slot="header">${msg("Sync User")}</span> <span slot="header">${msg("Sync Group")}</span>
<ak-sync-object-form <ak-sync-object-form
.provider=${this.providerId} .provider=${this.providerId}
model=${SyncObjectModelEnum.Group} model=${SyncObjectModelEnum.Group}

View File

@ -24,7 +24,7 @@ export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProvide
renderToolbar(): TemplateResult { renderToolbar(): TemplateResult {
return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}> return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Sync")}</span> <span slot="submit">${msg("Sync")}</span>
<span slot="header">${msg("Sync User")}</span> <span slot="header">${msg("Sync Group")}</span>
<ak-sync-object-form <ak-sync-object-form
.provider=${this.providerId} .provider=${this.providerId}
model=${SyncObjectModelEnum.Group} model=${SyncObjectModelEnum.Group}

View File

@ -3,6 +3,12 @@ import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api"; import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api";
export const defaultScopes = [
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-profile",
];
export async function oauth2PropertyMappingsProvider(page = 1, search = "") { export async function oauth2PropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi( const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG, DEFAULT_CONFIG,
@ -23,6 +29,5 @@ export function makeOAuth2PropertyMappingsSelector(instanceMappings: string[] |
return localMappings return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk) ? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, scope]: DualSelectPair<ScopeMapping>) => : ([_0, _1, _2, scope]: DualSelectPair<ScopeMapping>) =>
scope?.managed?.startsWith("goauthentik.io/providers/oauth2/scope-") && scope?.managed && defaultScopes.includes(scope?.managed);
scope?.managed !== "goauthentik.io/providers/oauth2/scope-offline_access";
} }

View File

@ -32,7 +32,7 @@ import {
makeOAuth2PropertyMappingsSelector, makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider, oauth2PropertyMappingsProvider,
} from "./OAuth2PropertyMappings.js"; } from "./OAuth2PropertyMappings.js";
import { oauth2SourcesProvider } from "./OAuth2Sources.js"; import { makeSourceSelector, oauth2SourcesProvider } from "./OAuth2Sources.js";
export const clientTypeOptions = [ export const clientTypeOptions = [
{ {
@ -52,12 +52,6 @@ export const clientTypeOptions = [
}, },
]; ];
export const defaultScopes = [
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-profile",
];
export const subjectModeOptions = [ export const subjectModeOptions = [
{ {
label: msg("Based on the User's hashed ID"), label: msg("Based on the User's hashed ID"),
@ -335,12 +329,12 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
label=${msg("Trusted OIDC Sources")} label=${msg("Trusted OIDC Sources")}
name="jwksSources" name="jwksSources"
> >
<ak-dual-select-provider <ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider} .provider=${oauth2SourcesProvider}
.selected=${provider?.jwksSources} .selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")} available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")} selected-label=${msg("Selected Sources")}
></ak-dual-select-provider> ></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.", "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",

View File

@ -1,6 +1,7 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import { SourcesApi } from "@goauthentik/api"; import { OAuthSource, SourcesApi } from "@goauthentik/api";
export async function oauth2SourcesProvider(page = 1, search = "") { export async function oauth2SourcesProvider(page = 1, search = "") {
const oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({ const oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({
@ -19,3 +20,11 @@ export async function oauth2SourcesProvider(page = 1, search = "") {
]), ]),
}; };
} }
export function makeSourceSelector(instanceSources: string[] | undefined) {
const localSources = instanceSources ? new Set(instanceSources) : undefined;
return localSources
? ([pk, _]: DualSelectPair) => localSources.has(pk)
: ([_0, _1, _2, prompt]: DualSelectPair<OAuthSource>) => prompt !== undefined;
}

View File

@ -1,12 +1,14 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { oauth2SourcesProvider } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js"; import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group"; import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
@ -403,12 +405,12 @@ ${this.instance?.skipPathRegex}</textarea
label=${msg("Trusted OIDC Sources")} label=${msg("Trusted OIDC Sources")}
name="jwksSources" name="jwksSources"
> >
<ak-dual-select-provider <ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider} .provider=${oauth2SourcesProvider}
.selected=${this.instance?.jwksSources} .selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")} available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")} selected-label=${msg("Selected Sources")}
></ak-dual-select-provider> ></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.", "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",

View File

@ -1,12 +1,14 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/sync/SyncObjectForm";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ProvidersApi, SCIMProviderGroup } from "@goauthentik/api"; import { ProvidersApi, SCIMProviderGroup, SyncObjectModelEnum } from "@goauthentik/api";
@customElement("ak-provider-scim-groups-list") @customElement("ak-provider-scim-groups-list")
export class SCIMProviderGroupList extends Table<SCIMProviderGroup> { export class SCIMProviderGroupList extends Table<SCIMProviderGroup> {
@ -20,6 +22,22 @@ export class SCIMProviderGroupList extends Table<SCIMProviderGroup> {
checkbox = true; checkbox = true;
clearOnRefresh = true; clearOnRefresh = true;
renderToolbar(): TemplateResult {
return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Sync")}</span>
<span slot="header">${msg("Sync Group")}</span>
<ak-sync-object-form
.provider=${this.providerId}
model=${SyncObjectModelEnum.Group}
.sync=${new ProvidersApi(DEFAULT_CONFIG).providersScimSyncObjectCreate}
slot="form"
>
</ak-sync-object-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Sync")}</button>
</ak-forms-modal>
${super.renderToolbar()}`;
}
renderToolbarSelected(): TemplateResult { renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1; const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk return html`<ak-forms-delete-bulk

View File

@ -1,12 +1,14 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/sync/SyncObjectForm";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ProvidersApi, SCIMProviderUser } from "@goauthentik/api"; import { ProvidersApi, SCIMProviderUser, SyncObjectModelEnum } from "@goauthentik/api";
@customElement("ak-provider-scim-users-list") @customElement("ak-provider-scim-users-list")
export class SCIMProviderUserList extends Table<SCIMProviderUser> { export class SCIMProviderUserList extends Table<SCIMProviderUser> {
@ -20,6 +22,22 @@ export class SCIMProviderUserList extends Table<SCIMProviderUser> {
checkbox = true; checkbox = true;
clearOnRefresh = true; clearOnRefresh = true;
renderToolbar(): TemplateResult {
return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Sync")}</span>
<span slot="header">${msg("Sync User")}</span>
<ak-sync-object-form
.provider=${this.providerId}
model=${SyncObjectModelEnum.User}
.sync=${new ProvidersApi(DEFAULT_CONFIG).providersScimSyncObjectCreate}
slot="form"
>
</ak-sync-object-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Sync")}</button>
</ak-forms-modal>
${super.renderToolbar()}`;
}
renderToolbarSelected(): TemplateResult { renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1; const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk return html`<ak-forms-delete-bulk

View File

@ -2,7 +2,9 @@ import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils"; import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Alert"; import "@goauthentik/elements/Alert";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider"; import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio"; import "@goauthentik/elements/forms/Radio";
@ -18,6 +20,7 @@ import {
DeviceClassesEnum, DeviceClassesEnum,
NotConfiguredActionEnum, NotConfiguredActionEnum,
PaginatedStageList, PaginatedStageList,
Stage,
StagesApi, StagesApi,
UserVerificationEnum, UserVerificationEnum,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -36,6 +39,14 @@ async function stagesProvider(page = 1, search = "") {
}; };
} }
export function makeStageSelector(instanceStages: string[] | undefined) {
const localStages = instanceStages ? new Set(instanceStages) : undefined;
return localStages
? ([pk, _]: DualSelectPair) => localStages.has(pk)
: ([_0, _1, _2, stage]: DualSelectPair<Stage>) => stage !== undefined;
}
async function authenticatorWebauthnDeviceTypesListProvider(page = 1, search = "") { async function authenticatorWebauthnDeviceTypesListProvider(page = 1, search = "") {
const devicetypes = await new StagesApi( const devicetypes = await new StagesApi(
DEFAULT_CONFIG, DEFAULT_CONFIG,
@ -205,14 +216,14 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
label=${msg("Configuration stages")} label=${msg("Configuration stages")}
name="configurationStages" name="configurationStages"
> >
<ak-dual-select-provider <ak-dual-select-dynamic-selected
.provider=${stagesProvider} .provider=${stagesProvider}
.selected=${Array.from( .selector=${makeStageSelector(
this.instance?.configurationStages ?? [], this.instance?.configurationStages,
)} )}
available-label="${msg("Available Stages")}" available-label="${msg("Available Stages")}"
selected-label="${msg("Selected Stages")}" selected-label="${msg("Selected Stages")}"
></ak-dual-select-provider> ></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(
"Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.", "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.",

View File

@ -41,12 +41,13 @@ async function sourcesProvider(page = 1, search = "") {
}; };
} }
async function makeSourcesSelector(instanceSources: string[] | undefined) { function makeSourcesSelector(instanceSources: string[] | undefined) {
const localSources = instanceSources ? new Set(instanceSources) : undefined; const localSources = instanceSources ? new Set(instanceSources) : undefined;
return localSources return localSources
? ([pk, _]: DualSelectPair) => localSources.has(pk) ? ([pk, _]: DualSelectPair) => localSources.has(pk)
: ([_0, _1, _2, source]: DualSelectPair<Source>) => : // Creating a new instance, auto-select built-in source only when no other sources exist
([_0, _1, _2, source]: DualSelectPair<Source>) =>
source !== undefined && source.component === ""; source !== undefined && source.component === "";
} }
@ -75,11 +76,11 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
stageUuid: this.instance.pk || "", stageUuid: this.instance.pk || "",
identificationStageRequest: data, identificationStageRequest: data,
}); });
} else {
return new StagesApi(DEFAULT_CONFIG).stagesIdentificationCreate({
identificationStageRequest: data,
});
} }
return new StagesApi(DEFAULT_CONFIG).stagesIdentificationCreate({
identificationStageRequest: data,
});
} }
isUserFieldSelected(field: UserFieldsEnum): boolean { isUserFieldSelected(field: UserFieldsEnum): boolean {
@ -232,12 +233,12 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
?required=${true} ?required=${true}
name="sources" name="sources"
> >
<ak-dual-select-provider-dynamic-selected <ak-dual-select-dynamic-selected
.provider=${sourcesProvider} .provider=${sourcesProvider}
.selected=${makeSourcesSelector(this.instance?.sources)} .selector=${makeSourcesSelector(this.instance?.sources)}
available-label="${msg("Available Stages")}" available-label="${msg("Available Stages")}"
selected-label="${msg("Selected Stages")}" selected-label="${msg("Selected Stages")}"
></ak-dual-select-provider-dynamic-selected> ></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(
"Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP.", "Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP.",

View File

@ -1,4 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { parseAPIError } from "@goauthentik/common/errors";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
@ -22,7 +23,7 @@ import {
PromptTypeEnum, PromptTypeEnum,
ResponseError, ResponseError,
StagesApi, StagesApi,
ValidationErrorFromJSON, ValidationError,
} from "@goauthentik/api"; } from "@goauthentik/api";
class PreviewStageHost implements StageHost { class PreviewStageHost implements StageHost {
@ -83,10 +84,8 @@ export class PromptForm extends ModelForm<Prompt, string> {
}); });
this.previewError = undefined; this.previewError = undefined;
} catch (exc) { } catch (exc) {
const errorMessage = ValidationErrorFromJSON( const errorMessage = parseAPIError(exc as ResponseError);
await (exc as ResponseError).response.json(), this.previewError = (errorMessage as ValidationError).nonFieldErrors;
);
this.previewError = errorMessage.nonFieldErrors;
} }
} }

View File

@ -2,6 +2,8 @@ import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import "@goauthentik/admin/stages/prompt/PromptForm"; import "@goauthentik/admin/stages/prompt/PromptForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums"; import { PFSize } from "@goauthentik/common/enums";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
@ -11,9 +13,9 @@ import { TemplateResult, html, nothing } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { PoliciesApi, PromptStage, StagesApi } from "@goauthentik/api"; import { PoliciesApi, Policy, Prompt, PromptStage, StagesApi } from "@goauthentik/api";
async function promptsProvider(page = 1, search = "") { async function promptFieldsProvider(page = 1, search = "") {
const prompts = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsList({ const prompts = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsList({
ordering: "field_name", ordering: "field_name",
pageSize: 20, pageSize: 20,
@ -25,11 +27,19 @@ async function promptsProvider(page = 1, search = "") {
pagination: prompts.pagination, pagination: prompts.pagination,
options: prompts.results.map((prompt) => [ options: prompts.results.map((prompt) => [
prompt.pk, prompt.pk,
str`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`, msg(str`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`),
]), ]),
}; };
} }
function makeFieldSelector(instanceFields: string[] | undefined) {
const localFields = instanceFields ? new Set(instanceFields) : undefined;
return localFields
? ([pk, _]: DualSelectPair) => localFields.has(pk)
: ([_0, _1, _2, prompt]: DualSelectPair<Prompt>) => prompt !== undefined;
}
async function policiesProvider(page = 1, search = "") { async function policiesProvider(page = 1, search = "") {
const policies = await new PoliciesApi(DEFAULT_CONFIG).policiesAllList({ const policies = await new PoliciesApi(DEFAULT_CONFIG).policiesAllList({
ordering: "name", ordering: "name",
@ -47,6 +57,14 @@ async function policiesProvider(page = 1, search = "") {
}; };
} }
function makePoliciesSelector(instancePolicies: string[] | undefined) {
const localPolicies = instancePolicies ? new Set(instancePolicies) : undefined;
return localPolicies
? ([pk, _]: DualSelectPair) => localPolicies.has(pk)
: ([_0, _1, _2, policy]: DualSelectPair<Policy>) => policy !== undefined;
}
@customElement("ak-stage-prompt-form") @customElement("ak-stage-prompt-form")
export class PromptStageForm extends BaseStageForm<PromptStage> { export class PromptStageForm extends BaseStageForm<PromptStage> {
loadInstance(pk: string): Promise<PromptStage> { loadInstance(pk: string): Promise<PromptStage> {
@ -90,12 +108,12 @@ export class PromptStageForm extends BaseStageForm<PromptStage> {
?required=${true} ?required=${true}
name="fields" name="fields"
> >
<ak-dual-select-provider <ak-dual-select-dynamic-selected
.provider=${promptsProvider} .provider=${promptFieldsProvider}
.selected=${this.instance?.fields} .selector=${makeFieldSelector(this.instance?.fields)}
available-label="${msg("Available Fields")}" available-label="${msg("Available Fields")}"
selected-label="${msg("Selected Fields")}" selected-label="${msg("Selected Fields")}"
></ak-dual-select-provider> ></ak-dual-select-dynamic-selected>
${this.instance ${this.instance
? html`<ak-forms-modal size=${PFSize.XLarge}> ? html`<ak-forms-modal size=${PFSize.XLarge}>
<span slot="submit"> ${msg("Create")} </span> <span slot="submit"> ${msg("Create")} </span>
@ -115,12 +133,12 @@ export class PromptStageForm extends BaseStageForm<PromptStage> {
label=${msg("Validation Policies")} label=${msg("Validation Policies")}
name="validationPolicies" name="validationPolicies"
> >
<ak-dual-select-provider <ak-dual-select-dynamic-selected
.provider=${policiesProvider} .provider=${policiesProvider}
.selected=${this.instance?.validationPolicies} .selector=${makePoliciesSelector(this.instance?.validationPolicies)}
available-label="${msg("Available Fields")}" available-label="${msg("Available Fields")}"
selected-label="${msg("Selected Fields")}" selected-label="${msg("Selected Fields")}"
></ak-dual-select-provider> ></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${msg( ${msg(
"Selected policies are executed when the stage is submitted to validate the data.", "Selected policies are executed when the stage is submitted to validate the data.",

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger"; export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress"; export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current"; export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2024.6.4"; export const VERSION = "2024.8.2";
export const TITLE_DEFAULT = "authentik"; export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";"; export const ROUTE_SEPARATOR = ";";

View File

@ -12,11 +12,17 @@ export class RequestError extends Error {}
export type APIErrorTypes = ValidationError | GenericError; export type APIErrorTypes = ValidationError | GenericError;
export const HTTP_BAD_REQUEST = 400;
export const HTTP_INTERNAL_SERVICE_ERROR = 500;
export async function parseAPIError(error: Error): Promise<APIErrorTypes> { export async function parseAPIError(error: Error): Promise<APIErrorTypes> {
if (!(error instanceof ResponseError)) { if (!(error instanceof ResponseError)) {
return error; return error;
} }
if (error.response.status < 400 || error.response.status > 499) { if (
error.response.status < HTTP_BAD_REQUEST ||
error.response.status >= HTTP_INTERNAL_SERVICE_ERROR
) {
return error; return error;
} }
const body = await error.response.json(); const body = await error.response.json();

View File

@ -50,3 +50,9 @@ export class AkDualSelectDynamic extends AkDualSelectProvider {
></ak-dual-select>`; ></ak-dual-select>`;
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ak-dual-select-dynamic-selected": AkDualSelectDynamic;
}
}

View File

@ -1,4 +1,5 @@
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { parseAPIError } from "@goauthentik/common/errors";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
import { camelToSnake, convertToSlug, dateToUTC } from "@goauthentik/common/utils"; import { camelToSnake, convertToSlug, dateToUTC } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
@ -6,6 +7,7 @@ import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFor
import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers"; import { PreventFormSubmit } from "@goauthentik/elements/forms/helpers";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { property, state } from "lit/decorators.js"; import { property, state } from "lit/decorators.js";
@ -18,7 +20,7 @@ import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-gro
import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css"; import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ResponseError, ValidationError, ValidationErrorFromJSON } from "@goauthentik/api"; import { ResponseError, ValidationError, instanceOfValidationError } from "@goauthentik/api";
export class APIError extends Error { export class APIError extends Error {
constructor(public response: ValidationError) { constructor(public response: ValidationError) {
@ -124,9 +126,6 @@ export function serializeForm<T extends KeyUnknown>(
return json as unknown as T; return json as unknown as T;
} }
const HTTP_BAD_REQUEST = 400;
const HTTP_INTERNAL_SERVICE_ERROR = 500;
/** /**
* Form * Form
* *
@ -307,18 +306,9 @@ export abstract class Form<T> extends AKElement {
return response; return response;
} catch (ex) { } catch (ex) {
if (ex instanceof ResponseError) { if (ex instanceof ResponseError) {
let msg = ex.response.statusText; let errorMessage = ex.response.statusText;
if ( const error = await parseAPIError(ex);
ex.response.status >= HTTP_BAD_REQUEST && if (instanceOfValidationError(error)) {
ex.response.status < HTTP_INTERNAL_SERVICE_ERROR
) {
const errorMessage = ValidationErrorFromJSON(await ex.response.json());
if (!errorMessage) {
return errorMessage;
}
if (errorMessage instanceof Error) {
throw errorMessage;
}
// assign all input-related errors to their elements // assign all input-related errors to their elements
const elements = const elements =
this.shadowRoot?.querySelectorAll<HorizontalFormElement>( this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
@ -330,26 +320,28 @@ export abstract class Form<T> extends AKElement {
if (!elementName) { if (!elementName) {
return; return;
} }
if (camelToSnake(elementName) in errorMessage) { if (camelToSnake(elementName) in error) {
element.errorMessages = errorMessage[camelToSnake(elementName)]; element.errorMessages = (error as ValidationError)[
camelToSnake(elementName)
];
element.invalid = true; element.invalid = true;
} else { } else {
element.errorMessages = []; element.errorMessages = [];
element.invalid = false; element.invalid = false;
} }
}); });
if (errorMessage.nonFieldErrors) { if ((error as ValidationError).nonFieldErrors) {
this.nonFieldErrors = errorMessage.nonFieldErrors; this.nonFieldErrors = (error as ValidationError).nonFieldErrors;
} }
errorMessage = msg("Invalid update request.");
// Only change the message when we have `detail`. // Only change the message when we have `detail`.
// Everything else is handled in the form. // Everything else is handled in the form.
if ("detail" in errorMessage) { if ("detail" in (error as ValidationError)) {
msg = errorMessage.detail; errorMessage = (error as ValidationError).detail;
} }
} }
// error is local or not from rest_framework
showMessage({ showMessage({
message: msg, message: errorMessage,
level: MessageLevel.error, level: MessageLevel.error,
}); });
} }

View File

@ -9,6 +9,7 @@ import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/messages/Message"; import "@goauthentik/elements/messages/Message";
import { APIMessage } from "@goauthentik/elements/messages/Message"; import { APIMessage } from "@goauthentik/elements/messages/Message";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
@ -20,6 +21,9 @@ export function showMessage(message: APIMessage, unique = false): void {
if (!container) { if (!container) {
throw new SentryIgnoredError("failed to find message container"); throw new SentryIgnoredError("failed to find message container");
} }
if (message.message.trim() === "") {
message.message = msg("Error");
}
container.addMessage(message, unique); container.addMessage(message, unique);
container.requestUpdate(); container.requestUpdate();
} }

View File

@ -125,8 +125,10 @@ export class MFADevicesPage extends Table<Device> {
return [ return [
html`${item.name}`, html`${item.name}`,
html`${deviceTypeName(item)}`, html`${deviceTypeName(item)}`,
html`<div>${getRelativeTime(item.created)}</div> html`${item.created.getTime() > 0
<small>${item.created.toLocaleString()}</small>`, ? html`<div>${getRelativeTime(item.created)}</div>
<small>${item.created.toLocaleString()}</small>`
: html`-`}`,
html`${item.lastUsed html`${item.lastUsed
? html`<div>${getRelativeTime(item.lastUsed)}</div> ? html`<div>${getRelativeTime(item.lastUsed)}</div>
<small>${item.lastUsed.toLocaleString()}</small>` <small>${item.lastUsed.toLocaleString()}</small>`

View File

@ -36,11 +36,11 @@ To disable existing blueprints, an empty file can be mounted over the existing b
File-based blueprints are automatically removed once they become unavailable, however none of the objects created by those blueprints afre affected by this. File-based blueprints are automatically removed once they become unavailable, however none of the objects created by those blueprints afre affected by this.
:::info :::info
Please note that, by default, blueprint discovery and evaluation is not guaranteed to follow any specific order. Please note that, by default, blueprint discovery and evaluation is not guaranteed to follow any specific order.
If you have dependencies between blueprints, you should use [meta models](/developer-docs/blueprints/v1/meta#authentik_blueprintsmetaapplyblueprint) to make sure that objects are created in the correct order. If you have dependencies between blueprints, you should use [meta models](./v1/meta#authentik_blueprintsmetaapplyblueprint) to make sure that objects are created in the correct order.
::: :::
## Storage - OCI ## Storage - OCI

View File

@ -105,11 +105,7 @@ The following events occur when a license expeires and is not renewed within two
### About users and licenses ### About users and licenses
License usage is calculated based on total user counts and log-in data data that authentik regularly captures. This data is checked against all valid licenses, and the sum total of all users. License usage is calculated based on total user counts that authentik regularly captures. This data is checked against all valid licenses, and the sum total of all users. Internal and external users are counted based on the number of active users of the respective type saved in authentik. Service account users are not counted towards the license.
- The **_internal user_** count is calculated based on actual users assigned to the organization.
- The **_external user_** count is calculated based on how many external users were active (i.e. logged in) since the start of the current month.
:::info :::info
An **internal** user is typically a team member, such as company employees, who has access to the full Enterprise feature set. An **external** user might be an external consultant, a volunteer in a charitable site, or a B2C customer who logged onto your website to shop. These users don't get access to Enterprise features. An **internal** user is typically a team member, such as company employees, who has access to the full Enterprise feature set. An **external** user might be an external consultant, a volunteer in a charitable site, or a B2C customer who logged onto your website to shop. These users don't get access to Enterprise features.

View File

@ -18,7 +18,8 @@ Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials& grant_type=client_credentials&
client_id=application_client_id& client_id=application_client_id&
username=my-service-account& username=my-service-account&
password=my-token password=my-token&
scope=profile
``` ```
This will return a JSON response with an `access_token`, which is a signed JWT token. This token can be sent along requests to other hosts, which can then validate the JWT based on the signing key configured in authentik. This will return a JSON response with an `access_token`, which is a signed JWT token. This token can be sent along requests to other hosts, which can then validate the JWT based on the signing key configured in authentik.

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,43 @@ See the [overview](../property-mappings/index.md) for information on how propert
### Expression data ### Expression data
The following variables are available to SCIM source property mappings: Each top level SCIM attribute is available as a variable in the expression. For example given an SCIM request with the payload of
- `data`: A Python dictionary containing data from the SCIM source. ```json
{
"schemas": [
"urn:scim:schemas:core:2.0",
"urn:scim:schemas:extension:enterprise:2.0"
],
"userName": "foo.bar",
"name": {
"familyName": "bar",
"givenName": "foo",
"formatted": "foo.bar"
},
"emails": [
{
"value": "foo.bar@authentik.company",
"type": "work",
"primary": true
}
],
"title": "",
"urn:scim:schemas:extension:enterprise:2.0": {
"department": ""
}
}
```
The following variables are available in the expression:
- `schemas` as a list of strings
- `userName` as a string
- `name` as a dictionary
- `emails` as a dictionary
- `title` as a string
- `urn_scim_schemas_extension_enterprise_2_0` as a dictionary
:::info
Top-level keys which include symbols not allowed in python syntax are converted to `_`.
:::

View File

@ -77,13 +77,17 @@ const docsSidebar = {
items: [ items: [
{ {
type: "category", type: "category",
label: "Property Mappings", label: "OAuth2 Provider",
link: { link: {
type: "doc", type: "doc",
id: "providers/property-mappings/index", id: "providers/oauth2/index",
}, },
items: ["providers/property-mappings/expression"], items: [
"providers/oauth2/client_credentials",
"providers/oauth2/device_code",
],
}, },
"providers/saml/index",
{ {
type: "category", type: "category",
label: "Google Workspace Provider", label: "Google Workspace Provider",
@ -117,19 +121,6 @@ const docsSidebar = {
"providers/entra/add-entra-provider", "providers/entra/add-entra-provider",
], ],
}, },
{
type: "category",
label: "OAuth2 Provider",
link: {
type: "doc",
id: "providers/oauth2/index",
},
items: [
"providers/oauth2/client_credentials",
"providers/oauth2/device_code",
],
},
"providers/saml/index",
"providers/radius/index", "providers/radius/index",
{ {
type: "category", type: "category",
@ -167,6 +158,15 @@ const docsSidebar = {
}, },
items: ["providers/rac/how-to-rac"], items: ["providers/rac/how-to-rac"],
}, },
{
type: "category",
label: "Property Mappings",
link: {
type: "doc",
id: "providers/property-mappings/index",
},
items: ["providers/property-mappings/expression"],
},
], ],
}, },
{ {
@ -422,13 +422,14 @@ const docsSidebar = {
description: "Release Notes for recent authentik versions", description: "Release Notes for recent authentik versions",
}, },
items: [ items: [
"releases/2024/v2024.8",
"releases/2024/v2024.6", "releases/2024/v2024.6",
"releases/2024/v2024.4", "releases/2024/v2024.4",
"releases/2024/v2024.2",
{ {
type: "category", type: "category",
label: "Previous versions", label: "Previous versions",
items: [ items: [
"releases/2024/v2024.2",
"releases/2023/v2023.10", "releases/2023/v2023.10",
"releases/2023/v2023.8", "releases/2023/v2023.8",
"releases/2023/v2023.6", "releases/2023/v2023.6",