Compare commits

...

26 Commits

Author SHA1 Message Date
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
62 changed files with 4556 additions and 222 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2024.6.4 current_version = 2024.8.1
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.1"
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

@ -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

@ -901,7 +901,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

@ -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,7 +117,7 @@ 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:
@ -134,7 +134,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

@ -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

@ -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

@ -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.1 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.1}
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.1}
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.1"

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

@ -1,5 +1,5 @@
{ {
"name": "@goauthentik/authentik", "name": "@goauthentik/authentik",
"version": "2024.6.4", "version": "2024.8.1",
"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.1"
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.1
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))

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";
@ -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

@ -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.1";
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

@ -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",