From 557724768a51d08318486e76172d885616084d68 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 10 Aug 2021 13:54:59 +0200 Subject: [PATCH] core: add API to directly send recovery link to user Signed-off-by: Jens Langhammer --- authentik/core/api/users.py | 102 +++++++-- authentik/core/tests/test_users_api.py | 78 +++++++ schema.yml | 30 +++ web/src/locales/en.po | 74 ++++++- web/src/locales/pseudo-LOCALE.po | 74 ++++++- web/src/pages/outposts/OutpostForm.ts | 2 +- web/src/pages/users/UserListPage.ts | 244 ++++++++++++++-------- web/src/pages/users/UserResetEmailForm.ts | 44 ++++ 8 files changed, 521 insertions(+), 127 deletions(-) create mode 100644 web/src/pages/users/UserResetEmailForm.ts diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index ce014f89b..9fc51ebeb 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -1,13 +1,16 @@ """User API Views""" from json import loads +from typing import Optional from django.db.models.query import QuerySet from django.urls import reverse_lazy from django.utils.http import urlencode +from django.utils.translation import gettext as _ from django_filters.filters import BooleanFilter, CharFilter from django_filters.filterset import FilterSet -from drf_spectacular.utils import extend_schema, extend_schema_field -from guardian.utils import get_anonymous_user +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_field +from guardian.shortcuts import get_anonymous_user, get_objects_for_user from rest_framework.decorators import action from rest_framework.fields import CharField, JSONField, SerializerMethodField from rest_framework.permissions import IsAuthenticated @@ -17,10 +20,12 @@ from rest_framework.serializers import ( BooleanField, ListSerializer, ModelSerializer, + Serializer, ValidationError, ) from rest_framework.viewsets import ModelViewSet from rest_framework_guardian.filters import ObjectPermissionsFilter +from structlog.stdlib import get_logger from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h from authentik.api.decorators import permission_required @@ -30,8 +35,13 @@ from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER from authentik.core.models import Token, TokenIntents, User from authentik.events.models import EventAction +from authentik.stages.email.models import EmailStage +from authentik.stages.email.tasks import send_mails +from authentik.stages.email.utils import TemplateEmailMessage from authentik.tenants.models import Tenant +LOGGER = get_logger() + class UserSerializer(ModelSerializer): """User Serializer""" @@ -171,6 +181,28 @@ class UserViewSet(UsedByMixin, ModelViewSet): def get_queryset(self): # pragma: no cover return User.objects.all().exclude(pk=get_anonymous_user().pk) + def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]: + """Create a recovery link (when the current tenant has a recovery flow set), + that can either be shown to an admin or sent to the user directly""" + tenant: Tenant = self.request._request.tenant + # Check that there is a recovery flow, if not return an error + flow = tenant.flow_recovery + if not flow: + LOGGER.debug("No recovery flow set") + return None, None + user: User = self.get_object() + token, __ = Token.objects.get_or_create( + identifier=f"{user.uid}-password-reset", + user=user, + intent=TokenIntents.INTENT_RECOVERY, + ) + querystring = urlencode({"token": token.key}) + link = self.request.build_absolute_uri( + reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) + + f"?{querystring}" + ) + return link, token + @extend_schema(responses={200: SessionUserSerializer(many=False)}) @action(detail=False, pagination_class=None, filter_backends=[]) # pylint: disable=invalid-name @@ -226,24 +258,60 @@ class UserViewSet(UsedByMixin, ModelViewSet): # pylint: disable=invalid-name, unused-argument def recovery(self, request: Request, pk: int) -> Response: """Create a temporary link that a user can use to recover their accounts""" - tenant: Tenant = request._request.tenant - # Check that there is a recovery flow, if not return an error - flow = tenant.flow_recovery - if not flow: + link, _ = self._create_recovery_link() + if not link: + LOGGER.debug("Couldn't create token") return Response({"link": ""}, status=404) - user: User = self.get_object() - token, __ = Token.objects.get_or_create( - identifier=f"{user.uid}-password-reset", - user=user, - intent=TokenIntents.INTENT_RECOVERY, - ) - querystring = urlencode({"token": token.key}) - link = request.build_absolute_uri( - reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) - + f"?{querystring}" - ) return Response({"link": link}) + @permission_required("authentik_core.reset_user_password") + @extend_schema( + parameters=[ + OpenApiParameter( + name="email_stage", + location=OpenApiParameter.QUERY, + type=OpenApiTypes.STR, + required=True, + ) + ], + responses={ + "204": Serializer(), + "404": Serializer(), + }, + ) + @action(detail=True, pagination_class=None, filter_backends=[]) + # pylint: disable=invalid-name, unused-argument + def recovery_email(self, request: Request, pk: int) -> Response: + """Create a temporary link that a user can use to recover their accounts""" + for_user = self.get_object() + if for_user.email == "": + LOGGER.debug("User doesn't have an email address") + return Response(status=404) + link, token = self._create_recovery_link() + if not link: + LOGGER.debug("Couldn't create token") + return Response(status=404) + # Lookup the email stage to assure the current user can access it + stages = get_objects_for_user( + request.user, "authentik_stages_email.view_emailstage" + ).filter(pk=request.query_params.get("email_stage")) + if not stages.exists(): + LOGGER.debug("Email stage does not exist/user has no permissions") + return Response(status=404) + email_stage: EmailStage = stages.first() + message = TemplateEmailMessage( + subject=_(email_stage.subject), + template_name=email_stage.template, + to=[for_user.email], + template_context={ + "url": link, + "user": for_user, + "expires": token.expires, + }, + ) + send_mails(email_stage, message) + return Response(status=204) + def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: """Custom filter_queryset method which ignores guardian, but still supports sorting""" for backend in list(self.filter_backends): diff --git a/authentik/core/tests/test_users_api.py b/authentik/core/tests/test_users_api.py index f2f3cdd72..2567c9908 100644 --- a/authentik/core/tests/test_users_api.py +++ b/authentik/core/tests/test_users_api.py @@ -3,6 +3,9 @@ from django.urls.base import reverse from rest_framework.test import APITestCase from authentik.core.models import User +from authentik.flows.models import Flow, FlowDesignation +from authentik.stages.email.models import EmailStage +from authentik.tenants.models import Tenant class TestUsersAPI(APITestCase): @@ -27,3 +30,78 @@ class TestUsersAPI(APITestCase): reverse("authentik_api:user-metrics", kwargs={"pk": self.user.pk}) ) self.assertEqual(response.status_code, 403) + + def test_recovery_no_flow(self): + """Test user recovery link (no recovery flow set)""" + self.client.force_login(self.admin) + response = self.client.get( + reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}) + ) + self.assertEqual(response.status_code, 404) + + def test_recovery(self): + """Test user recovery link (no recovery flow set)""" + flow = Flow.objects.create( + name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY + ) + tenant: Tenant = Tenant.objects.first() + tenant.flow_recovery = flow + tenant.save() + self.client.force_login(self.admin) + response = self.client.get( + reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}) + ) + self.assertEqual(response.status_code, 200) + + def test_recovery_email_no_flow(self): + """Test user recovery link (no recovery flow set)""" + self.client.force_login(self.admin) + response = self.client.get( + reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) + ) + self.assertEqual(response.status_code, 404) + self.user.email = "foo@bar.baz" + self.user.save() + response = self.client.get( + reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) + ) + self.assertEqual(response.status_code, 404) + + def test_recovery_email_no_stage(self): + """Test user recovery link (no email stage)""" + self.user.email = "foo@bar.baz" + self.user.save() + flow = Flow.objects.create( + name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY + ) + tenant: Tenant = Tenant.objects.first() + tenant.flow_recovery = flow + tenant.save() + self.client.force_login(self.admin) + response = self.client.get( + reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) + ) + self.assertEqual(response.status_code, 404) + + def test_recovery_email(self): + """Test user recovery link""" + self.user.email = "foo@bar.baz" + self.user.save() + flow = Flow.objects.create( + name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY + ) + tenant: Tenant = Tenant.objects.first() + tenant.flow_recovery = flow + tenant.save() + + stage = EmailStage.objects.create(name="email") + + self.client.force_login(self.admin) + response = self.client.get( + reverse( + "authentik_api:user-recovery-email", + kwargs={"pk": self.user.pk}, + ) + + f"?email_stage={stage.pk}" + ) + self.assertEqual(response.status_code, 204) diff --git a/schema.yml b/schema.yml index f3be16972..4a6a9d360 100644 --- a/schema.yml +++ b/schema.yml @@ -3185,6 +3185,36 @@ paths: $ref: '#/components/schemas/ValidationError' '403': $ref: '#/components/schemas/GenericError' + /api/v2beta/core/users/{id}/recovery_email/: + get: + operationId: core_users_recovery_email_retrieve + description: Create a temporary link that a user can use to recover their accounts + parameters: + - in: query + name: email_stage + schema: + type: string + required: true + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this User. + required: true + tags: + - core + security: + - authentik: [] + - cookieAuth: [] + responses: + '204': + description: No response body + '404': + description: No response body + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' /api/v2beta/core/users/{id}/used_by/: get: operationId: core_users_used_by_list diff --git a/web/src/locales/en.po b/web/src/locales/en.po index 18cc77183..06b4a85a5 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -122,8 +122,13 @@ msgstr "Actions" msgid "Actions over the last 24 hours" msgstr "Actions over the last 24 hours" +#: src/pages/users/UserListPage.ts +msgid "Activate" +msgstr "Activate" + #: src/pages/groups/MemberSelectModal.ts #: src/pages/users/UserListPage.ts +#: src/pages/users/UserListPage.ts #: src/pages/users/UserViewPage.ts msgid "Active" msgstr "Active" @@ -560,6 +565,10 @@ msgstr "Certificates" msgid "Change password" msgstr "Change password" +#: src/pages/users/UserListPage.ts +msgid "Change status" +msgstr "Change status" + #: src/pages/user-settings/settings/UserSettingsPassword.ts msgid "Change your password" msgstr "Change your password" @@ -862,6 +871,10 @@ msgstr "Copy Key" msgid "Copy download URL" msgstr "Copy download URL" +#: src/pages/users/UserListPage.ts +msgid "Copy recovery link" +msgstr "Copy recovery link" + #: src/pages/applications/ApplicationForm.ts #: src/pages/applications/ApplicationListPage.ts #: src/pages/applications/ApplicationListPage.ts @@ -1042,6 +1055,10 @@ msgstr "Date" msgid "Date Time" msgstr "Date Time" +#: src/pages/users/UserListPage.ts +msgid "Deactivate" +msgstr "Deactivate" + #: src/pages/flows/FlowForm.ts msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." msgstr "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." @@ -1200,10 +1217,10 @@ msgstr "Digest algorithm" msgid "Digits" msgstr "Digits" -#: src/pages/users/UserListPage.ts -#: src/pages/users/UserListPage.ts -msgid "Disable" -msgstr "Disable" +#: +#: +#~ msgid "Disable" +#~ msgstr "Disable" #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts msgid "Disable Duo authenticator" @@ -1286,7 +1303,6 @@ msgstr "Each provider has a different issuer, based on the application slug." #: src/pages/sources/oauth/OAuthSourceViewPage.ts #: src/pages/sources/plex/PlexSourceViewPage.ts #: src/pages/sources/saml/SAMLSourceViewPage.ts -#: src/pages/users/UserListPage.ts #: src/pages/users/UserViewPage.ts msgid "Edit" msgstr "Edit" @@ -1333,18 +1349,26 @@ msgstr "Email address" msgid "Email info:" msgstr "Email info:" +#: src/pages/users/UserListPage.ts +msgid "Email recovery link" +msgstr "Email recovery link" + #: src/pages/events/utils.ts msgid "Email sent" msgstr "Email sent" +#: src/pages/users/UserResetEmailForm.ts +msgid "Email stage" +msgstr "Email stage" + #: src/pages/stages/prompt/PromptForm.ts msgid "Email: Text field with Email type." msgstr "Email: Text field with Email type." -#: src/pages/users/UserListPage.ts -#: src/pages/users/UserListPage.ts -msgid "Enable" -msgstr "Enable" +#: +#: +#~ msgid "Enable" +#~ msgstr "Enable" #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts msgid "Enable Duo authenticator" @@ -1942,6 +1966,10 @@ msgstr "Import certificates of external providers or create certificates to sign msgid "In case you can't access any other method." msgstr "In case you can't access any other method." +#: src/pages/users/UserListPage.ts +msgid "Inactive" +msgstr "Inactive" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." msgstr "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." @@ -2210,6 +2238,7 @@ msgstr "Loading" #: src/pages/tenants/TenantForm.ts #: src/pages/tenants/TenantForm.ts #: src/pages/tenants/TenantForm.ts +#: src/pages/users/UserResetEmailForm.ts msgid "Loading..." msgstr "Loading..." @@ -3066,6 +3095,7 @@ msgid "Receive a push notification on your phone to prove your identity." msgstr "Receive a push notification on your phone to prove your identity." #: src/pages/flows/FlowForm.ts +#: src/pages/users/UserListPage.ts msgid "Recovery" msgstr "Recovery" @@ -3082,6 +3112,10 @@ msgstr "Recovery flow. If left empty, the first applicable flow sorted by the sl msgid "Recovery keys" msgstr "Recovery keys" +#: src/pages/users/UserListPage.ts +msgid "Recovery link cannot be emailed, user has no email address saved." +msgstr "Recovery link cannot be emailed, user has no email address saved." + #: src/pages/providers/saml/SAMLProviderForm.ts msgid "Redirect" msgstr "Redirect" @@ -3114,6 +3148,10 @@ msgstr "Register device" msgid "Regular expressions for which authentication is not required. Each new line is interpreted as a new Regular Expression." msgstr "Regular expressions for which authentication is not required. Each new line is interpreted as a new Regular Expression." +#: src/pages/users/UserListPage.ts +msgid "Regular user" +msgstr "Regular user" + #: src/pages/applications/ApplicationViewPage.ts #: src/pages/flows/FlowViewPage.ts msgid "Related" @@ -3161,7 +3199,6 @@ msgstr "Required" msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." msgstr "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." -#: src/pages/users/UserListPage.ts #: src/pages/users/UserViewPage.ts msgid "Reset Password" msgstr "Reset Password" @@ -3362,6 +3399,10 @@ msgstr "Selection of backends to test the password against." msgid "Send Email again." msgstr "Send Email again." +#: src/pages/users/UserListPage.ts +msgid "Send link" +msgstr "Send link" + #: src/pages/events/RuleListPage.ts msgid "Send notifications whenever a specific Event is created and matched by policies." msgstr "Send notifications whenever a specific Event is created and matched by policies." @@ -3370,6 +3411,10 @@ msgstr "Send notifications whenever a specific Event is created and matched by p msgid "Send once" msgstr "Send once" +#: src/pages/users/UserListPage.ts +msgid "Send recovery link to user" +msgstr "Send recovery link to user" + #: src/pages/events/RuleListPage.ts msgid "Sent to group" msgstr "Sent to group" @@ -3793,6 +3838,10 @@ msgstr "Successfully imported flow." msgid "Successfully imported provider." msgstr "Successfully imported provider." +#: src/pages/users/UserResetEmailForm.ts +msgid "Successfully sent email." +msgstr "Successfully sent email." + #: src/pages/applications/ApplicationCheckAccessForm.ts #: src/pages/policies/PolicyTestForm.ts #: src/pages/property-mappings/PropertyMappingTestForm.ts @@ -3925,6 +3974,7 @@ msgstr "Successfully updated user." msgid "Successfully updated {0} {1}" msgstr "Successfully updated {0} {1}" +#: src/pages/users/UserListPage.ts #: src/pages/users/UserViewPage.ts msgid "Superuser" msgstr "Superuser" @@ -4569,6 +4619,10 @@ msgstr "User object filter" msgid "User password writeback" msgstr "User password writeback" +#: src/pages/users/UserListPage.ts +msgid "User status" +msgstr "User status" + #: src/pages/events/utils.ts msgid "User was written to" msgstr "User was written to" diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index a2dfae292..4fbfb1470 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -122,8 +122,13 @@ msgstr "" msgid "Actions over the last 24 hours" msgstr "" +#: src/pages/users/UserListPage.ts +msgid "Activate" +msgstr "" + #: src/pages/groups/MemberSelectModal.ts #: src/pages/users/UserListPage.ts +#: src/pages/users/UserListPage.ts #: src/pages/users/UserViewPage.ts msgid "Active" msgstr "" @@ -556,6 +561,10 @@ msgstr "" msgid "Change password" msgstr "" +#: src/pages/users/UserListPage.ts +msgid "Change status" +msgstr "" + #: src/pages/user-settings/settings/UserSettingsPassword.ts msgid "Change your password" msgstr "" @@ -856,6 +865,10 @@ msgstr "" msgid "Copy download URL" msgstr "" +#: src/pages/users/UserListPage.ts +msgid "Copy recovery link" +msgstr "" + #: src/pages/applications/ApplicationForm.ts #: src/pages/applications/ApplicationListPage.ts #: src/pages/applications/ApplicationListPage.ts @@ -1036,6 +1049,10 @@ msgstr "" msgid "Date Time" msgstr "" +#: src/pages/users/UserListPage.ts +msgid "Deactivate" +msgstr "" + #: src/pages/flows/FlowForm.ts msgid "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik." msgstr "" @@ -1192,10 +1209,10 @@ msgstr "" msgid "Digits" msgstr "" -#: src/pages/users/UserListPage.ts -#: src/pages/users/UserListPage.ts -msgid "Disable" -msgstr "" +#: +#: +#~ msgid "Disable" +#~ msgstr "" #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts msgid "Disable Duo authenticator" @@ -1278,7 +1295,6 @@ msgstr "" #: src/pages/sources/oauth/OAuthSourceViewPage.ts #: src/pages/sources/plex/PlexSourceViewPage.ts #: src/pages/sources/saml/SAMLSourceViewPage.ts -#: src/pages/users/UserListPage.ts #: src/pages/users/UserViewPage.ts msgid "Edit" msgstr "" @@ -1325,18 +1341,26 @@ msgstr "" msgid "Email info:" msgstr "" +#: src/pages/users/UserListPage.ts +msgid "Email recovery link" +msgstr "" + #: src/pages/events/utils.ts msgid "Email sent" msgstr "" +#: src/pages/users/UserResetEmailForm.ts +msgid "Email stage" +msgstr "" + #: src/pages/stages/prompt/PromptForm.ts msgid "Email: Text field with Email type." msgstr "" -#: src/pages/users/UserListPage.ts -#: src/pages/users/UserListPage.ts -msgid "Enable" -msgstr "" +#: +#: +#~ msgid "Enable" +#~ msgstr "" #: src/pages/user-settings/settings/UserSettingsAuthenticatorDuo.ts msgid "Enable Duo authenticator" @@ -1934,6 +1958,10 @@ msgstr "" msgid "In case you can't access any other method." msgstr "" +#: src/pages/users/UserListPage.ts +msgid "Inactive" +msgstr "" + #: src/pages/providers/oauth2/OAuth2ProviderForm.ts msgid "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." msgstr "" @@ -2202,6 +2230,7 @@ msgstr "" #: src/pages/tenants/TenantForm.ts #: src/pages/tenants/TenantForm.ts #: src/pages/tenants/TenantForm.ts +#: src/pages/users/UserResetEmailForm.ts msgid "Loading..." msgstr "" @@ -3058,6 +3087,7 @@ msgid "Receive a push notification on your phone to prove your identity." msgstr "" #: src/pages/flows/FlowForm.ts +#: src/pages/users/UserListPage.ts msgid "Recovery" msgstr "" @@ -3074,6 +3104,10 @@ msgstr "" msgid "Recovery keys" msgstr "" +#: src/pages/users/UserListPage.ts +msgid "Recovery link cannot be emailed, user has no email address saved." +msgstr "" + #: src/pages/providers/saml/SAMLProviderForm.ts msgid "Redirect" msgstr "" @@ -3106,6 +3140,10 @@ msgstr "" msgid "Regular expressions for which authentication is not required. Each new line is interpreted as a new Regular Expression." msgstr "" +#: src/pages/users/UserListPage.ts +msgid "Regular user" +msgstr "" + #: src/pages/applications/ApplicationViewPage.ts #: src/pages/flows/FlowViewPage.ts msgid "Related" @@ -3153,7 +3191,6 @@ msgstr "" msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." msgstr "" -#: src/pages/users/UserListPage.ts #: src/pages/users/UserViewPage.ts msgid "Reset Password" msgstr "" @@ -3354,6 +3391,10 @@ msgstr "" msgid "Send Email again." msgstr "" +#: src/pages/users/UserListPage.ts +msgid "Send link" +msgstr "" + #: src/pages/events/RuleListPage.ts msgid "Send notifications whenever a specific Event is created and matched by policies." msgstr "" @@ -3362,6 +3403,10 @@ msgstr "" msgid "Send once" msgstr "" +#: src/pages/users/UserListPage.ts +msgid "Send recovery link to user" +msgstr "" + #: src/pages/events/RuleListPage.ts msgid "Sent to group" msgstr "" @@ -3785,6 +3830,10 @@ msgstr "" msgid "Successfully imported provider." msgstr "" +#: src/pages/users/UserResetEmailForm.ts +msgid "Successfully sent email." +msgstr "" + #: src/pages/applications/ApplicationCheckAccessForm.ts #: src/pages/policies/PolicyTestForm.ts #: src/pages/property-mappings/PropertyMappingTestForm.ts @@ -3917,6 +3966,7 @@ msgstr "" msgid "Successfully updated {0} {1}" msgstr "" +#: src/pages/users/UserListPage.ts #: src/pages/users/UserViewPage.ts msgid "Superuser" msgstr "" @@ -4554,6 +4604,10 @@ msgstr "" msgid "User password writeback" msgstr "" +#: src/pages/users/UserListPage.ts +msgid "User status" +msgstr "" + #: src/pages/events/utils.ts msgid "User was written to" msgstr "" diff --git a/web/src/pages/outposts/OutpostForm.ts b/web/src/pages/outposts/OutpostForm.ts index 785d0b2e1..5ef084d70 100644 --- a/web/src/pages/outposts/OutpostForm.ts +++ b/web/src/pages/outposts/OutpostForm.ts @@ -16,7 +16,7 @@ export class OutpostForm extends ModelForm { type: OutpostTypeEnum = OutpostTypeEnum.Proxy; @property({ type: Boolean }) - embedded: boolean = false; + embedded = false; loadInstance(pk: string): Promise { return new OutpostsApi(DEFAULT_CONFIG) diff --git a/web/src/pages/users/UserListPage.ts b/web/src/pages/users/UserListPage.ts index 9f40637f2..b0da5c975 100644 --- a/web/src/pages/users/UserListPage.ts +++ b/web/src/pages/users/UserListPage.ts @@ -1,10 +1,10 @@ import { t } from "@lingui/macro"; -import { customElement, html, property, TemplateResult } from "lit-element"; +import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; import { AKResponse } from "../../api/Client"; import { TablePage } from "../../elements/table/TablePage"; +import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import "../../elements/forms/ModalForm"; -import "../../elements/buttons/Dropdown"; import "../../elements/buttons/ActionButton"; import { TableColumn } from "../../elements/table/Table"; import { PAGE_SIZE } from "../../constants"; @@ -13,6 +13,7 @@ import { DEFAULT_CONFIG, tenant } from "../../api/Config"; import "../../elements/forms/DeleteForm"; import "./UserActiveForm"; import "./UserForm"; +import "./UserResetEmailForm"; import { showMessage } from "../../elements/messages/MessageContainer"; import { MessageLevel } from "../../elements/messages/Message"; import { first } from "../../utils"; @@ -20,6 +21,9 @@ import { until } from "lit-html/directives/until"; @customElement("ak-user-list") export class UserListPage extends TablePage { + expandable = true; + checkbox = true; + searchEnabled(): boolean { return true; } @@ -39,6 +43,10 @@ export class UserListPage extends TablePage { @property({ type: Boolean }) hideServiceAccounts = true; + static get styles(): CSSResult[] { + return super.styles.concat(PFDescriptionList); + } + apiEndpoint(page: number): Promise> { return new CoreApi(DEFAULT_CONFIG).coreUsersList({ ordering: this.order, @@ -62,6 +70,29 @@ export class UserListPage extends TablePage { ]; } + renderToolbarSelected(): TemplateResult { + const disabled = this.selectedElements.length !== 1; + const item = this.selectedElements[0]; + return html` { + return new CoreApi(DEFAULT_CONFIG).coreUsersUsedByList({ + id: item.pk, + }); + }} + .delete=${() => { + return new CoreApi(DEFAULT_CONFIG).coreUsersDestroy({ + id: item.pk, + }); + }} + > + + `; + } + row(item: User): TemplateResult[] { return [ html` @@ -74,100 +105,135 @@ export class UserListPage extends TablePage { ${t`Update`} ${t`Update User`} - - - - - - - ${until( - tenant().then((te) => { - if (te.flowRecovery) { - return html` { - return new CoreApi(DEFAULT_CONFIG) - .coreUsersRecoveryRetrieve({ - id: item.pk || 0, - }) - .then((rec) => { - showMessage({ - level: MessageLevel.success, - message: t`Successfully generated recovery link`, - description: rec.link, - }); - }) - .catch((ex: Response) => { - ex.json().then(() => { - showMessage({ - level: MessageLevel.error, - message: t`No recovery flow is configured.`, - }); - }); - }); - }} - > - ${t`Reset Password`} - `; - } - return html``; - }), - )} + ${t`Impersonate`} `, ]; } + renderExpanded(item: User): TemplateResult { + return html` +
+
+
+
+ ${t`User status`} +
+
+
+ ${item.isActive ? t`Active` : t`Inactive`} +
+
+ ${item.isSuperuser ? t`Superuser` : t`Regular user`} +
+
+
+
+
+ ${t`Change status`} +
+
+
+ { + return new CoreApi( + DEFAULT_CONFIG, + ).coreUsersPartialUpdate({ + id: item.pk || 0, + patchedUserRequest: { + username: item.username, + name: item.name, + isActive: !item.isActive, + }, + }); + }} + > + + +
+
+
+ ${until( + tenant().then((te) => { + if (!te.flowRecovery) { + return html``; + } + return html`
+
+ ${t`Recovery`} +
+
+
+ { + return new CoreApi(DEFAULT_CONFIG) + .coreUsersRecoveryRetrieve({ + id: item.pk || 0, + }) + .then((rec) => { + showMessage({ + level: MessageLevel.success, + message: t`Successfully generated recovery link`, + description: rec.link, + }); + }) + .catch((ex: Response) => { + ex.json().then(() => { + showMessage({ + level: MessageLevel.error, + message: t`No recovery flow is configured.`, + }); + }); + }); + }} + > + ${t`Copy recovery link`} + + ${item.email + ? html` + ${t`Send link`} + + ${t`Send recovery link to user`} + + + + + ` + : html`${t`Recovery link cannot be emailed, user has no email address saved.`}`} +
+
+
`; + }), + )} +
+
+ + + `; + } + renderToolbar(): TemplateResult { return html` diff --git a/web/src/pages/users/UserResetEmailForm.ts b/web/src/pages/users/UserResetEmailForm.ts new file mode 100644 index 000000000..e4915a9f7 --- /dev/null +++ b/web/src/pages/users/UserResetEmailForm.ts @@ -0,0 +1,44 @@ +import { CoreApi, CoreUsersRecoveryEmailRetrieveRequest, StagesApi, User } from "authentik-api"; +import { t } from "@lingui/macro"; +import { customElement, property } from "lit-element"; +import { html, TemplateResult } from "lit-html"; +import { DEFAULT_CONFIG } from "../../api/Config"; +import { Form } from "../../elements/forms/Form"; +import { until } from "lit-html/directives/until"; +import "../../elements/forms/HorizontalFormElement"; + +@customElement("ak-user-reset-email-form") +export class UserResetEmailForm extends Form { + @property({ attribute: false }) + user!: User; + + getSuccessMessage(): string { + return t`Successfully sent email.`; + } + + send = (data: CoreUsersRecoveryEmailRetrieveRequest): Promise => { + data.id = this.user.pk; + return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailRetrieve(data); + }; + + renderForm(): TemplateResult { + return html`
+ + + +
`; + } +}