core: app entitlements (#12090)

* core: initial app entitlements

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

* base off of pbm

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

* add tests and oauth2

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

* add to proxy

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

* rewrite to use bindings

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

* make policy bindings form and list more customizable

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

* fix tests

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

* double fix

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

* refine permissions

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

* add missing rbac modal to app entitlements

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

* separate scope for app entitlements

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

* include entitlements mapping in proxy

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

* fix tests

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

* add API validation to prevent policies from being bound to entitlements

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

* make preview

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

* add initial docs

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

* fix

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

* remove duplicate docs

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2024-12-18 14:32:44 +01:00
committed by GitHub
parent 675a4a6788
commit 40a7135c0c
39 changed files with 1246 additions and 94 deletions

View File

@ -0,0 +1,54 @@
"""Application Roles API Viewset"""
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import (
Application,
ApplicationEntitlement,
User,
)
class ApplicationEntitlementSerializer(ModelSerializer):
"""ApplicationEntitlement Serializer"""
def validate_app(self, app: Application) -> Application:
"""Ensure user has permission to view"""
user: User = self._context["request"].user
if user.has_perm("view_application", app) or user.has_perm(
"authentik_core.view_application"
):
return app
raise ValidationError(_("User does not have access to application."), code="invalid")
class Meta:
model = ApplicationEntitlement
fields = [
"pbm_uuid",
"name",
"app",
"attributes",
]
class ApplicationEntitlementViewSet(UsedByMixin, ModelViewSet):
"""ApplicationEntitlement Viewset"""
queryset = ApplicationEntitlement.objects.all()
serializer_class = ApplicationEntitlementSerializer
search_fields = [
"pbm_uuid",
"name",
"app",
"attributes",
]
filterset_fields = [
"pbm_uuid",
"name",
"app",
]
ordering = ["name"]

View File

@ -22,7 +22,7 @@ from authentik.blueprints.v1.common import (
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
from authentik.core.api.applications import ApplicationSerializer from authentik.core.api.applications import ApplicationSerializer
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider from authentik.core.models import Application, Provider
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.bindings import PolicyBindingSerializer from authentik.policies.api.bindings import PolicyBindingSerializer
@ -51,6 +51,13 @@ class TransactionProviderField(DictField):
class TransactionPolicyBindingSerializer(PolicyBindingSerializer): class TransactionPolicyBindingSerializer(PolicyBindingSerializer):
"""PolicyBindingSerializer which does not require target as target is set implicitly""" """PolicyBindingSerializer which does not require target as target is set implicitly"""
def validate(self, attrs):
# As the PolicyBindingSerializer checks that the correct things can be bound to a target
# but we don't have a target here as that's set by the blueprint, pass in an empty app
# which will have the correct allowed combination of group/user/policy.
attrs["target"] = Application()
return super().validate(attrs)
class Meta(PolicyBindingSerializer.Meta): class Meta(PolicyBindingSerializer.Meta):
fields = [x for x in PolicyBindingSerializer.Meta.fields if x != "target"] fields = [x for x in PolicyBindingSerializer.Meta.fields if x != "target"]

View File

@ -0,0 +1,45 @@
# Generated by Django 5.0.9 on 2024-11-20 15:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0040_provider_invalidation_flow"),
("authentik_policies", "0011_policybinding_failure_result_and_more"),
]
operations = [
migrations.CreateModel(
name="ApplicationEntitlement",
fields=[
(
"policybindingmodel_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_policies.policybindingmodel",
),
),
("attributes", models.JSONField(blank=True, default=dict)),
("name", models.TextField()),
(
"app",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.application"
),
),
],
options={
"verbose_name": "Application Entitlement",
"verbose_name_plural": "Application Entitlements",
"unique_together": {("app", "name")},
},
bases=("authentik_policies.policybindingmodel", models.Model),
),
]

View File

@ -314,6 +314,32 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
always_merger.merge(final_attributes, self.attributes) always_merger.merge(final_attributes, self.attributes)
return final_attributes return final_attributes
def app_entitlements(self, app: "Application | None") -> QuerySet["ApplicationEntitlement"]:
"""Get all entitlements this user has for `app`."""
if not app:
return []
all_groups = self.all_groups()
qs = app.applicationentitlement_set.filter(
Q(
Q(bindings__user=self) | Q(bindings__group__in=all_groups),
bindings__negate=False,
)
| Q(
Q(~Q(bindings__user=self), bindings__user__isnull=False)
| Q(~Q(bindings__group__in=all_groups), bindings__group__isnull=False),
bindings__negate=True,
),
bindings__enabled=True,
).order_by("name")
return qs
def app_entitlements_attributes(self, app: "Application | None") -> dict:
"""Get a dictionary containing all merged attributes from app entitlements for `app`."""
final_attributes = {}
for attrs in self.app_entitlements(app).values_list("attributes", flat=True):
always_merger.merge(final_attributes, attrs)
return final_attributes
@property @property
def serializer(self) -> Serializer: def serializer(self) -> Serializer:
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
@ -581,6 +607,31 @@ class Application(SerializerModel, PolicyBindingModel):
verbose_name_plural = _("Applications") verbose_name_plural = _("Applications")
class ApplicationEntitlement(AttributesMixin, SerializerModel, PolicyBindingModel):
"""Application-scoped entitlement to control authorization in an application"""
name = models.TextField()
app = models.ForeignKey(Application, on_delete=models.CASCADE)
class Meta:
verbose_name = _("Application Entitlement")
verbose_name_plural = _("Application Entitlements")
unique_together = (("app", "name"),)
def __str__(self):
return f"Application Entitlement {self.name} for app {self.app_id}"
@property
def serializer(self) -> type[Serializer]:
from authentik.core.api.application_entitlements import ApplicationEntitlementSerializer
return ApplicationEntitlementSerializer
def supported_policy_binding_targets(self):
return ["group", "user"]
class SourceUserMatchingModes(models.TextChoices): class SourceUserMatchingModes(models.TextChoices):
"""Different modes a source can handle new/returning users""" """Different modes a source can handle new/returning users"""

View File

