core: prevent self-impersonation (#6885)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-09-26 12:04:40 +02:00 committed by GitHub
parent 44ac944706
commit 3e81824388
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 84 additions and 18 deletions

View File

@ -616,8 +616,10 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not request.user.has_perm("impersonate"):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return Response(status=401)
user_to_be = self.get_object()
if user_to_be.pk == self.request.user.pk:
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
return Response(status=401)
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be

View File

@ -6,6 +6,7 @@ from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.config import CONFIG
class TestImpersonation(APITestCase):
@ -46,12 +47,42 @@ class TestImpersonation(APITestCase):
"""test impersonation without permissions"""
self.client.force_login(self.other_user)
self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}))
response = self.client.post(
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})
)
self.assertEqual(response.status_code, 403)
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())
self.assertEqual(response_body["user"]["username"], self.other_user.username)
@CONFIG.patch("impersonation", False)
def test_impersonate_disabled(self):
"""test impersonation that is disabled"""
self.client.force_login(self.user)
response = self.client.post(
reverse("authentik_api:user-impersonate", kwargs={"pk": self.other_user.pk})
)
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_self(self):
"""test impersonation that user can't impersonate themselves"""
self.client.force_login(self.user)
response = self.client.post(
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})
)
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_un_impersonate_empty(self):
"""test un-impersonation without impersonating first"""
self.client.force_login(self.other_user)

View File

@ -3,6 +3,7 @@ import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserPasswordForm";
import "@goauthentik/admin/users/UserResetEmailForm";
import { me } from "@goauthentik/app/common/users";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { MessageLevel } from "@goauthentik/common/messages";
import { uiConfig } from "@goauthentik/common/ui/config";
@ -37,6 +38,7 @@ import {
CoreUsersListTypeEnum,
Group,
ResponseError,
SessionUser,
User,
} from "@goauthentik/api";
@ -123,12 +125,15 @@ export class RelatedUserList extends Table<User> {
@property({ type: Boolean })
hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true);
@state()
me?: SessionUser;
static get styles(): CSSResult[] {
return super.styles.concat(PFDescriptionList, PFAlert, PFBanner);
}
async apiEndpoint(page: number): Promise<PaginatedResponse<User>> {
return new CoreApi(DEFAULT_CONFIG).coreUsersList({
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
@ -138,6 +143,8 @@ export class RelatedUserList extends Table<User> {
? [CoreUsersListTypeEnum.External, CoreUsersListTypeEnum.Internal]
: undefined,
});
this.me = await me();
return users;
}
columns(): TableColumn[] {
@ -181,6 +188,9 @@ export class RelatedUserList extends Table<User> {
}
row(item: User): TemplateResult[] {
const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
item.pk !== this.me?.user.pk;
return [
html`<a href="#/identity/users/${item.pk}">
<div>${item.username}</div>
@ -200,7 +210,7 @@ export class RelatedUserList extends Table<User> {
</pf-tooltip>
</button>
</ak-forms-modal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
${canImpersonate
? html`
<ak-action-button
class="pf-m-tertiary"

View File

@ -4,6 +4,7 @@ import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserPasswordForm";
import "@goauthentik/admin/users/UserResetEmailForm";
import { me } from "@goauthentik/app/common/users";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { MessageLevel } from "@goauthentik/common/messages";
import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
@ -30,7 +31,14 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { CapabilitiesEnum, CoreApi, ResponseError, User, UserPath } from "@goauthentik/api";
import {
CapabilitiesEnum,
CoreApi,
ResponseError,
SessionUser,
User,
UserPath,
} from "@goauthentik/api";
@customElement("ak-user-list")
export class UserListPage extends TablePage<User> {
@ -62,6 +70,9 @@ export class UserListPage extends TablePage<User> {
@state()
userPaths?: UserPath;
@state()
me?: SessionUser;
static get styles(): CSSResult[] {
return super.styles.concat(PFDescriptionList, PFCard, PFAlert);
}
@ -88,6 +99,7 @@ export class UserListPage extends TablePage<User> {
this.userPaths = await new CoreApi(DEFAULT_CONFIG).coreUsersPathsRetrieve({
search: this.search,
});
this.me = await me();
return users;
}
@ -179,6 +191,9 @@ export class UserListPage extends TablePage<User> {
}
row(item: User): TemplateResult[] {
const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
item.pk !== this.me?.user.pk;
return [
html`<a href="#/identity/users/${item.pk}">
<div>${item.username}</div>
@ -198,7 +213,7 @@ export class UserListPage extends TablePage<User> {
</pf-tooltip>
</button>
</ak-forms-modal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate)
${canImpersonate
? html`
<ak-action-button
class="pf-m-tertiary"

View File

@ -3,6 +3,7 @@ import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserChart";
import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserPasswordForm";
import { me } from "@goauthentik/app/common/users";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { MessageLevel } from "@goauthentik/common/messages";
@ -24,7 +25,7 @@ import "@goauthentik/elements/user/UserConsentList";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
@ -37,7 +38,7 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css";
import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css";
import { CapabilitiesEnum, CoreApi, User } from "@goauthentik/api";
import { CapabilitiesEnum, CoreApi, SessionUser, User } from "@goauthentik/api";
import "./UserDevicesList";
@ -45,6 +46,8 @@ import "./UserDevicesList";
export class UserViewPage extends AKElement {
@property({ type: Number })
set userId(id: number) {
me().then((me) => {
this.me = me;
new CoreApi(DEFAULT_CONFIG)
.coreUsersRetrieve({
id: id,
@ -52,11 +55,15 @@ export class UserViewPage extends AKElement {
.then((user) => {
this.user = user;
});
});
}
@property({ attribute: false })
user?: User;
@state()
me?: SessionUser;
static get styles(): CSSResult[] {
return [
PFBase,
@ -103,6 +110,9 @@ export class UserViewPage extends AKElement {
if (!this.user) {
return html``;
}
const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
this.user.pk !== this.me?.user.pk;
return html`
<div class="pf-c-card__title">${msg("User Info")}</div>
<div class="pf-c-card__body">
@ -213,9 +223,7 @@ export class UserViewPage extends AKElement {
</pf-tooltip>
</button>
</ak-user-active-form>
${rootInterface()?.config?.capabilities.includes(
CapabilitiesEnum.CanImpersonate,
)
${canImpersonate
? html`
<ak-action-button
class="pf-m-secondary pf-m-block"