Compare commits
	
		
			9 Commits
		
	
	
		
			enterprise
			...
			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
	