@ -0,0 +1,153 @@
"""Test Application Entitlements API"""
from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase
from authentik.core.models import Application, ApplicationEntitlement, Group
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import OAuth2Provider
class TestApplicationEntitlements(APITestCase):
"""Test application entitlements"""
def setUp(self) -> None:
self.user = create_test_user()
self.other_user = create_test_user()
self.provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=create_test_flow(),
)
self.app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
provider=self.provider,
)
def test_user(self):
"""Test user-direct assignment"""
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
PolicyBinding.objects.create(target=ent, user=self.user, order=0)
ents = self.user.app_entitlements(self.app)
self.assertEqual(len(ents), 1)
self.assertEqual(ents[0].name, ent.name)
def test_group(self):
"""Test direct group"""
group = Group.objects.create(name=generate_id())
self.user.ak_groups.add(group)
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
PolicyBinding.objects.create(target=ent, group=group, order=0)
ents = self.user.app_entitlements(self.app)
self.assertEqual(len(ents), 1)
self.assertEqual(ents[0].name, ent.name)
def test_group_indirect(self):
"""Test indirect group"""
parent = Group.objects.create(name=generate_id())
group = Group.objects.create(name=generate_id(), parent=parent)
self.user.ak_groups.add(group)
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
PolicyBinding.objects.create(target=ent, group=parent, order=0)
ents = self.user.app_entitlements(self.app)
self.assertEqual(len(ents), 1)
self.assertEqual(ents[0].name, ent.name)
def test_negate_user(self):
"""Test with negate flag"""
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
PolicyBinding.objects.create(target=ent, user=self.other_user, order=0, negate=True)
ents = self.user.app_entitlements(self.app)
self.assertEqual(len(ents), 1)
self.assertEqual(ents[0].name, ent.name)
def test_negate_group(self):
"""Test with negate flag"""
other_group = Group.objects.create(name=generate_id())
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
PolicyBinding.objects.create(target=ent, group=other_group, order=0, negate=True)
ents = self.user.app_entitlements(self.app)
self.assertEqual(len(ents), 1)
self.assertEqual(ents[0].name, ent.name)
def test_api_perms_global(self):
"""Test API creation with global permissions"""
assign_perm("authentik_core.add_applicationentitlement", self.user)
assign_perm("authentik_core.view_application", self.user)
self.client.force_login(self.user)
res = self.client.post(
reverse("authentik_api:applicationentitlement-list"),
data={
"name": generate_id(),
"app": self.app.pk,
},
)
self.assertEqual(res.status_code, 201)
def test_api_perms_scoped(self):
"""Test API creation with scoped permissions"""
assign_perm("authentik_core.add_applicationentitlement", self.user)
assign_perm("authentik_core.view_application", self.user, self.app)
self.client.force_login(self.user)
res = self.client.post(
reverse("authentik_api:applicationentitlement-list"),
data={
"name": generate_id(),
"app": self.app.pk,
},
)
self.assertEqual(res.status_code, 201)
def test_api_perms_missing(self):
"""Test API creation with no permissions"""
assign_perm("authentik_core.add_applicationentitlement", self.user)
self.client.force_login(self.user)
res = self.client.post(
reverse("authentik_api:applicationentitlement-list"),
data={
"name": generate_id(),
"app": self.app.pk,
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(res.content, {"app": ["User does not have access to application."]})
def test_api_bindings_policy(self):
"""Test that API doesn't allow policies to be bound to this"""
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
policy = DummyPolicy.objects.create(name=generate_id())
admin = create_test_admin_user()
self.client.force_login(admin)
response = self.client.post(
reverse("authentik_api:policybinding-list"),
data={
"target": ent.pbm_uuid,
"policy": policy.pk,
"order": 0,
},
)
self.assertJSONEqual(
response.content.decode(),
{"non_field_errors": ["One of 'group', 'user' must be set."]},
)
def test_api_bindings_group(self):
"""Test that API doesn't allow policies to be bound to this"""
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
group = Group.objects.create(name=generate_id())
admin = create_test_admin_user()
self.client.force_login(admin)
response = self.client.post(
reverse("authentik_api:policybinding-list"),
data={
"target": ent.pbm_uuid,
"group": group.pk,
"order": 0,
},
)
self.assertEqual(response.status_code, 201)
self.assertTrue(PolicyBinding.objects.filter(target=ent.pbm_uuid).exists())

View File

@ -6,6 +6,7 @@ from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.urls import path from django.urls import path
from authentik.core.api.application_entitlements import ApplicationEntitlementViewSet
from authentik.core.api.applications import ApplicationViewSet from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
@ -69,6 +70,7 @@ urlpatterns = [
api_urlpatterns = [ api_urlpatterns = [
("core/authenticated_sessions", AuthenticatedSessionViewSet), ("core/authenticated_sessions", AuthenticatedSessionViewSet),
("core/applications", ApplicationViewSet), ("core/applications", ApplicationViewSet),
("core/application_entitlements", ApplicationEntitlementViewSet),
path( path(
"core/transactional/applications/", "core/transactional/applications/",
TransactionalApplicationView.as_view(), TransactionalApplicationView.as_view(),

View File

@ -84,19 +84,17 @@ class PolicyBindingSerializer(ModelSerializer):
def validate(self, attrs: OrderedDict) -> OrderedDict: def validate(self, attrs: OrderedDict) -> OrderedDict:
"""Check that either policy, group or user is set.""" """Check that either policy, group or user is set."""
count = sum( target: PolicyBindingModel = attrs.get("target")
[ supported = target.supported_policy_binding_targets()
bool(attrs.get("policy", None)), supported.sort()
bool(attrs.get("group", None)), count = sum([bool(attrs.get(x, None)) for x in supported])
bool(attrs.get("user", None)),
]
)
invalid = count > 1 invalid = count > 1
empty = count < 1 empty = count < 1
warning = ", ".join(f"'{x}'" for x in supported)
if invalid: if invalid:
raise ValidationError("Only one of 'policy', 'group' or 'user' can be set.") raise ValidationError(f"Only one of {warning} can be set.")
if empty: if empty:
raise ValidationError("One of 'policy', 'group' or 'user' must be set.") raise ValidationError(f"One of {warning} must be set.")
return attrs return attrs

View File

@ -1,4 +1,6 @@
# Generated by Django 4.2.5 on 2023-09-13 18:07 # Generated by Django 4.2.5 on 2023-09-13 18:07
import authentik.lib.models
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -23,4 +25,13 @@ class Migration(migrations.Migration):
default=30, help_text="Timeout after which Policy execution is terminated." default=30, help_text="Timeout after which Policy execution is terminated."
), ),
), ),
migrations.AlterField(
model_name="policybinding",
name="target",
field=authentik.lib.models.InheritanceForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="bindings",
to="authentik_policies.policybindingmodel",
),
),
] ]

View File

@ -47,6 +47,10 @@ class PolicyBindingModel(models.Model):
def __str__(self) -> str: def __str__(self) -> str:
return f"PolicyBindingModel {self.pbm_uuid}" return f"PolicyBindingModel {self.pbm_uuid}"
def supported_policy_binding_targets(self):
"""Return the list of objects that can be bound to this object."""
return ["policy", "user", "group"]
class PolicyBinding(SerializerModel): class PolicyBinding(SerializerModel):
"""Relationship between a Policy and a PolicyBindingModel.""" """Relationship between a Policy and a PolicyBindingModel."""
@ -81,7 +85,9 @@ class PolicyBinding(SerializerModel):
blank=True, blank=True,
) )
target = InheritanceForeignKey(PolicyBindingModel, on_delete=models.CASCADE, related_name="+") target = InheritanceForeignKey(
PolicyBindingModel, on_delete=models.CASCADE, related_name="bindings"
)
negate = models.BooleanField( negate = models.BooleanField(
default=False, default=False,
help_text=_("Negates the outcome of the policy. Messages are unaffected."), help_text=_("Negates the outcome of the policy. Messages are unaffected."),

View File

@ -38,7 +38,7 @@ class TestBindingsAPI(APITestCase):
) )
self.assertJSONEqual( self.assertJSONEqual(
response.content.decode(), response.content.decode(),
{"non_field_errors": ["Only one of 'policy', 'group' or 'user' can be set."]}, {"non_field_errors": ["Only one of 'group', 'policy', 'user' can be set."]},
) )
def test_invalid_too_little(self): def test_invalid_too_little(self):
@ -49,5 +49,5 @@ class TestBindingsAPI(APITestCase):
) )
self.assertJSONEqual( self.assertJSONEqual(
response.content.decode(), response.content.decode(),
{"non_field_errors": ["One of 'policy', 'group' or 'user' must be set."]}, {"non_field_errors": ["One of 'group', 'policy', 'user' must be set."]},
) )

View File

@ -127,6 +127,7 @@ class Traefik3MiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
authResponseHeaders=[ authResponseHeaders=[
"X-authentik-username", "X-authentik-username",
"X-authentik-groups", "X-authentik-groups",
"X-authentik-entitlements",
"X-authentik-email", "X-authentik-email",
"X-authentik-name", "X-authentik-name",
"X-authentik-uid", "X-authentik-uid",

View File

@ -147,6 +147,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
"goauthentik.io/providers/oauth2/scope-openid", "goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-profile", "goauthentik.io/providers/oauth2/scope-profile",
"goauthentik.io/providers/oauth2/scope-email", "goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-entitlements",
"goauthentik.io/providers/proxy/scope-proxy", "goauthentik.io/providers/proxy/scope-proxy",
] ]
) )

