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:
@ -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")]]],
|
||||
|
||||
@ -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>`;
|
||||
|
||||
150
web/src/admin/rbac/InitialPermissionsForm.ts
Normal file
150
web/src/admin/rbac/InitialPermissionsForm.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
115
web/src/admin/rbac/InitialPermissionsListPage.ts
Normal file
115
web/src/admin/rbac/InitialPermissionsListPage.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
14
web/src/admin/rbac/utils.ts
Normal file
14
web/src/admin/rbac/utils.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user