Merge branch 'main' into dev

* main: (38 commits)
  crypto: fix race conditions when creating self-signed certificates on startup (#7344)
  blueprints: fix entries with state: absent not being deleted if their serializer has errors (#7345)
  web/admin: fix @change handler for ak-radio elements (#7348)
  rbac: handle lookup error (#7341)
  website/docs: add warning about upgrading to 2023.10 (#7340)
  web/admin: fix role form reacting to enter (#7330)
  core: bump github.com/google/uuid from 1.3.1 to 1.4.0 (#7333)
  core: bump goauthentik.io/api/v3 from 3.2023083.10 to 3.2023101.1 (#7334)
  core: bump ruff from 0.1.2 to 0.1.3 (#7335)
  core: bump pydantic-scim from 0.0.7 to 0.0.8 (#7336)
  website/blogs: Blog dockers (#7328)
  providers/proxy: attempt to fix duplicate cookie (#7324)
  stages/email: fix sending emails from task (#7325)
  web: bump API Client version (#7321)
  website/docs: update release notes for 2023.10.1 (#7316)
  release: 2023.10.1
  lifecycle: fix otp merge migration (#7315)
  root: fix pylint errors (#7312)
  web: bump API Client version (#7311)
  release: 2023.10.0
  ...
This commit is contained in:
Ken Sternberg 2023-10-27 09:47:08 -07:00
commit 639a8ceb5a
79 changed files with 5709 additions and 3742 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2023.8.3 current_version = 2023.10.1
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -27,8 +27,10 @@ jobs:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: make empty ts client - name: make empty clients
run: mkdir -p ./gen-ts-client run: |
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
- name: Build Docker Image - name: Build Docker Image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
@ -69,6 +71,10 @@ jobs:
- name: prepare variables - name: prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
- name: make empty clients
run: |
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
- name: Docker Login Registry - name: Docker Login Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@ -93,6 +99,7 @@ jobs:
ghcr.io/goauthentik/${{ matrix.type }}:latest ghcr.io/goauthentik/${{ matrix.type }}:latest
file: ${{ matrix.type }}.Dockerfile file: ${{ matrix.type }}.Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
context: .
build-args: | build-args: |
VERSION=${{ steps.ev.outputs.version }} VERSION=${{ steps.ev.outputs.version }}
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }} VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}

View File

@ -16,6 +16,7 @@ jobs:
echo "PG_PASS=$(openssl rand -base64 32)" >> .env echo "PG_PASS=$(openssl rand -base64 32)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
docker buildx install docker buildx install
mkdir -p ./gen-ts-api
docker build -t testing:latest . docker build -t testing:latest .
echo "AUTHENTIK_IMAGE=testing" >> .env echo "AUTHENTIK_IMAGE=testing" >> .env
echo "AUTHENTIK_TAG=latest" >> .env echo "AUTHENTIK_TAG=latest" >> .env

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
from typing import Optional from typing import Optional
__version__ = "2023.8.3" __version__ = "2023.10.1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -584,12 +584,17 @@ class EntryInvalidError(SentryIgnoredException):
entry_model: Optional[str] entry_model: Optional[str]
entry_id: Optional[str] entry_id: Optional[str]
validation_error: Optional[ValidationError] validation_error: Optional[ValidationError]
serializer: Optional[Serializer] = None
def __init__(self, *args: object, validation_error: Optional[ValidationError] = None) -> None: def __init__(
self, *args: object, validation_error: Optional[ValidationError] = None, **kwargs
) -> None:
super().__init__(*args) super().__init__(*args)
self.entry_model = None self.entry_model = None
self.entry_id = None self.entry_id = None
self.validation_error = validation_error self.validation_error = validation_error
for key, value in kwargs.items():
setattr(self, key, value)
@staticmethod @staticmethod
def from_entry( def from_entry(

View File

@ -255,7 +255,10 @@ class Importer:
try: try:
full_data = self.__update_pks_for_attrs(entry.get_attrs(self._import)) full_data = self.__update_pks_for_attrs(entry.get_attrs(self._import))
except ValueError as exc: except ValueError as exc:
raise EntryInvalidError.from_entry(exc, entry) from exc raise EntryInvalidError.from_entry(
exc,
entry,
) from exc
always_merger.merge(full_data, updated_identifiers) always_merger.merge(full_data, updated_identifiers)
serializer_kwargs["data"] = full_data serializer_kwargs["data"] = full_data
@ -272,6 +275,7 @@ class Importer:
f"Serializer errors {serializer.errors}", f"Serializer errors {serializer.errors}",
validation_error=exc, validation_error=exc,
entry=entry, entry=entry,
serializer=serializer,
) from exc ) from exc
return serializer return serializer
@ -300,16 +304,18 @@ class Importer:
) )
return False return False
# Validate each single entry # Validate each single entry
serializer = None
try: try:
serializer = self._validate_single(entry) serializer = self._validate_single(entry)
except EntryInvalidError as exc: except EntryInvalidError as exc:
# For deleting objects we don't need the serializer to be valid # For deleting objects we don't need the serializer to be valid
if entry.get_state(self._import) == BlueprintEntryDesiredState.ABSENT: if entry.get_state(self._import) == BlueprintEntryDesiredState.ABSENT:
continue serializer = exc.serializer
self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc) else:
if raise_errors: self.logger.warning(f"entry invalid: {exc}", entry=entry, error=exc)
raise exc if raise_errors:
return False raise exc
return False
if not serializer: if not serializer:
continue continue

View File

@ -82,7 +82,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
path = Path(event.src_path) path = Path(event.src_path)
root = Path(CONFIG.get("blueprints_dir")).absolute() root = Path(CONFIG.get("blueprints_dir")).absolute()
rel_path = str(path.relative_to(root)) rel_path = str(path.relative_to(root))
for instance in BlueprintInstance.objects.filter(path=rel_path): for instance in BlueprintInstance.objects.filter(path=rel_path, enabled=True):
LOGGER.debug("modified blueprint file, starting apply", instance=instance) LOGGER.debug("modified blueprint file, starting apply", instance=instance)
apply_blueprint.delay(instance.pk.hex) apply_blueprint.delay(instance.pk.hex)

View File

@ -98,6 +98,7 @@ class ApplicationSerializer(ModelSerializer):
class ApplicationViewSet(UsedByMixin, ModelViewSet): class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Application Viewset""" """Application Viewset"""
# pylint: disable=no-member
queryset = Application.objects.all().prefetch_related("provider") queryset = Application.objects.all().prefetch_related("provider")
serializer_class = ApplicationSerializer serializer_class = ApplicationSerializer
search_fields = [ search_fields = [

View File

@ -139,6 +139,7 @@ class UserAccountSerializer(PassiveSerializer):
class GroupViewSet(UsedByMixin, ModelViewSet): class GroupViewSet(UsedByMixin, ModelViewSet):
"""Group Viewset""" """Group Viewset"""
# pylint: disable=no-member
queryset = Group.objects.all().select_related("parent").prefetch_related("users") queryset = Group.objects.all().select_related("parent").prefetch_related("users")
serializer_class = GroupSerializer serializer_class = GroupSerializer
search_fields = ["name", "is_superuser"] search_fields = ["name", "is_superuser"]

View File

@ -97,6 +97,7 @@ class SourceFlowManager:
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
new_connection.user = self.request.user new_connection.user = self.request.user
new_connection = self.update_connection(new_connection, **kwargs) new_connection = self.update_connection(new_connection, **kwargs)
# pylint: disable=no-member
new_connection.save() new_connection.save()
return Action.LINK, new_connection return Action.LINK, new_connection

View File

@ -1,13 +1,10 @@
"""authentik crypto app config""" """authentik crypto app config"""
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, Optional from typing import Optional
from authentik.blueprints.apps import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
if TYPE_CHECKING:
from authentik.crypto.models import CertificateKeyPair
MANAGED_KEY = "goauthentik.io/crypto/jwt-managed" MANAGED_KEY = "goauthentik.io/crypto/jwt-managed"
@ -23,33 +20,37 @@ class AuthentikCryptoConfig(ManagedAppConfig):
"""Load crypto tasks""" """Load crypto tasks"""
self.import_module("authentik.crypto.tasks") self.import_module("authentik.crypto.tasks")
def _create_update_cert(self, cert: Optional["CertificateKeyPair"] = None): def _create_update_cert(self):
from authentik.crypto.builder import CertificateBuilder from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
builder = CertificateBuilder("authentik Internal JWT Certificate") common_name = "authentik Internal JWT Certificate"
builder = CertificateBuilder(common_name)
builder.build( builder.build(
subject_alt_names=["goauthentik.io"], subject_alt_names=["goauthentik.io"],
validity_days=360, validity_days=360,
) )
if not cert: CertificateKeyPair.objects.update_or_create(
cert = CertificateKeyPair() managed=MANAGED_KEY,
builder.cert = cert defaults={
builder.cert.managed = MANAGED_KEY "name": common_name,
builder.save() "certificate_data": builder.certificate,
"key_data": builder.private_key,
},
)
def reconcile_managed_jwt_cert(self): def reconcile_managed_jwt_cert(self):
"""Ensure managed JWT certificate""" """Ensure managed JWT certificate"""
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
certs = CertificateKeyPair.objects.filter(managed=MANAGED_KEY) cert: Optional[CertificateKeyPair] = CertificateKeyPair.objects.filter(
if not certs.exists(): managed=MANAGED_KEY
self._create_update_cert() ).first()
return
cert: CertificateKeyPair = certs.first()
now = datetime.now() now = datetime.now()
if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after: if not cert or (
self._create_update_cert(cert) now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after
):
self._create_update_cert()
def reconcile_self_signed(self): def reconcile_self_signed(self):
"""Create self-signed keypair""" """Create self-signed keypair"""
@ -61,4 +62,10 @@ class AuthentikCryptoConfig(ManagedAppConfig):
return return
builder = CertificateBuilder(name) builder = CertificateBuilder(name)
builder.build(subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"]) builder.build(subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"])
builder.save() CertificateKeyPair.objects.get_or_create(
name=name,
defaults={
"certificate_data": builder.certificate,
"key_data": builder.private_key,
},
)

View File

@ -32,13 +32,19 @@ class PermissionSerializer(ModelSerializer):
def get_app_label_verbose(self, instance: Permission) -> str: def get_app_label_verbose(self, instance: Permission) -> str:
"""Human-readable app label""" """Human-readable app label"""
return apps.get_app_config(instance.content_type.app_label).verbose_name try:
return apps.get_app_config(instance.content_type.app_label).verbose_name
except LookupError:
return f"{instance.content_type.app_label}.{instance.content_type.model}"
def get_model_verbose(self, instance: Permission) -> str: def get_model_verbose(self, instance: Permission) -> str:
"""Human-readable model name""" """Human-readable model name"""
return apps.get_model( try:
instance.content_type.app_label, instance.content_type.model return apps.get_model(
)._meta.verbose_name instance.content_type.app_label, instance.content_type.model
)._meta.verbose_name
except LookupError:
return f"{instance.content_type.app_label}.{instance.content_type.model}"
class Meta: class Meta:
model = Permission model = Permission

View File

@ -28,9 +28,12 @@ class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
def get_model_verbose(self, instance: GroupObjectPermission) -> str: def get_model_verbose(self, instance: GroupObjectPermission) -> str:
"""Get model label from permission's model""" """Get model label from permission's model"""
return apps.get_model( try:
instance.content_type.app_label, instance.content_type.model return apps.get_model(
)._meta.verbose_name instance.content_type.app_label, instance.content_type.model
)._meta.verbose_name
except LookupError:
return f"{instance.content_type.app_label}.{instance.content_type.model}"
def get_object_description(self, instance: GroupObjectPermission) -> Optional[str]: def get_object_description(self, instance: GroupObjectPermission) -> Optional[str]:
"""Get model description from attached model. This operation takes at least """Get model description from attached model. This operation takes at least
@ -38,7 +41,10 @@ class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
view_ permission on the object""" view_ permission on the object"""
app_label = instance.content_type.app_label app_label = instance.content_type.app_label
model = instance.content_type.model model = instance.content_type.model
model_class = apps.get_model(app_label, model) try:
model_class = apps.get_model(app_label, model)
except LookupError:
return None
objects = get_objects_for_group(instance.group, f"{app_label}.view_{model}", model_class) objects = get_objects_for_group(instance.group, f"{app_label}.view_{model}", model_class)
obj = objects.first() obj = objects.first()
if not obj: if not obj:

View File

@ -28,9 +28,12 @@ class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
def get_model_verbose(self, instance: UserObjectPermission) -> str: def get_model_verbose(self, instance: UserObjectPermission) -> str:
"""Get model label from permission's model""" """Get model label from permission's model"""
return apps.get_model( try:
instance.content_type.app_label, instance.content_type.model return apps.get_model(
)._meta.verbose_name instance.content_type.app_label, instance.content_type.model
)._meta.verbose_name
except LookupError:
return f"{instance.content_type.app_label}.{instance.content_type.model}"
def get_object_description(self, instance: UserObjectPermission) -> Optional[str]: def get_object_description(self, instance: UserObjectPermission) -> Optional[str]:
"""Get model description from attached model. This operation takes at least """Get model description from attached model. This operation takes at least
@ -38,7 +41,10 @@ class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
view_ permission on the object""" view_ permission on the object"""
app_label = instance.content_type.app_label app_label = instance.content_type.app_label
model = instance.content_type.model model = instance.content_type.model
model_class = apps.get_model(app_label, model) try:
model_class = apps.get_model(app_label, model)
except LookupError:
return None
objects = get_objects_for_user(instance.user, f"{app_label}.view_{model}", model_class) objects = get_objects_for_user(instance.user, f"{app_label}.view_{model}", model_class)
obj = objects.first() obj = objects.first()
if not obj: if not obj:

View File

@ -13,6 +13,7 @@ from authentik.events.models import Event, EventAction
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.utils import logo_data
LOGGER = get_logger() LOGGER = get_logger()
@ -81,6 +82,10 @@ def send_mail(self: MonitoredTask, message: dict[Any, Any], email_stage_pk: Opti
# Because we use the Message-ID as UID for the task, manually assign it # Because we use the Message-ID as UID for the task, manually assign it
message_object.extra_headers["Message-ID"] = message_id message_object.extra_headers["Message-ID"] = message_id
# Add the logo (we can't add it in the previous message since MIMEImage
# can't be converted to json)
message_object.attach(logo_data())
LOGGER.debug("Sending mail", to=message_object.to) LOGGER.debug("Sending mail", to=message_object.to)
backend.send_messages([message_object]) backend.send_messages([message_object])
Event.new( Event.new(

View File

@ -1,6 +1,7 @@
"""email utils""" """email utils"""
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
from functools import lru_cache from functools import lru_cache
from pathlib import Path
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -8,9 +9,12 @@ from django.utils import translation
@lru_cache() @lru_cache()
def logo_data(): def logo_data() -> MIMEImage:
"""Get logo as MIME Image for emails""" """Get logo as MIME Image for emails"""
with open("web/icons/icon_left_brand.png", "rb") as _logo_file: path = Path("web/icons/icon_left_brand.png")
if not path.exists():
path = Path("web/dist/assets/icons/icon_left_brand.png")
with open(path, "rb") as _logo_file:
logo = MIMEImage(_logo_file.read()) logo = MIMEImage(_logo_file.read())
logo.add_header("Content-ID", "logo.png") logo.add_header("Content-ID", "logo.png")
return logo return logo
@ -25,5 +29,4 @@ class TemplateEmailMessage(EmailMultiAlternatives):
super().__init__(**kwargs) super().__init__(**kwargs)
self.content_subtype = "html" self.content_subtype = "html"
self.mixed_subtype = "related" self.mixed_subtype = "related"
self.attach(logo_data())
self.attach_alternative(html_content, "text/html") self.attach_alternative(html_content, "text/html")

View File

@ -15,6 +15,7 @@ class UserWriteStageSerializer(StageSerializer):
"user_creation_mode", "user_creation_mode",
"create_users_as_inactive", "create_users_as_inactive",
"create_users_group", "create_users_group",
"user_type",
"user_path_template", "user_path_template",
] ]

View File

@ -0,0 +1,25 @@
# Generated by Django 4.2.6 on 2023-10-25 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_user_write", "0007_remove_userwritestage_can_create_users_and_more"),
]
operations = [
migrations.AddField(
model_name="userwritestage",
name="user_type",
field=models.TextField(
choices=[
("internal", "Internal"),
("external", "External"),
("service_account", "Service Account"),
("internal_service_account", "Internal Service Account"),
],
default="external",
),
),
]

View File

@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
from authentik.core.models import Group from authentik.core.models import Group, UserTypes
from authentik.flows.models import Stage from authentik.flows.models import Stage
@ -39,6 +39,10 @@ class UserWriteStage(Stage):
help_text=_("Optionally add newly created users to this group."), help_text=_("Optionally add newly created users to this group."),
) )
user_type = models.TextField(
choices=UserTypes.choices,
default=UserTypes.EXTERNAL,
)
user_path_template = models.TextField( user_path_template = models.TextField(
default="", default="",
blank=True, blank=True,

View File

@ -9,7 +9,7 @@ from django.utils.translation import gettext as _
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER
from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
@ -22,6 +22,7 @@ from authentik.stages.user_write.models import UserCreationMode
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_TYPE = "user_type"
PLAN_CONTEXT_USER_PATH = "user_path" PLAN_CONTEXT_USER_PATH = "user_path"
@ -55,6 +56,19 @@ class UserWriteStageView(StageView):
) )
if path == "": if path == "":
path = User.default_path() path = User.default_path()
try:
user_type = UserTypes(
self.executor.plan.context.get(
PLAN_CONTEXT_USER_TYPE,
self.executor.current_stage.user_type,
)
)
except ValueError:
user_type = self.executor.current_stage.user_type
if user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
user_type = UserTypes.SERVICE_ACCOUNT
if not self.request.user.is_anonymous: if not self.request.user.is_anonymous:
self.executor.plan.context.setdefault(PLAN_CONTEXT_PENDING_USER, self.request.user) self.executor.plan.context.setdefault(PLAN_CONTEXT_PENDING_USER, self.request.user)
if ( if (
@ -66,6 +80,7 @@ class UserWriteStageView(StageView):
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, path=path,
type=user_type,
) )
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(

View File

@ -8368,6 +8368,16 @@
"title": "Create users group", "title": "Create users group",
"description": "Optionally add newly created users to this group." "description": "Optionally add newly created users to this group."
}, },
"user_type": {
"type": "string",
"enum": [
"internal",
"external",
"service_account",
"internal_service_account"
],
"title": "User type"
},
"user_path_template": { "user_path_template": {
"type": "string", "type": "string",
"title": "User path template" "title": "User path template"

View File

@ -32,7 +32,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.8.3} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.1}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -53,7 +53,7 @@ services:
- postgresql - postgresql
- redis - redis
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.8.3} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.1}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

4
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/go-openapi/runtime v0.26.0 github.com/go-openapi/runtime v0.26.0
github.com/go-openapi/strfmt v0.21.7 github.com/go-openapi/strfmt v0.21.7
github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.3.1 github.com/google/uuid v1.4.0
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.1
@ -27,7 +27,7 @@ require (
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
goauthentik.io/api/v3 v3.2023083.10 goauthentik.io/api/v3 v3.2023101.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.13.0 golang.org/x/oauth2 v0.13.0
golang.org/x/sync v0.4.0 golang.org/x/sync v0.4.0

7
go.sum
View File

@ -211,8 +211,9 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
@ -355,8 +356,8 @@ go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyK
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2023083.10 h1:mMCOfsqjouSSxedSkCK4k0Cwtt68CWzQgR7Um6ooOQs= goauthentik.io/api/v3 v3.2023101.1 h1:KIQ4wmxjE+geAVB0wBfmxW9Uzo/tA0dbd2hSUJ7YJ3M=
goauthentik.io/api/v3 v3.2023083.10/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2023101.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=

View File

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

View File

@ -50,7 +50,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
Domain: *p.CookieDomain, Domain: *p.CookieDomain,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
MaxAge: maxAge, MaxAge: maxAge,
Path: externalHost.Path, Path: "/",
}) })
a.log.Trace("using redis session backend") a.log.Trace("using redis session backend")

View File

@ -2,6 +2,7 @@
from lifecycle.migrate import BaseMigration from lifecycle.migrate import BaseMigration
SQL_STATEMENT = """ SQL_STATEMENT = """
BEGIN TRANSACTION;
DELETE FROM django_migrations WHERE app = 'otp_static'; DELETE FROM django_migrations WHERE app = 'otp_static';
DELETE FROM django_migrations WHERE app = 'otp_totp'; DELETE FROM django_migrations WHERE app = 'otp_totp';
-- Rename tables (static) -- Rename tables (static)
@ -12,6 +13,7 @@ ALTER SEQUENCE otp_static_staticdevice_id_seq RENAME TO authentik_stages_authent
-- Rename tables (totp) -- Rename tables (totp)
ALTER TABLE otp_totp_totpdevice RENAME TO authentik_stages_authenticator_totp_totpdevice; ALTER TABLE otp_totp_totpdevice RENAME TO authentik_stages_authenticator_totp_totpdevice;
ALTER SEQUENCE otp_totp_totpdevice_id_seq RENAME TO authentik_stages_authenticator_totp_totpdevice_id_seq; ALTER SEQUENCE otp_totp_totpdevice_id_seq RENAME TO authentik_stages_authenticator_totp_totpdevice_id_seq;
COMMIT;
""" """

50
poetry.lock generated
View File

@ -1359,13 +1359,13 @@ files = [
[[package]] [[package]]
name = "duo-client" name = "duo-client"
version = "5.1.0" version = "5.2.0"
description = "Reference client for Duo Security APIs" description = "Reference client for Duo Security APIs"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "duo_client-5.1.0-py2.py3-none-any.whl", hash = "sha256:5dd6e7a526ea79952c078e5a5be93a1d70d36e685fad9478188156587e85b571"}, {file = "duo_client-5.2.0-py3-none-any.whl", hash = "sha256:da3237e34300665c40ba5215f1e6656fec1a0136295917541aa973e7fcbf027e"},
{file = "duo_client-5.1.0.tar.gz", hash = "sha256:0dd8b7223a105beca4fdbfa71d400e813d9f33250c3da5fd63e437fb571b55f2"}, {file = "duo_client-5.2.0.tar.gz", hash = "sha256:f82361740792b06303f9721e7ba593916080461769396b4f73c0502c0bfcee44"},
] ]
[package.dependencies] [package.dependencies]
@ -2780,13 +2780,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]] [[package]]
name = "pydantic-scim" name = "pydantic-scim"
version = "0.0.7" version = "0.0.8"
description = "Pydantic types for SCIM" description = "Pydantic types for SCIM"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "pydantic-scim-0.0.7.tar.gz", hash = "sha256:bc043da51c346051dfd372f12d1837c0846b815236340156d663a8514cba5761"}, {file = "pydantic-scim-0.0.8.tar.gz", hash = "sha256:b6c62031126e8c54f0fc7df837678e63934a5b068533fc52e5dfb6cfc24d59e9"},
{file = "pydantic_scim-0.0.7-py3-none-any.whl", hash = "sha256:058eb195f75ef32d04eaf6369c125d5fb7052891694686f8e55e04d184ab1360"}, {file = "pydantic_scim-0.0.8-py3-none-any.whl", hash = "sha256:407b3bf55240947155c77a6dd839881d63368c61d64076d6b167ef124ceac79a"},
] ]
[package.dependencies] [package.dependencies]
@ -3374,28 +3374,28 @@ pyasn1 = ">=0.1.3"
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.1.2" version = "0.1.3"
description = "An extremely fast Python linter, written in Rust." description = "An extremely fast Python linter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.1.2-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:0d3ee66b825b713611f89aa35d16de984f76f26c50982a25d52cd0910dff3923"}, {file = "ruff-0.1.3-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b46d43d51f7061652eeadb426a9e3caa1e0002470229ab2fc19de8a7b0766901"},
{file = "ruff-0.1.2-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f85f850a320ff532b8f93e8d1da6a36ef03698c446357c8c43b46ef90bb321eb"}, {file = "ruff-0.1.3-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b8afeb9abd26b4029c72adc9921b8363374f4e7edb78385ffaa80278313a15f9"},
{file = "ruff-0.1.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:809c6d4e45683696d19ca79e4c6bd3b2e9204fe9546923f2eb3b126ec314b0dc"}, {file = "ruff-0.1.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca3cf365bf32e9ba7e6db3f48a4d3e2c446cd19ebee04f05338bc3910114528b"},
{file = "ruff-0.1.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46005e4abb268e93cad065244e17e2ea16b6fcb55a5c473f34fbc1fd01ae34cb"}, {file = "ruff-0.1.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4874c165f96c14a00590dcc727a04dca0cfd110334c24b039458c06cf78a672e"},
{file = "ruff-0.1.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10cdb302f519664d5e2cf954562ac86c9d20ca05855e5b5c2f9d542228f45da4"}, {file = "ruff-0.1.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eec2dd31eed114e48ea42dbffc443e9b7221976554a504767ceaee3dd38edeb8"},
{file = "ruff-0.1.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f89ebcbe57a1eab7d7b4ceb57ddf0af9ed13eae24e443a7c1dc078000bd8cc6b"}, {file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dc3ec4edb3b73f21b4aa51337e16674c752f1d76a4a543af56d7d04e97769613"},
{file = "ruff-0.1.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7344eaca057d4c32373c9c3a7afb7274f56040c225b6193dd495fcf69453b436"}, {file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e3de9ed2e39160800281848ff4670e1698037ca039bda7b9274f849258d26ce"},
{file = "ruff-0.1.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dffa25f6e03c4950b6ac6f216bc0f98a4be9719cb0c5260c8e88d1bac36f1683"}, {file = "ruff-0.1.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c595193881922cc0556a90f3af99b1c5681f0c552e7a2a189956141d8666fe8"},
{file = "ruff-0.1.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42ddaea52cb7ba7c785e8593a7532866c193bc774fe570f0e4b1ccedd95b83c5"}, {file = "ruff-0.1.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f75e670d529aa2288cd00fc0e9b9287603d95e1536d7a7e0cafe00f75e0dd9d"},
{file = "ruff-0.1.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8533efda625bbec0bf27da2886bd641dae0c209104f6c39abc4be5b7b22de2a"}, {file = "ruff-0.1.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76dd49f6cd945d82d9d4a9a6622c54a994689d8d7b22fa1322983389b4892e20"},
{file = "ruff-0.1.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b0b1b82221ba7c50e03b7a86b983157b5d3f4d8d4f16728132bdf02c6d651f77"}, {file = "ruff-0.1.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:918b454bc4f8874a616f0d725590277c42949431ceb303950e87fef7a7d94cb3"},
{file = "ruff-0.1.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c1362eb9288f8cc95535294cb03bd4665c8cef86ec32745476a4e5c6817034c"}, {file = "ruff-0.1.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8859605e729cd5e53aa38275568dbbdb4fe882d2ea2714c5453b678dca83784"},
{file = "ruff-0.1.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ffa7ef5ded0563329a35bd5a1cfdae40f05a75c0cc2dd30f00b1320b1fb461fc"}, {file = "ruff-0.1.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0b6c55f5ef8d9dd05b230bb6ab80bc4381ecb60ae56db0330f660ea240cb0d4a"},
{file = "ruff-0.1.2-py3-none-win32.whl", hash = "sha256:6e8073f85e47072256e2e1909f1ae515cf61ff5a4d24730a63b8b4ac24b6704a"}, {file = "ruff-0.1.3-py3-none-win32.whl", hash = "sha256:3e7afcbdcfbe3399c34e0f6370c30f6e529193c731b885316c5a09c9e4317eef"},
{file = "ruff-0.1.2-py3-none-win_amd64.whl", hash = "sha256:b836ddff662a45385948ee0878b0a04c3a260949905ad861a37b931d6ee1c210"}, {file = "ruff-0.1.3-py3-none-win_amd64.whl", hash = "sha256:7a18df6638cec4a5bd75350639b2bb2a2366e01222825562c7346674bdceb7ea"},
{file = "ruff-0.1.2-py3-none-win_arm64.whl", hash = "sha256:b0c42d00db5639dbd5f7f9923c63648682dd197bf5de1151b595160c96172691"}, {file = "ruff-0.1.3-py3-none-win_arm64.whl", hash = "sha256:12fd53696c83a194a2db7f9a46337ce06445fb9aa7d25ea6f293cf75b21aca9f"},
{file = "ruff-0.1.2.tar.gz", hash = "sha256:afd4785ae060ce6edcd52436d0c197628a918d6d09e3107a892a1bad6a4c6608"}, {file = "ruff-0.1.3.tar.gz", hash = "sha256:3ba6145369a151401d5db79f0a47d50e470384d0d89d0d6f7fab0b589ad07c34"},
] ]
[[package]] [[package]]
@ -4332,4 +4332,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "e6b1df989cb5c50609540c1229d05d8458ef1cc343fb5868402db8b7679ad73c" content-hash = "2fc746976187f4674f04575cffd6a367744723bf78c356b6951c2370bc47ceae"

View File

@ -113,7 +113,7 @@ filterwarnings = [
[tool.poetry] [tool.poetry]
name = "authentik" name = "authentik"
version = "2023.8.3" version = "2023.10.1"
description = "" description = ""
authors = ["authentik Team <hello@goauthentik.io>"] authors = ["authentik Team <hello@goauthentik.io>"]
@ -152,7 +152,7 @@ paramiko = "*"
psycopg = { extras = ["c"], version = "*" } psycopg = { extras = ["c"], version = "*" }
pycryptodome = "*" pycryptodome = "*"
pydantic = "<3.0.0" pydantic = "<3.0.0"
pydantic-scim = "^0.0.7" pydantic-scim = "^0.0.8"
pyjwt = "*" pyjwt = "*"
python = "^3.11" python = "^3.11"
pyyaml = "*" pyyaml = "*"

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2023.8.3 version: 2023.10.1
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io
@ -27494,6 +27494,20 @@ paths:
name: user_path_template name: user_path_template
schema: schema:
type: string type: string
- in: query
name: user_type
schema:
type: string
enum:
- external
- internal
- internal_service_account
- service_account
description: |-
* `internal` - Internal
* `external` - External
* `service_account` - Service Account
* `internal_service_account` - Internal Service Account
tags: tags:
- stages - stages
security: security:
@ -38052,6 +38066,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_type:
$ref: '#/components/schemas/UserTypeEnum'
user_path_template: user_path_template:
type: string type: string
PatchedWebAuthnDeviceRequest: PatchedWebAuthnDeviceRequest:
@ -42422,6 +42438,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_type:
$ref: '#/components/schemas/UserTypeEnum'
user_path_template: user_path_template:
type: string type: string
required: required:
@ -42452,6 +42470,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_type:
$ref: '#/components/schemas/UserTypeEnum'
user_path_template: user_path_template:
type: string type: string
required: required:

1982
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,15 +38,15 @@
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.0", "@formatjs/intl-listformat": "^7.5.0",
"@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-free": "^6.4.2",
"@goauthentik/api": "^2023.8.3-1697813667", "@goauthentik/api": "^2023.10.1-1698348102",
"@lit-labs/context": "^0.4.1", "@lit-labs/context": "^0.4.1",
"@lit-labs/task": "^3.1.0", "@lit-labs/task": "^3.1.0",
"@lit/localize": "^0.11.4", "@lit/localize": "^0.11.4",
"@open-wc/lit-helpers": "^0.6.0", "@open-wc/lit-helpers": "^0.6.0",
"@patternfly/elements": "^2.4.0", "@patternfly/elements": "^2.4.0",
"@patternfly/patternfly": "^4.224.2", "@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.75.0", "@sentry/browser": "^7.75.1",
"@sentry/tracing": "^7.75.0", "@sentry/tracing": "^7.75.1",
"@webcomponents/webcomponentsjs": "^2.8.0", "@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"chart.js": "^4.4.0", "chart.js": "^4.4.0",
@ -55,9 +55,9 @@
"construct-style-sheets-polyfill": "^3.1.0", "construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.33.1", "core-js": "^3.33.1",
"country-flag-icons": "^1.5.7", "country-flag-icons": "^1.5.7",
"fuse.js": "^6.6.2", "fuse.js": "^7.0.0",
"lit": "^2.8.0", "lit": "^2.8.0",
"mermaid": "^10.5.1", "mermaid": "^10.6.0",
"rapidoc": "^9.3.4", "rapidoc": "^9.3.4",
"style-mod": "^4.1.0", "style-mod": "^4.1.0",
"webcomponent-qr-code": "^1.2.0", "webcomponent-qr-code": "^1.2.0",
@ -102,7 +102,7 @@
"eslint-plugin-lit": "^1.10.1", "eslint-plugin-lit": "^1.10.1",
"eslint-plugin-sonarjs": "^0.21.0", "eslint-plugin-sonarjs": "^0.21.0",
"eslint-plugin-storybook": "^0.6.15", "eslint-plugin-storybook": "^0.6.15",
"lit-analyzer": "^1.2.1", "lit-analyzer": "^2.0.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"pseudolocale": "^2.0.0", "pseudolocale": "^2.0.0",
@ -115,7 +115,7 @@
"rollup-plugin-postcss-lit": "^2.1.0", "rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^7.5.1", "storybook": "^7.5.1",
"storybook-addon-mock": "^4.3.0", "storybook-addon-mock": "^4.3.0",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^2.0.0",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"turnstile-types": "^1.1.3", "turnstile-types": "^1.1.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",

View File

@ -114,8 +114,8 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
label=${msg("Client type")} label=${msg("Client type")}
.value=${provider?.clientType} .value=${provider?.clientType}
required required
@change=${(ev: CustomEvent<ClientTypeEnum>) => { @change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
this.showClientSecret = ev.detail !== ClientTypeEnum.Public; this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
}} }}
.options=${clientTypeOptions} .options=${clientTypeOptions}
> >

View File

@ -78,8 +78,8 @@ export class TransportForm extends ModelForm<NotificationTransport, string> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Mode")} ?required=${true} name="mode"> <ak-form-element-horizontal label=${msg("Mode")} ?required=${true} name="mode">
<ak-radio <ak-radio
@change=${(ev: CustomEvent<NotificationTransportModeEnum>) => { @change=${(ev: CustomEvent<{ value: NotificationTransportModeEnum }>) => {
this.onModeChange(ev.detail); this.onModeChange(ev.detail.value);
}} }}
.options=${[ .options=${[
{ {

View File

@ -210,8 +210,8 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
label=${msg("Client type")} label=${msg("Client type")}
.value=${provider?.clientType} .value=${provider?.clientType}
required required
@change=${(ev: CustomEvent<ClientTypeEnum>) => { @change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
this.showClientSecret = ev.detail !== ClientTypeEnum.Public; this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
}} }}
.options=${clientTypeOptions} .options=${clientTypeOptions}
> >

View File

@ -42,15 +42,13 @@ export class RoleForm extends ModelForm<Role, string> {
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name"> <input
<input type="text"
type="text" value="${ifDefined(this.instance?.name)}"
value="${ifDefined(this.instance?.name)}" class="pf-c-form-control"
class="pf-c-form-control" required
required />
/> </ak-form-element-horizontal>`;
</ak-form-element-horizontal>
</form>`;
} }
} }

View File

@ -10,8 +10,11 @@ import { TablePage } from "@goauthentik/elements/table/TablePage";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { CSSResult, TemplateResult, html } 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 PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import { RbacApi, Role } from "@goauthentik/api"; import { RbacApi, Role } from "@goauthentik/api";
@ -34,6 +37,10 @@ export class RoleListPage extends TablePage<Role> {
@property() @property()
order = "name"; order = "name";
static get styles(): CSSResult[] {
return [...super.styles, PFBanner];
}
async apiEndpoint(page: number): Promise<PaginatedResponse<Role>> { async apiEndpoint(page: number): Promise<PaginatedResponse<Role>> {
return new RbacApi(DEFAULT_CONFIG).rbacRolesList({ return new RbacApi(DEFAULT_CONFIG).rbacRolesList({
ordering: this.order, ordering: this.order,
@ -69,6 +76,22 @@ export class RoleListPage extends TablePage<Role> {
</ak-forms-delete-bulk>`; </ak-forms-delete-bulk>`;
} }
render(): TemplateResult {
return html`<ak-page-header
icon=${this.pageIcon()}
header=${this.pageTitle()}
description=${ifDefined(this.pageDescription())}
>
</ak-page-header>
<div class="pf-c-banner pf-m-info">
${msg("RBAC is in preview.")}
<a href="mailto:hello@goauthentik.io">${msg("Send us feedback!")}</a>
</div>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">${this.renderTable()}</div>
</section>`;
}
row(item: Role): TemplateResult[] { row(item: Role): TemplateResult[] {
return [ return [
html`<a href="#/identity/roles/${item.pk}">${item.name}</a>`, html`<a href="#/identity/roles/${item.pk}">${item.name}</a>`,

View File

@ -15,6 +15,7 @@ import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
@ -52,6 +53,7 @@ export class RoleViewPage extends AKElement {
PFContent, PFContent,
PFCard, PFCard,
PFDescriptionList, PFDescriptionList,
PFBanner,
css` css`
.pf-c-description-list__description ak-action-button { .pf-c-description-list__description ak-action-button {
margin-right: 6px; margin-right: 6px;
@ -85,60 +87,69 @@ export class RoleViewPage extends AKElement {
if (!this._role) { if (!this._role) {
return html``; return html``;
} }
return html`<ak-tabs> return html`<div class="pf-c-banner pf-m-info">
<section ${msg("RBAC is in preview.")}
slot="page-overview" <a href="mailto:hello@goauthentik.io">${msg("Send us feedback!")}</a>
data-tab-title="${msg("Overview")}" </div>
class="pf-c-page__main-section pf-m-no-padding-mobile" <ak-tabs>
> <section
<div class="pf-l-grid pf-m-gutter"> slot="page-overview"
<div data-tab-title="${msg("Overview")}"
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-3-col-on-xl pf-m-3-col-on-2xl" class="pf-c-page__main-section pf-m-no-padding-mobile"
> >
<div class="pf-c-card__title">${msg("Role Info")}</div> <div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card__body"> <div
<dl class="pf-c-description-list"> class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-3-col-on-xl pf-m-3-col-on-2xl"
<div class="pf-c-description-list__group"> >
<dt class="pf-c-description-list__term"> <div class="pf-c-card__title">${msg("Role Info")}</div>
<span class="pf-c-description-list__text" <div class="pf-c-card__body">
>${msg("Name")}</span <dl class="pf-c-description-list">
> <div class="pf-c-description-list__group">
</dt> <dt class="pf-c-description-list__term">
<dd class="pf-c-description-list__description"> <span class="pf-c-description-list__text"
<div class="pf-c-description-list__text"> >${msg("Name")}</span
${this._role.name} >
</div> </dt>
</dd> <dd class="pf-c-description-list__description">
</div> <div class="pf-c-description-list__text">
</dl> ${this._role.name}
</div>
</dd>
</div>
</dl>
</div>
</div>
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-9-col-on-xl pf-m-9-col-on-2xl"
>
<div class="pf-c-card__title">
${msg("Assigned global permissions")}
</div>
<div class="pf-c-card__body">
<ak-role-permissions-global-table
roleUuid=${this._role.pk}
></ak-role-permissions-global-table>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">
${msg("Assigned object permissions")}
</div>
<div class="pf-c-card__body">
<ak-role-permissions-object-table
roleUuid=${this._role.pk}
></ak-role-permissions-object-table>
</div>
</div> </div>
</div> </div>
<div </section>
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-9-col-on-xl pf-m-9-col-on-2xl" <ak-rbac-object-permission-page
> slot="page-permissions"
<div class="pf-c-card__title">${msg("Assigned global permissions")}</div> data-tab-title="${msg("Permissions")}"
<div class="pf-c-card__body"> model=${RbacPermissionsAssignedByUsersListModelEnum.RbacRole}
<ak-role-permissions-global-table objectPk=${this._role.pk}
roleUuid=${this._role.pk} .showBanner=${false}
></ak-role-permissions-global-table> ></ak-rbac-object-permission-page>
</div> </ak-tabs>`;
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${msg("Assigned object permissions")}</div>
<div class="pf-c-card__body">
<ak-role-permissions-object-table
roleUuid=${this._role.pk}
></ak-role-permissions-object-table>
</div>
</div>
</div>
</section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.RbacRole}
objectPk=${this._role.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`;
} }
} }

View File

@ -12,7 +12,14 @@ 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 { CoreApi, CoreGroupsListRequest, Group, StagesApi, UserWriteStage } from "@goauthentik/api"; import {
CoreApi,
CoreGroupsListRequest,
Group,
StagesApi,
UserTypeEnum,
UserWriteStage,
} from "@goauthentik/api";
@customElement("ak-stage-user-write-form") @customElement("ak-stage-user-write-form")
export class UserWriteStageForm extends ModelForm<UserWriteStage, string> { export class UserWriteStageForm extends ModelForm<UserWriteStage, string> {
@ -111,6 +118,42 @@ export class UserWriteStageForm extends ModelForm<UserWriteStage, string> {
${msg("Mark newly created users as inactive.")} ${msg("Mark newly created users as inactive.")}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("User path template")}
name="userPathTemplate"
>
<ak-radio
.options=${[
{
label: "Internal",
value: UserTypeEnum.Internal,
default: true,
description: html`${msg(
"Internal users might be users such as company employees, which will get access to the full Enterprise feature set.",
)}`,
},
{
label: "External",
value: UserTypeEnum.External,
description: html`${msg(
"External users might be external consultants or B2C customers. These users don't get access to enterprise features.",
)}`,
},
{
label: "Service account",
value: UserTypeEnum.ServiceAccount,
description: html`${msg(
"Service accounts should be used for machine-to-machine authentication or other automations.",
)}`,
},
]}
.value=${this.instance?.userType}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg("User type used for newly created users.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User path template")} label=${msg("User path template")}
name="userPathTemplate" name="userPathTemplate"

View File

@ -1,5 +1,4 @@
import "@goauthentik/admin/users/GroupSelectModal"; import "@goauthentik/admin/users/GroupSelectModal";
import { UserTypeEnum } from "@goauthentik/api/dist/models/UserTypeEnum";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
@ -14,7 +13,7 @@ import { CSSResult, TemplateResult, css, 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 { CoreApi, User } from "@goauthentik/api"; import { CoreApi, User, UserTypeEnum } from "@goauthentik/api";
@customElement("ak-user-form") @customElement("ak-user-form")
export class UserForm extends ModelForm<User, number> { export class UserForm extends ModelForm<User, number> {

View File

@ -33,6 +33,7 @@ import { msg, str } from "@lit/localize";
import { css, html, nothing } from "lit"; import { css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
@ -86,6 +87,7 @@ export class UserViewPage extends AKElement {
PFCard, PFCard,
PFDescriptionList, PFDescriptionList,
PFSizing, PFSizing,
PFBanner,
css` css`
.ak-button-collection { .ak-button-collection {
display: flex; display: flex;
@ -465,28 +467,38 @@ export class UserViewPage extends AKElement {
model=${RbacPermissionsAssignedByUsersListModelEnum.CoreUser} model=${RbacPermissionsAssignedByUsersListModelEnum.CoreUser}
objectPk=${this.user.pk} objectPk=${this.user.pk}
></ak-rbac-object-permission-page> ></ak-rbac-object-permission-page>
<section <div
slot="page-mfa-assigned-permissions" slot="page-mfa-assigned-permissions"
data-tab-title="${msg("Assigned permissions")}" data-tab-title="${msg("Assigned permissions")}"
class="pf-c-page__main-section pf-m-no-padding-mobile" class=""
> >
<div class="pf-l-grid pf-m-gutter"> <div class="pf-c-banner pf-m-info">
<div class="pf-c-card pf-l-grid__item pf-m-12-col"> ${msg("RBAC is in preview.")}
<div class="pf-c-card__title">${msg("Assigned global permissions")}</div> <a href="mailto:hello@goauthentik.io">${msg("Send us feedback!")}</a>
<div class="pf-c-card__body">
<ak-user-assigned-global-permissions-table userId=${this.user.pk}>
</ak-user-assigned-global-permissions-table>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${msg("Assigned object permissions")}</div>
<div class="pf-c-card__body">
<ak-user-assigned-object-permissions-table userId=${this.user.pk}>
</ak-user-assigned-object-permissions-table>
</div>
</div>
</div> </div>
</section> <section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">
${msg("Assigned global permissions")}
</div>
<div class="pf-c-card__body">
<ak-user-assigned-global-permissions-table userId=${this.user.pk}>
</ak-user-assigned-global-permissions-table>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">
${msg("Assigned object permissions")}
</div>
<div class="pf-c-card__body">
<ak-user-assigned-object-permissions-table userId=${this.user.pk}>
</ak-user-assigned-object-permissions-table>
</div>
</div>
</div>
</section>
</div>
</ak-tabs>`; </ak-tabs>`;
} }
} }

View File

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

View File

@ -31,6 +31,93 @@ export interface KeyUnknown {
[key: string]: unknown; [key: string]: unknown;
} }
/**
* Recursively assign `value` into `json` while interpreting the dot-path of `element.name`
*/
function assignValue(element: HTMLInputElement, value: unknown, json: KeyUnknown): void {
let parent = json;
if (!element.name?.includes(".")) {
parent[element.name] = value;
return;
}
const nameElements = element.name.split(".");
for (let index = 0; index < nameElements.length - 1; index++) {
const nameEl = nameElements[index];
// Ensure all nested structures exist
if (!(nameEl in parent)) parent[nameEl] = {};
parent = parent[nameEl] as { [key: string]: unknown };
}
parent[nameElements[nameElements.length - 1]] = value;
}
/**
* Convert the elements of the form to JSON.[4]
*
*/
export function serializeForm<T extends KeyUnknown>(
elements: NodeListOf<HorizontalFormElement>,
): T | undefined {
const json: { [key: string]: unknown } = {};
elements.forEach((element) => {
element.requestUpdate();
const inputElement = element.querySelector<HTMLInputElement>("[name]");
if (element.hidden || !inputElement) {
return;
}
// Skip elements that are writeOnly where the user hasn't clicked on the value
if (element.writeOnly && !element.writeOnlyActivated) {
return;
}
if (
inputElement.tagName.toLowerCase() === "select" &&
"multiple" in inputElement.attributes
) {
const selectElement = inputElement as unknown as HTMLSelectElement;
assignValue(
inputElement,
Array.from(selectElement.selectedOptions).map((v) => v.value),
json,
);
} else if (inputElement.tagName.toLowerCase() === "input" && inputElement.type === "date") {
assignValue(inputElement, inputElement.valueAsDate, json);
} else if (
inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "datetime-local"
) {
assignValue(inputElement, new Date(inputElement.valueAsNumber), json);
} else if (
inputElement.tagName.toLowerCase() === "input" &&
"type" in inputElement.dataset &&
inputElement.dataset["type"] === "datetime-local"
) {
// Workaround for Firefox <93, since 92 and older don't support
// datetime-local fields
assignValue(inputElement, new Date(inputElement.value), json);
} else if (
inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "checkbox"
) {
assignValue(inputElement, inputElement.checked, json);
} else if ("selectedFlow" in inputElement) {
assignValue(inputElement, inputElement.value, json);
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
const select = inputElement as unknown as SearchSelect<unknown>;
try {
const value = select.toForm();
assignValue(inputElement, value, json);
} catch (exc) {
if (exc instanceof PreventFormSubmit) {
throw new PreventFormSubmit(exc.message, element);
}
throw exc;
}
} else {
assignValue(inputElement, inputElement.value, json);
}
});
return json as unknown as T;
}
/** /**
* Form * Form
* *
@ -177,95 +264,13 @@ export abstract class Form<T> extends AKElement {
* *
*/ */
serializeForm(): T | undefined { serializeForm(): T | undefined {
const elements = const elements = this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
this.shadowRoot?.querySelectorAll<HorizontalFormElement>( "ak-form-element-horizontal",
"ak-form-element-horizontal", );
) || []; if (!elements) {
const json: { [key: string]: unknown } = {}; return {} as T;
elements.forEach((element) => {
element.requestUpdate();
const inputElement = element.querySelector<HTMLInputElement>("[name]");
if (element.hidden || !inputElement) {
return;
}
// Skip elements that are writeOnly where the user hasn't clicked on the value
if (element.writeOnly && !element.writeOnlyActivated) {
return;
}
if (
inputElement.tagName.toLowerCase() === "select" &&
"multiple" in inputElement.attributes
) {
const selectElement = inputElement as unknown as HTMLSelectElement;
this.assignValue(
inputElement,
Array.from(selectElement.selectedOptions).map((v) => v.value),
json,
);
} else if (
inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "date"
) {
this.assignValue(inputElement, inputElement.valueAsDate, json);
} else if (
inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "datetime-local"
) {
this.assignValue(inputElement, new Date(inputElement.valueAsNumber), json);
} else if (
inputElement.tagName.toLowerCase() === "input" &&
"type" in inputElement.dataset &&
inputElement.dataset["type"] === "datetime-local"
) {
// Workaround for Firefox <93, since 92 and older don't support
// datetime-local fields
this.assignValue(inputElement, new Date(inputElement.value), json);
} else if (
inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "checkbox"
) {
this.assignValue(inputElement, inputElement.checked, json);
} else if ("selectedFlow" in inputElement) {
this.assignValue(inputElement, inputElement.value, json);
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
const select = inputElement as unknown as SearchSelect<unknown>;
try {
const value = select.toForm();
this.assignValue(inputElement, value, json);
} catch (exc) {
if (exc instanceof PreventFormSubmit) {
throw new PreventFormSubmit(exc.message, element);
}
throw exc;
}
} else {
this.assignValue(inputElement, inputElement.value, json);
}
});
return json as unknown as T;
}
/**
* Recursively assign `value` into `json` while interpreting the dot-path of `element.name`
*/
private assignValue(
element: HTMLInputElement,
value: unknown,
json: { [key: string]: unknown },
): void {
let parent = json;
if (!element.name?.includes(".")) {
parent[element.name] = value;
return;
} }
const nameElements = element.name.split("."); return serializeForm(elements) as T;
for (let index = 0; index < nameElements.length - 1; index++) {
const nameEl = nameElements[index];
// Ensure all nested structures exist
if (!(nameEl in parent)) parent[nameEl] = {};
parent = parent[nameEl] as { [key: string]: unknown };
}
parent[nameElements[nameElements.length - 1]] = value;
} }
/** /**

View File

@ -7,6 +7,7 @@ import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
@ -51,13 +52,17 @@ export class ObjectPermissionModal extends AKElement {
objectPk?: string | number; objectPk?: string | number;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, PFButton]; return [PFBase, PFButton, PFBanner];
} }
render(): TemplateResult { render(): TemplateResult {
return html` return html`
<ak-forms-modal .showSubmitButton=${false} cancelText=${msg("Close")}> <ak-forms-modal .showSubmitButton=${false} cancelText=${msg("Close")}>
<span slot="header"> ${msg("Update Permissions")} </span> <span slot="header"> ${msg("Update Permissions")} </span>
<div class="pf-c-banner pf-m-info" slot="above-form">
${msg("RBAC is in preview.")}
<a href="mailto:hello@goauthentik.io">${msg("Send us feedback!")}</a>
</div>
<ak-rbac-object-permission-modal-form <ak-rbac-object-permission-modal-form
slot="form" slot="form"
.model=${this.model} .model=${this.model}

View File

@ -7,6 +7,7 @@ import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css"; 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";
@ -22,48 +23,57 @@ export class ObjectPermissionPage extends AKElement {
@property() @property()
objectPk?: string | number; objectPk?: string | number;
@property({ type: Boolean })
showBanner = true;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, PFGrid, PFPage, PFCard]; return [PFBase, PFGrid, PFPage, PFCard, PFBanner];
} }
render(): TemplateResult { render(): TemplateResult {
return html`<ak-tabs pageIdentifier="permissionPage"> return html`${this.showBanner
<section ? html`<div class="pf-c-banner pf-m-info">
slot="page-object-user" ${msg("RBAC is in preview.")}
data-tab-title="${msg("User Object Permissions")}" <a href="mailto:hello@goauthentik.io">${msg("Send us feedback!")}</a>
class="pf-c-page__main-section pf-m-no-padding-mobile" </div>`
> : html``}
<div class="pf-l-grid pf-m-gutter"> <ak-tabs pageIdentifier="permissionPage">
<div class="pf-c-card pf-l-grid__item pf-m-12-col"> <section
<div class="pf-c-card__title">User Object Permissions</div> slot="page-object-user"
<div class="pf-c-card__body"> data-tab-title="${msg("User Object Permissions")}"
<ak-rbac-user-object-permission-table class="pf-c-page__main-section pf-m-no-padding-mobile"
.model=${this.model} >
.objectPk=${this.objectPk} <div class="pf-l-grid pf-m-gutter">
> <div class="pf-c-card pf-l-grid__item pf-m-12-col">
</ak-rbac-user-object-permission-table> <div class="pf-c-card__title">User Object Permissions</div>
<div class="pf-c-card__body">
<ak-rbac-user-object-permission-table
.model=${this.model}
.objectPk=${this.objectPk}
>
</ak-rbac-user-object-permission-table>
</div>
</div> </div>
</div> </div>
</div> </section>
</section> <section
<section slot="page-object-role"
slot="page-object-role" data-tab-title="${msg("Role Object Permissions")}"
data-tab-title="${msg("Role Object Permissions")}" class="pf-c-page__main-section pf-m-no-padding-mobile"
class="pf-c-page__main-section pf-m-no-padding-mobile" >
> <div class="pf-l-grid pf-m-gutter">
<div class="pf-l-grid pf-m-gutter"> <div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card pf-l-grid__item pf-m-12-col"> <div class="pf-c-card__title">Role Object Permissions</div>
<div class="pf-c-card__title">Role Object Permissions</div> <div class="pf-c-card__body">
<div class="pf-c-card__body"> <ak-rbac-role-object-permission-table
<ak-rbac-role-object-permission-table .model=${this.model}
.model=${this.model} .objectPk=${this.objectPk}
.objectPk=${this.objectPk} >
> </ak-rbac-role-object-permission-table>
</ak-rbac-role-object-permission-table> </div>
</div> </div>
</div> </div>
</div> </section>
</section> </ak-tabs>`;
</ak-tabs>`;
} }
} }

View File

@ -1,6 +1,7 @@
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch"; import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { FuseResult } from "fuse.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { css, html } from "lit"; import { css, html } from "lit";
@ -66,7 +67,7 @@ export class LibraryPageApplicationList extends AKElement {
}); });
} }
onSelected(apps: Fuse.FuseResult<Application>[]) { onSelected(apps: FuseResult<Application>[]) {
this.dispatchEvent( this.dispatchEvent(
customEvent(SEARCH_UPDATED, { customEvent(SEARCH_UPDATED, {
apps: apps.map((app) => app.item), apps: apps.map((app) => app.item),

View File

@ -5798,12 +5798,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s945a6b94361ee45b"> <trans-unit id="s945a6b94361ee45b">
<source>For transparent reverse proxies with required authentication</source> <source>For transparent reverse proxies with required authentication</source>
</trans-unit> </trans-unit>
<trans-unit id="sadf073913458acbd">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="se770e9498b3bacf6">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="s40830ec037f34626"> <trans-unit id="s40830ec037f34626">
<source>Configure SAML provider manually</source> <source>Configure SAML provider manually</source>
</trans-unit> </trans-unit>
@ -6031,6 +6025,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s0924f51b028233a3"> <trans-unit id="s0924f51b028233a3">
<source>&lt;No name set&gt;</source> <source>&lt;No name set&gt;</source>
</trans-unit>
<trans-unit id="sdc9a6ad1af30572c">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="sfc31264ef7ff86ef">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="sc615309d10a9228c">
<source>RBAC is in preview.</source>
</trans-unit>
<trans-unit id="s32babfed740fd3c1">
<source>User type used for newly created users.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6079,12 +6079,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s945a6b94361ee45b"> <trans-unit id="s945a6b94361ee45b">
<source>For transparent reverse proxies with required authentication</source> <source>For transparent reverse proxies with required authentication</source>
</trans-unit> </trans-unit>
<trans-unit id="sadf073913458acbd">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="se770e9498b3bacf6">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="s40830ec037f34626"> <trans-unit id="s40830ec037f34626">
<source>Configure SAML provider manually</source> <source>Configure SAML provider manually</source>
</trans-unit> </trans-unit>
@ -6312,6 +6306,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s0924f51b028233a3"> <trans-unit id="s0924f51b028233a3">
<source>&lt;No name set&gt;</source> <source>&lt;No name set&gt;</source>
</trans-unit>
<trans-unit id="sdc9a6ad1af30572c">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="sfc31264ef7ff86ef">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="sc615309d10a9228c">
<source>RBAC is in preview.</source>
</trans-unit>
<trans-unit id="s32babfed740fd3c1">
<source>User type used for newly created users.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -5713,12 +5713,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s945a6b94361ee45b"> <trans-unit id="s945a6b94361ee45b">
<source>For transparent reverse proxies with required authentication</source> <source>For transparent reverse proxies with required authentication</source>
</trans-unit> </trans-unit>
<trans-unit id="sadf073913458acbd">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="se770e9498b3bacf6">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="s40830ec037f34626"> <trans-unit id="s40830ec037f34626">
<source>Configure SAML provider manually</source> <source>Configure SAML provider manually</source>
</trans-unit> </trans-unit>
@ -5946,6 +5940,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s0924f51b028233a3"> <trans-unit id="s0924f51b028233a3">
<source>&lt;No name set&gt;</source> <source>&lt;No name set&gt;</source>
</trans-unit>
<trans-unit id="sdc9a6ad1af30572c">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="sfc31264ef7ff86ef">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="sc615309d10a9228c">
<source>RBAC is in preview.</source>
</trans-unit>
<trans-unit id="s32babfed740fd3c1">
<source>User type used for newly created users.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

File diff suppressed because it is too large Load Diff

View File

@ -5921,12 +5921,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s945a6b94361ee45b"> <trans-unit id="s945a6b94361ee45b">
<source>For transparent reverse proxies with required authentication</source> <source>For transparent reverse proxies with required authentication</source>
</trans-unit> </trans-unit>
<trans-unit id="sadf073913458acbd">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="se770e9498b3bacf6">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="s40830ec037f34626"> <trans-unit id="s40830ec037f34626">
<source>Configure SAML provider manually</source> <source>Configure SAML provider manually</source>
</trans-unit> </trans-unit>
@ -6154,6 +6148,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s0924f51b028233a3"> <trans-unit id="s0924f51b028233a3">
<source>&lt;No name set&gt;</source> <source>&lt;No name set&gt;</source>
</trans-unit>
<trans-unit id="sdc9a6ad1af30572c">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="sfc31264ef7ff86ef">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="sc615309d10a9228c">
<source>RBAC is in preview.</source>
</trans-unit>
<trans-unit id="s32babfed740fd3c1">
<source>User type used for newly created users.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -7557,14 +7557,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>For transparent reverse proxies with required authentication</source> <source>For transparent reverse proxies with required authentication</source>
<target>Ƒōŕ ţŕàńśƥàŕēńţ ŕēvēŕśē ƥŕōxĩēś ŵĩţĥ ŕēǫũĩŕēď àũţĥēńţĩćàţĩōń</target> <target>Ƒōŕ ţŕàńśƥàŕēńţ ŕēvēŕśē ƥŕōxĩēś ŵĩţĥ ŕēǫũĩŕēď àũţĥēńţĩćàţĩōń</target>
</trans-unit> </trans-unit>
<trans-unit id="sadf073913458acbd">
<source>For nginx's auth_request or traefik's forwardAuth</source>
<target>Ƒōŕ ńĝĩńx'ś àũţĥ_ŕēǫũēśţ ōŕ ţŕàēƒĩx'ś ƒōŕŵàŕďÀũţĥ</target>
</trans-unit>
<trans-unit id="se770e9498b3bacf6">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
<target>Ƒōŕ ńĝĩńx'ś àũţĥ_ŕēǫũēśţ ōŕ ţŕàēƒĩx'ś ƒōŕŵàŕďÀũţĥ ƥēŕ ŕōōţ ďōmàĩń</target>
</trans-unit>
<trans-unit id="s40830ec037f34626"> <trans-unit id="s40830ec037f34626">
<source>Configure SAML provider manually</source> <source>Configure SAML provider manually</source>
<target>Ćōńƒĩĝũŕē ŚÀMĹ ƥŕōvĩďēŕ màńũàĺĺŷ</target> <target>Ćōńƒĩĝũŕē ŚÀMĹ ƥŕōvĩďēŕ màńũàĺĺŷ</target>
@ -7844,4 +7836,16 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s0924f51b028233a3"> <trans-unit id="s0924f51b028233a3">
<source>&lt;No name set&gt;</source> <source>&lt;No name set&gt;</source>
</trans-unit> </trans-unit>
<trans-unit id="sdc9a6ad1af30572c">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="sfc31264ef7ff86ef">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="sc615309d10a9228c">
<source>RBAC is in preview.</source>
</trans-unit>
<trans-unit id="s32babfed740fd3c1">
<source>User type used for newly created users.</source>
</trans-unit>
</body></file></xliff> </body></file></xliff>

View File

@ -5706,12 +5706,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s945a6b94361ee45b"> <trans-unit id="s945a6b94361ee45b">
<source>For transparent reverse proxies with required authentication</source> <source>For transparent reverse proxies with required authentication</source>
</trans-unit> </trans-unit>
<trans-unit id="sadf073913458acbd">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="se770e9498b3bacf6">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="s40830ec037f34626"> <trans-unit id="s40830ec037f34626">
<source>Configure SAML provider manually</source> <source>Configure SAML provider manually</source>
</trans-unit> </trans-unit>
@ -5939,6 +5933,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s0924f51b028233a3"> <trans-unit id="s0924f51b028233a3">
<source>&lt;No name set&gt;</source> <source>&lt;No name set&gt;</source>
</trans-unit>
<trans-unit id="sdc9a6ad1af30572c">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="sfc31264ef7ff86ef">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="sc615309d10a9228c">
<source>RBAC is in preview.</source>
</trans-unit>
<trans-unit id="s32babfed740fd3c1">
<source>User type used for newly created users.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -7620,14 +7620,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>For transparent reverse proxies with required authentication</source> <source>For transparent reverse proxies with required authentication</source>
<target>适用于需要验证身份的透明反向代理</target> <target>适用于需要验证身份的透明反向代理</target>
</trans-unit> </trans-unit>
<trans-unit id="sadf073913458acbd">
<source>For nginx's auth_request or traefik's forwardAuth</source>
<target>适用于 nginx 的 auth_request 或 traefik 的 forwardAuth</target>
</trans-unit>
<trans-unit id="se770e9498b3bacf6">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
<target>适用于按根域名配置的 nginx 的 auth_request 或 traefik 的 forwardAuth</target>
</trans-unit>
<trans-unit id="s40830ec037f34626"> <trans-unit id="s40830ec037f34626">
<source>Configure SAML provider manually</source> <source>Configure SAML provider manually</source>
<target>手动配置 SAML 提供程序</target> <target>手动配置 SAML 提供程序</target>
@ -7912,21 +7904,39 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s84fcddede27b8e2a"> <trans-unit id="s84fcddede27b8e2a">
<source>External</source> <source>External</source>
<target>外部</target>
</trans-unit> </trans-unit>
<trans-unit id="s1a635369edaf4dc3"> <trans-unit id="s1a635369edaf4dc3">
<source>Service account</source> <source>Service account</source>
<target>服务账户</target>
</trans-unit> </trans-unit>
<trans-unit id="sff930bf2834e2201"> <trans-unit id="sff930bf2834e2201">
<source>Service account (internal)</source> <source>Service account (internal)</source>
<target>服务账户(内部)</target>
</trans-unit> </trans-unit>
<trans-unit id="s66313b45b69cfc88"> <trans-unit id="s66313b45b69cfc88">
<source>Check the release notes</source> <source>Check the release notes</source>
<target>查看发行日志</target>
</trans-unit> </trans-unit>
<trans-unit id="sb4d7bae2440d9781"> <trans-unit id="sb4d7bae2440d9781">
<source>User Statistics</source> <source>User Statistics</source>
<target>用户统计</target>
</trans-unit> </trans-unit>
<trans-unit id="s0924f51b028233a3"> <trans-unit id="s0924f51b028233a3">
<source>&lt;No name set&gt;</source> <source>&lt;No name set&gt;</source>
<target>&lt;未设置名称&gt;</target>
</trans-unit>
<trans-unit id="sdc9a6ad1af30572c">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="sfc31264ef7ff86ef">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="sc615309d10a9228c">
<source>RBAC is in preview.</source>
</trans-unit>
<trans-unit id="s32babfed740fd3c1">
<source>User type used for newly created users.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -5754,12 +5754,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s945a6b94361ee45b"> <trans-unit id="s945a6b94361ee45b">
<source>For transparent reverse proxies with required authentication</source> <source>For transparent reverse proxies with required authentication</source>
</trans-unit> </trans-unit>
<trans-unit id="sadf073913458acbd">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="se770e9498b3bacf6">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="s40830ec037f34626"> <trans-unit id="s40830ec037f34626">
<source>Configure SAML provider manually</source> <source>Configure SAML provider manually</source>
</trans-unit> </trans-unit>
@ -5987,6 +5981,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s0924f51b028233a3"> <trans-unit id="s0924f51b028233a3">
<source>&lt;No name set&gt;</source> <source>&lt;No name set&gt;</source>
</trans-unit>
<trans-unit id="sdc9a6ad1af30572c">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="sfc31264ef7ff86ef">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="sc615309d10a9228c">
<source>RBAC is in preview.</source>
</trans-unit>
<trans-unit id="s32babfed740fd3c1">
<source>User type used for newly created users.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -658,11 +658,6 @@
<source>Manage users</source> <source>Manage users</source>
<target>管理用户</target> <target>管理用户</target>
</trans-unit>
<trans-unit id="s51cda7b1ebcd970f">
<source>Check release notes</source>
<target>查看发行日志</target>
</trans-unit> </trans-unit>
<trans-unit id="s8763a33c3d46aaf5"> <trans-unit id="s8763a33c3d46aaf5">
<source>Outpost status</source> <source>Outpost status</source>
@ -694,11 +689,6 @@
<source>Objects created</source> <source>Objects created</source>
<target>已创建对象</target> <target>已创建对象</target>
</trans-unit>
<trans-unit id="s770baa8560fd8aa1">
<source>User statistics</source>
<target>用户统计</target>
</trans-unit> </trans-unit>
<trans-unit id="sfbadb77fbc61efb8"> <trans-unit id="sfbadb77fbc61efb8">
<source>Users created per day in the last month</source> <source>Users created per day in the last month</source>
@ -7919,6 +7909,30 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="se5c795faf2c07514"> <trans-unit id="se5c795faf2c07514">
<source>Create Recovery Link</source> <source>Create Recovery Link</source>
<target>创建恢复链接</target> <target>创建恢复链接</target>
</trans-unit>
<trans-unit id="s84fcddede27b8e2a">
<source>External</source>
<target>外部</target>
</trans-unit>
<trans-unit id="s1a635369edaf4dc3">
<source>Service account</source>
<target>服务账户</target>
</trans-unit>
<trans-unit id="sff930bf2834e2201">
<source>Service account (internal)</source>
<target>服务账户(内部)</target>
</trans-unit>
<trans-unit id="s66313b45b69cfc88">
<source>Check the release notes</source>
<target>查看发行日志</target>
</trans-unit>
<trans-unit id="sb4d7bae2440d9781">
<source>User Statistics</source>
<target>用户统计</target>
</trans-unit>
<trans-unit id="s0924f51b028233a3">
<source>&lt;No name set&gt;</source>
<target>&lt;未设置名称&gt;</target>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -5753,12 +5753,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s945a6b94361ee45b"> <trans-unit id="s945a6b94361ee45b">
<source>For transparent reverse proxies with required authentication</source> <source>For transparent reverse proxies with required authentication</source>
</trans-unit> </trans-unit>
<trans-unit id="sadf073913458acbd">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="se770e9498b3bacf6">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="s40830ec037f34626"> <trans-unit id="s40830ec037f34626">
<source>Configure SAML provider manually</source> <source>Configure SAML provider manually</source>
</trans-unit> </trans-unit>
@ -5986,6 +5980,18 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s0924f51b028233a3"> <trans-unit id="s0924f51b028233a3">
<source>&lt;No name set&gt;</source> <source>&lt;No name set&gt;</source>
</trans-unit>
<trans-unit id="sdc9a6ad1af30572c">
<source>For nginx's auth_request or traefik's forwardAuth</source>
</trans-unit>
<trans-unit id="sfc31264ef7ff86ef">
<source>For nginx's auth_request or traefik's forwardAuth per root domain</source>
</trans-unit>
<trans-unit id="sc615309d10a9228c">
<source>RBAC is in preview.</source>
</trans-unit>
<trans-unit id="s32babfed740fd3c1">
<source>User type used for newly created users.</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -0,0 +1,130 @@
---
title: 3 ways you (might be) doing containers wrong
description: “Using containers is not a best practice in itself. Here are some mistakes beginners make with containers, and how we set them up correctly at authentik.”
authors:
- name: Jens Langhammer
title: CTO at Authentik Security Inc
url: https://github.com/BeryJu
image_url: https://github.com/BeryJu.png
tags:
- application
- runtime
- SSO
- Docker
- containers
- :latest
- identity provider
- security
- authentication
hide_table_of_contents: false
---
_authentik is an open source Identity Provider that unifies your identity needs into a single platform, replacing Okta, Active Directory, and Auth0. Authentik Security is a [public benefit company](https://github.com/OpenCoreVentures/ocv-public-benefit-company/blob/main/ocv-public-benefit-company-charter.md) building on top of the open source project._
---
There are two ways to judge an application:
1. Does it do what its supposed to do?
2. Is it easy to run?
This post is about the second.
Using containers is not a best practice in itself. As an infrastructure engineer by background, Im pretty opinionated about how to set up containers properly. Doing things the “right” way makes things easier not just for you, but for your users as well.
Below are some common mistakes that I see beginners make with containers:
1. Using one container per application
2. Installing things at runtime
3. Writing logs to files instead of stdout
## Mistake #1: One container per application
There tend to be two mindsets when approaching setting up containers:
- The inexperienced usually think 1 container = 1 application
- The other option is 1 container = 1 service
Your application usually consists of multiple services, and to my mind these should always be separated into their own containers (in keeping with the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle)).
For example, authentik consists of four components (services):
- Server
- Worker
- Database
- Cache
With our deployment, that means you get four different containers because they each run one of those four services.
### Why you should use one container per _service_
At the point where you need to scale, or need High Availability, having different processes in separate containers enables horizontal scaling. Because of how authentik deploys, if we need to handle more traffic we can scale up to 50 servers, rather than having to scale up _everything_. This wouldnt work if all those components were all bundled together.
Additionally, if youre using a container orchestrator (whether thats Kubernetes or something simpler like [Docker Compose](https://goauthentik.io/docs/installation/docker-compose)), if its all bundled together, the orchestrator cant distinguish between components because theyre all in the black box of your container.
Say you want to start up processes in a specific order. This isnt possible if theyre in a single container (unless you rebuild the entire image). If those processes are separate, you can just tell Docker Compose to start them up in the order you want, or you can run specific components on specific servers.
Of course, your application architecture and deployment model need to support this setup, which is why its critical to think about these things when youre starting out. If youre reading this and thinking, I have a small-scale, hobby project, this doesnt apply to me—let me put it this way: you will never regret setting things up the “right” way. Its not going to come back to bite you if your situation changes later. It also gives users who install the application a lot more freedom and flexibility in how _they_ want to run it.
## Mistake #2: Installing things at runtime
Your container image should be complete in itself: it should contain all code and dependencies—everything it needs to run. This is the point of a container—its self contained.
Ive seen people set up their container to download an application from the vendor and install it into the container on startup. While this does work, what happens if you dont have internet access? What if the vendor shut down and that URL now points to a malicious bit of code?
If you have 100 instances downloading files at startup (or end up scaling to that point), this can lead to rate limiting, failed downloads, or your internet connection getting saturated—its just inefficient and causes problems that can be avoided.
### Also, dont use :latest
This leads me to a different but related bad practice: using the `:latest` tag. Its a common pitfall for folks who use containers but dont necessarily build them themselves.
Its easy to get started with the `:latest` tag and its understandable to want the latest version without having to go into files and manually edit everything. But what can happen is that you update and suddenly its pointing to a new version and breaking things.
Ive seen this happen where youre just running something on a local server and your disk is full, so you empty out your Docker images. The next time you pull, its with a new version which now no longer works and youre stuck trying to figure out what version you were on before.
### Instead: Pin your dependencies
You should be pinning your dependencies to a specific version, and updating to newer versions intentionally rather than by default.
The most reliable way to do this is with a process called GitOps:
- In the context of Kubernetes, all the YAML files you deploy with Kubernetes are stored in the central Git repository.
- You have software in your Kubernetes cluster that automatically pulls the files from your Git repo and installs them into the cluster.
- Then you can use a tool like [Dependabot](https://github.com/dependabot) or [Renovate](https://github.com/renovatebot/renovate) to automatically create PRs with a new version (if there is one) so you can test and approve it, and its all captured in your Git history.
GitOps might be a bit excessive if youre only running a small hobby project on a single server, but in any case you should still pin a version.
For a long time, authentik purposefully didnt have a `:latest` tag, because people would use it inadvertently (sometimes not realizing they had an auto-updater running). Suddenly something wouldnt work and there wasnt really a way to downgrade.
We have since added it due to popular request. This is how authentiks version tags work:
- Our version number is 3 digits reflecting the date of the release, so the latest currently is [2023.10.1](https://goauthentik.io/docs/releases/2023.10).
- You can either use 2023.10.1 as the tag, pinning to that specific version
- You can pin to 2023.10, which you means that you always get the latest patch version, or
- You can use 2023, which means you always get the latest version within that year.
The principle is roughly the same with any project using [SemVer](https://semver.org/): you could just lock to v1, which means you get the latest v1 with all minor patches and fixes, without breaking updates. Then you switch to v2 when youre ready.
With this approach you are putting some trust in the developer not to publish any breaking changes with the wrong version number (but youre technically always putting trust in some developer when using someone elses software!).
## Mistake #3: Writing logs to files instead of stdout
This is another issue on the infrastructure side that mainly happens when you put legacy applications into containers. It used to be standard that applications put their log output into a file, and youd probably have a system daemon set up to rotate those files and archive the old ones. This was great when everything ran on the same server without containers.
A lot of software still logs to files by default, but this makes collecting and aggregating your services logs much harder. Docker (and containers in general) expect that you log to standard output so your orchestration platform can route the logs to your monitoring tool of choice.
Docker puts the logs into a JSON file that it can read itself and see the timestamps and which container the log refers to. You can set up log forwarding with both Docker and Kubernetes. If you have a central logging server, the plugin gets the standard output of a container and sends it to that server.
Not logging to `stdout` just makes it harder for everyone, including making it harder to debug: Instead of just running `docker logs` + the name of the container, you need to `exec` into the container, go to find the files, then look at the files to start debugging.
### This bad practice is arguably the easiest one to work around
As an engineer you can easily redirect the logs back from a file into the standard output, but theres no real reason not to do it the “correct” way.
There arent many use cases where theres an advantage to writing your logs directly to a file instead of stdout—in fact the main one is for when youre making the first mistake (having your whole application in one container)! If youre running multiple services in one container, then youll have logs from multiple different processes in one place, which _could_ be easier to work with in a file vs stdout.
Even if you specifically want your logs to exist in a file, by default if you run `docker logs` it just reads a JSON file that it adds the logs to, so youre not losing anything by logging to stdout. You can configure Docker to just put the logs into a plain text file wherever you want to.
Its a little simplistic, but Id encourage you to check out [The Twelve-Factor App](https://12factor.net/) which outlines good practices for making software thats easy to run.
Are you doing containers differently and is it working for you? Let us know in the comments, or send us an email at hello@goauthentik.io!

View File

@ -66,7 +66,7 @@ return ak_is_group_member(request.user, name="test_group")
Fetch a user matching `**filters`. Fetch a user matching `**filters`.
Returns "None" if no user was found, otherwise returns the [User](/docs/user-group/user) object. Returns "None" if no user was found, otherwise returns the [User](/docs/user-group-role/user) object.
Example: Example:

View File

@ -1,4 +1,4 @@
- `user`: The current user. This may be `None` if there is no contextual user. See [User](../user-group/user/user_ref.md#object-properties). - `user`: The current user. This may be `None` if there is no contextual user. See [User](../user-group-role/user/user_ref.md#object-properties).
Example: Example:

View File

@ -22,7 +22,7 @@ Keys prefixed with `goauthentik.io` are used internally by authentik and are sub
### Common keys ### Common keys
#### `pending_user` ([User object](../../user-group/user/user_ref.md#object-properties)) #### `pending_user` ([User object](../../user-group-role/user/user_ref.md#object-properties))
`pending_user` is used by multiple stages. In the context of most flow executions, it represents the data of the user that is executing the flow. This value is not set automatically, it is set via the [Identification stage](../stages/identification/). `pending_user` is used by multiple stages. In the context of most flow executions, it represents the data of the user that is executing the flow. This value is not set automatically, it is set via the [Identification stage](../stages/identification/).
@ -110,9 +110,9 @@ Optionally overwrite the deny message shown, has a higher priority than the mess
#### User write stage #### User write stage
##### `groups` (List of [Group objects](../../user-group/group.md)) ##### `groups` (List of [Group objects](../../user-group-role/groups/index.mdx))
See [Group](../../user-group/group.md). If set in the flow context, the `pending_user` will be added to all the groups in this list. See [Group](../../user-group-role/groups/index.mdx). If set in the flow context, the `pending_user` will be added to all the groups in this list.
If set, this must be a list of group objects and not group names. If set, this must be a list of group objects and not group names.
@ -120,6 +120,14 @@ If set, this must be a list of group objects and not group names.
Path the `pending_user` will be written to. If not set in the flow, falls back to the value set in the user_write stage, and otherwise to the `users` path. Path the `pending_user` will be written to. If not set in the flow, falls back to the value set in the user_write stage, and otherwise to the `users` path.
##### `user_type` (string)
:::info
Requires authentik 2023.10
:::
Type the `pending_user` will be created as. Must be one of `internal`, `external` or `service_account`.
#### Password stage #### Password stage
##### `user_backend` (string) ##### `user_backend` (string)

View File

@ -41,7 +41,7 @@ import Objects from "../expressions/_objects.md";
- `request`: A PolicyRequest object, which has the following properties: - `request`: A PolicyRequest object, which has the following properties:
- `request.user`: The current user, against which the policy is applied. See [User](../user-group/user/user_ref.md#object-properties) - `request.user`: The current user, against which the policy is applied. See [User](../user-group-role/user/user_ref.md#object-properties)
:::caution :::caution
When a policy is executed in the context of a flow, this will be set to the user initiaing request, and will only be changed by a `user_login` stage. For that reason, using this value in authentication flow policies may not return the expected user. Use `context['pending_user']` instead; User Identification and other stages update this value during flow execution. When a policy is executed in the context of a flow, this will be set to the user initiaing request, and will only be changed by a `user_login` stage. For that reason, using this value in authentication flow policies may not return the expected user. Use `context['pending_user']` instead; User Identification and other stages update this value during flow execution.
@ -77,7 +77,7 @@ This includes the following:
- `context['prompt_data']`: Data which has been saved from a prompt stage or an external source. (Optional) - `context['prompt_data']`: Data which has been saved from a prompt stage or an external source. (Optional)
- `context['application']`: The application the user is in the process of authorizing. (Optional) - `context['application']`: The application the user is in the process of authorizing. (Optional)
- `context['source']`: The source the user is authenticating/enrolling with. (Optional) - `context['source']`: The source the user is authenticating/enrolling with. (Optional)
- `context['pending_user']`: The currently pending user, see [User](../user-group/user/user_ref.md#object-properties) - `context['pending_user']`: The currently pending user, see [User](../user-group-role/user/user_ref.md#object-properties)
- `context['is_restored']`: Contains the flow token when the flow plan was restored from a link, for example the user clicked a link to a flow which was sent by an email stage. (Optional) - `context['is_restored']`: Contains the flow token when the flow plan was restored from a link, for example the user clicked a link to a flow which was sent by an email stage. (Optional)
- `context['auth_method']`: Authentication method (this value is set by password stages) (Optional) - `context['auth_method']`: Authentication method (this value is set by password stages) (Optional)

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -0,0 +1,16 @@
---
title: About access control
---
import DocCardList from "@theme/DocCardList";
import { useCurrentSidebarCategory } from "@docusaurus/theme-common";
To comply with important regulations such as PCI-DSS, HIPAA, SOC 2, and GDPR, it's necessary to have the ability to control which users have access to specific areas of the system, what [permissions](./permissions.md) they have globally and on certain objects, and a way to monitor [events](../../events) related to user activity.
In authentik, we provide role-based access control (RBAC), an industry standard for managing access control. By carefully designing roles with appropriate permissions, and then assigning those roles to groups, RBAC provides a fine-tuned approach to controlling user access.
RBAC is a way of ensuring the well-known [principal of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) whereby "every module (such as a process, a user, or a program, depending on the subject) must be able to access only the information and resources that are necessary for its legitimate purpose."
To learn more about access control with authentik, refer to these topics:
<DocCardList items={useCurrentSidebarCategory().items} />

View File

@ -0,0 +1,118 @@
---
title: "Manage permissions"
description: "Learn how to use global and object permissions in authentik."
---
Refer to the following topics for instructions to view and manage permissions.
## View permissions
You can view all permissions that are assigned to a user, group, role, flow, or stage.
### View user, group, and role permissions
To view _object_ permissions for a specific user, role, or group:
1. Go to the Admin interface and navigate to **Directory**.
2. Select either **Users**, **Groups**, or **Roles**
3. Select a specific user/group/role by clicking on the name (this opens the details page).
4. Click the **Assigned Permissions** tab at the top of the page (to the right of the **Permissions** tab).
5. Scroll down to see both the global and object-level permissions.
:::info
Note that groups do not have global permissions.
:::
### View flow permissions
1. Go to the Admin interface and navigate to **Flows and Stages -> Flows**.
2. Click the name of the flow (this opens the details page).
3. Click the **Permissions** tab at the top of the page.
4. View the assigned permissions using the **User Object Permissions** and the **Role Object Permissions** tabs.
### View stage permissions
1. Go to the Admin interface and navigate to **Flows and Stages -> Stagess**.
2. On the row for the specific stage whose permissions you want to view, click the lock icon.
3. On the **Update Permissions** tab, you can view the assigned permissions using the **User Object Permissions** and the **Role Object Permissions** tabs.
## Manage permissions
You can assign or remove permissions to a user, role, group, flow, or stage.
### Assign, modify, or remove permissions for a user
To assign or remove _object_ permissions for a specific user:
1. Go to the Admin interface and navigate to **Directory -> Users**.
2. Select a specific user by clicking on the user's name.
3. Click the **Permissions** tab at the top of the page.
4. To assign or remove permissions that another _user_ has on this specific user:
1. Click the **User Object Permissions** tab, click **Assign to new user**.
2. In the **User** drop-down, select the user object.
3. Use the toggles to set which permissions on that selected user object you want to grant to (or remove from) the specific user.
4. Click **Assign** to save your settings and close the modal.
5. To assign or remove permissions that another _role_ has on this specific user:
Click the **Role Object Permissions** tab, click **Assign to new role**. 2. In the **User** drop-down, select the user object. 3. Use the toggles to set which permissions you want to grant to (or remove from) the selected role. 4. Click **Assign** to save your settings and close the modal.
To assign or remove _global_ permissions for a user:
1. Go to the Admin interface and navigate to **Directory -> Users**.
2. Select a specific user the clicking on the user's name.
3. Click the **Assigned Permissions** tab at the top of the page (to the right of the **Permissions** tab).
4. In the **Assigned Global Permissions** area, click **Assign Permission**.
5. In the **Assign permissions to user** modal, click the plus sign (**+**) and then click the checkbox beside each permission that you want to assign to the user. To remove permissions, deselect the checkbox.
6. Click **Add**, and then click **Assign** to save your changes and close the modal.
### Assign or remove permissions on a specific group
:::info
Note that groups themselves do not have permissions. Rather, users and roles have permissions assigned that allow them to create, modify, delete, etc., a group.
Also there are no global permissions for groups.
:::
To assign or remove _object_ permissions on a specific group by users and roles:
1. Go to the Admin interface and navigate to **Directory -> Groups**.
2. Select a specific group by clicking the the group's name.
3. Click the **Permissions** tab at the top of the page.
To assign or remove permissions that another _user_ has on this specific group:
1. Click the **User Object Permissions** tab, click **Assign to new user**.
2. In the **User** drop-down, select the user object.
3. Use the toggles to set which permissions on that selected group you want to grant to (or remove from) the specific user.
4. Click **Assign** to save your settings and close the modal.
4. To assign or remove permissions that another _role_ has on this specific group:
Click the **Role Object Permissions** tab, click **Assign to new role**. 2. In the **Role** drop-down, select the role. 3. Use the toggles to set which permissions you want to grant to (or remove from ) the selected role. 4. Click **Assign** to save your settings and close the modal.
### Assign or remove permissions for a specific role
To assign or remove _object_ permissions for a specific role:
1. Go to the Admin interface and navigate to **Directory -> Roles**.
2. Select a specific role the clicking on the role's name.
3. Click the **Permissions** tab at the top of the page.
To assign or remove permissions that another _user_ has on this specific role: 1. Click the **User Object Permissions** tab, click **Assign to new user**. 2. In the **User** drop-down, select the user object. 3. Use the toggles to set which permissions on that role you want to grant to (or remove from) the selected user. 4. Click **Assign** to save your settings and close the modal.
4. To assign or remove permissions that another _role_ has on this specific group:
Click the **Role Object Permissions** tab, click **Assign to new role**. 2. In the **Role** drop-down, select the role. 3. Use the toggles to set which permissions you want to grant to (or remove from) the selected role. 4. Click **Assign** to save your settings and close the modal.
To assign or remove _global_ permissions for a role:
1. Go to the Admin interface and navigate to **Directory -> Roles**.
2. Select a specific role by clicking on the role's name.
3. The **Overview** tab at the top of the page displays all assigned global permissions for the role.
4. In the **Assigned Global Permissions** area, click **Assign Permission**.
5. In the **Assign permissions to role** modal, click the plus sign (**+**) and then click the checkbox beside each permission that you want to assign to the role. To remove permissions, deselect the checkbox.
6. Click **Assign** to save your changes and close the modal.
### Assign or remove flow permissions
1. Go to the Admin interface and navigate to **Flows and Stages -> Flows**.
2. Click the name of the flow (this opens the details page).
3. Click the **Permissions** tab at the top of the page.
4. Add or remove permissions using the **User Object Permissions** and the **Role Object Permissions** tabs.
### Assign or remove stage permissions
1. Go to the Admin interface and navigate to **Flows and Stages -> Stagess**.
2. On the row for the specific stage that you want to manage permissions, click the lock icon.
3. On the **Update Permissions** tab, you can add or remove the assigned permissions using the **User Object Permissions** and the **Role Object Permissions** tabs.

View File

@ -0,0 +1,44 @@
---
title: "About permissions"
description: "Learn about global and object permissions in authentik."
---
Permissions are the central components in all access control systems, the lowest-level components, the controlling pieces of access data. Permissions are assigned to (or removed from!) to define exactly WHO can do WHAT to WHICH part of the overall software system.
:::info
Note that global and object permissions only apply to objects within authentik, and not to who can access certain applications (which are access-controlled using [policies](../../policies/index.md).
:::
## Fundamentals of authentik permissions
There are two main types of permissions in authentik:
- [**Global permissions**](#global-permissions)
- [**Object permissions**](#object-permissions)
### Global permissions
Global permissions define who can do what on a global level across the entire system. Some examples in authentik are the ability to add new [flows](../../flow/index.md) or to create a URL for users to recover their login credentials.
You can assign _global permissions_ to individual [users](../user/index.mdx) or to [roles](../roles/index.mdx). The most common and best practice is to assign permissions to roles.
### Object permissions
Object permissions have two categories:
- **_User_ object permissions**: defines WHO (which user) can change the **_object_**
- **_Role_ object permissions**: defines which ROLE can change the **_object_**
Object permissions are assigned, as the name indicates, to an object (users, [groups](../groups/index.mdx), roles, flows, and stages), and the assigned permissions state exactly what a user or role can do TO the object (i.e. what permissions does the user or role have on that object).
When working with object permissions, it is important to understand that when you are viewing the page for an object the permissions table shows which users or roles have permissions ON that object. Those permissions describe what those users or roles can do TO the object detailed on the page.
For example, the UI below shows a user page for the user named Peter.
![](./user-page.png)
You can see in the **User Object Permissions** table that another user, roberto, has permissions on Peter (that is, on the user object Peter).
Looking at another example, with a flow object called `default-recovery-flow` you can see that the Admin user (akadmin) has all object permissions on the flow, but roberto only has a few permissions on that flow.
![](./flow-page.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@ -1,5 +1,6 @@
--- ---
title: Group title: About groups
description: Learn about groups in authentik
--- ---
## Hierarchy ## Hierarchy

View File

@ -0,0 +1,45 @@
---
title: Manage groups
description: "Learn how to work with groups in authentik."
---
A group is a collection of users. Refer to the following sections to learn how to create and manage groups, assign users and roles to groups, and how [permissions](../access-control/manage_permissions.md) work on a group level.
## Create a group
To create a new group, follow these steps:
1. In the Admin interface, navigate to **Directory > Groups**.
2. Click **Create** at the top of the Groups page.
3. In the Create modal, define the following:
- name of the group
- whether or not users in that group will all be superusers (means anyone in that group has all permissions on everything)
- the parent group
- any custom attributes
4. Click **Create**.
## Modify a group
To edit the group's name, parent group, whether or not the group is for superusers, associated roles, and any custom attributes, click the Edit icon beside the role's name. Make the changes, and then click **Update**.
To [add or remove users](../user/user_basic_operations.md#add-a-user-to-a-group) from the group, or to manage permissions assigned to the group, click on the name of the group to go to the group's detail page.
For more information about permissions, refer to ["Assign or remove permissions for a specific group"](../access-control/manage_permissions.md#assign-or-remove-permissions-for-a-specific-group).
## Delete a group
To delete a group, follow these steps:
1. In the Admin interface, navigate to **Directory > Groups**.
2. Select the checkbox beside the name of the group that you want to delete.
3. Click **Delete**.
## Assign, modify, or remove permissions for a group
You can grant a group specific global or object-level permissions. Any user who is a member of a group inherits all of the group's permissions.
For more information, review ["Permissions"](../access-control/permissions.md).
## Assign a role to a group
You can assign a role to a group, and then all users in the group inherit the permissions assigned to that role. For instructions and more information, see ["Assign a role to a group"](../roles/manage_roles.md#assign-a-role-to-a-group).

View File

@ -0,0 +1,20 @@
---
title: About roles
---
import DocCardList from "@theme/DocCardList";
import { useCurrentSidebarCategory } from "@docusaurus/theme-common";
Roles are a way to simplify the assignment of permissions. Roles are also the backbone of role-based access control (RBAC), an industry standard for managing [access control](../access-control). In authentik, RBAC is how you manage access to system components and specific objects such as flows, stages, users, etc.
Think of roles as a collection of permissions. A role, along with its "bucket" of assigned permissions, can then be assigned to a group, which means that every user who is a part of that group will inherit all of the permissions in that role's "bucket".
For example, let's take a look at the following scenario:
> You need to add 5 new users, all new hires, to authentik, your identity management system. These users will be the first team members on the brand new Security team, so they will need some high-level permissions, with object permissions to create and remove other users, revoke permissions, and send recovery emails. They will also need [global permissions](../access-control/permissions#fundamentals-of-authentik-permissions) to control access to flows and stages.
The easiest workflow for setting up these new users involves [creating a role](./manage_roles.md#create-a-role) specifically for their type of work, and then [assigning that role to a group](./manage_roles.md#assign-a-role-to-a-group) to which all of the users belong.
To learn more about working with roles in authentik, refer to the following topics:
<DocCardList items={useCurrentSidebarCategory().items} />

View File

@ -0,0 +1,48 @@
---
title: "Manage roles"
description: "Learn how to work with roles and permissions in authentik."
---
Roles are a collection of permissions, which can then be assigned, en masse, to a group. Using roles is a way to quickly grant permissions; by adding a user to the group with the appropriate assigned roles, any user in that group then inherits all of those permissions that are assigned to the role.
:::info
In authentik, we assign roles to groups, not to individual users.
:::
## Create a role
To create a new role, follow these steps:
1. In the Admin interface, navigate to **Directory > Roles**.
2. Click **Create**, enter the name of the role, and then click **Create** in the modal.
3. Next, [assign permissions to the role](../access-control/permissions.md#assign-or-remove-permissions-for-a-specific-role).
## Modify a role
To modify a role, follow these steps:
- To edit the name of the role, click the Edit icon beside the role's name.
- To modify the permissions that are assigned to the role click on the role's name to go to the role's detail page. There you can add, modify, or remove permissions. For more information, refer to ["Assign or remove permissions for a specific role"](../access-control/permissions.md#assign-or-remove-permissions-for-a-specific-role).
## Delete a role
To delete a role, follow these steps:
1. In the Admin interface, navigate to **Directory > Roles**.
2. Select the checkbox beside the name of the role that you want to delete.
3. Click **Delete**.
## Assign a role to a group
In authentik, roles are assigned to [groups](../groups/index.mdx), not to individual users.
1. To assign the role to a group, navigate to **Directory -> Groups**.
2. Click the name of the group to which you want to add a role.
3. On the group's detail page, on the Overview tab, click **Edit** in the **Group Info** area.
4. On the **Update Group** modal, in the **Roles** field, scroll through the list of existent roles, and click to select the one you want to add to the group. (You can select multiple roles at once by holding the Control and Command keys while selecting the roles.)
5. Click **Update** to add the role(s) and close the modal.
:::info
To remove a role from a group, hold the Command key and click the name of the role that you want to remove from the group. This desepcts the role. Then click **Update**.
:::

View File

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

@ -9,11 +9,8 @@ The following topics are for the basic management of users: how to create, modif
> If you want to automate user creation, you can do that either by [invitations](./invitations.md), [`user_write` stage](../../flow/stages/user_write), or [using the API](/developer-docs/api/browser). > If you want to automate user creation, you can do that either by [invitations](./invitations.md), [`user_write` stage](../../flow/stages/user_write), or [using the API](/developer-docs/api/browser).
1. In the Admin interface of your authentik instance, select **Directory > Users** in the left side menu. 1. In the Admin interface of your authentik instance, select **Directory > Users** in the left side menu.
2. Select the folder where you want to create a user. 2. Select the folder where you want to create a user.
3. Click **Create** (for a default user). 3. Click **Create** (for a default user).
4. Fill in the required fields: 4. Fill in the required fields:
- **Username**: This value must be unique across your user folders. - **Username**: This value must be unique across your user folders.
@ -22,8 +19,8 @@ The following topics are for the basic management of users: how to create, modif
5. Fill the **_optional_** fields if needed: 5. Fill the **_optional_** fields if needed:
- **Name**: The display name of the user. - **Name**: The display name of the user.
- **Email**: The email address of the user. That will be used if there is a [notification rule](../../events/notifications) triggered or for [email stages](../../flow/stages/email). - **Email**: The email address of the user. Email addresses are used in [email stages](../../flow/stages/email) and to receive [notifications](../../events/notifications), if configured.
- **Is active**: Define is the newly created user account is active. Selected by default. - **Is active**: Define if the newly created user account is active. Selected by default.
- **Attributes**: Custom attributes definition for the user, in YAML or JSON format. These attributes can be used to enforce additional prompts on authentication stages or define conditions to enforce specific policies if the current implementation does not fit your use case. The value is an empty dictionary by default. - **Attributes**: Custom attributes definition for the user, in YAML or JSON format. These attributes can be used to enforce additional prompts on authentication stages or define conditions to enforce specific policies if the current implementation does not fit your use case. The value is an empty dictionary by default.
6. Click **Create** 6. Click **Create**
@ -43,7 +40,7 @@ To view details about a specific user:
2. To see further details, click any of the other tabs: 2. To see further details, click any of the other tabs:
- **Session** shows the active sessions established by the user. If there is any need, you can clean up the connected devices for a user by selecting the device(s) and then clicking **Delete**. This forces the user to authenticate again on the deleted devices. - **Session** shows the active sessions established by the user. If there is any need, you can clean up the connected devices for a user by selecting the device(s) and then clicking **Delete**. This forces the user to authenticate again on the deleted devices.
- **Groups** allows you to manage the group membership of the user. You can find more details on [groups](../group). - **Groups** allows you to manage the group membership of the user. You can find more details on [groups](../groups/index.mdx).
- **User events** displays all the events generated by the user during a session, such as login, logout, application authorisation, password reset, user info update, etc. - **User events** displays all the events generated by the user during a session, such as login, logout, application authorisation, password reset, user info update, etc.
- **Explicit consent** lists all the permissions the user has given explicitly to an application. Entries will only appear if the user is validating an [explicit consent flow in an OAuth2 provider](../../providers/oauth2/). If you want to delete the explicit consent (because the application is requiring new permissions, or the user has explicitly asked to reset his consent on third-party apps), select the applications and click **Delete**. The user will be asked to again give explicit consent to share information with the application. - **Explicit consent** lists all the permissions the user has given explicitly to an application. Entries will only appear if the user is validating an [explicit consent flow in an OAuth2 provider](../../providers/oauth2/). If you want to delete the explicit consent (because the application is requiring new permissions, or the user has explicitly asked to reset his consent on third-party apps), select the applications and click **Delete**. The user will be asked to again give explicit consent to share information with the application.
- **OAuth Refresh Tokens** lists all the OAuth tokens currently distributed. You can remove the tokens by selecting the applications and then clicking **Delete**. - **OAuth Refresh Tokens** lists all the OAuth tokens currently distributed. You can remove the tokens by selecting the applications and then clicking **Delete**.
@ -53,32 +50,38 @@ To view details about a specific user:
After the creation of the user, you can edit any parameter defined during the creation. After the creation of the user, you can edit any parameter defined during the creation.
To modify a user object, go to **Directory > Users**, and click the edit icon beside the name. To modify a user object, go to **Directory > Users**, and click the edit icon beside the name. You can also go into [user details](#view-user-details), and click **Edit**.
You can also go into [user details](#view-user-details), and click **Edit**. ### Assign, modify, or remove permissions for a user
## User recovery You can grant a user specific global or object-level permissions. Alternatively, you can add a user to a group that has the appropriate permissions, and the user inherits all of the group's permissions.
For more information, review ["Permissions"](../access-control/permissions.md).
## Add a user to a group
1. To add a user to a group, navigate to **Directory > Users** to display all users.
2. Click the name of the user to display the full user details page.
3. Click the **Groups** tab, and then click either **Add to existing group** or **Add to new group**.
## User credentials recovery
If a user has lost their credentials, there are several options. If a user has lost their credentials, there are several options.
### Email them a recovery link ### Email them a recovery link
1. In the Admin interface, navigate to **Directory > Users** to display all users. 1. In the Admin interface, navigate to **Directory > Users** to display all users.
2. Either click the name of the user to display the full User details page, or click the chevron (the symbol) beside their name to expand the options.
2. Either click the name of the user to display the full User details page, or click the chevron (the symbol) beside their name to expand the toptions.
3. To generate a recovery link, which you can then copy and paste into an email, click **View recovery link**. 3. To generate a recovery link, which you can then copy and paste into an email, click **View recovery link**.
A pop-up will appear on your browser with the link for you to copy and to send to the user. A pop-up will appear on your browser with the link for you to copy and to send to the user.
### Automate email to a user ### Automate email to a user
You can use our automated email to send a link with the URL for the user to reset their password. This option will only work if you have properly [configured a SMTP server during the installation](../../installation/docker-compose#email-configuration-optional-but-recommended) and set an email address for the user. You can use our automated email to send a link with the URL for the user to reset their password. This option will only work if you have properly [configured a SMTP server during the installation](../../installation/docker-compose#email-configuration-optional-but-recommended) and set an email address for the user.
1. In the Admin interface, navigate to **Directory > Users** to display all users. 1. In the Admin interface, navigate to **Directory > Users** to display all users.
2. Either click the name of the user to display the full User details page, or click the chevron beside their name to expand the toptions. 2. Either click the name of the user to display the full User details page, or click the chevron beside their name to expand the toptions.
3. To send the automated email to the user, click **Email recovery link**. 3. To send the automated email to the user, click **Email recovery link**.
If the user does not receive the email, check if the mail server parameters [are properly configured](../../troubleshooting/emails). If the user does not receive the email, check if the mail server parameters [are properly configured](../../troubleshooting/emails).
@ -88,9 +91,7 @@ If the user does not receive the email, check if the mail server parameters [are
As an Admin, you can simply reset the password for the user. As an Admin, you can simply reset the password for the user.
1. In the Admin interface, navigate to **Directory > Users** to display all users. 1. In the Admin interface, navigate to **Directory > Users** to display all users.
2. Either click the name of the user to display the full User details page, or click the chevron beside their name to expand the toptions. 2. Either click the name of the user to display the full User details page, or click the chevron beside their name to expand the toptions.
3. To reset the user's password, click **Reset password**, and then define the new value. 3. To reset the user's password, click **Reset password**, and then define the new value.
## Deactivate or Delete user ## Deactivate or Delete user
@ -98,7 +99,6 @@ As an Admin, you can simply reset the password for the user.
#### To deactivate a user: #### To deactivate a user:
1. Go into the user list or detail, and click **Deactivate**. 1. Go into the user list or detail, and click **Deactivate**.
2. Review the changes and click **Update**. 2. Review the changes and click **Update**.
The active sessions are revoked and the authentication of the user blocked. You can reactivate the account by following the same procedure. The active sessions are revoked and the authentication of the user blocked. You can reactivate the account by following the same procedure.
@ -111,7 +111,6 @@ You may instead deactivate the account to preserve identity data.
::: :::
1. Go into the user list and select one (or multiple users) to delete and click **Delete** on the top-right of the page. 1. Go into the user list and select one (or multiple users) to delete and click **Delete** on the top-right of the page.
2. Review the changes and click **Delete**. 2. Review the changes and click **Delete**.
The user list refreshes and no longer displays the removed users. The user list refreshes and no longer displays the removed users.

View File

@ -43,7 +43,7 @@ elif ak_is_group_member(request.user, name="Minio users"):
return None return None
``` ```
Note that you can assign multiple policies to a user by returning a list, and returning `None` will map no policies to the user, resulting in no access to the MinIO instance. For more information on writing expressions, see [Expressions](../../../docs/property-mappings/expression) and [User](../../../docs/user-group/user#object-attributes) docs. Note that you can assign multiple policies to a user by returning a list, and returning `None` will map no policies to the user, resulting in no access to the MinIO instance. For more information on writing expressions, see [Expressions](../../../docs/property-mappings/expression) and [User](../../../docs/user-group-role/user/user_ref#object-properties) docs.
### Creating application and provider ### Creating application and provider

View File

@ -13,7 +13,7 @@ const docsSidebar = {
{ {
type: "category", type: "category",
label: "Installation", label: "Installation",
collapsed: false, collapsed: true,
link: { link: {
type: "doc", type: "doc",
id: "installation/index", id: "installation/index",
@ -259,22 +259,51 @@ const docsSidebar = {
}, },
{ {
type: "category", type: "category",
label: "Users & Groups", label: "Users, Groups, & Roles",
items: [ items: [
{ {
type: "category", type: "category",
label: "Users", label: "Users",
link: { link: {
type: "doc", type: "doc",
id: "user-group/user/index", id: "user-group-role/user/index",
}, },
items: [ items: [
"user-group/user/user_basic_operations", "user-group-role/user/user_basic_operations",
"user-group/user/user_ref", "user-group-role/user/user_ref",
"user-group/user/invitations", "user-group-role/user/invitations",
],
},
{
type: "category",
label: "Groups",
link: {
type: "doc",
id: "user-group-role/groups/index",
},
items: ["user-group-role/groups/manage_groups"],
},
{
type: "category",
label: "Roles",
link: {
type: "doc",
id: "user-group-role/roles/index",
},
items: ["user-group-role/roles/manage_roles"],
},
{
type: "category",
label: "Access control",
link: {
type: "doc",
id: "user-group-role/access-control/index",
},
items: [
"user-group-role/access-control/permissions",
"user-group-role/access-control/manage_permissions",
], ],
}, },
"user-group/group",
], ],
}, },
{ {
@ -287,13 +316,14 @@ const docsSidebar = {
description: "Release notes for recent authentik versions", description: "Release notes for recent authentik versions",
}, },
items: [ items: [
"releases/2023/v2023.10",
"releases/2023/v2023.8", "releases/2023/v2023.8",
"releases/2023/v2023.6", "releases/2023/v2023.6",
"releases/2023/v2023.5",
{ {
type: "category", type: "category",
label: "Previous versions", label: "Previous versions",
items: [ items: [
"releases/2023/v2023.5",
"releases/2023/v2023.4", "releases/2023/v2023.4",
"releases/2023/v2023.3", "releases/2023/v2023.3",
"releases/2023/v2023.2", "releases/2023/v2023.2",