core: prevent self-impersonation (#6885)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		| @ -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,18 +46,24 @@ 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) { | ||||||
|         new CoreApi(DEFAULT_CONFIG) |         me().then((me) => { | ||||||
|             .coreUsersRetrieve({ |             this.me = me; | ||||||
|                 id: id, |             new CoreApi(DEFAULT_CONFIG) | ||||||
|             }) |                 .coreUsersRetrieve({ | ||||||
|             .then((user) => { |                     id: id, | ||||||
|                 this.user = user; |                 }) | ||||||
|             }); |                 .then((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
	
	Block a user
	 Jens L
					Jens L