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