core: add ability to provide reason for impersonation (#11951)
* core: add ability to provide reason for impersonation Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * tenants api things Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * add missing implem Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * A tooltip needs a DOM object to determine the coordinates where it should render. A solitary string is not enough; a is needed here. * web: user impersonation reason To determine where to render the Tooltip content, the object associated with the Tooltip must be a DOM object with an HTML tag. A naked string is not enough; a `<span>` will do nicely here. Also, fixed a build failure: PFSize was not defined in RelatedUserList. * add and fix tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * avoid migration change Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * small fixes Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> --------- Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Co-authored-by: Ken Sternberg <ken@goauthentik.io>
This commit is contained in:

committed by
GitHub

parent
6d5a61187e
commit
0cffe0c953
@ -666,7 +666,12 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
|
|
||||||
@permission_required("authentik_core.impersonate")
|
@permission_required("authentik_core.impersonate")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=OpenApiTypes.NONE,
|
request=inline_serializer(
|
||||||
|
"ImpersonationSerializer",
|
||||||
|
{
|
||||||
|
"reason": CharField(required=True),
|
||||||
|
},
|
||||||
|
),
|
||||||
responses={
|
responses={
|
||||||
"204": OpenApiResponse(description="Successfully started impersonation"),
|
"204": OpenApiResponse(description="Successfully started impersonation"),
|
||||||
"401": OpenApiResponse(description="Access denied"),
|
"401": OpenApiResponse(description="Access denied"),
|
||||||
@ -679,6 +684,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
LOGGER.debug("User attempted to impersonate", user=request.user)
|
LOGGER.debug("User attempted to impersonate", user=request.user)
|
||||||
return Response(status=401)
|
return Response(status=401)
|
||||||
user_to_be = self.get_object()
|
user_to_be = self.get_object()
|
||||||
|
reason = request.data.get("reason", "")
|
||||||
# Check both object-level perms and global perms
|
# Check both object-level perms and global perms
|
||||||
if not request.user.has_perm(
|
if not request.user.has_perm(
|
||||||
"authentik_core.impersonate", user_to_be
|
"authentik_core.impersonate", user_to_be
|
||||||
@ -688,11 +694,16 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
if user_to_be.pk == self.request.user.pk:
|
if user_to_be.pk == self.request.user.pk:
|
||||||
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
|
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
|
||||||
return Response(status=401)
|
return Response(status=401)
|
||||||
|
if not reason and request.tenant.impersonation_require_reason:
|
||||||
|
LOGGER.debug(
|
||||||
|
"User attempted to impersonate without providing a reason", user=request.user
|
||||||
|
)
|
||||||
|
return Response(status=401)
|
||||||
|
|
||||||
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||||
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
Event.new(EventAction.IMPERSONATION_STARTED, reason=reason).from_http(request, user_to_be)
|
||||||
|
|
||||||
return Response(status=201)
|
return Response(status=201)
|
||||||
|
|
||||||
|
@ -29,7 +29,8 @@ class TestImpersonation(APITestCase):
|
|||||||
reverse(
|
reverse(
|
||||||
"authentik_api:user-impersonate",
|
"authentik_api:user-impersonate",
|
||||||
kwargs={"pk": self.other_user.pk},
|
kwargs={"pk": self.other_user.pk},
|
||||||
)
|
),
|
||||||
|
data={"reason": "some reason"},
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
@ -55,7 +56,8 @@ class TestImpersonation(APITestCase):
|
|||||||
reverse(
|
reverse(
|
||||||
"authentik_api:user-impersonate",
|
"authentik_api:user-impersonate",
|
||||||
kwargs={"pk": self.other_user.pk},
|
kwargs={"pk": self.other_user.pk},
|
||||||
)
|
),
|
||||||
|
data={"reason": "some reason"},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 201)
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
@ -75,7 +77,8 @@ class TestImpersonation(APITestCase):
|
|||||||
reverse(
|
reverse(
|
||||||
"authentik_api:user-impersonate",
|
"authentik_api:user-impersonate",
|
||||||
kwargs={"pk": self.other_user.pk},
|
kwargs={"pk": self.other_user.pk},
|
||||||
)
|
),
|
||||||
|
data={"reason": "some reason"},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 201)
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
@ -89,7 +92,8 @@ class TestImpersonation(APITestCase):
|
|||||||
self.client.force_login(self.other_user)
|
self.client.force_login(self.other_user)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})
|
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}),
|
||||||
|
data={"reason": "some reason"},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -105,7 +109,8 @@ class TestImpersonation(APITestCase):
|
|||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-impersonate", kwargs={"pk": self.other_user.pk})
|
reverse("authentik_api:user-impersonate", kwargs={"pk": self.other_user.pk}),
|
||||||
|
data={"reason": "some reason"},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
@ -118,7 +123,22 @@ class TestImpersonation(APITestCase):
|
|||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})
|
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}),
|
||||||
|
data={"reason": "some reason"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
|
response_body = loads(response.content.decode())
|
||||||
|
self.assertEqual(response_body["user"]["username"], self.user.username)
|
||||||
|
|
||||||
|
def test_impersonate_reason_required(self):
|
||||||
|
"""test impersonation that user must provide reason"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}),
|
||||||
|
data={"reason": ""},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ def default_event_duration():
|
|||||||
"""Default duration an Event is saved.
|
"""Default duration an Event is saved.
|
||||||
This is used as a fallback when no brand is available"""
|
This is used as a fallback when no brand is available"""
|
||||||
try:
|
try:
|
||||||
tenant = get_current_tenant()
|
tenant = get_current_tenant(only=["event_retention"])
|
||||||
return now() + timedelta_from_string(tenant.event_retention)
|
return now() + timedelta_from_string(tenant.event_retention)
|
||||||
except Tenant.DoesNotExist:
|
except Tenant.DoesNotExist:
|
||||||
return now() + timedelta(days=365)
|
return now() + timedelta(days=365)
|
||||||
|
@ -23,6 +23,7 @@ class SettingsSerializer(ModelSerializer):
|
|||||||
"footer_links",
|
"footer_links",
|
||||||
"gdpr_compliance",
|
"gdpr_compliance",
|
||||||
"impersonation",
|
"impersonation",
|
||||||
|
"impersonation_require_reason",
|
||||||
"default_token_duration",
|
"default_token_duration",
|
||||||
"default_token_length",
|
"default_token_length",
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-11-07 15:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_tenants", "0003_alter_tenant_default_token_duration"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tenant",
|
||||||
|
name="impersonation_require_reason",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
help_text="Require administrators to provide a reason for impersonating a user.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -85,6 +85,10 @@ 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
|
||||||
)
|
)
|
||||||
|
impersonation_require_reason = models.BooleanField(
|
||||||
|
help_text=_("Require administrators to provide a reason for impersonating a user."),
|
||||||
|
default=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,
|
||||||
|
@ -8,9 +8,11 @@ from authentik.root.install_id import get_install_id
|
|||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
|
|
||||||
def get_current_tenant() -> Tenant:
|
def get_current_tenant(only: list[str] | None = None) -> Tenant:
|
||||||
"""Get tenant for current request"""
|
"""Get tenant for current request"""
|
||||||
return Tenant.objects.get(schema_name=connection.schema_name)
|
if only is None:
|
||||||
|
only = []
|
||||||
|
return Tenant.objects.only(*only).get(schema_name=connection.schema_name)
|
||||||
|
|
||||||
|
|
||||||
def get_unique_identifier() -> str:
|
def get_unique_identifier() -> str:
|
||||||
|
26
schema.yml
26
schema.yml
@ -5295,6 +5295,12 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
tags:
|
tags:
|
||||||
- core
|
- core
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ImpersonationRequest'
|
||||||
|
required: true
|
||||||
security:
|
security:
|
||||||
- authentik: []
|
- authentik: []
|
||||||
responses:
|
responses:
|
||||||
@ -42720,6 +42726,14 @@ components:
|
|||||||
incorrect user info is entered.
|
incorrect user info is entered.
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
|
ImpersonationRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
reason:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
required:
|
||||||
|
- reason
|
||||||
InstallID:
|
InstallID:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -50029,6 +50043,10 @@ components:
|
|||||||
impersonation:
|
impersonation:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Globally enable/disable impersonation.
|
description: Globally enable/disable impersonation.
|
||||||
|
impersonation_require_reason:
|
||||||
|
type: boolean
|
||||||
|
description: Require administrators to provide a reason for impersonating
|
||||||
|
a user.
|
||||||
default_token_duration:
|
default_token_duration:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
@ -53758,6 +53776,10 @@ components:
|
|||||||
impersonation:
|
impersonation:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Globally enable/disable impersonation.
|
description: Globally enable/disable impersonation.
|
||||||
|
impersonation_require_reason:
|
||||||
|
type: boolean
|
||||||
|
description: Require administrators to provide a reason for impersonating
|
||||||
|
a user.
|
||||||
default_token_duration:
|
default_token_duration:
|
||||||
type: string
|
type: string
|
||||||
description: Default token duration
|
description: Default token duration
|
||||||
@ -53797,6 +53819,10 @@ components:
|
|||||||
impersonation:
|
impersonation:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Globally enable/disable impersonation.
|
description: Globally enable/disable impersonation.
|
||||||
|
impersonation_require_reason:
|
||||||
|
type: boolean
|
||||||
|
description: Require administrators to provide a reason for impersonating
|
||||||
|
a user.
|
||||||
default_token_duration:
|
default_token_duration:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
@ -193,6 +193,13 @@ 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-switch-input
|
||||||
|
name="impersonationRequireReason"
|
||||||
|
label=${msg("Require reason for impersonation")}
|
||||||
|
?checked="${this._settings?.impersonationRequireReason}"
|
||||||
|
help=${msg("Require administrators to provide a reason for impersonating a user.")}
|
||||||
|
>
|
||||||
|
</ak-switch-input>
|
||||||
<ak-text-input
|
<ak-text-input
|
||||||
name="defaultTokenDuration"
|
name="defaultTokenDuration"
|
||||||
label=${msg("Default token duration")}
|
label=${msg("Default token duration")}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import "@goauthentik/admin/users/ServiceAccountForm";
|
import "@goauthentik/admin/users/ServiceAccountForm";
|
||||||
import "@goauthentik/admin/users/UserActiveForm";
|
import "@goauthentik/admin/users/UserActiveForm";
|
||||||
import "@goauthentik/admin/users/UserForm";
|
import "@goauthentik/admin/users/UserForm";
|
||||||
|
import "@goauthentik/admin/users/UserImpersonateForm";
|
||||||
import "@goauthentik/admin/users/UserPasswordForm";
|
import "@goauthentik/admin/users/UserPasswordForm";
|
||||||
import "@goauthentik/admin/users/UserResetEmailForm";
|
import "@goauthentik/admin/users/UserResetEmailForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import { PFSize } from "@goauthentik/common/enums.js";
|
||||||
import { MessageLevel } from "@goauthentik/common/messages";
|
import { MessageLevel } from "@goauthentik/common/messages";
|
||||||
import { me } from "@goauthentik/common/users";
|
import { me } from "@goauthentik/common/users";
|
||||||
import { getRelativeTime } from "@goauthentik/common/utils";
|
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||||
@ -213,20 +215,22 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
|||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
${canImpersonate
|
${canImpersonate
|
||||||
? html`
|
? html`
|
||||||
<ak-action-button
|
<ak-forms-modal size=${PFSize.Medium} id="impersonate-request">
|
||||||
class="pf-m-tertiary"
|
<span slot="submit">${msg("Impersonate")}</span>
|
||||||
.apiRequest=${() => {
|
<span slot="header">${msg("Impersonate")} ${item.username}</span>
|
||||||
return new CoreApi(DEFAULT_CONFIG)
|
<ak-user-impersonate-form
|
||||||
.coreUsersImpersonateCreate({
|
slot="form"
|
||||||
id: item.pk,
|
.instancePk=${item.pk}
|
||||||
})
|
></ak-user-impersonate-form>
|
||||||
.then(() => {
|
<button slot="trigger" class="pf-c-button pf-m-tertiary">
|
||||||
window.location.href = "/";
|
<pf-tooltip
|
||||||
});
|
position="top"
|
||||||
}}
|
content=${msg("Temporarily assume the identity of this user")}
|
||||||
>
|
>
|
||||||
${msg("Impersonate")}
|
<span>${msg("Impersonate")}</span>
|
||||||
</ak-action-button>
|
</pf-tooltip>
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
`
|
`
|
||||||
: html``}`,
|
: html``}`,
|
||||||
];
|
];
|
||||||
|
40
web/src/admin/users/UserImpersonateForm.ts
Normal file
40
web/src/admin/users/UserImpersonateForm.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
|
import "@goauthentik/components/ak-text-input";
|
||||||
|
import { Form } from "@goauthentik/elements/forms/Form";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { TemplateResult, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import { CoreApi, ImpersonationRequest } from "@goauthentik/api";
|
||||||
|
|
||||||
|
@customElement("ak-user-impersonate-form")
|
||||||
|
export class UserImpersonateForm extends Form<ImpersonationRequest> {
|
||||||
|
@property({ type: Number })
|
||||||
|
instancePk?: number;
|
||||||
|
|
||||||
|
async send(data: ImpersonationRequest): Promise<void> {
|
||||||
|
return new CoreApi(DEFAULT_CONFIG)
|
||||||
|
.coreUsersImpersonateCreate({
|
||||||
|
id: this.instancePk || 0,
|
||||||
|
impersonationRequest: data,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
window.location.href = "/";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderForm(): TemplateResult {
|
||||||
|
return html`<ak-text-input
|
||||||
|
name="reason"
|
||||||
|
label=${msg("Reason")}
|
||||||
|
help=${msg("Reason for impersonating the user")}
|
||||||
|
></ak-text-input>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ak-user-impersonate-form": UserImpersonateForm;
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { AdminInterface } from "@goauthentik/admin/AdminInterface";
|
|||||||
import "@goauthentik/admin/users/ServiceAccountForm";
|
import "@goauthentik/admin/users/ServiceAccountForm";
|
||||||
import "@goauthentik/admin/users/UserActiveForm";
|
import "@goauthentik/admin/users/UserActiveForm";
|
||||||
import "@goauthentik/admin/users/UserForm";
|
import "@goauthentik/admin/users/UserForm";
|
||||||
|
import "@goauthentik/admin/users/UserImpersonateForm";
|
||||||
import "@goauthentik/admin/users/UserPasswordForm";
|
import "@goauthentik/admin/users/UserPasswordForm";
|
||||||
import "@goauthentik/admin/users/UserResetEmailForm";
|
import "@goauthentik/admin/users/UserResetEmailForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
@ -266,20 +267,22 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
|||||||
</ak-forms-modal>
|
</ak-forms-modal>
|
||||||
${canImpersonate
|
${canImpersonate
|
||||||
? html`
|
? html`
|
||||||
<ak-action-button
|
<ak-forms-modal size=${PFSize.Medium} id="impersonate-request">
|
||||||
class="pf-m-tertiary"
|
<span slot="submit">${msg("Impersonate")}</span>
|
||||||
.apiRequest=${() => {
|
<span slot="header">${msg("Impersonate")} ${item.username}</span>
|
||||||
return new CoreApi(DEFAULT_CONFIG)
|
<ak-user-impersonate-form
|
||||||
.coreUsersImpersonateCreate({
|
slot="form"
|
||||||
id: item.pk,
|
.instancePk=${item.pk}
|
||||||
})
|
></ak-user-impersonate-form>
|
||||||
.then(() => {
|
<button slot="trigger" class="pf-c-button pf-m-tertiary">
|
||||||
window.location.href = "/";
|
<pf-tooltip
|
||||||
});
|
position="top"
|
||||||
}}
|
content=${msg("Temporarily assume the identity of this user")}
|
||||||
>
|
>
|
||||||
${msg("Impersonate")}
|
<span>${msg("Impersonate")}</span>
|
||||||
</ak-action-button>
|
</pf-tooltip>
|
||||||
|
</button>
|
||||||
|
</ak-forms-modal>
|
||||||
`
|
`
|
||||||
: html``}`,
|
: html``}`,
|
||||||
];
|
];
|
||||||
|
@ -5,6 +5,7 @@ import "@goauthentik/admin/users/UserActiveForm";
|
|||||||
import "@goauthentik/admin/users/UserApplicationTable";
|
import "@goauthentik/admin/users/UserApplicationTable";
|
||||||
import "@goauthentik/admin/users/UserChart";
|
import "@goauthentik/admin/users/UserChart";
|
||||||
import "@goauthentik/admin/users/UserForm";
|
import "@goauthentik/admin/users/UserForm";
|
||||||
|
import "@goauthentik/admin/users/UserImpersonateForm";
|
||||||
import {
|
import {
|
||||||
renderRecoveryEmailRequest,
|
renderRecoveryEmailRequest,
|
||||||
requestRecoveryLink,
|
requestRecoveryLink,
|
||||||
@ -208,26 +209,22 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
|
|||||||
</ak-user-active-form>
|
</ak-user-active-form>
|
||||||
${canImpersonate
|
${canImpersonate
|
||||||
? html`
|
? html`
|
||||||
<ak-action-button
|
<ak-forms-modal size=${PFSize.Medium} id="impersonate-request">
|
||||||
class="pf-m-secondary pf-m-block"
|
<span slot="submit">${msg("Impersonate")}</span>
|
||||||
id="impersonate-user-button"
|
<span slot="header">${msg("Impersonate")} ${user.username}</span>
|
||||||
.apiRequest=${() => {
|
<ak-user-impersonate-form
|
||||||
return new CoreApi(DEFAULT_CONFIG)
|
slot="form"
|
||||||
.coreUsersImpersonateCreate({
|
.instancePk=${user.pk}
|
||||||
id: user.pk,
|
></ak-user-impersonate-form>
|
||||||
})
|
<button slot="trigger" class="pf-c-button pf-m-secondary pf-m-block">
|
||||||
.then(() => {
|
<pf-tooltip
|
||||||
window.location.href = "/";
|
position="top"
|
||||||
});
|
content=${msg("Temporarily assume the identity of this user")}
|
||||||
}}
|
>
|
||||||
>
|
<span>${msg("Impersonate")}</span>
|
||||||
<pf-tooltip
|
</pf-tooltip>
|
||||||
position="top"
|
</button>
|
||||||
content=${msg("Temporarily assume the identity of this user")}
|
</ak-forms-modal>
|
||||||
>
|
|
||||||
${msg("Impersonate")}
|
|
||||||
</pf-tooltip>
|
|
||||||
</ak-action-button>
|
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div> `;
|
</div> `;
|
||||||
|
Reference in New Issue
Block a user