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"): if not request.user.has_perm("impersonate"):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user) LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return Response(status=401) return Response(status=401)
user_to_be = self.get_object() 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_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be 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.models import User
from authentik.core.tests.utils import create_test_admin_user from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.config import CONFIG
class TestImpersonation(APITestCase): class TestImpersonation(APITestCase):
@ -46,12 +47,42 @@ class TestImpersonation(APITestCase):
"""test impersonation without permissions""" """test impersonation without permissions"""
self.client.force_login(self.other_user) 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 = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode()) response_body = loads(response.content.decode())
self.assertEqual(response_body["user"]["username"], self.other_user.username) 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): def test_un_impersonate_empty(self):
"""test un-impersonation without impersonating first""" """test un-impersonation without impersonating first"""
self.client.force_login(self.other_user) 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/UserForm";
import "@goauthentik/admin/users/UserPasswordForm"; import "@goauthentik/admin/users/UserPasswordForm";
import "@goauthentik/admin/users/UserResetEmailForm"; import "@goauthentik/admin/users/UserResetEmailForm";
import { me } from "@goauthentik/app/common/users";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
import { uiConfig } from "@goauthentik/common/ui/config"; import { uiConfig } from "@goauthentik/common/ui/config";
@ -37,6 +38,7 @@ import {
CoreUsersListTypeEnum, CoreUsersListTypeEnum,
Group, Group,
ResponseError, ResponseError,
SessionUser,
User, User,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -123,12 +125,15 @@ export class RelatedUserList extends Table<User> {
@property({ type: Boolean }) @property({ type: Boolean })
hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true); hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true);
@state()
me?: SessionUser;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return super.styles.concat(PFDescriptionList, PFAlert, PFBanner); return super.styles.concat(PFDescriptionList, PFAlert, PFBanner);
} }
async apiEndpoint(page: number): Promise<PaginatedResponse<User>> { async apiEndpoint(page: number): Promise<PaginatedResponse<User>> {
return new CoreApi(DEFAULT_CONFIG).coreUsersList({ const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({
ordering: this.order, ordering: this.order,
page: page, page: page,
pageSize: (await uiConfig()).pagination.perPage, pageSize: (await uiConfig()).pagination.perPage,
@ -138,6 +143,8 @@ export class RelatedUserList extends Table<User> {
? [CoreUsersListTypeEnum.External, CoreUsersListTypeEnum.Internal] ? [CoreUsersListTypeEnum.External, CoreUsersListTypeEnum.Internal]
: undefined, : undefined,
}); });
this.me = await me();
return users;
} }
columns(): TableColumn[] { columns(): TableColumn[] {
@ -181,6 +188,9 @@ export class RelatedUserList extends Table<User> {
} }
row(item: User): TemplateResult[] { row(item: User): TemplateResult[] {
const canImpersonate =
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
item.pk !== this.me?.user.pk;
return [ return [
html`<a href="#/identity/users/${item.pk}"> html`<a href="#/identity/users/${item.pk}">
<div>${item.username}</div> <div>${item.username}</div>
@ -200,7 +210,7 @@ export class RelatedUserList extends Table<User> {
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal> </ak-forms-modal>
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) ${canImpersonate
? html` ? html`
<ak-action-button <ak-action-button
class="pf-m-tertiary" class="pf-m-tertiary"

View File

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

View File

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