View File

@ -3201,6 +3201,46 @@
} }
} }
}, },
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_core.applicationentitlement"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_core.applicationentitlement_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_core.applicationentitlement"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_core.applicationentitlement"
}
}
},
{ {
"type": "object", "type": "object",
"required": [ "required": [
@ -4640,6 +4680,7 @@
"authentik_core.group", "authentik_core.group",
"authentik_core.user", "authentik_core.user",
"authentik_core.application", "authentik_core.application",
"authentik_core.applicationentitlement",
"authentik_core.token", "authentik_core.token",
"authentik_enterprise.license", "authentik_enterprise.license",
"authentik_providers_google_workspace.googleworkspaceprovider", "authentik_providers_google_workspace.googleworkspaceprovider",
@ -6369,6 +6410,7 @@
"authentik_brands.delete_brand", "authentik_brands.delete_brand",
"authentik_brands.view_brand", "authentik_brands.view_brand",
"authentik_core.add_application", "authentik_core.add_application",
"authentik_core.add_applicationentitlement",
"authentik_core.add_authenticatedsession", "authentik_core.add_authenticatedsession",
"authentik_core.add_group", "authentik_core.add_group",
"authentik_core.add_groupsourceconnection", "authentik_core.add_groupsourceconnection",
@ -6381,6 +6423,7 @@
"authentik_core.add_usersourceconnection", "authentik_core.add_usersourceconnection",
"authentik_core.assign_user_permissions", "authentik_core.assign_user_permissions",
"authentik_core.change_application", "authentik_core.change_application",
"authentik_core.change_applicationentitlement",
"authentik_core.change_authenticatedsession", "authentik_core.change_authenticatedsession",
"authentik_core.change_group", "authentik_core.change_group",
"authentik_core.change_groupsourceconnection", "authentik_core.change_groupsourceconnection",
@ -6391,6 +6434,7 @@
"authentik_core.change_user", "authentik_core.change_user",
"authentik_core.change_usersourceconnection", "authentik_core.change_usersourceconnection",
"authentik_core.delete_application", "authentik_core.delete_application",
"authentik_core.delete_applicationentitlement",
"authentik_core.delete_authenticatedsession", "authentik_core.delete_authenticatedsession",
"authentik_core.delete_group", "authentik_core.delete_group",
"authentik_core.delete_groupsourceconnection", "authentik_core.delete_groupsourceconnection",
@ -6406,6 +6450,7 @@
"authentik_core.reset_user_password", "authentik_core.reset_user_password",
"authentik_core.unassign_user_permissions", "authentik_core.unassign_user_permissions",
"authentik_core.view_application", "authentik_core.view_application",
"authentik_core.view_applicationentitlement",
"authentik_core.view_authenticatedsession", "authentik_core.view_authenticatedsession",
"authentik_core.view_group", "authentik_core.view_group",
"authentik_core.view_groupsourceconnection", "authentik_core.view_groupsourceconnection",
@ -12614,6 +12659,7 @@
"authentik_brands.delete_brand", "authentik_brands.delete_brand",
"authentik_brands.view_brand", "authentik_brands.view_brand",
"authentik_core.add_application", "authentik_core.add_application",
"authentik_core.add_applicationentitlement",
"authentik_core.add_authenticatedsession", "authentik_core.add_authenticatedsession",
"authentik_core.add_group", "authentik_core.add_group",
"authentik_core.add_groupsourceconnection", "authentik_core.add_groupsourceconnection",
@ -12626,6 +12672,7 @@
"authentik_core.add_usersourceconnection", "authentik_core.add_usersourceconnection",
"authentik_core.assign_user_permissions", "authentik_core.assign_user_permissions",
"authentik_core.change_application", "authentik_core.change_application",
"authentik_core.change_applicationentitlement",
"authentik_core.change_authenticatedsession", "authentik_core.change_authenticatedsession",
"authentik_core.change_group", "authentik_core.change_group",
"authentik_core.change_groupsourceconnection", "authentik_core.change_groupsourceconnection",
@ -12636,6 +12683,7 @@
"authentik_core.change_user", "authentik_core.change_user",
"authentik_core.change_usersourceconnection", "authentik_core.change_usersourceconnection",
"authentik_core.delete_application", "authentik_core.delete_application",
"authentik_core.delete_applicationentitlement",
"authentik_core.delete_authenticatedsession", "authentik_core.delete_authenticatedsession",
"authentik_core.delete_group", "authentik_core.delete_group",
"authentik_core.delete_groupsourceconnection", "authentik_core.delete_groupsourceconnection",
@ -12651,6 +12699,7 @@
"authentik_core.reset_user_password", "authentik_core.reset_user_password",
"authentik_core.unassign_user_permissions", "authentik_core.unassign_user_permissions",
"authentik_core.view_application", "authentik_core.view_application",
"authentik_core.view_applicationentitlement",
"authentik_core.view_authenticatedsession", "authentik_core.view_authenticatedsession",
"authentik_core.view_group", "authentik_core.view_group",
"authentik_core.view_groupsourceconnection", "authentik_core.view_groupsourceconnection",
@ -13263,6 +13312,52 @@
} }
} }
}, },
"model_authentik_core.applicationentitlement": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"app": {
"type": "integer",
"title": "App"
},
"attributes": {
"type": "object",
"additionalProperties": true,
"title": "Attributes"
}
},
"required": []
},
"model_authentik_core.applicationentitlement_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_applicationentitlement",
"change_applicationentitlement",
"delete_applicationentitlement",
"view_applicationentitlement"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_core.token": { "model_authentik_core.token": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -42,9 +42,21 @@ entries:
"given_name": request.user.name, "given_name": request.user.name,
"preferred_username": request.user.username, "preferred_username": request.user.username,
"nickname": request.user.username, "nickname": request.user.username,
# groups is not part of the official userinfo schema, but is a quasi-standard
"groups": [group.name for group in request.user.ak_groups.all()], "groups": [group.name for group in request.user.ak_groups.all()],
} }
- identifiers:
managed: goauthentik.io/providers/oauth2/scope-entitlements
model: authentik_providers_oauth2.scopemapping
attrs:
name: "authentik default OAuth Mapping: Application Entitlements"
scope_name: entitlements
description: "Application entitlements"
expression: |
entitlements = [entitlement.name for entitlement in request.user.app_entitlements(provider.application)]
return {
"entitlements": entitlements,
"roles": entitlements,
}
- identifiers: - identifiers:
managed: goauthentik.io/providers/oauth2/scope-offline_access managed: goauthentik.io/providers/oauth2/scope-offline_access
model: authentik_providers_oauth2.scopemapping model: authentik_providers_oauth2.scopemapping

View File

