core: user paths (#3085)
* init Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add user_path_template Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add to sources and flow Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add outposts & api Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * dark theme for treeview Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add search Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add docs and tests for validation Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add to user write stage Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add web ui Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: improve error handling Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
c4b4c7134d
commit
1c62a3db6e
2
Makefile
2
Makefile
|
@ -106,7 +106,7 @@ run:
|
||||||
web-build: web-install
|
web-build: web-install
|
||||||
cd web && npm run build
|
cd web && npm run build
|
||||||
|
|
||||||
web: web-lint-fix web-lint web-extract
|
web: web-lint-fix web-lint
|
||||||
|
|
||||||
web-install:
|
web-install:
|
||||||
cd web && npm ci
|
cd web && npm ci
|
||||||
|
|
|
@ -53,6 +53,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||||
"policy_engine_mode",
|
"policy_engine_mode",
|
||||||
"user_matching_mode",
|
"user_matching_mode",
|
||||||
"managed",
|
"managed",
|
||||||
|
"user_path_template",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ from drf_spectacular.utils import (
|
||||||
)
|
)
|
||||||
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
from rest_framework.fields import CharField, JSONField, ListField, SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import (
|
from rest_framework.serializers import (
|
||||||
|
@ -50,6 +50,7 @@ from authentik.core.middleware import (
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
USER_ATTRIBUTE_SA,
|
USER_ATTRIBUTE_SA,
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||||
|
USER_PATH_SERVICE_ACCOUNT,
|
||||||
Group,
|
Group,
|
||||||
Token,
|
Token,
|
||||||
TokenIntents,
|
TokenIntents,
|
||||||
|
@ -77,6 +78,15 @@ class UserSerializer(ModelSerializer):
|
||||||
uid = CharField(read_only=True)
|
uid = CharField(read_only=True)
|
||||||
username = CharField(max_length=150)
|
username = CharField(max_length=150)
|
||||||
|
|
||||||
|
def validate_path(self, path: str) -> str:
|
||||||
|
"""Validate path"""
|
||||||
|
if path[:1] == "/" or path[-1] == "/":
|
||||||
|
raise ValidationError(_("No leading or trailing slashes allowed."))
|
||||||
|
for segment in path.split("/"):
|
||||||
|
if segment == "":
|
||||||
|
raise ValidationError(_("No empty segments in user path allowed."))
|
||||||
|
return path
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
|
@ -93,6 +103,7 @@ class UserSerializer(ModelSerializer):
|
||||||
"avatar",
|
"avatar",
|
||||||
"attributes",
|
"attributes",
|
||||||
"uid",
|
"uid",
|
||||||
|
"path",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"name": {"allow_blank": True},
|
"name": {"allow_blank": True},
|
||||||
|
@ -208,6 +219,11 @@ class UsersFilter(FilterSet):
|
||||||
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
||||||
uuid = CharFilter(field_name="uuid")
|
uuid = CharFilter(field_name="uuid")
|
||||||
|
|
||||||
|
path = CharFilter(
|
||||||
|
field_name="path",
|
||||||
|
)
|
||||||
|
path_startswith = CharFilter(field_name="path", lookup_expr="startswith")
|
||||||
|
|
||||||
groups_by_name = ModelMultipleChoiceFilter(
|
groups_by_name = ModelMultipleChoiceFilter(
|
||||||
field_name="ak_groups__name",
|
field_name="ak_groups__name",
|
||||||
to_field_name="name",
|
to_field_name="name",
|
||||||
|
@ -314,6 +330,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||||
username=username,
|
username=username,
|
||||||
name=username,
|
name=username,
|
||||||
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
||||||
|
path=USER_PATH_SERVICE_ACCOUNT,
|
||||||
)
|
)
|
||||||
if create_group and self.request.user.has_perm("authentik_core.add_group"):
|
if create_group and self.request.user.has_perm("authentik_core.add_group"):
|
||||||
group = Group.objects.create(
|
group = Group.objects.create(
|
||||||
|
@ -464,3 +481,32 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||||
if self.request.user.has_perm("authentik_core.view_user"):
|
if self.request.user.has_perm("authentik_core.view_user"):
|
||||||
return self._filter_queryset_for_list(queryset)
|
return self._filter_queryset_for_list(queryset)
|
||||||
return super().filter_queryset(queryset)
|
return super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
responses={
|
||||||
|
200: inline_serializer(
|
||||||
|
"UserPathSerializer", {"paths": ListField(child=CharField(), read_only=True)}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="search",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@action(detail=False, pagination_class=None)
|
||||||
|
def paths(self, request: Request) -> Response:
|
||||||
|
"""Get all user paths"""
|
||||||
|
return Response(
|
||||||
|
data={
|
||||||
|
"paths": list(
|
||||||
|
self.filter_queryset(self.get_queryset())
|
||||||
|
.values("path")
|
||||||
|
.distinct()
|
||||||
|
.order_by("path")
|
||||||
|
.values_list("path", flat=True)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -12,9 +12,9 @@ import authentik.core.models
|
||||||
|
|
||||||
|
|
||||||
def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
# We have to use a direct import here, otherwise we get an object manager error
|
from django.contrib.auth.hashers import make_password
|
||||||
from authentik.core.models import User
|
|
||||||
|
|
||||||
|
User = apps.get_model("authentik_core", "User")
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
||||||
|
@ -28,9 +28,9 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
||||||
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
||||||
if password:
|
if password:
|
||||||
akadmin.set_password(password, signal=False)
|
akadmin.password = make_password(password)
|
||||||
else:
|
else:
|
||||||
akadmin.set_unusable_password()
|
akadmin.password = make_password(None)
|
||||||
akadmin.save()
|
akadmin.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,9 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
|
||||||
def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
# We have to use a direct import here, otherwise we get an object manager error
|
from django.contrib.auth.hashers import make_password
|
||||||
from authentik.core.models import User
|
|
||||||
|
|
||||||
|
User = apps.get_model("authentik_core", "User")
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
||||||
|
@ -24,9 +24,9 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
||||||
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
||||||
if password:
|
if password:
|
||||||
akadmin.set_password(password, signal=False)
|
akadmin.password = make_password(password)
|
||||||
else:
|
else:
|
||||||
akadmin.set_unusable_password()
|
akadmin.password = make_password(None)
|
||||||
akadmin.save()
|
akadmin.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 4.0.5 on 2022-06-13 18:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0020_application_open_in_new_tab"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="source",
|
||||||
|
name="user_path_template",
|
||||||
|
field=models.TextField(default="goauthentik.io/sources/%(slug)s"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="path",
|
||||||
|
field=models.TextField(default="users"),
|
||||||
|
),
|
||||||
|
]
|
|
@ -46,6 +46,9 @@ USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
|
||||||
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
|
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
|
||||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
||||||
|
|
||||||
|
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
|
||||||
|
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
|
||||||
|
|
||||||
GRAVATAR_URL = "https://secure.gravatar.com"
|
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||||
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
||||||
|
|
||||||
|
@ -138,6 +141,7 @@ class User(GuardianUserMixin, AbstractUser):
|
||||||
|
|
||||||
uuid = models.UUIDField(default=uuid4, editable=False)
|
uuid = models.UUIDField(default=uuid4, editable=False)
|
||||||
name = models.TextField(help_text=_("User's display name."))
|
name = models.TextField(help_text=_("User's display name."))
|
||||||
|
path = models.TextField(default="users")
|
||||||
|
|
||||||
sources = models.ManyToManyField("Source", through="UserSourceConnection")
|
sources = models.ManyToManyField("Source", through="UserSourceConnection")
|
||||||
ak_groups = models.ManyToManyField("Group", related_name="users")
|
ak_groups = models.ManyToManyField("Group", related_name="users")
|
||||||
|
@ -147,6 +151,11 @@ class User(GuardianUserMixin, AbstractUser):
|
||||||
|
|
||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def default_path() -> str:
|
||||||
|
"""Get the default user path"""
|
||||||
|
return User._meta.get_field("path").default
|
||||||
|
|
||||||
def group_attributes(self, request: Optional[HttpRequest] = None) -> dict[str, Any]:
|
def group_attributes(self, request: Optional[HttpRequest] = None) -> dict[str, Any]:
|
||||||
"""Get a dictionary containing the attributes from all groups the user belongs to,
|
"""Get a dictionary containing the attributes from all groups the user belongs to,
|
||||||
including the users attributes"""
|
including the users attributes"""
|
||||||
|
@ -373,6 +382,8 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||||
name = models.TextField(help_text=_("Source's display Name."))
|
name = models.TextField(help_text=_("Source's display Name."))
|
||||||
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
|
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
|
||||||
|
|
||||||
|
user_path_template = models.TextField(default="goauthentik.io/sources/%(slug)s")
|
||||||
|
|
||||||
enabled = models.BooleanField(default=True)
|
enabled = models.BooleanField(default=True)
|
||||||
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
|
property_mappings = models.ManyToManyField("PropertyMapping", default=None, blank=True)
|
||||||
|
|
||||||
|
@ -408,6 +419,17 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
def get_user_path(self) -> str:
|
||||||
|
"""Get user path, fallback to default for formatting errors"""
|
||||||
|
try:
|
||||||
|
return self.user_path_template % {
|
||||||
|
"slug": self.slug,
|
||||||
|
}
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
except Exception as exc:
|
||||||
|
LOGGER.warning("Failed to template user path", exc=exc, source=self)
|
||||||
|
return User.default_path()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
"""Return component used to edit this object"""
|
"""Return component used to edit this object"""
|
||||||
|
|
|
@ -31,6 +31,7 @@ from authentik.policies.utils import delete_none_keys
|
||||||
from authentik.stages.password import BACKEND_INBUILT
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
|
from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH
|
||||||
|
|
||||||
|
|
||||||
class Action(Enum):
|
class Action(Enum):
|
||||||
|
@ -291,5 +292,6 @@ class SourceFlowManager:
|
||||||
connection,
|
connection,
|
||||||
**{
|
**{
|
||||||
PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info),
|
PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info),
|
||||||
|
PLAN_CONTEXT_USER_PATH: self.source.get_user_path(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from rest_framework.test import APITestCase
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
||||||
from authentik.flows.models import FlowDesignation
|
from authentik.flows.models import FlowDesignation
|
||||||
from authentik.lib.generators import generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
|
@ -149,3 +149,65 @@ class TestUsersAPI(APITestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_paths(self):
|
||||||
|
"""Test path"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:user-paths"),
|
||||||
|
)
|
||||||
|
print(response.content)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertJSONEqual(response.content.decode(), {"paths": ["users"]})
|
||||||
|
|
||||||
|
def test_path_valid(self):
|
||||||
|
"""Test path"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-list"),
|
||||||
|
data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "foo"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
|
def test_path_invalid(self):
|
||||||
|
"""Test path (invalid)"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-list"),
|
||||||
|
data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "/foo"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
response.content.decode(), {"path": ["No leading or trailing slashes allowed."]}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-list"),
|
||||||
|
data={"name": generate_id(), "username": generate_id(), "groups": [], "path": ""},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertJSONEqual(response.content.decode(), {"path": ["This field may not be blank."]})
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-list"),
|
||||||
|
data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "foo/"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
response.content.decode(), {"path": ["No leading or trailing slashes allowed."]}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-list"),
|
||||||
|
data={
|
||||||
|
"name": generate_id(),
|
||||||
|
"username": generate_id(),
|
||||||
|
"groups": [],
|
||||||
|
"path": "fos//o",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
response.content.decode(), {"path": ["No empty segments in user path allowed."]}
|
||||||
|
)
|
||||||
|
|
|
@ -20,6 +20,7 @@ from authentik import __version__, get_build_hash
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP,
|
USER_ATTRIBUTE_CAN_OVERRIDE_IP,
|
||||||
USER_ATTRIBUTE_SA,
|
USER_ATTRIBUTE_SA,
|
||||||
|
USER_PATH_SYSTEM_PREFIX,
|
||||||
Provider,
|
Provider,
|
||||||
Token,
|
Token,
|
||||||
TokenIntents,
|
TokenIntents,
|
||||||
|
@ -39,6 +40,8 @@ OUR_VERSION = parse(__version__)
|
||||||
OUTPOST_HELLO_INTERVAL = 10
|
OUTPOST_HELLO_INTERVAL = 10
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
USER_PATH_OUTPOSTS = USER_PATH_SYSTEM_PREFIX + "/outposts"
|
||||||
|
|
||||||
|
|
||||||
class ServiceConnectionInvalid(SentryIgnoredException):
|
class ServiceConnectionInvalid(SentryIgnoredException):
|
||||||
"""Exception raised when a Service Connection has invalid parameters"""
|
"""Exception raised when a Service Connection has invalid parameters"""
|
||||||
|
@ -339,6 +342,7 @@ class Outpost(ManagedModel):
|
||||||
user.attributes[USER_ATTRIBUTE_SA] = True
|
user.attributes[USER_ATTRIBUTE_SA] = True
|
||||||
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
|
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
|
||||||
user.name = f"Outpost {self.name} Service-Account"
|
user.name = f"Outpost {self.name} Service-Account"
|
||||||
|
user.path = USER_PATH_OUTPOSTS
|
||||||
user.save()
|
user.save()
|
||||||
if should_create_user:
|
if should_create_user:
|
||||||
self.build_user_permissions(user)
|
self.build_user_permissions(user)
|
||||||
|
|
|
@ -64,7 +64,9 @@ class BaseLDAPSynchronizer:
|
||||||
|
|
||||||
def build_user_properties(self, user_dn: str, **kwargs) -> dict[str, Any]:
|
def build_user_properties(self, user_dn: str, **kwargs) -> dict[str, Any]:
|
||||||
"""Build attributes for User object based on property mappings."""
|
"""Build attributes for User object based on property mappings."""
|
||||||
return self._build_object_properties(user_dn, self._source.property_mappings, **kwargs)
|
props = self._build_object_properties(user_dn, self._source.property_mappings, **kwargs)
|
||||||
|
props["path"] = self._source.get_user_path()
|
||||||
|
return props
|
||||||
|
|
||||||
def build_group_properties(self, group_dn: str, **kwargs) -> dict[str, Any]:
|
def build_group_properties(self, group_dn: str, **kwargs) -> dict[str, Any]:
|
||||||
"""Build attributes for Group object based on property mappings."""
|
"""Build attributes for Group object based on property mappings."""
|
||||||
|
|
|
@ -146,6 +146,7 @@ class ResponseProcessor:
|
||||||
USER_ATTRIBUTE_DELETE_ON_LOGOUT: True,
|
USER_ATTRIBUTE_DELETE_ON_LOGOUT: True,
|
||||||
USER_ATTRIBUTE_EXPIRES: expiry,
|
USER_ATTRIBUTE_EXPIRES: expiry,
|
||||||
},
|
},
|
||||||
|
path=self._source.get_user_path(),
|
||||||
)
|
)
|
||||||
LOGGER.debug("Created temporary user for NameID Transient", username=name_id)
|
LOGGER.debug("Created temporary user for NameID Transient", username=name_id)
|
||||||
user.set_unusable_password()
|
user.set_unusable_password()
|
||||||
|
|
|
@ -12,7 +12,11 @@ class UserWriteStageSerializer(StageSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = UserWriteStage
|
model = UserWriteStage
|
||||||
fields = StageSerializer.Meta.fields + ["create_users_as_inactive", "create_users_group"]
|
fields = StageSerializer.Meta.fields + [
|
||||||
|
"create_users_as_inactive",
|
||||||
|
"create_users_group",
|
||||||
|
"user_path_template",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class UserWriteStageViewSet(UsedByMixin, ModelViewSet):
|
class UserWriteStageViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.0.5 on 2022-06-14 20:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_stages_user_write", "0004_userwritestage_create_users_group"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="userwritestage",
|
||||||
|
name="user_path_template",
|
||||||
|
field=models.TextField(default="", blank=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -26,6 +26,11 @@ class UserWriteStage(Stage):
|
||||||
help_text=_("Optionally add newly created users to this group."),
|
help_text=_("Optionally add newly created users to this group."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user_path_template = models.TextField(
|
||||||
|
default="",
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> BaseSerializer:
|
def serializer(self) -> BaseSerializer:
|
||||||
from authentik.stages.user_write.api import UserWriteStageSerializer
|
from authentik.stages.user_write.api import UserWriteStageSerializer
|
||||||
|
|
|
@ -19,6 +19,7 @@ from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
from authentik.stages.user_write.signals import user_write
|
from authentik.stages.user_write.signals import user_write
|
||||||
|
|
||||||
PLAN_CONTEXT_GROUPS = "groups"
|
PLAN_CONTEXT_GROUPS = "groups"
|
||||||
|
PLAN_CONTEXT_USER_PATH = "user_path"
|
||||||
|
|
||||||
|
|
||||||
class UserWriteStageView(StageView):
|
class UserWriteStageView(StageView):
|
||||||
|
@ -49,9 +50,15 @@ class UserWriteStageView(StageView):
|
||||||
def ensure_user(self) -> tuple[User, bool]:
|
def ensure_user(self) -> tuple[User, bool]:
|
||||||
"""Ensure a user exists"""
|
"""Ensure a user exists"""
|
||||||
user_created = False
|
user_created = False
|
||||||
|
path = self.executor.plan.context.get(
|
||||||
|
PLAN_CONTEXT_USER_PATH, self.executor.current_stage.user_path_template
|
||||||
|
)
|
||||||
|
if path == "":
|
||||||
|
path = User.default_path()
|
||||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
|
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
|
||||||
is_active=not self.executor.current_stage.create_users_as_inactive
|
is_active=not self.executor.current_stage.create_users_as_inactive,
|
||||||
|
path=path,
|
||||||
)
|
)
|
||||||
self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
|
self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
|
|
101
schema.yml
101
schema.yml
|
@ -3067,6 +3067,14 @@ paths:
|
||||||
description: Number of results to return per page.
|
description: Number of results to return per page.
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
|
- in: query
|
||||||
|
name: path
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: path_startswith
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
- name: search
|
- name: search
|
||||||
required: false
|
required: false
|
||||||
in: query
|
in: query
|
||||||
|
@ -3390,6 +3398,30 @@ paths:
|
||||||
$ref: '#/components/schemas/ValidationError'
|
$ref: '#/components/schemas/ValidationError'
|
||||||
'403':
|
'403':
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
|
/core/users/paths/:
|
||||||
|
get:
|
||||||
|
operationId: core_users_paths_retrieve
|
||||||
|
description: Get all user paths
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: search
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
tags:
|
||||||
|
- core
|
||||||
|
security:
|
||||||
|
- authentik: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserPath'
|
||||||
|
description: ''
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/schemas/GenericError'
|
||||||
/core/users/service_account/:
|
/core/users/service_account/:
|
||||||
post:
|
post:
|
||||||
operationId: core_users_service_account_create
|
operationId: core_users_service_account_create
|
||||||
|
@ -13133,6 +13165,10 @@ paths:
|
||||||
- username_link
|
- username_link
|
||||||
description: How the source determines if an existing user should be authenticated
|
description: How the source determines if an existing user should be authenticated
|
||||||
or a new user enrolled.
|
or a new user enrolled.
|
||||||
|
- in: query
|
||||||
|
name: user_path_template
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
security:
|
security:
|
||||||
|
@ -18826,6 +18862,10 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
- in: query
|
||||||
|
name: user_path_template
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
tags:
|
tags:
|
||||||
- stages
|
- stages
|
||||||
security:
|
security:
|
||||||
|
@ -22705,6 +22745,8 @@ components:
|
||||||
can be overwritten by migrations. You can still modify the objects via
|
can be overwritten by migrations. You can still modify the objects via
|
||||||
the API, but expect changes to be overwritten in a later update.
|
the API, but expect changes to be overwritten in a later update.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
server_uri:
|
server_uri:
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: uri
|
||||||
|
@ -22808,6 +22850,9 @@ components:
|
||||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||||
description: How the source determines if an existing user should be authenticated
|
description: How the source determines if an existing user should be authenticated
|
||||||
or a new user enrolled.
|
or a new user enrolled.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
server_uri:
|
server_uri:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
@ -23417,6 +23462,8 @@ components:
|
||||||
can be overwritten by migrations. You can still modify the objects via
|
can be overwritten by migrations. You can still modify the objects via
|
||||||
the API, but expect changes to be overwritten in a later update.
|
the API, but expect changes to be overwritten in a later update.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
provider_type:
|
provider_type:
|
||||||
$ref: '#/components/schemas/ProviderTypeEnum'
|
$ref: '#/components/schemas/ProviderTypeEnum'
|
||||||
request_token_url:
|
request_token_url:
|
||||||
|
@ -23504,6 +23551,9 @@ components:
|
||||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||||
description: How the source determines if an existing user should be authenticated
|
description: How the source determines if an existing user should be authenticated
|
||||||
or a new user enrolled.
|
or a new user enrolled.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
provider_type:
|
provider_type:
|
||||||
$ref: '#/components/schemas/ProviderTypeEnum'
|
$ref: '#/components/schemas/ProviderTypeEnum'
|
||||||
request_token_url:
|
request_token_url:
|
||||||
|
@ -27500,6 +27550,9 @@ components:
|
||||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||||
description: How the source determines if an existing user should be authenticated
|
description: How the source determines if an existing user should be authenticated
|
||||||
or a new user enrolled.
|
or a new user enrolled.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
server_uri:
|
server_uri:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
@ -27734,6 +27787,9 @@ components:
|
||||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||||
description: How the source determines if an existing user should be authenticated
|
description: How the source determines if an existing user should be authenticated
|
||||||
or a new user enrolled.
|
or a new user enrolled.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
provider_type:
|
provider_type:
|
||||||
$ref: '#/components/schemas/ProviderTypeEnum'
|
$ref: '#/components/schemas/ProviderTypeEnum'
|
||||||
request_token_url:
|
request_token_url:
|
||||||
|
@ -27938,6 +27994,9 @@ components:
|
||||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||||
description: How the source determines if an existing user should be authenticated
|
description: How the source determines if an existing user should be authenticated
|
||||||
or a new user enrolled.
|
or a new user enrolled.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
client_id:
|
client_id:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
@ -28251,6 +28310,9 @@ components:
|
||||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||||
description: How the source determines if an existing user should be authenticated
|
description: How the source determines if an existing user should be authenticated
|
||||||
or a new user enrolled.
|
or a new user enrolled.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
pre_authentication_flow:
|
pre_authentication_flow:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
@ -28519,6 +28581,9 @@ components:
|
||||||
attributes:
|
attributes:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
PatchedUserWriteStageRequest:
|
PatchedUserWriteStageRequest:
|
||||||
type: object
|
type: object
|
||||||
description: UserWriteStage Serializer
|
description: UserWriteStage Serializer
|
||||||
|
@ -28538,6 +28603,8 @@ components:
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Optionally add newly created users to this group.
|
description: Optionally add newly created users to this group.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
PatchedWebAuthnDeviceRequest:
|
PatchedWebAuthnDeviceRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Serializer for WebAuthn authenticator devices
|
description: Serializer for WebAuthn authenticator devices
|
||||||
|
@ -28647,6 +28714,8 @@ components:
|
||||||
can be overwritten by migrations. You can still modify the objects via
|
can be overwritten by migrations. You can still modify the objects via
|
||||||
the API, but expect changes to be overwritten in a later update.
|
the API, but expect changes to be overwritten in a later update.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
client_id:
|
client_id:
|
||||||
type: string
|
type: string
|
||||||
description: Client identifier used to talk to Plex.
|
description: Client identifier used to talk to Plex.
|
||||||
|
@ -28743,6 +28812,9 @@ components:
|
||||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||||
description: How the source determines if an existing user should be authenticated
|
description: How the source determines if an existing user should be authenticated
|
||||||
or a new user enrolled.
|
or a new user enrolled.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
client_id:
|
client_id:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
@ -30048,6 +30120,8 @@ components:
|
||||||
can be overwritten by migrations. You can still modify the objects via
|
can be overwritten by migrations. You can still modify the objects via
|
||||||
the API, but expect changes to be overwritten in a later update.
|
the API, but expect changes to be overwritten in a later update.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
pre_authentication_flow:
|
pre_authentication_flow:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
@ -30138,6 +30212,9 @@ components:
|
||||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||||
description: How the source determines if an existing user should be authenticated
|
description: How the source determines if an existing user should be authenticated
|
||||||
or a new user enrolled.
|
or a new user enrolled.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
pre_authentication_flow:
|
pre_authentication_flow:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
@ -30484,6 +30561,8 @@ components:
|
||||||
can be overwritten by migrations. You can still modify the objects via
|
can be overwritten by migrations. You can still modify the objects via
|
||||||
the API, but expect changes to be overwritten in a later update.
|
the API, but expect changes to be overwritten in a later update.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- component
|
- component
|
||||||
- managed
|
- managed
|
||||||
|
@ -30526,6 +30605,9 @@ components:
|
||||||
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
- $ref: '#/components/schemas/UserMatchingModeEnum'
|
||||||
description: How the source determines if an existing user should be authenticated
|
description: How the source determines if an existing user should be authenticated
|
||||||
or a new user enrolled.
|
or a new user enrolled.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- slug
|
- slug
|
||||||
|
@ -31107,6 +31189,8 @@ components:
|
||||||
uid:
|
uid:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- avatar
|
- avatar
|
||||||
- groups
|
- groups
|
||||||
|
@ -31369,6 +31453,16 @@ components:
|
||||||
minLength: 1
|
minLength: 1
|
||||||
required:
|
required:
|
||||||
- password
|
- password
|
||||||
|
UserPath:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
paths:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
required:
|
||||||
|
- paths
|
||||||
UserRequest:
|
UserRequest:
|
||||||
type: object
|
type: object
|
||||||
description: User Serializer
|
description: User Serializer
|
||||||
|
@ -31402,6 +31496,9 @@ components:
|
||||||
attributes:
|
attributes:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
required:
|
required:
|
||||||
- groups
|
- groups
|
||||||
- name
|
- name
|
||||||
|
@ -31578,6 +31675,8 @@ components:
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Optionally add newly created users to this group.
|
description: Optionally add newly created users to this group.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- component
|
- component
|
||||||
- meta_model_name
|
- meta_model_name
|
||||||
|
@ -31604,6 +31703,8 @@ components:
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: true
|
nullable: true
|
||||||
description: Optionally add newly created users to this group.
|
description: Optionally add newly created users to this group.
|
||||||
|
user_path_template:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
ValidationError:
|
ValidationError:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CoreApi, SessionUser } from "@goauthentik/api";
|
import { CoreApi, ResponseError, SessionUser } from "@goauthentik/api";
|
||||||
import { activateLocale } from "../interfaces/locale";
|
import { activateLocale } from "../interfaces/locale";
|
||||||
import { DEFAULT_CONFIG } from "./Config";
|
import { DEFAULT_CONFIG } from "./Config";
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export function me(): Promise<SessionUser> {
|
||||||
activateLocale(locale);
|
activateLocale(locale);
|
||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}).catch((ex) => {
|
}).catch((ex: ResponseError) => {
|
||||||
const defaultUser: SessionUser = {
|
const defaultUser: SessionUser = {
|
||||||
user: {
|
user: {
|
||||||
pk: -1,
|
pk: -1,
|
||||||
|
|
|
@ -277,6 +277,12 @@ html > form > input {
|
||||||
.pf-c-select__menu-item:hover {
|
.pf-c-select__menu-item:hover {
|
||||||
--pf-c-select__menu-item--hover--BackgroundColor: var(--ak-dark-background-lighter);
|
--pf-c-select__menu-item--hover--BackgroundColor: var(--ak-dark-background-lighter);
|
||||||
}
|
}
|
||||||
|
.pf-c-select__menu-wrapper:focus-within,
|
||||||
|
.pf-c-select__menu-wrapper.pf-m-focus,
|
||||||
|
.pf-c-select__menu-item:focus,
|
||||||
|
.pf-c-select__menu-item.pf-m-focus {
|
||||||
|
--pf-c-select__menu-item--focus--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||||
|
}
|
||||||
.pf-c-button.pf-m-plain:hover {
|
.pf-c-button.pf-m-plain:hover {
|
||||||
color: var(--ak-dark-foreground);
|
color: var(--ak-dark-foreground);
|
||||||
}
|
}
|
||||||
|
@ -395,6 +401,14 @@ html > form > input {
|
||||||
.pf-c-wizard__nav-link::before {
|
.pf-c-wizard__nav-link::before {
|
||||||
--pf-c-wizard__nav-link--before--BackgroundColor: transparent;
|
--pf-c-wizard__nav-link--before--BackgroundColor: transparent;
|
||||||
}
|
}
|
||||||
|
/* tree view */
|
||||||
|
.pf-c-tree-view__node:focus {
|
||||||
|
--pf-c-tree-view__node--focus--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||||
|
}
|
||||||
|
.pf-c-tree-view__content:hover,
|
||||||
|
.pf-c-tree-view__content:focus-within {
|
||||||
|
--pf-c-tree-view__node--hover--BackgroundColor: var(--ak-dark-background-light-ish);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-c-data-list__item {
|
.pf-c-data-list__item {
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
import { CSSResult, LitElement, TemplateResult, html } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import AKGlobal from "../authentik.css";
|
||||||
|
import PFTreeView from "@patternfly/patternfly/components/TreeView/tree-view.css";
|
||||||
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
|
import { EVENT_REFRESH } from "../constants";
|
||||||
|
import { setURLParams } from "./router/RouteMatch";
|
||||||
|
|
||||||
|
export interface TreeViewItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
childItems: TreeViewItem[];
|
||||||
|
parent?: TreeViewItem;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ak-treeview-node")
|
||||||
|
export class TreeViewNode extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
item?: TreeViewItem;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
open = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
host?: TreeView;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
path = "";
|
||||||
|
|
||||||
|
@property()
|
||||||
|
separator = "";
|
||||||
|
|
||||||
|
get openable(): boolean {
|
||||||
|
return (this.item?.childItems || []).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fullPath(): string {
|
||||||
|
const pathItems = [];
|
||||||
|
let item = this.item;
|
||||||
|
while (item) {
|
||||||
|
pathItems.push(item.id);
|
||||||
|
item = item.parent;
|
||||||
|
}
|
||||||
|
return pathItems.reverse().join(this.separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createRenderRoot(): Element {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated(): void {
|
||||||
|
const pathSegments = this.path.split(this.separator);
|
||||||
|
const level = this.item?.level || 0;
|
||||||
|
// Ignore the last item as that shouldn't be expanded
|
||||||
|
pathSegments.pop();
|
||||||
|
if (pathSegments[level] == this.item?.id) {
|
||||||
|
this.open = true;
|
||||||
|
}
|
||||||
|
if (this.path === this.fullPath && this.host !== undefined) {
|
||||||
|
this.host.activeNode = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
const shouldRenderChildren = (this.item?.childItems || []).length > 0 && this.open;
|
||||||
|
return html`
|
||||||
|
<li
|
||||||
|
class="pf-c-tree-view__list-item ${this.open ? "pf-m-expanded" : ""}"
|
||||||
|
role="treeitem"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div class="pf-c-tree-view__content">
|
||||||
|
<button
|
||||||
|
class="pf-c-tree-view__node ${this.host?.activeNode === this
|
||||||
|
? "pf-m-current"
|
||||||
|
: ""}"
|
||||||
|
@click=${() => {
|
||||||
|
if (this.host) {
|
||||||
|
this.host.activeNode = this;
|
||||||
|
}
|
||||||
|
setURLParams({ path: this.fullPath });
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_REFRESH, {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="pf-c-tree-view__node-container">
|
||||||
|
${this.openable
|
||||||
|
? html` <button
|
||||||
|
class="pf-c-tree-view__node-toggle"
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
if (this.openable) {
|
||||||
|
this.open = !this.open;
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="pf-c-tree-view__node-toggle-icon">
|
||||||
|
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</button>`
|
||||||
|
: html``}
|
||||||
|
<span class="pf-c-tree-view__node-icon">
|
||||||
|
<i
|
||||||
|
class="fas ${this.open ? "fa-folder-open" : "fa-folder"}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
<span class="pf-c-tree-view__node-text">${this.item?.label}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul class="pf-c-tree-view__list" role="group" ?hidden=${!shouldRenderChildren}>
|
||||||
|
${this.item?.childItems.map((item) => {
|
||||||
|
return html`<ak-treeview-node
|
||||||
|
.item=${item}
|
||||||
|
path=${this.path}
|
||||||
|
separator=${this.separator}
|
||||||
|
.host=${this.host}
|
||||||
|
></ak-treeview-node>`;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ak-treeview")
|
||||||
|
export class TreeView extends LitElement {
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [PFBase, PFTreeView, AKGlobal];
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
items: string[] = [];
|
||||||
|
|
||||||
|
@property()
|
||||||
|
path = "";
|
||||||
|
|
||||||
|
@state()
|
||||||
|
activeNode?: TreeViewNode;
|
||||||
|
|
||||||
|
separator = "/";
|
||||||
|
|
||||||
|
createNode(path: string[], tree: TreeViewItem[], level: number): TreeViewItem {
|
||||||
|
const id = path.shift();
|
||||||
|
const idx = tree.findIndex((e: TreeViewItem) => {
|
||||||
|
return e.id == id;
|
||||||
|
});
|
||||||
|
if (idx < 0) {
|
||||||
|
const item: TreeViewItem = {
|
||||||
|
id: id || "",
|
||||||
|
label: id || "",
|
||||||
|
childItems: [],
|
||||||
|
level: level,
|
||||||
|
};
|
||||||
|
tree.push(item);
|
||||||
|
if (path.length !== 0) {
|
||||||
|
const child = this.createNode(path, tree[tree.length - 1].childItems, level + 1);
|
||||||
|
child.parent = item;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
} else {
|
||||||
|
return this.createNode(path, tree[idx].childItems, level + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(data: string[]): TreeViewItem[] {
|
||||||
|
const tree: TreeViewItem[] = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const path: string = data[i];
|
||||||
|
const split: string[] = path.split(this.separator);
|
||||||
|
this.createNode(split, tree, 0);
|
||||||
|
}
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
const result = this.parse(this.items);
|
||||||
|
return html`<div class="pf-c-tree-view pf-m-guides">
|
||||||
|
<ul class="pf-c-tree-view__list" role="tree">
|
||||||
|
<!-- @ts-ignore -->
|
||||||
|
<ak-treeview-node
|
||||||
|
.item=${{
|
||||||
|
id: "",
|
||||||
|
label: t`Root`,
|
||||||
|
childItems: result,
|
||||||
|
level: -1,
|
||||||
|
} as TreeViewItem}
|
||||||
|
path=${this.path}
|
||||||
|
?open=${true}
|
||||||
|
separator=${this.separator}
|
||||||
|
.host=${this}
|
||||||
|
></ak-treeview-node>
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
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 { CoreApi } from "@goauthentik/api";
|
import { CoreApi, ResponseError } from "@goauthentik/api";
|
||||||
|
|
||||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
import { ERROR_CLASS, SECONDARY_CLASS, SUCCESS_CLASS } from "../../constants";
|
import { ERROR_CLASS, SECONDARY_CLASS, SUCCESS_CLASS } from "../../constants";
|
||||||
|
@ -37,15 +37,15 @@ export class TokenCopyButton extends ActionButton {
|
||||||
this.buttonClass = SUCCESS_CLASS;
|
this.buttonClass = SUCCESS_CLASS;
|
||||||
return token.key;
|
return token.key;
|
||||||
})
|
})
|
||||||
.catch((err: Error | Response | undefined) => {
|
.catch((err: Error | ResponseError | undefined) => {
|
||||||
this.buttonClass = ERROR_CLASS;
|
this.buttonClass = ERROR_CLASS;
|
||||||
if (err instanceof Error) {
|
if (!(err instanceof ResponseError)) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.buttonClass = SECONDARY_CLASS;
|
this.buttonClass = SECONDARY_CLASS;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
return err?.json().then((errResp) => {
|
return err.response.json().then((errResp) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.buttonClass = SECONDARY_CLASS;
|
this.buttonClass = SECONDARY_CLASS;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
@ -92,15 +92,15 @@ export class TokenCopyButton extends ActionButton {
|
||||||
this.setDone(SUCCESS_CLASS);
|
this.setDone(SUCCESS_CLASS);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err: Response | Error) => {
|
.catch((err: ResponseError | Error) => {
|
||||||
if (err instanceof Error) {
|
if (!(err instanceof ResponseError)) {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return err?.json().then((errResp) => {
|
return err.response.json().then((errResp) => {
|
||||||
this.setDone(ERROR_CLASS);
|
this.setDone(ERROR_CLASS);
|
||||||
throw new Error(errResp["detail"]);
|
throw new Error(errResp["detail"]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co
|
||||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import { ValidationError } from "@goauthentik/api";
|
import { ResponseError, ValidationError } from "@goauthentik/api";
|
||||||
|
|
||||||
import { EVENT_REFRESH } from "../../constants";
|
import { EVENT_REFRESH } from "../../constants";
|
||||||
import { showMessage } from "../../elements/messages/MessageContainer";
|
import { showMessage } from "../../elements/messages/MessageContainer";
|
||||||
|
@ -209,13 +209,13 @@ export class Form<T> extends LitElement {
|
||||||
);
|
);
|
||||||
return r;
|
return r;
|
||||||
})
|
})
|
||||||
.catch(async (ex: Response | Error) => {
|
.catch(async (ex: Error | ResponseError) => {
|
||||||
if (ex instanceof Error) {
|
if (!(ex instanceof ResponseError)) {
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
let msg = ex.statusText;
|
let msg = ex.response.statusText;
|
||||||
if (ex.status > 399 && ex.status < 500) {
|
if (ex.response.status > 399 && ex.response.status < 500) {
|
||||||
const errorMessage: ValidationError = await ex.json();
|
const errorMessage: ValidationError = await ex.response.json();
|
||||||
if (!errorMessage) return errorMessage;
|
if (!errorMessage) return errorMessage;
|
||||||
if (errorMessage instanceof Error) {
|
if (errorMessage instanceof Error) {
|
||||||
throw errorMessage;
|
throw errorMessage;
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
FlowsApi,
|
FlowsApi,
|
||||||
LayoutEnum,
|
LayoutEnum,
|
||||||
RedirectChallenge,
|
RedirectChallenge,
|
||||||
|
ResponseError,
|
||||||
ShellChallenge,
|
ShellChallenge,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
@ -193,7 +194,7 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.catch((e: Error | Response) => {
|
.catch((e: Error | ResponseError) => {
|
||||||
this.errorMessage(e);
|
this.errorMessage(e);
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
|
@ -226,7 +227,7 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||||
this.setBackground(this.challenge.flowInfo.background);
|
this.setBackground(this.challenge.flowInfo.background);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e: Error | Response) => {
|
.catch((e: Error | ResponseError) => {
|
||||||
// Catch JSON or Update errors
|
// Catch JSON or Update errors
|
||||||
this.errorMessage(e);
|
this.errorMessage(e);
|
||||||
})
|
})
|
||||||
|
@ -235,9 +236,11 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async errorMessage(error: Error | Response): Promise<void> {
|
async errorMessage(error: Error | ResponseError): Promise<void> {
|
||||||
let body = "";
|
let body = "";
|
||||||
if (error instanceof Error) {
|
if (error instanceof ResponseError) {
|
||||||
|
body = await error.response.text();
|
||||||
|
} else if (error instanceof Error) {
|
||||||
body = error.message;
|
body = error.message;
|
||||||
}
|
}
|
||||||
this.challenge = {
|
this.challenge = {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import {
|
import {
|
||||||
PlexAuthenticationChallenge,
|
PlexAuthenticationChallenge,
|
||||||
PlexAuthenticationChallengeResponseRequest,
|
PlexAuthenticationChallengeResponseRequest,
|
||||||
|
ResponseError,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
import { SourcesApi } from "@goauthentik/api";
|
import { SourcesApi } from "@goauthentik/api";
|
||||||
|
|
||||||
|
@ -48,8 +49,8 @@ export class PlexLoginInit extends BaseStage<
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
window.location.assign(r.to);
|
window.location.assign(r.to);
|
||||||
})
|
})
|
||||||
.catch((r: Response) => {
|
.catch((r: ResponseError) => {
|
||||||
r.json().then((body: { detail: string }) => {
|
r.response.json().then((body: { detail: string }) => {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
message: body.detail,
|
message: body.detail,
|
||||||
|
|
|
@ -12,7 +12,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import { Flow, FlowsApi } from "@goauthentik/api";
|
import { Flow, FlowsApi, ResponseError } from "@goauthentik/api";
|
||||||
|
|
||||||
import { AndNext, DEFAULT_CONFIG } from "../../api/Config";
|
import { AndNext, DEFAULT_CONFIG } from "../../api/Config";
|
||||||
import "../../elements/PageHeader";
|
import "../../elements/PageHeader";
|
||||||
|
@ -164,10 +164,13 @@ export class FlowViewPage extends LitElement {
|
||||||
)}`;
|
)}`;
|
||||||
window.open(finalURL, "_blank");
|
window.open(finalURL, "_blank");
|
||||||
})
|
})
|
||||||
.catch((exc: Response) => {
|
.catch((exc: ResponseError) => {
|
||||||
// This request can return a HTTP 400 when a flow
|
// This request can return a HTTP 400 when a flow
|
||||||
// is not applicable.
|
// is not applicable.
|
||||||
window.open(exc.url, "_blank");
|
window.open(
|
||||||
|
exc.response.url,
|
||||||
|
"_blank",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -209,7 +209,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
|
||||||
<ak-search-select
|
<ak-search-select
|
||||||
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||||
const args: CoreGroupsListRequest = {
|
const args: CoreGroupsListRequest = {
|
||||||
ordering: "username",
|
ordering: "name",
|
||||||
};
|
};
|
||||||
if (query !== undefined) {
|
if (query !== undefined) {
|
||||||
args.search = query;
|
args.search = query;
|
||||||
|
|
|
@ -7,7 +7,9 @@ import { until } from "lit/directives/until.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CoreApi,
|
CoreApi,
|
||||||
|
CoreGroupsListRequest,
|
||||||
CryptoApi,
|
CryptoApi,
|
||||||
|
Group,
|
||||||
LDAPSource,
|
LDAPSource,
|
||||||
LDAPSourceRequest,
|
LDAPSourceRequest,
|
||||||
PropertymappingsApi,
|
PropertymappingsApi,
|
||||||
|
@ -15,6 +17,7 @@ import {
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
|
import "../../../elements/SearchSelect";
|
||||||
import "../../../elements/forms/FormGroup";
|
import "../../../elements/forms/FormGroup";
|
||||||
import "../../../elements/forms/HorizontalFormElement";
|
import "../../../elements/forms/HorizontalFormElement";
|
||||||
import { ModelForm } from "../../../elements/forms/ModelForm";
|
import { ModelForm } from "../../../elements/forms/ModelForm";
|
||||||
|
@ -301,31 +304,49 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
|
||||||
<span slot="header"> ${t`Additional settings`} </span>
|
<span slot="header"> ${t`Additional settings`} </span>
|
||||||
<div slot="body" class="pf-c-form">
|
<div slot="body" class="pf-c-form">
|
||||||
<ak-form-element-horizontal label=${t`Group`} name="syncParentGroup">
|
<ak-form-element-horizontal label=${t`Group`} name="syncParentGroup">
|
||||||
<select class="pf-c-form-control">
|
<!-- @ts-ignore -->
|
||||||
<option
|
<ak-search-select
|
||||||
value=""
|
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||||
?selected=${this.instance?.syncParentGroup === undefined}
|
const args: CoreGroupsListRequest = {
|
||||||
|
ordering: "name",
|
||||||
|
};
|
||||||
|
if (query !== undefined) {
|
||||||
|
args.search = query;
|
||||||
|
}
|
||||||
|
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
return groups.results;
|
||||||
|
}}
|
||||||
|
.renderElement=${(group: Group): string => {
|
||||||
|
return group.name;
|
||||||
|
}}
|
||||||
|
.value=${(group: Group | undefined): string | undefined => {
|
||||||
|
return group ? group.pk : undefined;
|
||||||
|
}}
|
||||||
|
.selected=${(group: Group): boolean => {
|
||||||
|
return group.pk === this.instance?.syncParentGroup;
|
||||||
|
}}
|
||||||
|
?blankable=${true}
|
||||||
>
|
>
|
||||||
---------
|
</ak-search-select>
|
||||||
</option>
|
|
||||||
${until(
|
|
||||||
new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => {
|
|
||||||
return groups.results.map((group) => {
|
|
||||||
return html`<option
|
|
||||||
value=${ifDefined(group.pk)}
|
|
||||||
?selected=${this.instance?.syncParentGroup === group.pk}
|
|
||||||
>
|
|
||||||
${group.name}
|
|
||||||
</option>`;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
html`<option>${t`Loading...`}</option>`,
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${t`Parent group for all the groups imported from LDAP.`}
|
${t`Parent group for all the groups imported from LDAP.`}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(
|
||||||
|
this.instance?.userPathTemplate,
|
||||||
|
"goauthentik.io/sources/%(slug)s",
|
||||||
|
)}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${t`Addition User DN`}
|
label=${t`Addition User DN`}
|
||||||
name="additionalUserDn"
|
name="additionalUserDn"
|
||||||
|
|
|
@ -268,6 +268,19 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(
|
||||||
|
this.instance?.userPathTemplate,
|
||||||
|
"goauthentik.io/sources/%(slug)s",
|
||||||
|
)}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
<ak-form-group .expanded=${true}>
|
<ak-form-group .expanded=${true}>
|
||||||
<span slot="header"> ${t`Protocol settings`} </span>
|
<span slot="header"> ${t`Protocol settings`} </span>
|
||||||
|
|
|
@ -215,6 +215,19 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(
|
||||||
|
this.instance?.userPathTemplate,
|
||||||
|
"goauthentik.io/sources/%(slug)s",
|
||||||
|
)}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
|
||||||
<ak-form-group .expanded=${true}>
|
<ak-form-group .expanded=${true}>
|
||||||
<span slot="header"> ${t`Protocol settings`} </span>
|
<span slot="header"> ${t`Protocol settings`} </span>
|
||||||
|
|
|
@ -232,6 +232,19 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(
|
||||||
|
this.instance?.userPathTemplate,
|
||||||
|
"goauthentik.io/sources/%(slug)s",
|
||||||
|
)}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${t`Delete temporary users after`}
|
label=${t`Delete temporary users after`}
|
||||||
?required=${true}
|
?required=${true}
|
||||||
|
|
|
@ -3,11 +3,11 @@ import { t } from "@lingui/macro";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } 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 { until } from "lit/directives/until.js";
|
|
||||||
|
|
||||||
import { CoreApi, StagesApi, UserWriteStage } from "@goauthentik/api";
|
import { CoreApi, CoreGroupsListRequest, Group, StagesApi, UserWriteStage } from "@goauthentik/api";
|
||||||
|
|
||||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
|
import "../../../elements/SearchSelect";
|
||||||
import "../../../elements/forms/FormGroup";
|
import "../../../elements/forms/FormGroup";
|
||||||
import "../../../elements/forms/HorizontalFormElement";
|
import "../../../elements/forms/HorizontalFormElement";
|
||||||
import { ModelForm } from "../../../elements/forms/ModelForm";
|
import { ModelForm } from "../../../elements/forms/ModelForm";
|
||||||
|
@ -74,29 +74,44 @@ export class UserWriteStageForm extends ModelForm<UserWriteStage, string> {
|
||||||
${t`Mark newly created users as inactive.`}
|
${t`Mark newly created users as inactive.`}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal name="userPathTemplate">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(this.instance?.userPathTemplate, "")}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${t`Path new users will be created under.`}
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal label=${t`Group`} name="createUsersGroup">
|
<ak-form-element-horizontal label=${t`Group`} name="createUsersGroup">
|
||||||
<select class="pf-c-form-control">
|
<!-- @ts-ignore -->
|
||||||
<option
|
<ak-search-select
|
||||||
value=""
|
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||||
?selected=${this.instance?.createUsersGroup === undefined}
|
const args: CoreGroupsListRequest = {
|
||||||
|
ordering: "name",
|
||||||
|
};
|
||||||
|
if (query !== undefined) {
|
||||||
|
args.search = query;
|
||||||
|
}
|
||||||
|
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
return groups.results;
|
||||||
|
}}
|
||||||
|
.renderElement=${(group: Group): string => {
|
||||||
|
return group.name;
|
||||||
|
}}
|
||||||
|
.value=${(group: Group | undefined): string | undefined => {
|
||||||
|
return group ? group.pk : undefined;
|
||||||
|
}}
|
||||||
|
.selected=${(group: Group): boolean => {
|
||||||
|
return group.pk === this.instance?.createUsersGroup;
|
||||||
|
}}
|
||||||
|
?blankable=${true}
|
||||||
>
|
>
|
||||||
---------
|
</ak-search-select>
|
||||||
</option>
|
|
||||||
${until(
|
|
||||||
new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => {
|
|
||||||
return groups.results.map((group) => {
|
|
||||||
return html`<option
|
|
||||||
value=${ifDefined(group.pk)}
|
|
||||||
?selected=${this.instance?.createUsersGroup ===
|
|
||||||
group.pk}
|
|
||||||
>
|
|
||||||
${group.name}
|
|
||||||
</option>`;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
html`<option>${t`Loading...`}</option>`,
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${t`Newly created users are added to this group, if a group is selected.`}
|
${t`Newly created users are added to this group, if a group is selected.`}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { until } from "lit/directives/until.js";
|
||||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||||
|
|
||||||
import { CapabilitiesEnum, CoreApi, User } from "@goauthentik/api";
|
import { CapabilitiesEnum, CoreApi, ResponseError, User } from "@goauthentik/api";
|
||||||
|
|
||||||
import { AKResponse } from "../../api/Client";
|
import { AKResponse } from "../../api/Client";
|
||||||
import { DEFAULT_CONFIG, config, tenant } from "../../api/Config";
|
import { DEFAULT_CONFIG, config, tenant } from "../../api/Config";
|
||||||
|
@ -244,8 +244,8 @@ export class RelatedUserList extends Table<User> {
|
||||||
description: rec.link,
|
description: rec.link,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((ex: Response) => {
|
.catch((ex: ResponseError) => {
|
||||||
ex.json().then(() => {
|
ex.response.json().then(() => {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
message: t`No recovery flow is configured.`,
|
message: t`No recovery flow is configured.`,
|
||||||
|
|
|
@ -69,6 +69,14 @@ export class UserForm extends ModelForm<User, number> {
|
||||||
${t`User's primary identifier. 150 characters or fewer.`}
|
${t`User's primary identifier. 150 characters or fewer.`}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal label=${t`Path`} ?required=${true} name="path">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value="${first(this.instance?.path, "users")}"
|
||||||
|
class="pf-c-form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal label=${t`Name`} name="name">
|
<ak-form-element-horizontal label=${t`Name`} name="name">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
@ -4,10 +4,12 @@ import { CSSResult, TemplateResult, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
import { until } from "lit/directives/until.js";
|
import { until } from "lit/directives/until.js";
|
||||||
|
|
||||||
|
import AKGlobal from "../../authentik.css";
|
||||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||||
|
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||||
|
|
||||||
import { CapabilitiesEnum, CoreApi, User } from "@goauthentik/api";
|
import { CapabilitiesEnum, CoreApi, ResponseError, User } from "@goauthentik/api";
|
||||||
|
|
||||||
import { AKResponse } from "../../api/Client";
|
import { AKResponse } from "../../api/Client";
|
||||||
import { DEFAULT_CONFIG, config, tenant } from "../../api/Config";
|
import { DEFAULT_CONFIG, config, tenant } from "../../api/Config";
|
||||||
|
@ -15,12 +17,13 @@ import { me } from "../../api/Users";
|
||||||
import { uiConfig } from "../../common/config";
|
import { uiConfig } from "../../common/config";
|
||||||
import { PFColor } from "../../elements/Label";
|
import { PFColor } from "../../elements/Label";
|
||||||
import { PFSize } from "../../elements/Spinner";
|
import { PFSize } from "../../elements/Spinner";
|
||||||
|
import "../../elements/TreeView";
|
||||||
import "../../elements/buttons/ActionButton";
|
import "../../elements/buttons/ActionButton";
|
||||||
import "../../elements/forms/DeleteBulkForm";
|
import "../../elements/forms/DeleteBulkForm";
|
||||||
import "../../elements/forms/ModalForm";
|
import "../../elements/forms/ModalForm";
|
||||||
import { MessageLevel } from "../../elements/messages/Message";
|
import { MessageLevel } from "../../elements/messages/Message";
|
||||||
import { showMessage } from "../../elements/messages/MessageContainer";
|
import { showMessage } from "../../elements/messages/MessageContainer";
|
||||||
import { getURLParam, updateURLParams } from "../../elements/router/RouteMatch";
|
import { getURLParam } from "../../elements/router/RouteMatch";
|
||||||
import { TableColumn } from "../../elements/table/Table";
|
import { TableColumn } from "../../elements/table/Table";
|
||||||
import { TablePage } from "../../elements/table/TablePage";
|
import { TablePage } from "../../elements/table/TablePage";
|
||||||
import { first } from "../../utils";
|
import { first } from "../../utils";
|
||||||
|
@ -51,11 +54,11 @@ export class UserListPage extends TablePage<User> {
|
||||||
@property()
|
@property()
|
||||||
order = "last_login";
|
order = "last_login";
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property()
|
||||||
hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true);
|
path = getURLParam<string>("path", "/");
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return super.styles.concat(PFDescriptionList, PFAlert);
|
return super.styles.concat(PFDescriptionList, PFCard, PFAlert, AKGlobal);
|
||||||
}
|
}
|
||||||
|
|
||||||
async apiEndpoint(page: number): Promise<AKResponse<User>> {
|
async apiEndpoint(page: number): Promise<AKResponse<User>> {
|
||||||
|
@ -64,11 +67,7 @@ export class UserListPage extends TablePage<User> {
|
||||||
page: page,
|
page: page,
|
||||||
pageSize: (await uiConfig()).pagination.perPage,
|
pageSize: (await uiConfig()).pagination.perPage,
|
||||||
search: this.search || "",
|
search: this.search || "",
|
||||||
attributes: this.hideServiceAccounts
|
pathStartswith: getURLParam("path", ""),
|
||||||
? JSON.stringify({
|
|
||||||
"goauthentik.io/user/service-account__isnull": true,
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,8 +250,8 @@ export class UserListPage extends TablePage<User> {
|
||||||
description: rec.link,
|
description: rec.link,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((ex: Response) => {
|
.catch((ex: ResponseError) => {
|
||||||
ex.json().then(() => {
|
ex.response.json().then(() => {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
message: t`No recovery flow is configured.`,
|
message: t`No recovery flow is configured.`,
|
||||||
|
@ -320,31 +319,23 @@ export class UserListPage extends TablePage<User> {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderToolbarAfter(): TemplateResult {
|
renderSidebarBefore(): TemplateResult {
|
||||||
return html`
|
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
||||||
<div class="pf-c-toolbar__group pf-m-filter-group">
|
<div class="pf-c-card">
|
||||||
<div class="pf-c-toolbar__item pf-m-search-filter">
|
<div class="pf-c-card__title">${t`User folders`}</div>
|
||||||
<div class="pf-c-input-group">
|
<div class="pf-c-card__body">
|
||||||
<div class="pf-c-check">
|
${until(
|
||||||
<input
|
new CoreApi(DEFAULT_CONFIG)
|
||||||
class="pf-c-check__input"
|
.coreUsersPathsRetrieve({
|
||||||
type="checkbox"
|
search: this.search,
|
||||||
id="hide-service-accounts"
|
})
|
||||||
name="hide-service-accounts"
|
.then((paths) => {
|
||||||
?checked=${this.hideServiceAccounts}
|
return html`<ak-treeview
|
||||||
@change=${() => {
|
.items=${paths.paths}
|
||||||
this.hideServiceAccounts = !this.hideServiceAccounts;
|
path=${this.path}
|
||||||
this.page = 1;
|
></ak-treeview>`;
|
||||||
this.fetch();
|
}),
|
||||||
updateURLParams({
|
)}
|
||||||
hideServiceAccounts: this.hideServiceAccounts,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label class="pf-c-check__label" for="hide-service-accounts">
|
|
||||||
${t`Hide service-accounts`}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
FlowChallengeResponseRequest,
|
FlowChallengeResponseRequest,
|
||||||
FlowsApi,
|
FlowsApi,
|
||||||
RedirectChallenge,
|
RedirectChallenge,
|
||||||
|
ResponseError,
|
||||||
ShellChallenge,
|
ShellChallenge,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
@ -80,7 +81,7 @@ export class UserSettingsFlowExecutor extends LitElement implements StageHost {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.catch((e: Error | Response) => {
|
.catch((e: Error | ResponseError) => {
|
||||||
this.errorMessage(e);
|
this.errorMessage(e);
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
title: Release 2022.7
|
||||||
|
slug: "2022.7"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Breaking changes
|
||||||
|
|
||||||
|
- Removal of verification certificates for Machine-to-Machine authentication in OAuth 2 Provider
|
||||||
|
|
||||||
|
Instead, create an OAuth Source with the certificate configured as JWKS Data, and enable the source in the provider.
|
||||||
|
|
||||||
|
## New features
|
||||||
|
|
||||||
|
- User paths
|
||||||
|
|
||||||
|
To better organize users, they can now be assigned a path. This allows for organization of users based on sources they enrolled with/got imported from, organizational structure or any other structure.
|
||||||
|
|
||||||
|
Sources now have a path template to specify which path users created by it should be assigned. Additionally, you can set the path in the user_write stage in any flow, and it can be dynamically overwritten within a flow's context.
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
This release does not introduce any new requirements.
|
||||||
|
|
||||||
|
### docker-compose
|
||||||
|
|
||||||
|
Download the docker-compose file for 2022.7 from [here](https://goauthentik.io/version/2022.7/docker-compose.yml). Afterwards, simply run `docker-compose up -d`.
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
Update your values to use the new images:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/goauthentik/server
|
||||||
|
tag: 2022.7.1
|
||||||
|
```
|
|
@ -2,6 +2,14 @@
|
||||||
title: User
|
title: User
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Path
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Requires authentik 2022.7
|
||||||
|
:::
|
||||||
|
|
||||||
|
Paths can be used to organize users into folders depending on which source created them or organizational structure. Paths may not start or end with a slash, but they can contain any other character as path segments. The paths are currently purely used for organization, it does not affect their permissions, group memberships, or anything else.
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
### `goauthentik.io/user/token-expires`:
|
### `goauthentik.io/user/token-expires`:
|
||||||
|
|
Reference in New Issue