core: prevent self-impersonation (#6885)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
44ac944706
commit
3e81824388
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Reference in New Issue