@ -14,6 +14,7 @@ type Claims struct {
Name string `json:"name"` Name string `json:"name"`
PreferredUsername string `json:"preferred_username"` PreferredUsername string `json:"preferred_username"`
Groups []string `json:"groups"` Groups []string `json:"groups"`
Entitlements []string `json:"entitlements"`
Sid string `json:"sid"` Sid string `json:"sid"`
Proxy *ProxyClaims `json:"ak_proxy"` Proxy *ProxyClaims `json:"ak_proxy"`

View File

@ -41,6 +41,7 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) {
// https://goauthentik.io/docs/providers/proxy/proxy // https://goauthentik.io/docs/providers/proxy/proxy
headers.Set("X-authentik-username", c.PreferredUsername) headers.Set("X-authentik-username", c.PreferredUsername)
headers.Set("X-authentik-groups", strings.Join(c.Groups, "|")) headers.Set("X-authentik-groups", strings.Join(c.Groups, "|"))
headers.Set("X-authentik-entitlements", strings.Join(c.Entitlements, "|"))
headers.Set("X-authentik-email", c.Email) headers.Set("X-authentik-email", c.Email)
headers.Set("X-authentik-name", c.Name) headers.Set("X-authentik-name", c.Name)
headers.Set("X-authentik-uid", c.Sub) headers.Set("X-authentik-uid", c.Sub)

View File

@ -3097,6 +3097,285 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/core/application_entitlements/:
get:
operationId: core_application_entitlements_list
description: ApplicationEntitlement Viewset
parameters:
- in: query
name: app
schema:
type: string
format: uuid
- in: query
name: name
schema:
type: string
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- in: query
name: pbm_uuid
schema:
type: string
format: uuid
- name: search
required: false
in: query
description: A search term.
schema:
type: string
tags:
- core
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedApplicationEntitlementList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: core_application_entitlements_create
description: ApplicationEntitlement Viewset
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlementRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlement'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/application_entitlements/{pbm_uuid}/:
get:
operationId: core_application_entitlements_retrieve
description: ApplicationEntitlement Viewset
parameters:
- in: path
name: pbm_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Application Entitlement.
required: true
tags:
- core
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlement'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: core_application_entitlements_update
description: ApplicationEntitlement Viewset
parameters:
- in: path
name: pbm_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Application Entitlement.
required: true
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlementRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlement'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: core_application_entitlements_partial_update
description: ApplicationEntitlement Viewset
parameters:
- in: path
name: pbm_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Application Entitlement.
required: true
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedApplicationEntitlementRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlement'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: core_application_entitlements_destroy
description: ApplicationEntitlement Viewset
parameters:
- in: path
name: pbm_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Application Entitlement.
required: true
tags:
- core
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/application_entitlements/{pbm_uuid}/used_by/:
get:
operationId: core_application_entitlements_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: pbm_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Application Entitlement.
required: true
tags:
- core
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/applications/: /core/applications/:
get: get:
operationId: core_applications_list operationId: core_applications_list
@ -23305,6 +23584,7 @@ paths:
- authentik_blueprints.blueprintinstance - authentik_blueprints.blueprintinstance
- authentik_brands.brand - authentik_brands.brand
- authentik_core.application - authentik_core.application
- authentik_core.applicationentitlement
- authentik_core.group - authentik_core.group
- authentik_core.token - authentik_core.token
- authentik_core.user - authentik_core.user
@ -23545,6 +23825,7 @@ paths:
- authentik_blueprints.blueprintinstance - authentik_blueprints.blueprintinstance
- authentik_brands.brand - authentik_brands.brand
- authentik_core.application - authentik_core.application
- authentik_core.applicationentitlement
- authentik_core.group - authentik_core.group
- authentik_core.token - authentik_core.token
- authentik_core.user - authentik_core.user
@ -38137,6 +38418,38 @@ components:
- pk - pk
- provider_obj - provider_obj
- slug - slug
ApplicationEntitlement:
type: object
description: ApplicationEntitlement Serializer
properties:
pbm_uuid:
type: string
format: uuid
readOnly: true
name:
type: string
app:
type: string
format: uuid
attributes: {}
required:
- app
- name
- pbm_uuid
ApplicationEntitlementRequest:
type: object
description: ApplicationEntitlement Serializer
properties:
name:
type: string
minLength: 1
app:
type: string
format: uuid
attributes: {}
required:
- app
- name
ApplicationRequest: ApplicationRequest:
type: object type: object
description: Application Serializer description: Application Serializer
@ -42944,7 +43257,9 @@ components:
flow_designation: flow_designation:
$ref: '#/components/schemas/FlowDesignationEnum' $ref: '#/components/schemas/FlowDesignationEnum'
captcha_stage: captcha_stage:
$ref: '#/components/schemas/CaptchaChallenge' allOf:
- $ref: '#/components/schemas/CaptchaChallenge'
nullable: true
enroll_url: enroll_url:
type: string type: string
recovery_url: recovery_url:
@ -44875,6 +45190,7 @@ components:
- authentik_core.group - authentik_core.group
- authentik_core.user - authentik_core.user
- authentik_core.application - authentik_core.application
- authentik_core.applicationentitlement
- authentik_core.token - authentik_core.token
- authentik_enterprise.license - authentik_enterprise.license
- authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_google_workspace.googleworkspaceprovider
@ -45955,6 +46271,18 @@ components:
- radius - radius
- rac - rac
type: string type: string
PaginatedApplicationEntitlementList:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
results:
type: array
items:
$ref: '#/components/schemas/ApplicationEntitlement'
required:
- pagination
- results
PaginatedApplicationList: PaginatedApplicationList:
type: object type: object
properties: properties:
@ -47857,6 +48185,17 @@ components:
required: required:
- backends - backends
- name - name
PatchedApplicationEntitlementRequest:
type: object
description: ApplicationEntitlement Serializer
properties:
name:
type: string
minLength: 1
app:
type: string
format: uuid
attributes: {}
PatchedApplicationRequest: PatchedApplicationRequest:
type: object type: object
description: Application Serializer description: Application Serializer

View File

@ -9,7 +9,7 @@ http://localhost {
uri /outpost.goauthentik.io/auth/caddy uri /outpost.goauthentik.io/auth/caddy
# capitalization of the headers is important, otherwise they will be empty # capitalization of the headers is important, otherwise they will be empty
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
# optional, in this config trust all private ranges, should probably be set to the outposts IP # optional, in this config trust all private ranges, should probably be set to the outposts IP
trusted_proxies private_ranges trusted_proxies private_ranges

View File

@ -23,12 +23,14 @@ server {
# translate headers from the outposts back to the actual upstream # translate headers from the outposts back to the actual upstream
auth_request_set $authentik_username $upstream_http_x_authentik_username; auth_request_set $authentik_username $upstream_http_x_authentik_username;
auth_request_set $authentik_groups $upstream_http_x_authentik_groups; auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
auth_request_set $authentik_entitlements $upstream_http_x_authentik_entitlements;
auth_request_set $authentik_email $upstream_http_x_authentik_email; auth_request_set $authentik_email $upstream_http_x_authentik_email;
auth_request_set $authentik_name $upstream_http_x_authentik_name; auth_request_set $authentik_name $upstream_http_x_authentik_name;
auth_request_set $authentik_uid $upstream_http_x_authentik_uid; auth_request_set $authentik_uid $upstream_http_x_authentik_uid;
proxy_set_header X-authentik-username $authentik_username; proxy_set_header X-authentik-username $authentik_username;
proxy_set_header X-authentik-groups $authentik_groups; proxy_set_header X-authentik-groups $authentik_groups;
proxy_set_header X-authentik-entitlements $authentik_entitlements;
proxy_set_header X-authentik-email $authentik_email; proxy_set_header X-authentik-email $authentik_email;
proxy_set_header X-authentik-name $authentik_name; proxy_set_header X-authentik-name $authentik_name;
proxy_set_header X-authentik-uid $authentik_uid; proxy_set_header X-authentik-uid $authentik_uid;

View File

@ -26,6 +26,7 @@ http:
authResponseHeaders: authResponseHeaders:
- X-authentik-username - X-authentik-username
- X-authentik-groups - X-authentik-groups
- X-authentik-entitlements
- X-authentik-email - X-authentik-email
- X-authentik-name - X-authentik-name
- X-authentik-uid - X-authentik-uid

View File

@ -1,6 +1,7 @@
import "@goauthentik/admin/applications/ApplicationAuthorizeChart"; import "@goauthentik/admin/applications/ApplicationAuthorizeChart";
import "@goauthentik/admin/applications/ApplicationCheckAccessForm"; import "@goauthentik/admin/applications/ApplicationCheckAccessForm";
import "@goauthentik/admin/applications/ApplicationForm"; import "@goauthentik/admin/applications/ApplicationForm";
import "@goauthentik/admin/applications/entitlements/ApplicationEntitlementPage";
import "@goauthentik/admin/policies/BoundPoliciesList"; import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage"; import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
@ -301,6 +302,28 @@ export class ApplicationViewPage extends AKElement {
</div> </div>
</div> </div>
</section> </section>
<section
slot="page-app-entitlements"
data-tab-title="${msg("Application entitlements")}"
>
<div slot="header" class="pf-c-banner pf-m-info">
${msg("Application entitlements are in preview.")}
<a href="mailto:hello+feature/app-ent@goauthentik.io"
>${msg("Send us feedback!")}</a
>
</div>
<div class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__title">
${msg(
"These entitlements can be used to configure user access in this application.",
)}
</div>
<ak-application-entitlements-list .app=${this.application.pk}>
</ak-application-entitlements-list>
</div>
</div>
</section>
<section <section
slot="page-policy-bindings" slot="page-policy-bindings"
data-tab-title="${msg("Policy / Group / User Bindings")}" data-tab-title="${msg("Policy / Group / User Bindings")}"

View File

@ -0,0 +1,89 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml";
import { msg } from "@lit/localize";
import { CSSResult } from "lit";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import { ApplicationEntitlement, CoreApi } from "@goauthentik/api";
@customElement("ak-application-entitlement-form")
export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement, string> {
async loadInstance(pk: string): Promise<ApplicationEntitlement> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsRetrieve({
pbmUuid: pk,
});
}
@property()
targetPk?: string;
getSuccessMessage(): string {
if (this.instance?.pbmUuid) {
return msg("Successfully updated entitlement.");
} else {
return msg("Successfully created entitlement.");
}
}
static get styles(): CSSResult[] {
return [...super.styles, PFContent];
}
send(data: ApplicationEntitlement): Promise<unknown> {
if (this.targetPk) {
data.app = this.targetPk;
}
if (this.instance?.pbmUuid) {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsUpdate({
pbmUuid: this.instance.pbmUuid || "",
applicationEntitlementRequest: data,
});
} else {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Attributes")}
?required=${false}
name="attributes"
>
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Set custom attributes using YAML or JSON.")}
</p>
</ak-form-element-horizontal>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-entitlement-form": ApplicationEntitlementForm;
}
}

View File

@ -0,0 +1,152 @@
import "@goauthentik/admin/applications/entitlements/ApplicationEntitlementForm";
import "@goauthentik/admin/policies/BoundPoliciesList";
import { PolicyBindingCheckTarget } from "@goauthentik/admin/policies/utils";
import "@goauthentik/admin/rbac/ObjectPermissionModal";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/forms/ProxyForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
ApplicationEntitlement,
CoreApi,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
@customElement("ak-application-entitlements-list")
export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> {
@property()
app?: string;
checkbox = true;
clearOnRefresh = true;
expandable = true;
order = "order";
async apiEndpoint(): Promise<PaginatedResponse<ApplicationEntitlement>> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsList({
...(await this.defaultEndpointConfig()),
app: this.app || "",
});
}
columns(): TableColumn[] {
return [new TableColumn(msg("Name"), "name"), new TableColumn(msg("Actions"))];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Application entitlement(s)")}
.objects=${this.selectedElements}
.usedBy=${(item: ApplicationEntitlement) => {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsUsedByList({
pbmUuid: item.pbmUuid || "",
});
}}
.delete=${(item: ApplicationEntitlement) => {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsDestroy({
pbmUuid: item.pbmUuid || "",
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
row(item: ApplicationEntitlement): TemplateResult[] {
return [
html`${item.name}`,
html`<ak-forms-modal size=${PFSize.Medium}>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Entitlement")} </span>
<ak-application-entitlement-form
slot="form"
.instancePk=${item.pbmUuid}
targetPk=${ifDefined(this.app)}
>
</ak-application-entitlement-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.CoreApplicationentitlement}
objectPk=${item.pbmUuid}
>
</ak-rbac-object-permission-modal>`,
];
}
renderExpanded(item: ApplicationEntitlement): TemplateResult {
return html` <td></td>
<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<div class="pf-c-content">
<p>
${msg(
"These bindings control which users have access to this entitlement.",
)}
</p>
<ak-bound-policies-list
.target=${item.pbmUuid}
.allowedTypes=${[
PolicyBindingCheckTarget.group,
PolicyBindingCheckTarget.user,
]}
>
</ak-bound-policies-list>
</div>
</div>
</td>`;
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state
header=${msg("No app entitlements created.")}
icon="pf-icon-module"
>
<div slot="body">
${msg(
"This application does currently not have any application entitlement defined.",
)}
</div>
<div slot="primary"></div>
</ak-empty-state>`,
);
}
renderToolbar(): TemplateResult {
return html`<ak-forms-modal size=${PFSize.Medium}>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Entitlement")} </span>
<ak-application-entitlement-form slot="form" targetPk=${ifDefined(this.app)}>
</ak-application-entitlement-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Create entitlement")}
</button>
</ak-forms-modal> `;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-roles-list": ApplicationEntitlementsPage;
}
}

View File

@ -1,6 +1,11 @@
import "@goauthentik/admin/groups/GroupForm"; import "@goauthentik/admin/groups/GroupForm";
import "@goauthentik/admin/policies/PolicyBindingForm"; import "@goauthentik/admin/policies/PolicyBindingForm";
import { PolicyBindingNotice } from "@goauthentik/admin/policies/PolicyBindingForm";
import "@goauthentik/admin/policies/PolicyWizard"; import "@goauthentik/admin/policies/PolicyWizard";
import {
PolicyBindingCheckTarget,
PolicyBindingCheckTargetToLabel,
} from "@goauthentik/admin/policies/utils";
import "@goauthentik/admin/users/UserForm"; import "@goauthentik/admin/users/UserForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js"; import { PFSize } from "@goauthentik/common/enums.js";
@ -13,7 +18,7 @@ import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table"; import { Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
@ -24,14 +29,25 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
@property() @property()
target?: string; target?: string;
@property({ type: Boolean }) @property({ type: Array })
policyOnly = false; allowedTypes: PolicyBindingCheckTarget[] = [
PolicyBindingCheckTarget.group,
PolicyBindingCheckTarget.user,
PolicyBindingCheckTarget.policy,
];
@property({ type: Array })
typeNotices: PolicyBindingNotice[] = [];
checkbox = true; checkbox = true;
clearOnRefresh = true; clearOnRefresh = true;
order = "order"; order = "order";
get allowedTypesLabel(): string {
return this.allowedTypes.map((ct) => PolicyBindingCheckTargetToLabel(ct)).join(" / ");
}
async apiEndpoint(): Promise<PaginatedResponse<PolicyBinding>> { async apiEndpoint(): Promise<PaginatedResponse<PolicyBinding>> {
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({ return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({
...(await this.defaultEndpointConfig()), ...(await this.defaultEndpointConfig()),
@ -42,7 +58,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
columns(): TableColumn[] { columns(): TableColumn[] {
return [ return [
new TableColumn(msg("Order"), "order"), new TableColumn(msg("Order"), "order"),
new TableColumn(msg("Policy / User / Group")), new TableColumn(this.allowedTypesLabel),
new TableColumn(msg("Enabled"), "enabled"), new TableColumn(msg("Enabled"), "enabled"),
new TableColumn(msg("Timeout"), "timeout"), new TableColumn(msg("Timeout"), "timeout"),
new TableColumn(msg("Actions")), new TableColumn(msg("Actions")),
@ -121,7 +137,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
return [ return [
{ key: msg("Order"), value: item.order.toString() }, { key: msg("Order"), value: item.order.toString() },
{ {
key: msg("Policy / User / Group"), key: this.allowedTypesLabel,
value: this.getPolicyUserGroupRowLabel(item), value: this.getPolicyUserGroupRowLabel(item),
}, },
]; ];
@ -156,8 +172,9 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
<ak-policy-binding-form <ak-policy-binding-form
slot="form" slot="form"
.instancePk=${item.pk} .instancePk=${item.pk}
.allowedTypes=${this.allowedTypes}
.typeNotices=${this.typeNotices}
targetPk=${ifDefined(this.target)} targetPk=${ifDefined(this.target)}
?policyOnly=${this.policyOnly}
> >
</ak-policy-binding-form> </ak-policy-binding-form>
<button slot="trigger" class="pf-c-button pf-m-secondary"> <button slot="trigger" class="pf-c-button pf-m-secondary">
@ -183,7 +200,8 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
<ak-policy-binding-form <ak-policy-binding-form
slot="form" slot="form"
targetPk=${ifDefined(this.target)} targetPk=${ifDefined(this.target)}
?policyOnly=${this.policyOnly} .allowedTypes=${this.allowedTypes}
.typeNotices=${this.typeNotices}
> >
</ak-policy-binding-form> </ak-policy-binding-form>
<button slot="trigger" class="pf-c-button pf-m-primary"> <button slot="trigger" class="pf-c-button pf-m-primary">
@ -196,22 +214,25 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
} }
renderToolbar(): TemplateResult { renderToolbar(): TemplateResult {
return html`<ak-policy-wizard return html`${this.allowedTypes.includes(PolicyBindingCheckTarget.policy)
createText=${msg("Create and bind Policy")} ? html`<ak-policy-wizard
?showBindingPage=${true} createText=${msg("Create and bind Policy")}
bindingTarget=${ifDefined(this.target)} ?showBindingPage=${true}
></ak-policy-wizard> bindingTarget=${ifDefined(this.target)}
></ak-policy-wizard>`
: nothing}
<ak-forms-modal size=${PFSize.Medium}> <ak-forms-modal size=${PFSize.Medium}>
<span slot="submit"> ${msg("Create")} </span> <span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Binding")} </span> <span slot="header"> ${msg("Create Binding")} </span>
<ak-policy-binding-form <ak-policy-binding-form
slot="form" slot="form"
targetPk=${ifDefined(this.target)} targetPk=${ifDefined(this.target)}
?policyOnly=${this.policyOnly} .allowedTypes=${this.allowedTypes}
.typeNotices=${this.typeNotices}
> >
</ak-policy-binding-form> </ak-policy-binding-form>
<button slot="trigger" class="pf-c-button pf-m-primary"> <button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Bind existing policy/group/user")} ${msg(str`Bind existing ${this.allowedTypesLabel}`)}
</button> </button>
</ak-forms-modal> `; </ak-forms-modal> `;
} }

View File

@ -1,3 +1,7 @@
import {
PolicyBindingCheckTarget,
PolicyBindingCheckTargetToLabel,
} from "@goauthentik/admin/policies/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, groupBy } from "@goauthentik/common/utils"; import { first, groupBy } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group"; import "@goauthentik/components/ak-toggle-group";
@ -7,7 +11,7 @@ import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult } from "lit"; import { CSSResult, nothing } from "lit";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
@ -25,11 +29,7 @@ import {
User, User,
} from "@goauthentik/api"; } from "@goauthentik/api";
enum target { export type PolicyBindingNotice = { type: PolicyBindingCheckTarget; notice: string };
policy = "policy",
group = "group",
user = "user",
}
@customElement("ak-policy-binding-form") @customElement("ak-policy-binding-form")
export class PolicyBindingForm extends ModelForm<PolicyBinding, string> { export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
@ -38,13 +38,13 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
policyBindingUuid: pk, policyBindingUuid: pk,
}); });
if (binding?.policyObj) { if (binding?.policyObj) {
this.policyGroupUser = target.policy; this.policyGroupUser = PolicyBindingCheckTarget.policy;
} }
if (binding?.groupObj) { if (binding?.groupObj) {
this.policyGroupUser = target.group; this.policyGroupUser = PolicyBindingCheckTarget.group;
} }
if (binding?.userObj) { if (binding?.userObj) {
this.policyGroupUser = target.user; this.policyGroupUser = PolicyBindingCheckTarget.user;
} }
this.defaultOrder = await this.getOrder(); this.defaultOrder = await this.getOrder();
return binding; return binding;
@ -54,10 +54,17 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
targetPk?: string; targetPk?: string;
@state() @state()
policyGroupUser: target = target.policy; policyGroupUser: PolicyBindingCheckTarget = PolicyBindingCheckTarget.policy;
@property({ type: Boolean }) @property({ type: Array })
policyOnly = false; allowedTypes: PolicyBindingCheckTarget[] = [
PolicyBindingCheckTarget.group,
PolicyBindingCheckTarget.user,
PolicyBindingCheckTarget.policy,
];
@property({ type: Array })
typeNotices: PolicyBindingNotice[] = [];
@state() @state()
defaultOrder = 0; defaultOrder = 0;
@ -74,20 +81,26 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
return [...super.styles, PFContent]; return [...super.styles, PFContent];
} }
async load(): Promise<void> {
// Overwrite the default for policyGroupUser with the first allowed type,
// as this function is called when the correct parameters are set
this.policyGroupUser = this.allowedTypes[0];
}
send(data: PolicyBinding): Promise<unknown> { send(data: PolicyBinding): Promise<unknown> {
if (this.targetPk) { if (this.targetPk) {
data.target = this.targetPk; data.target = this.targetPk;
} }
switch (this.policyGroupUser) { switch (this.policyGroupUser) {
case target.policy: case PolicyBindingCheckTarget.policy:
data.user = null; data.user = null;
data.group = null; data.group = null;
break; break;
case target.group: case PolicyBindingCheckTarget.group:
data.policy = null; data.policy = null;
data.user = null; data.user = null;
break; break;
case target.user: case PolicyBindingCheckTarget.user:
data.policy = null; data.policy = null;
data.group = null; data.group = null;
break; break;
@ -122,13 +135,18 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
renderModeSelector(): TemplateResult { renderModeSelector(): TemplateResult {
return html` <ak-toggle-group return html` <ak-toggle-group
value=${this.policyGroupUser} value=${this.policyGroupUser}
@ak-toggle=${(ev: CustomEvent<{ value: target }>) => { @ak-toggle=${(ev: CustomEvent<{ value: PolicyBindingCheckTarget }>) => {
this.policyGroupUser = ev.detail.value; this.policyGroupUser = ev.detail.value;
}} }}
> >
<option value=${target.policy}>${msg("Policy")}</option> ${Object.keys(PolicyBindingCheckTarget).map((ct) => {
<option value=${target.group}>${msg("Group")}</option> if (this.allowedTypes.includes(ct as PolicyBindingCheckTarget)) {
<option value=${target.user}>${msg("User")}</option> return html`<option value=${ct}>
${PolicyBindingCheckTargetToLabel(ct as PolicyBindingCheckTarget)}
</option>`;
}
return nothing;
})}
</ak-toggle-group>`; </ak-toggle-group>`;
} }
@ -139,7 +157,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Policy")} label=${msg("Policy")}
name="policy" name="policy"
?hidden=${this.policyGroupUser !== target.policy} ?hidden=${this.policyGroupUser !== PolicyBindingCheckTarget.policy}
> >
<ak-search-select <ak-search-select
.groupBy=${(items: Policy[]) => { .groupBy=${(items: Policy[]) => {
@ -169,11 +187,16 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
?blankable=${true} ?blankable=${true}
> >
</ak-search-select> </ak-search-select>
${this.typeNotices
.filter(({ type }) => type === PolicyBindingCheckTarget.policy)
.map((msg) => {
return html`<p class="pf-c-form__helper-text">${msg.notice}</p>`;
})}
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Group")} label=${msg("Group")}
name="group" name="group"
?hidden=${this.policyGroupUser !== target.group} ?hidden=${this.policyGroupUser !== PolicyBindingCheckTarget.group}
> >
<ak-search-select <ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => { .fetchObjects=${async (query?: string): Promise<Group[]> => {
@ -201,18 +224,16 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
?blankable=${true} ?blankable=${true}
> >
</ak-search-select> </ak-search-select>
${this.policyOnly ${this.typeNotices
? html`<p class="pf-c-form__helper-text"> .filter(({ type }) => type === PolicyBindingCheckTarget.group)
${msg( .map((msg) => {
"Group mappings can only be checked if a user is already logged in when trying to access this source.", return html`<p class="pf-c-form__helper-text">${msg.notice}</p>`;
)} })}
</p>`
: html``}
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User")} label=${msg("User")}
name="user" name="user"
?hidden=${this.policyGroupUser !== target.user} ?hidden=${this.policyGroupUser !== PolicyBindingCheckTarget.user}
> >
<ak-search-select <ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => { .fetchObjects=${async (query?: string): Promise<User[]> => {
@ -240,13 +261,11 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
?blankable=${true} ?blankable=${true}
> >
</ak-search-select> </ak-search-select>
${this.policyOnly ${this.typeNotices
? html`<p class="pf-c-form__helper-text"> .filter(({ type }) => type === PolicyBindingCheckTarget.user)
${msg( .map((msg) => {
"User mappings can only be checked if a user is already logged in when trying to access this source.", return html`<p class="pf-c-form__helper-text">${msg.notice}</p>`;
)} })}
</p>`
: html``}
</ak-form-element-horizontal> </ak-form-element-horizontal>
</div> </div>
</div> </div>

View File

@ -0,0 +1,18 @@
import { msg } from "@lit/localize";
export enum PolicyBindingCheckTarget {
policy = "policy",
group = "group",
user = "user",
}
export function PolicyBindingCheckTargetToLabel(ct: PolicyBindingCheckTarget): string {
switch (ct) {
case PolicyBindingCheckTarget.group:
return msg("Group");
case PolicyBindingCheckTarget.user:
return msg("User");
case PolicyBindingCheckTarget.policy:
return msg("Policy");
}
}

View File

@ -2,6 +2,7 @@ import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage"; import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import "@goauthentik/admin/sources/oauth/OAuthSourceDiagram"; import "@goauthentik/admin/sources/oauth/OAuthSourceDiagram";
import "@goauthentik/admin/sources/oauth/OAuthSourceForm"; import "@goauthentik/admin/sources/oauth/OAuthSourceForm";
import { sourceBindingTypeNotices } from "@goauthentik/admin/sources/utils";
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 "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -240,7 +241,10 @@ export class OAuthSourceViewPage extends AKElement {
)} )}
</div> </div>
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<ak-bound-policies-list .target=${this.source.pk} ?policyOnly=${true}> <ak-bound-policies-list
.target=${this.source.pk}
.typeNotices=${sourceBindingTypeNotices()}
>
</ak-bound-policies-list> </ak-bound-policies-list>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
import "@goauthentik/admin/policies/BoundPoliciesList"; import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage"; import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import "@goauthentik/admin/sources/plex/PlexSourceForm"; import "@goauthentik/admin/sources/plex/PlexSourceForm";
import { sourceBindingTypeNotices } from "@goauthentik/admin/sources/utils";
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 "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -130,7 +131,10 @@ export class PlexSourceViewPage extends AKElement {
)} )}
</div> </div>
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<ak-bound-policies-list .target=${this.source.pk} ?policyOnly=${true}> <ak-bound-policies-list
.target=${this.source.pk}
.typeNotices=${sourceBindingTypeNotices()}
>
</ak-bound-policies-list> </ak-bound-policies-list>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
import "@goauthentik/admin/policies/BoundPoliciesList"; import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage"; import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import "@goauthentik/admin/sources/saml/SAMLSourceForm"; import "@goauthentik/admin/sources/saml/SAMLSourceForm";
import { sourceBindingTypeNotices } from "@goauthentik/admin/sources/utils";
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 "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -207,7 +208,10 @@ export class SAMLSourceViewPage extends AKElement {
)} )}
</div> </div>
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<ak-bound-policies-list .target=${this.source.pk} ?policyOnly=${true}> <ak-bound-policies-list
.target=${this.source.pk}
.typeNotices=${sourceBindingTypeNotices()}
>
</ak-bound-policies-list> </ak-bound-policies-list>
</div> </div>
</div> </div>

View File

@ -1,3 +1,6 @@
import { PolicyBindingCheckTarget } from "@goauthentik/admin/policies/utils";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
export function renderSourceIcon(name: string, iconUrl: string | undefined | null): TemplateResult { export function renderSourceIcon(name: string, iconUrl: string | undefined | null): TemplateResult {
@ -11,3 +14,20 @@ export function renderSourceIcon(name: string, iconUrl: string | undefined | nul
} }
return icon; return icon;
} }
export function sourceBindingTypeNotices() {
return [
{
type: PolicyBindingCheckTarget.group,
notice: msg(
"Group mappings can only be checked if a user is already logged in when trying to access this source.",
),
},
{
type: PolicyBindingCheckTarget.user,
notice: msg(
"User mappings can only be checked if a user is already logged in when trying to access this source.",
),
},
];
}

View File

@ -48,6 +48,16 @@ sequenceDiagram
rp->>user: User is logged in rp->>user: User is logged in
``` ```
| Endpoint | URL |
| -------------------- | -------------------------------------------------------------------- |
| Authorization | `/application/o/authorize/` |
| Token | `/application/o/token/` |
| User Info | `/application/o/userinfo/` |
| Token Revoke | `/application/o/revoke/` |
| End Session | `/application/o/<application slug>/end-session/` |
| JWKS | `/application/o/<application slug>/jwks/` |
| OpenID Configuration | `/application/o/<application slug>/.well-known/openid-configuration` |
### Additional configuration options with Redirect URIs ### Additional configuration options with Redirect URIs
When using an OAuth 2.0 provider in authentik, the OP must validate the provided redirect URI by the RP. An authentik admin can configure a list in the **Redirect URI** field on the Provider. When using an OAuth 2.0 provider in authentik, the OP must validate the provided redirect URI by the RP. An authentik admin can configure a list in the **Redirect URI** field on the Provider.
@ -122,16 +132,6 @@ Starting with authentik 2024.2, the refresh token grant type requires the `offli
Scopes can be configured using scope mappings, a type of [property mapping](../property-mappings/index.md#scope-mappings). Scopes can be configured using scope mappings, a type of [property mapping](../property-mappings/index.md#scope-mappings).
| Endpoint | URL |
| -------------------- | -------------------------------------------------------------------- |
| Authorization | `/application/o/authorize/` |
| Token | `/application/o/token/` |
| User Info | `/application/o/userinfo/` |
| Token Revoke | `/application/o/revoke/` |
| End Session | `/application/o/<application slug>/end-session/` |
| JWKS | `/application/o/<application slug>/jwks/` |
| OpenID Configuration | `/application/o/<application slug>/.well-known/openid-configuration` |
## Scope authorization ## Scope authorization
By default, every user that has access to an application can request any of the configured scopes. Starting with authentik 2022.4, you can do additional checks for the scope in an expression policy (bound to the application): By default, every user that has access to an application can request any of the configured scopes. Starting with authentik 2022.4, you can do additional checks for the scope in an expression policy (bound to the application):
@ -143,7 +143,23 @@ if "my-admin-scope" in request.context["oauth_scopes"]:
return True return True
``` ```
## Special scopes ## Default & special scopes
When a client does not request any scopes, authentik will treat the request as if all configured scopes were requested. Depending on the configured authorization flow, consent still needs to be given, and all scopes are listed there.
This does _not_ apply to special scopes, as those are not configurable in the provider.
### Default
- `openid`: A scope required by the OpenID Connect spec to specify that an OAuth interaction is OpenID Connect. Does not add any data to the token.
- `profile`: Include basic profile information, such as username, name and group membership.
- `email`: Include the users' email address.
- `entitlements`: Include application entitlement data.
- `offline_access`: An OAuth 2.0 scope which indicates that the application is requesting a refresh token.
### authentik
- `goauthentik.io/api`: This scope grants the refresh token access to the authentik API on behalf of the user
### GitHub compatibility ### GitHub compatibility
@ -152,19 +168,9 @@ return True
- `user:email`: Allows read-only access to `/user`, including email address - `user:email`: Allows read-only access to `/user`, including email address
- `read:org`: Allows read-only access to `/user/teams`, listing all the user's groups as teams. - `read:org`: Allows read-only access to `/user/teams`, listing all the user's groups as teams.
### authentik
- `goauthentik.io/api`: This scope grants the refresh token access to the authentik API on behalf of the user
## Default scopes <span class="badge badge--version">authentik 2022.7+</span>
When a client does not request any scopes, authentik will treat the request as if all configured scopes were requested. Depending on the configured authorization flow, consent still needs to be given, and all scopes are listed there.
This does _not_ apply to special scopes, as those are not configurable in the provider.
## Signing & Encryption ## Signing & Encryption
[JWT](https://jwt.io/introduction)s created by authentik will always be signed. [JWTs](https://jwt.io/introduction) created by authentik will always be signed.
When a _Signing Key_ is selected in the provider, the JWT will be signed asymmetrically with the private key of the selected certificate, and can be verified using the public key of the certificate. The public key data of the signing key can be retrieved via the JWKS endpoint listed on the provider page. When a _Signing Key_ is selected in the provider, the JWT will be signed asymmetrically with the private key of the selected certificate, and can be verified using the public key of the certificate. The public key data of the signing key can be retrieved via the JWKS endpoint listed on the provider page.

View File

@ -12,7 +12,7 @@ app.company {
uri /outpost.goauthentik.io/auth/caddy uri /outpost.goauthentik.io/auth/caddy
# capitalization of the headers is important, otherwise they will be empty # capitalization of the headers is important, otherwise they will be empty
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
# optional, in this config trust all private ranges, should probably be set to the outposts IP # optional, in this config trust all private ranges, should probably be set to the outposts IP
trusted_proxies private_ranges trusted_proxies private_ranges

View File

@ -40,7 +40,7 @@ metadata:
nginx.ingress.kubernetes.io/auth-signin: |- nginx.ingress.kubernetes.io/auth-signin: |-
https://app.company/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri https://app.company/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-response-headers: |- nginx.ingress.kubernetes.io/auth-response-headers: |-
Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-entitlements,X-authentik-email,X-authentik-name,X-authentik-uid
nginx.ingress.kubernetes.io/auth-snippet: | nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Host $http_host;
``` ```

View File

@ -26,12 +26,14 @@ location / {
# translate headers from the outposts back to the actual upstream # translate headers from the outposts back to the actual upstream
auth_request_set $authentik_username $upstream_http_x_authentik_username; auth_request_set $authentik_username $upstream_http_x_authentik_username;
auth_request_set $authentik_groups $upstream_http_x_authentik_groups; auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
auth_request_set $authentik_entitlements $upstream_http_x_authentik_entitlements;
auth_request_set $authentik_email $upstream_http_x_authentik_email; auth_request_set $authentik_email $upstream_http_x_authentik_email;
auth_request_set $authentik_name $upstream_http_x_authentik_name; auth_request_set $authentik_name $upstream_http_x_authentik_name;
auth_request_set $authentik_uid $upstream_http_x_authentik_uid; auth_request_set $authentik_uid $upstream_http_x_authentik_uid;
proxy_set_header X-authentik-username $authentik_username; proxy_set_header X-authentik-username $authentik_username;
proxy_set_header X-authentik-groups $authentik_groups; proxy_set_header X-authentik-groups $authentik_groups;
proxy_set_header X-authentik-entitlements $authentik_entitlements;
proxy_set_header X-authentik-email $authentik_email; proxy_set_header X-authentik-email $authentik_email;
proxy_set_header X-authentik-name $authentik_name; proxy_set_header X-authentik-name $authentik_name;
proxy_set_header X-authentik-uid $authentik_uid; proxy_set_header X-authentik-uid $authentik_uid;

View File

@ -39,12 +39,14 @@ server {
# translate headers from the outposts back to the actual upstream # translate headers from the outposts back to the actual upstream
auth_request_set $authentik_username $upstream_http_x_authentik_username; auth_request_set $authentik_username $upstream_http_x_authentik_username;
auth_request_set $authentik_groups $upstream_http_x_authentik_groups; auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
auth_request_set $authentik_entitlements $upstream_http_x_authentik_entitlements;
auth_request_set $authentik_email $upstream_http_x_authentik_email; auth_request_set $authentik_email $upstream_http_x_authentik_email;
auth_request_set $authentik_name $upstream_http_x_authentik_name; auth_request_set $authentik_name $upstream_http_x_authentik_name;
auth_request_set $authentik_uid $upstream_http_x_authentik_uid; auth_request_set $authentik_uid $upstream_http_x_authentik_uid;
proxy_set_header X-authentik-username $authentik_username; proxy_set_header X-authentik-username $authentik_username;
proxy_set_header X-authentik-groups $authentik_groups; proxy_set_header X-authentik-groups $authentik_groups;
proxy_set_header X-authentik-entitlements $authentik_entitlements;
proxy_set_header X-authentik-email $authentik_email; proxy_set_header X-authentik-email $authentik_email;
proxy_set_header X-authentik-name $authentik_name; proxy_set_header X-authentik-name $authentik_name;
proxy_set_header X-authentik-uid $authentik_uid; proxy_set_header X-authentik-uid $authentik_uid;

View File

@ -32,7 +32,7 @@ services:
# `authentik-proxy` refers to the service name in the compose file. # `authentik-proxy` refers to the service name in the compose file.
traefik.http.middlewares.authentik.forwardauth.address: http://authentik-proxy:9000/outpost.goauthentik.io/auth/traefik traefik.http.middlewares.authentik.forwardauth.address: http://authentik-proxy:9000/outpost.goauthentik.io/auth/traefik
traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: true traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: true
traefik.http.middlewares.authentik.forwardauth.authResponseHeaders: X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version traefik.http.middlewares.authentik.forwardauth.authResponseHeaders: X-authentik-username,X-authentik-groups,X-authentik-entitlements,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version
restart: unless-stopped restart: unless-stopped
whoami: whoami:

View File

@ -13,6 +13,7 @@ spec:
authResponseHeaders: authResponseHeaders:
- X-authentik-username - X-authentik-username
- X-authentik-groups - X-authentik-groups
- X-authentik-entitlements
- X-authentik-email - X-authentik-email
- X-authentik-name - X-authentik-name
- X-authentik-uid - X-authentik-uid

View File

@ -8,6 +8,7 @@ http:
authResponseHeaders: authResponseHeaders:
- X-authentik-username - X-authentik-username
- X-authentik-groups - X-authentik-groups
- X-authentik-entitlements
- X-authentik-email - X-authentik-email
- X-authentik-name - X-authentik-name
- X-authentik-uid - X-authentik-uid

View File

@ -36,6 +36,12 @@ Example value: `foo|bar|baz`
The groups the user is member of, separated by a pipe The groups the user is member of, separated by a pipe
### `X-authentik-entitlements`
Example value: `foo|bar|baz`
The entitlements on the application this user has access to, separated by a pipe
### `X-authentik-email` ### `X-authentik-email`
Example value: `root@localhost` Example value: `root@localhost`