rbac: add InitialPermissions (#13795)

* add `InitialPermissions` model to RBAC

This is a powerful construct between Permission and Role to set initial
permissions for newly created objects.

* use safer `request.user`

* fixup! use safer `request.user`

* force all self-defined serializers to descend from our custom one

See https://github.com/goauthentik/authentik/pull/10139

* reorganize initial permission assignment

* fixup! reorganize initial permission assignment
This commit is contained in:
Simonyi Gergő
2025-04-14 17:55:49 +02:00
committed by GitHub
parent 38a9e46af3
commit 03d5dad867
21 changed files with 1047 additions and 10 deletions

View File

@ -131,6 +131,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
["/core/tokens", msg("Tokens and App passwords")],
["/flow/stages/invitations", msg("Invitations")]]],

View File

@ -84,6 +84,10 @@ export const ROUTES: Route[] = [
await import("@goauthentik/admin/roles/RoleListPage");
return html`<ak-role-list></ak-role-list>`;
}),
new Route(new RegExp("^/identity/initial-permissions$"), async () => {
await import("@goauthentik/admin/rbac/InitialPermissionsListPage");
return html`<ak-initial-permissions-list></ak-initial-permissions-list>`;
}),
new Route(new RegExp(`^/identity/roles/(?<id>${UUID_REGEX})$`), async (args) => {
await import("@goauthentik/admin/roles/RoleViewPage");
return html`<ak-role-view roleId=${args.id}></ak-role-view>`;

View File

@ -0,0 +1,150 @@
import { InitialPermissionsModeToLabel } from "@goauthentik/admin/rbac/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DataProvision, DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/chips/Chip";
import "@goauthentik/elements/chips/ChipGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
InitialPermissions,
InitialPermissionsModeEnum,
Permission,
RbacApi,
RbacRolesListRequest,
Role,
} from "@goauthentik/api";
export function rbacPermissionPair(item: Permission): DualSelectPair {
return [item.id.toString(), html`<div class="selection-main">${item.name}</div>`, item.name];
}
@customElement("ak-initial-permissions-form")
export class InitialPermissionsForm extends ModelForm<InitialPermissions, string> {
loadInstance(pk: string): Promise<InitialPermissions> {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsRetrieve({
id: Number(pk),
});
}
getSuccessMessage(): string {
return this.instance
? msg("Successfully updated initial permissions.")
: msg("Successfully created initial permissions.");
}
async send(data: InitialPermissions): Promise<InitialPermissions> {
if (this.instance?.pk) {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsPartialUpdate({
id: this.instance.pk,
patchedInitialPermissionsRequest: data,
});
} else {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsCreate({
initialPermissionsRequest: data,
});
}
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Role")} required name="role">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Role[]> => {
const args: RbacRolesListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const users = await new RbacApi(DEFAULT_CONFIG).rbacRolesList(args);
return users.results;
}}
.renderElement=${(role: Role): string => {
return role.name;
}}
.renderDescription=${(role: Role): TemplateResult => {
return html`${role.name}`;
}}
.value=${(role: Role | undefined): string | undefined => {
return role?.pk;
}}
.selected=${(role: Role): boolean => {
return this.instance?.role === role.pk;
}}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"When a user with the selected Role creates an object, the Initial Permissions will be applied to that object.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<select class="pf-c-form-control">
<option
value=${InitialPermissionsModeEnum.User}
?selected=${this.instance?.mode === InitialPermissionsModeEnum.User}
>
${InitialPermissionsModeToLabel(InitialPermissionsModeEnum.User)}
</option>
<option
value=${InitialPermissionsModeEnum.Role}
?selected=${this.instance?.mode === InitialPermissionsModeEnum.Role}
>
${InitialPermissionsModeToLabel(InitialPermissionsModeEnum.Role)}
</option>
</select>
<p class="pf-c-form__helper-text">
${msg(
"The Initial Permissions can either be placed on the User creating the object, or the Role selected in the previous field.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Permissions")} name="permissions">
<ak-dual-select-provider
.provider=${(page: number, search?: string): Promise<DataProvision> => {
return new RbacApi(DEFAULT_CONFIG)
.rbacPermissionsList({
page: page,
search: search,
})
.then((results) => {
return {
pagination: results.pagination,
options: results.results.map(rbacPermissionPair),
};
});
}}
.selected=${(this.instance?.permissionsObj ?? []).map(rbacPermissionPair)}
available-label="${msg("Available Permissions")}"
selected-label="${msg("Selected Permissions")}"
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg("Permissions to grant when a new object is created.")}
</p>
</ak-form-element-horizontal>
</form>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-initial-permissions-form": InitialPermissionsForm;
}
}

View File

@ -0,0 +1,115 @@
import "@goauthentik/admin/rbac/InitialPermissionsForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { InitialPermissions, RbacApi } from "@goauthentik/api";
@customElement("ak-initial-permissions-list")
export class InitialPermissionsListPage extends TablePage<InitialPermissions> {
checkbox = true;
clearOnRefresh = true;
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return msg("Initial Permissions");
}
pageDescription(): string {
return msg("Set initial permissions for newly created objects.");
}
pageIcon(): string {
return "fa fa-lock";
}
@property()
order = "name";
async apiEndpoint(): Promise<PaginatedResponse<InitialPermissions>> {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsList(
await this.defaultEndpointConfig(),
);
}
columns(): TableColumn[] {
return [new TableColumn(msg("Name"), "name"), new TableColumn(msg("Actions"))];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Initial Permissions")}
.objects=${this.selectedElements}
.usedBy=${(item: InitialPermissions) => {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsUsedByList({
id: item.pk,
});
}}
.delete=${(item: InitialPermissions) => {
return new RbacApi(DEFAULT_CONFIG).rbacInitialPermissionsDestroy({
id: item.pk,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
render(): TemplateResult {
return html`<ak-page-header
icon=${this.pageIcon()}
header=${this.pageTitle()}
description=${ifDefined(this.pageDescription())}
>
</ak-page-header>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">${this.renderTable()}</div>
</section>`;
}
row(item: InitialPermissions): TemplateResult[] {
return [
html`${item.name}`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Initial Permissions")} </span>
<ak-initial-permissions-form slot="form" .instancePk=${item.pk}>
</ak-initial-permissions-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i>
</pf-tooltip>
</button>
</ak-forms-modal>`,
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Initial Permissions")} </span>
<ak-initial-permissions-form slot="form"> </ak-initial-permissions-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"initial-permissions-list": InitialPermissionsListPage;
}
}

View File

@ -0,0 +1,14 @@
import { msg } from "@lit/localize";
import { InitialPermissionsModeEnum } from "@goauthentik/api";
export function InitialPermissionsModeToLabel(mode: InitialPermissionsModeEnum): string {
switch (mode) {
case InitialPermissionsModeEnum.User:
return msg("User");
case InitialPermissionsModeEnum.Role:
return msg("Role");
case InitialPermissionsModeEnum.UnknownDefaultOpenApi:
return msg("Unknown Initial Permissions mode");
}
}