Compare commits
9 Commits
main
...
user-direc
Author | SHA1 | Date | |
---|---|---|---|
f97f4a902c | |||
de8da15293 | |||
cd7d96cc58 | |||
dcaa41716b | |||
910b430d25 | |||
832c00c155 | |||
399fa0120c | |||
3de3c98ed8 | |||
92911d1d0f |
99
authentik/core/api/user_directory.py
Normal file
99
authentik/core/api/user_directory.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
"""User directory API Views"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||||
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
from rest_framework.serializers import CharField, DictField, ListField, ModelSerializer
|
||||||
|
from rest_framework.views import Request, Response
|
||||||
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import User
|
||||||
|
from authentik.rbac.permissions import HasPermission
|
||||||
|
from authentik.tenants.utils import get_current_tenant
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class UserDirectorySerializer(ModelSerializer):
|
||||||
|
"""User Directory Serializer"""
|
||||||
|
|
||||||
|
user_fields = SerializerMethodField()
|
||||||
|
attributes = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"user_fields",
|
||||||
|
"attributes",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_user_fields(self, obj: User) -> dict[str, Any]:
|
||||||
|
"""Get directory fields"""
|
||||||
|
fields = {}
|
||||||
|
user_directory_fields = get_current_tenant().user_directory_fields
|
||||||
|
for f in ("name", "username", "email", "avatar"):
|
||||||
|
if f in user_directory_fields:
|
||||||
|
fields[f] = getattr(obj, f)
|
||||||
|
if "groups" in user_directory_fields:
|
||||||
|
fields["groups"] = [g.name for g in obj.all_groups().order_by("name")]
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def get_attributes(self, obj: User) -> dict[str, Any]:
|
||||||
|
"""Get directory attributes"""
|
||||||
|
attributes = {}
|
||||||
|
for field in get_current_tenant().user_directory_attributes:
|
||||||
|
path = field.get("attribute", None)
|
||||||
|
if path is not None:
|
||||||
|
attributes[path] = obj.attributes.get(path, None)
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
class UserDirectoryViewSet(ReadOnlyModelViewSet):
|
||||||
|
"""User Directory Viewset"""
|
||||||
|
|
||||||
|
queryset = User.objects.none()
|
||||||
|
ordering = ["username"]
|
||||||
|
ordering_fields = ["username", "email", "name"]
|
||||||
|
serializer_class = UserDirectorySerializer
|
||||||
|
permission_classes = [HasPermission("authentik_rbac.view_user_directory")]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return User.objects.all().exclude(pk=get_anonymous_user().pk).filter(is_active=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def search_fields(self):
|
||||||
|
"""Get search fields"""
|
||||||
|
current_tenant = get_current_tenant()
|
||||||
|
return list(
|
||||||
|
f for f in current_tenant.user_directory_fields if f not in ("avatar", "groups")
|
||||||
|
) + list(
|
||||||
|
f"attributes__{attr['attribute']}"
|
||||||
|
for attr in current_tenant.user_directory_attributes
|
||||||
|
if "attribute" in attr
|
||||||
|
)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
responses={
|
||||||
|
200: inline_serializer(
|
||||||
|
"UserDirectoryFieldsSerializer",
|
||||||
|
{
|
||||||
|
"fields": ListField(child=CharField()),
|
||||||
|
"attributes": ListField(child=DictField(child=CharField())),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=False, pagination_class=None)
|
||||||
|
def fields(self, request: Request) -> Response:
|
||||||
|
"""Get user directory fields"""
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"fields": request.tenant.user_directory_fields,
|
||||||
|
"attributes": request.tenant.user_directory_attributes,
|
||||||
|
}
|
||||||
|
)
|
@ -17,6 +17,7 @@ from authentik.core.api.providers import ProviderViewSet
|
|||||||
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
||||||
from authentik.core.api.tokens import TokenViewSet
|
from authentik.core.api.tokens import TokenViewSet
|
||||||
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
||||||
|
from authentik.core.api.user_directory import UserDirectoryViewSet
|
||||||
from authentik.core.api.users import UserViewSet
|
from authentik.core.api.users import UserViewSet
|
||||||
from authentik.core.views import apps
|
from authentik.core.views import apps
|
||||||
from authentik.core.views.debug import AccessDeniedView
|
from authentik.core.views.debug import AccessDeniedView
|
||||||
@ -82,6 +83,7 @@ api_urlpatterns = [
|
|||||||
),
|
),
|
||||||
("core/groups", GroupViewSet),
|
("core/groups", GroupViewSet),
|
||||||
("core/users", UserViewSet),
|
("core/users", UserViewSet),
|
||||||
|
("core/user_directory", UserDirectoryViewSet),
|
||||||
("core/tokens", TokenViewSet),
|
("core/tokens", TokenViewSet),
|
||||||
("sources/all", SourceViewSet),
|
("sources/all", SourceViewSet),
|
||||||
("sources/user_connections/all", UserSourceConnectionViewSet),
|
("sources/user_connections/all", UserSourceConnectionViewSet),
|
||||||
|
@ -4,7 +4,6 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("authentik_rbac", "0003_alter_systempermission_options"),
|
("authentik_rbac", "0003_alter_systempermission_options"),
|
||||||
]
|
]
|
||||||
@ -17,6 +16,9 @@ class Migration(migrations.Migration):
|
|||||||
"managed": False,
|
"managed": False,
|
||||||
"permissions": [
|
"permissions": [
|
||||||
("view_system_info", "Can view system info"),
|
("view_system_info", "Can view system info"),
|
||||||
|
("view_system_tasks", "Can view system tasks"),
|
||||||
|
("view_user_directory", "Can view users in the user directory"),
|
||||||
|
("run_system_tasks", "Can run system tasks"),
|
||||||
("access_admin_interface", "Can access admin interface"),
|
("access_admin_interface", "Can access admin interface"),
|
||||||
("view_system_settings", "Can view system settings"),
|
("view_system_settings", "Can view system settings"),
|
||||||
("edit_system_settings", "Can edit system settings"),
|
("edit_system_settings", "Can edit system settings"),
|
||||||
|
@ -67,6 +67,9 @@ class SystemPermission(models.Model):
|
|||||||
verbose_name_plural = _("System permissions")
|
verbose_name_plural = _("System permissions")
|
||||||
permissions = [
|
permissions = [
|
||||||
("view_system_info", _("Can view system info")),
|
("view_system_info", _("Can view system info")),
|
||||||
|
("view_system_tasks", _("Can view system tasks")),
|
||||||
|
("view_user_directory", _("Can view users in the user directory")),
|
||||||
|
("run_system_tasks", _("Can run system tasks")),
|
||||||
("access_admin_interface", _("Can access admin interface")),
|
("access_admin_interface", _("Can access admin interface")),
|
||||||
("view_system_settings", _("Can view system settings")),
|
("view_system_settings", _("Can view system settings")),
|
||||||
("edit_system_settings", _("Can edit system settings")),
|
("edit_system_settings", _("Can edit system settings")),
|
||||||
|
@ -23,6 +23,8 @@ class SettingsSerializer(ModelSerializer):
|
|||||||
"footer_links",
|
"footer_links",
|
||||||
"gdpr_compliance",
|
"gdpr_compliance",
|
||||||
"impersonation",
|
"impersonation",
|
||||||
|
"user_directory_fields",
|
||||||
|
"user_directory_attributes",
|
||||||
"default_token_duration",
|
"default_token_duration",
|
||||||
"default_token_length",
|
"default_token_length",
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.0.1 on 2024-01-24 14:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import authentik.tenants.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("authentik_tenants", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tenant",
|
||||||
|
name="user_directory_attributes",
|
||||||
|
field=models.JSONField(
|
||||||
|
blank=True, default=list, help_text="Attributes to show in the user directory."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tenant",
|
||||||
|
name="user_directory_fields",
|
||||||
|
field=models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
default=authentik.tenants.models._default_user_directory_fields,
|
||||||
|
help_text="Fields to show in the user directory.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
13
authentik/tenants/migrations/0004_merge_20240524_1807.py
Normal file
13
authentik/tenants/migrations/0004_merge_20240524_1807.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-24 18:07
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_tenants", "0002_tenant_user_directory_and_more"),
|
||||||
|
("authentik_tenants", "0003_alter_tenant_default_token_duration"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
@ -37,6 +37,10 @@ def _validate_schema_name(name):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _default_user_directory_fields():
|
||||||
|
return ["avatar", "name", "username", "email", "groups"]
|
||||||
|
|
||||||
|
|
||||||
class Tenant(TenantMixin, SerializerModel):
|
class Tenant(TenantMixin, SerializerModel):
|
||||||
"""Tenant"""
|
"""Tenant"""
|
||||||
|
|
||||||
@ -85,6 +89,14 @@ class Tenant(TenantMixin, SerializerModel):
|
|||||||
impersonation = models.BooleanField(
|
impersonation = models.BooleanField(
|
||||||
help_text=_("Globally enable/disable impersonation."), default=True
|
help_text=_("Globally enable/disable impersonation."), default=True
|
||||||
)
|
)
|
||||||
|
user_directory_fields = models.JSONField(
|
||||||
|
help_text=_("Fields to show in the user directory."),
|
||||||
|
default=_default_user_directory_fields,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
user_directory_attributes = models.JSONField(
|
||||||
|
help_text=_("Attributes to show in the user directory."), default=list, blank=True
|
||||||
|
)
|
||||||
default_token_duration = models.TextField(
|
default_token_duration = models.TextField(
|
||||||
help_text=_("Default token duration"),
|
help_text=_("Default token duration"),
|
||||||
default=DEFAULT_TOKEN_DURATION,
|
default=DEFAULT_TOKEN_DURATION,
|
||||||
|
175
schema.yml
175
schema.yml
@ -4537,6 +4537,119 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
description: ''
|
description: ''
|
||||||
|
/core/user_directory/:
|
||||||
|
get:
|
||||||
|
operationId: core_user_directory_list
|
||||||
|
description: User Directory Viewset
|
||||||
|
parameters:
|
||||||
|
- name: ordering
|
||||||
|
required: false
|
||||||
|
in: query
|
||||||
|
description: Which field to use when ordering the results.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: page
|
||||||
|
required: false
|
||||||
|
in: query
|
||||||
|
description: A page number within the paginated result set.
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- name: page_size
|
||||||
|
required: false
|
||||||
|
in: query
|
||||||
|
description: Number of results to return per page.
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- name: search
|
||||||
|
required: false
|
||||||
|
in: query
|
||||||
|
description: A search term.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
tags:
|
||||||
|
- core
|
||||||
|
security:
|
||||||
|
- authentik: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PaginatedUserDirectoryList'
|
||||||
|
description: ''
|
||||||
|
'400':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
description: ''
|
||||||
|
'403':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GenericError'
|
||||||
|
description: ''
|
||||||
|
/core/user_directory/{id}/:
|
||||||
|
get:
|
||||||
|
operationId: core_user_directory_retrieve
|
||||||
|
description: User Directory Viewset
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
description: A unique integer value identifying this User.
|
||||||
|
required: true
|
||||||
|
tags:
|
||||||
|
- core
|
||||||
|
security:
|
||||||
|
- authentik: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserDirectory'
|
||||||
|
description: ''
|
||||||
|
'400':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
description: ''
|
||||||
|
'403':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GenericError'
|
||||||
|
description: ''
|
||||||
|
/core/user_directory/fields/:
|
||||||
|
get:
|
||||||
|
operationId: core_user_directory_fields_retrieve
|
||||||
|
description: Get user directory fields
|
||||||
|
tags:
|
||||||
|
- core
|
||||||
|
security:
|
||||||
|
- authentik: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserDirectoryFields'
|
||||||
|
description: ''
|
||||||
|
'400':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ValidationError'
|
||||||
|
description: ''
|
||||||
|
'403':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GenericError'
|
||||||
|
description: ''
|
||||||
/core/users/:
|
/core/users/:
|
||||||
get:
|
get:
|
||||||
operationId: core_users_list
|
operationId: core_users_list
|
||||||
@ -40859,6 +40972,18 @@ components:
|
|||||||
required:
|
required:
|
||||||
- pagination
|
- pagination
|
||||||
- results
|
- results
|
||||||
|
PaginatedUserDirectoryList:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pagination:
|
||||||
|
$ref: '#/components/schemas/Pagination'
|
||||||
|
results:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/UserDirectory'
|
||||||
|
required:
|
||||||
|
- pagination
|
||||||
|
- results
|
||||||
PaginatedUserList:
|
PaginatedUserList:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -43614,6 +43739,10 @@ components:
|
|||||||
impersonation:
|
impersonation:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Globally enable/disable impersonation.
|
description: Globally enable/disable impersonation.
|
||||||
|
user_directory_fields:
|
||||||
|
description: Fields to show in the user directory.
|
||||||
|
user_directory_attributes:
|
||||||
|
description: Attributes to show in the user directory.
|
||||||
default_token_duration:
|
default_token_duration:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
@ -46901,6 +47030,10 @@ components:
|
|||||||
impersonation:
|
impersonation:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Globally enable/disable impersonation.
|
description: Globally enable/disable impersonation.
|
||||||
|
user_directory_fields:
|
||||||
|
description: Fields to show in the user directory.
|
||||||
|
user_directory_attributes:
|
||||||
|
description: Attributes to show in the user directory.
|
||||||
default_token_duration:
|
default_token_duration:
|
||||||
type: string
|
type: string
|
||||||
description: Default token duration
|
description: Default token duration
|
||||||
@ -46940,6 +47073,10 @@ components:
|
|||||||
impersonation:
|
impersonation:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Globally enable/disable impersonation.
|
description: Globally enable/disable impersonation.
|
||||||
|
user_directory_fields:
|
||||||
|
description: Fields to show in the user directory.
|
||||||
|
user_directory_attributes:
|
||||||
|
description: Attributes to show in the user directory.
|
||||||
default_token_duration:
|
default_token_duration:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
@ -48032,6 +48169,44 @@ components:
|
|||||||
$ref: '#/components/schemas/FlowSetRequest'
|
$ref: '#/components/schemas/FlowSetRequest'
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
|
UserDirectory:
|
||||||
|
type: object
|
||||||
|
description: User Directory Serializer
|
||||||
|
properties:
|
||||||
|
pk:
|
||||||
|
type: integer
|
||||||
|
readOnly: true
|
||||||
|
title: ID
|
||||||
|
user_fields:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
|
description: Get directory fields
|
||||||
|
readOnly: true
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
|
description: Get directory attributes
|
||||||
|
readOnly: true
|
||||||
|
required:
|
||||||
|
- attributes
|
||||||
|
- pk
|
||||||
|
- user_fields
|
||||||
|
UserDirectoryFields:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
fields:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
attributes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- attributes
|
||||||
|
- fields
|
||||||
UserFieldsEnum:
|
UserFieldsEnum:
|
||||||
enum:
|
enum:
|
||||||
- email
|
- email
|
||||||
|
@ -193,6 +193,42 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
|||||||
help=${msg("Globally enable/disable impersonation.")}
|
help=${msg("Globally enable/disable impersonation.")}
|
||||||
>
|
>
|
||||||
</ak-switch-input>
|
</ak-switch-input>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("User directory fields")}
|
||||||
|
name="userDirectoryFields"
|
||||||
|
>
|
||||||
|
<ak-codemirror
|
||||||
|
mode=${CodeMirrorMode.YAML}
|
||||||
|
.value="${first(this._settings?.userDirectoryFields, [
|
||||||
|
"name",
|
||||||
|
"username",
|
||||||
|
"email",
|
||||||
|
"avatars",
|
||||||
|
"groups",
|
||||||
|
])}"
|
||||||
|
></ak-codemirror>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg(
|
||||||
|
"This option configures what user fields are shown in the user directory. It must be a valid JSON list and can be used as follows, with all possible values included:",
|
||||||
|
)}
|
||||||
|
<code>["name", "username", "email", "avatars", "groups"]</code>
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("User directory attributes")}
|
||||||
|
name="userDirectoryAttributes"
|
||||||
|
>
|
||||||
|
<ak-codemirror
|
||||||
|
mode=${CodeMirrorMode.YAML}
|
||||||
|
.value="${first(this._settings?.userDirectoryAttributes, [])}"
|
||||||
|
></ak-codemirror>
|
||||||
|
<p class="pf-c-form__helper-text">
|
||||||
|
${msg(
|
||||||
|
"This option configures what user attributes are shown in the user directory. It must be a valid JSON list and can be used as follows:",
|
||||||
|
)}
|
||||||
|
<code>[{"attribute": "phone_number", "display_name": "Phone"}]</code>
|
||||||
|
</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
<ak-text-input
|
<ak-text-input
|
||||||
name="defaultTokenDuration"
|
name="defaultTokenDuration"
|
||||||
label=${msg("Default token duration")}
|
label=${msg("Default token duration")}
|
||||||
|
@ -53,7 +53,7 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
|
|||||||
return msg("Automate and template configuration within authentik.");
|
return msg("Automate and template configuration within authentik.");
|
||||||
}
|
}
|
||||||
pageIcon(): string {
|
pageIcon(): string {
|
||||||
return "pf-icon pf-icon-blueprint";
|
return "fa fa-user";
|
||||||
}
|
}
|
||||||
|
|
||||||
expandable = true;
|
expandable = true;
|
||||||
|
@ -141,6 +141,7 @@ export class PageHeader extends WithBrandConfig(AKElement) {
|
|||||||
return html` <ak-enterprise-status interface="admin"></ak-enterprise-status>
|
return html` <ak-enterprise-status interface="admin"></ak-enterprise-status>
|
||||||
<div class="bar">
|
<div class="bar">
|
||||||
<button
|
<button
|
||||||
|
part="sidebar-trigger"
|
||||||
class="sidebar-trigger pf-c-button pf-m-plain"
|
class="sidebar-trigger pf-c-button pf-m-plain"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
|
@ -70,14 +70,17 @@ export abstract class TablePage<T> extends Table<T> {
|
|||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
renderPageHeader(): TemplateResult {
|
||||||
return html`<ak-page-header
|
return html`<ak-page-header
|
||||||
icon=${this.pageIcon()}
|
icon=${this.pageIcon()}
|
||||||
header=${this.pageTitle()}
|
header=${this.pageTitle()}
|
||||||
description=${ifDefined(this.pageDescription())}
|
description=${ifDefined(this.pageDescription())}
|
||||||
>
|
>
|
||||||
</ak-page-header>
|
</ak-page-header>`;
|
||||||
${this.renderSectionBefore()}
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return html`${this.renderPageHeader()} ${this.renderSectionBefore()}
|
||||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||||
<div class="pf-c-sidebar pf-m-gutter">
|
<div class="pf-c-sidebar pf-m-gutter">
|
||||||
<div class="pf-c-sidebar__main">
|
<div class="pf-c-sidebar__main">
|
||||||
|
@ -8,6 +8,10 @@ export const ROUTES: Route[] = [
|
|||||||
new Route(new RegExp("^/$")).redirect("/library"),
|
new Route(new RegExp("^/$")).redirect("/library"),
|
||||||
new Route(new RegExp("^#.*")).redirect("/library"),
|
new Route(new RegExp("^#.*")).redirect("/library"),
|
||||||
new Route(new RegExp("^/library$"), async () => html`<ak-library></ak-library>`),
|
new Route(new RegExp("^/library$"), async () => html`<ak-library></ak-library>`),
|
||||||
|
new Route(new RegExp("^/directory"), async () => {
|
||||||
|
await import("@goauthentik/user/user-directory/UserDirectoryPage");
|
||||||
|
return html`<ak-user-directory></ak-user-directory>`;
|
||||||
|
}),
|
||||||
new Route(new RegExp("^/settings$"), async () => {
|
new Route(new RegExp("^/settings$"), async () => {
|
||||||
await import("@goauthentik/user/user-settings/UserSettingsPage");
|
await import("@goauthentik/user/user-settings/UserSettingsPage");
|
||||||
return html`<ak-user-settings></ak-user-settings>`;
|
return html`<ak-user-settings></ak-user-settings>`;
|
||||||
|
@ -159,6 +159,13 @@ class UserInterfacePresentation extends AKElement {
|
|||||||
.otherwise(() => this.me.user.username);
|
.otherwise(() => this.me.user.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canAccessUserDirectory() {
|
||||||
|
return (
|
||||||
|
this.me.user.isSuperuser ||
|
||||||
|
this.me.user.systemPermissions.includes("can_view_user_directory")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get canAccessAdmin() {
|
get canAccessAdmin() {
|
||||||
return (
|
return (
|
||||||
this.me.user.isSuperuser ||
|
this.me.user.isSuperuser ||
|
||||||
@ -204,6 +211,8 @@ class UserInterfacePresentation extends AKElement {
|
|||||||
<!-- -->
|
<!-- -->
|
||||||
${this.renderNotificationDrawerTrigger()}
|
${this.renderNotificationDrawerTrigger()}
|
||||||
<!-- -->
|
<!-- -->
|
||||||
|
${this.renderUserDirectory()}
|
||||||
|
<!-- -->
|
||||||
${this.renderSettings()}
|
${this.renderSettings()}
|
||||||
<div class="pf-c-page__header-tools-item">
|
<div class="pf-c-page__header-tools-item">
|
||||||
<a
|
<a
|
||||||
@ -355,6 +364,20 @@ class UserInterfacePresentation extends AKElement {
|
|||||||
</a>`;
|
</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderUserDirectory() {
|
||||||
|
if (!this.canAccessUserDirectory) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html` <div class="pf-c-page__header-tools-item">
|
||||||
|
<a class="pf-c-button pf-m-plain" type="button" href="#/directory">
|
||||||
|
<pf-tooltip position="top" content=${msg("User directory")}>
|
||||||
|
<i class="pf-icon pf-icon-project" aria-hidden="true"></i>
|
||||||
|
</pf-tooltip>
|
||||||
|
</a>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
renderImpersonation() {
|
renderImpersonation() {
|
||||||
if (!this.me.original) {
|
if (!this.me.original) {
|
||||||
return nothing;
|
return nothing;
|
||||||
|
131
web/src/user/user-directory/UserDirectoryPage.ts
Normal file
131
web/src/user/user-directory/UserDirectoryPage.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import { renderDescriptionList } from "@goauthentik/components/DescriptionList.js";
|
||||||
|
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||||
|
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||||
|
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { css, html } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||||
|
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
||||||
|
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||||
|
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||||
|
|
||||||
|
import { CoreApi, UserDirectory } from "@goauthentik/api";
|
||||||
|
|
||||||
|
const knownFields: Record<string, string> = {
|
||||||
|
avatar: "",
|
||||||
|
username: msg("Username"),
|
||||||
|
name: msg("Name"),
|
||||||
|
email: msg("Email"),
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserFieldAttributes = { display_name: string; attribute: string };
|
||||||
|
|
||||||
|
@customElement("ak-user-directory")
|
||||||
|
export class UserDirectoryPage extends TablePage<UserDirectory> {
|
||||||
|
expandable = true;
|
||||||
|
|
||||||
|
searchEnabled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageTitle(): string {
|
||||||
|
return msg("User Directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
pageDescription(): string {
|
||||||
|
return msg("Display a list of users on this system.");
|
||||||
|
}
|
||||||
|
|
||||||
|
pageIcon(): string {
|
||||||
|
return "pf-icon pf-icon-project";
|
||||||
|
}
|
||||||
|
|
||||||
|
@property()
|
||||||
|
order = "username";
|
||||||
|
|
||||||
|
@state()
|
||||||
|
fields?: string[];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
userFieldAttributes?: object[] = [];
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
...super.styles,
|
||||||
|
PFDescriptionList,
|
||||||
|
PFCard,
|
||||||
|
PFAlert,
|
||||||
|
PFAvatar,
|
||||||
|
css`
|
||||||
|
ak-page-header::part(sidebar-trigger) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async apiEndpoint(): Promise<PaginatedResponse<UserDirectory>> {
|
||||||
|
const fields = await new CoreApi(DEFAULT_CONFIG).coreUserDirectoryFieldsRetrieve();
|
||||||
|
this.fields = fields.fields;
|
||||||
|
this.userFieldAttributes = fields.attributes;
|
||||||
|
return await new CoreApi(DEFAULT_CONFIG).coreUserDirectoryList(
|
||||||
|
await this.defaultEndpointConfig(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
columns() {
|
||||||
|
return (this.fields ?? [])
|
||||||
|
.filter((item) => item in knownFields)
|
||||||
|
.map((item) =>
|
||||||
|
item === "avatar"
|
||||||
|
? new TableColumn(knownFields[item])
|
||||||
|
: new TableColumn(knownFields[item], item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
row(item: UserDirectory) {
|
||||||
|
return (this.fields ?? [])
|
||||||
|
.filter((field: string) => Object.hasOwn(knownFields, field))
|
||||||
|
.map((field: string) =>
|
||||||
|
field !== "avatar"
|
||||||
|
? html`${item.userFields[field]}`
|
||||||
|
: html` <img
|
||||||
|
class="pf-c-avatar"
|
||||||
|
src=${item.userFields[field]}
|
||||||
|
alt="${msg("Avatar image")}"
|
||||||
|
/>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderExpanded(item: UserDirectory) {
|
||||||
|
const groupDescription =
|
||||||
|
this.fields?.includes("groups") && (item.userFields["groups"] ?? []).length > 0
|
||||||
|
? [
|
||||||
|
[msg("Groups")],
|
||||||
|
item.userFields["groups"].map(
|
||||||
|
(group: string) => html`
|
||||||
|
<div class="pf-c-description-list__text">${group}</div>
|
||||||
|
`,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const userDescriptions = ((this.userFieldAttributes ?? []) as UserFieldAttributes[])
|
||||||
|
.filter(({ attribute }) => attribute !== null)
|
||||||
|
.map(({ display_name, attribute }) => [display_name, item.attributes[attribute]]);
|
||||||
|
|
||||||
|
const toShow = [...groupDescription, ...userDescriptions];
|
||||||
|
|
||||||
|
return toShow.length > 1
|
||||||
|
? html`<td role="cell" colspan="3">
|
||||||
|
<div class="pf-c-table__expandable-row-content">
|
||||||
|
${renderDescriptionList(toShow)}
|
||||||
|
</div>
|
||||||
|
</td>`
|
||||||
|
: html``;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user