rbac: Add show all to roles tab, add role tab to groups (#19097)

* improve sort order and inherit visual

* Update web/src/admin/groups/GroupViewPage.ts

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Connor Peshek <connor@connorpeshek.me>

* Update web/src/admin/users/UserViewPage.ts

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Connor Peshek <connor@connorpeshek.me>

* Update web/src/admin/roles/RelatedRoleList.ts

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Connor Peshek <connor@connorpeshek.me>

* Update web/src/admin/roles/RelatedRoleList.ts

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Connor Peshek <connor@connorpeshek.me>

* Update web/src/admin/roles/RelatedRoleList.ts

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Connor Peshek <connor@connorpeshek.me>

* Update web/src/admin/roles/RelatedRoleList.ts

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Connor Peshek <connor@connorpeshek.me>

* setup include inherited roles and fix returning nothing

* update api calls

* fix rendering error

* do not use set

* change from exception handling

* go off query param

* fix wording

* fix linting error for new group api structure

---------

Signed-off-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
This commit is contained in:
Connor Peshek
2026-01-05 17:14:44 -06:00
committed by GitHub
parent 24d99eae41
commit 4ac01724a5
8 changed files with 310 additions and 32 deletions
+20
View File
@@ -85,6 +85,7 @@ class GroupSerializer(ModelSerializer):
source="roles",
required=False,
)
inherited_roles_obj = SerializerMethodField(allow_null=True)
num_pk = IntegerField(read_only=True)
@property
@@ -108,6 +109,13 @@ class GroupSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_parents", "false")).lower() == "true"
@property
def _should_include_inherited_roles(self) -> bool:
request: Request = self.context.get("request", None)
if not request:
return True
return str(request.query_params.get("include_inherited_roles", "false")).lower() == "true"
@extend_schema_field(PartialUserSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[PartialUserSerializer] | None:
if not self._should_include_users:
@@ -126,6 +134,15 @@ class GroupSerializer(ModelSerializer):
return None
return RelatedGroupSerializer(instance.parents, many=True).data
@extend_schema_field(RoleSerializer(many=True))
def get_inherited_roles_obj(self, instance: Group) -> list | None:
"""Return only inherited roles from ancestor groups (excludes direct roles)"""
if not self._should_include_inherited_roles:
return None
direct_role_pks = instance.roles.values_list("pk", flat=True)
inherited_roles = instance.all_roles().exclude(pk__in=direct_role_pks)
return RoleSerializer(inherited_roles, many=True).data
def validate_is_superuser(self, superuser: bool):
"""Ensure that the user creating this group has permissions to set the superuser flag"""
request: Request = self.context.get("request", None)
@@ -167,6 +184,7 @@ class GroupSerializer(ModelSerializer):
"attributes",
"roles",
"roles_obj",
"inherited_roles_obj",
"children",
"children_obj",
]
@@ -289,6 +307,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
OpenApiParameter("include_users", bool, default=True),
OpenApiParameter("include_children", bool, default=False),
OpenApiParameter("include_parents", bool, default=False),
OpenApiParameter("include_inherited_roles", bool, default=False),
]
)
def list(self, request, *args, **kwargs):
@@ -299,6 +318,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
OpenApiParameter("include_users", bool, default=True),
OpenApiParameter("include_children", bool, default=False),
OpenApiParameter("include_parents", bool, default=False),
OpenApiParameter("include_inherited_roles", bool, default=False),
]
)
def retrieve(self, request, *args, **kwargs):
+52 -4
View File
@@ -2,7 +2,7 @@
from django.contrib.auth.models import Permission
from django.http import Http404
from django_filters.filters import AllValuesMultipleFilter, BooleanFilter
from django_filters.filters import AllValuesMultipleFilter, BooleanFilter, CharFilter, NumberFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field
@@ -22,7 +22,7 @@ from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import User
from authentik.core.models import Group, User
from authentik.rbac.decorators import permission_required
from authentik.rbac.models import Role, get_permission_choices
@@ -65,15 +65,63 @@ class RoleSerializer(ManagedSerializer, ModelSerializer):
class RoleFilterSet(FilterSet):
"""Filter for PropertyMapping"""
"""Filter for Role"""
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
managed__isnull = BooleanFilter(field_name="managed", lookup_expr="isnull")
inherited = BooleanFilter(
method="filter_inherited",
label="Include inherited roles (requires users or ak_groups filter)",
)
users = extend_schema_field(OpenApiTypes.INT)(
NumberFilter(
method="filter_users",
label="Filter by user (use with inherited=true for all roles)",
)
)
ak_groups = extend_schema_field(OpenApiTypes.UUID)(
CharFilter(
method="filter_ak_groups",
label="Filter by group (use with inherited=true for all roles)",
)
)
def filter_inherited(self, queryset, name, value):
"""This filter is handled by filter_users and filter_ak_groups"""
return queryset
def filter_users(self, queryset, name, value):
"""Filter roles by user, optionally including inherited roles"""
user = User.objects.filter(pk=value).first()
if not user:
return queryset.none()
include_inherited = self.data.get("inherited", "").lower() == "true"
if include_inherited:
return user.all_roles()
return queryset.filter(users=user)
def filter_ak_groups(self, queryset, name, value):
"""Filter roles by group, optionally including inherited roles"""
group = Group.objects.filter(pk=value).first()
if not group:
return queryset.none()
include_inherited = self.data.get("inherited", "").lower() == "true"
if include_inherited:
return group.all_roles()
return queryset.filter(ak_groups=group)
class Meta:
model = Role
fields = ["name", "users", "managed"]
fields = [
"name",
"managed",
]
class RoleViewSet(UsedByMixin, ModelViewSet):
@@ -165,7 +165,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
for _, u := range g.UsersObj {
if flag.UserPk == u.Pk {
// TODO: Is there a better way to clone this object?
fg := api.NewGroup(g.Pk, g.NumPk, g.Name, []api.RelatedGroup{}, []api.PartialUser{u}, []api.Role{}, []string{}, []api.RelatedGroup{})
fg := api.NewGroup(g.Pk, g.NumPk, g.Name, []api.RelatedGroup{}, []api.PartialUser{u}, []api.Role{}, nil, []string{}, []api.RelatedGroup{})
fg.SetUsers([]int32{flag.UserPk})
fg.SetAttributes(g.Attributes)
fg.SetIsSuperuser(*g.IsSuperuser)
+28 -5
View File
@@ -3385,6 +3385,11 @@ paths:
schema:
type: boolean
default: false
- in: query
name: include_inherited_roles
schema:
type: boolean
default: false
- in: query
name: include_parents
schema:
@@ -3478,6 +3483,11 @@ paths:
schema:
type: boolean
default: false
- in: query
name: include_inherited_roles
schema:
type: boolean
default: false
- in: query
name: include_parents
schema:
@@ -20047,6 +20057,16 @@ paths:
operationId: rbac_roles_list
description: Role viewset
parameters:
- in: query
name: ak_groups
schema:
type: string
format: uuid
- in: query
name: inherited
schema:
type: boolean
description: Include inherited roles (requires users or ak_groups filter)
- in: query
name: managed
schema:
@@ -20067,11 +20087,7 @@ paths:
- in: query
name: users
schema:
type: array
items:
type: integer
explode: true
style: form
type: integer
tags:
- rbac
security:
@@ -38770,6 +38786,12 @@ components:
items:
$ref: '#/components/schemas/Role'
readOnly: true
inherited_roles_obj:
type: array
items:
$ref: '#/components/schemas/Role'
readOnly: true
nullable: true
children:
type: array
items:
@@ -38785,6 +38807,7 @@ components:
required:
- children
- children_obj
- inherited_roles_obj
- name
- num_pk
- parents_obj
+76 -1
View File
@@ -1,6 +1,7 @@
import "#admin/groups/GroupForm";
import "#admin/groups/RelatedUserList";
import "#admin/rbac/ObjectPermissionsPage";
import "#admin/roles/RelatedRoleList";
import "#components/ak-status-label";
import "#components/events/ObjectChangelog";
import "#elements/CodeMirror";
@@ -21,7 +22,7 @@ import { setPageDetails } from "#components/ak-page-navbar";
import { CoreApi, Group, RbacPermissionsAssignedByRolesListModelEnum } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { CSSResult, html, nothing, PropertyValues } from "lit";
import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@@ -43,6 +44,7 @@ export class GroupViewPage extends AKElement {
.coreGroupsRetrieve({
groupUuid: id,
includeUsers: false,
includeInheritedRoles: true,
})
.then((group) => {
this.group = group;
@@ -137,6 +139,34 @@ export class GroupViewPage extends AKElement {
</a>
</li>`;
})}
${(this.group.inheritedRolesObj ?? []).map(
(role) => {
return html`<li>
<a
href=${`#/identity/roles/${role.pk}`}
>${role.name}
</a>
<pf-tooltip
position="top"
content=${msg(
"Inherited from parent group",
)}
>
<span
class="pf-c-label pf-m-outline pf-m-cyan"
style="margin-left: 0.5rem;"
>
<span
class="pf-c-label__content"
>${msg(
"Inherited",
)}</span
>
</span>
</pf-tooltip>
</li>`;
},
)}
</ul>
</div>
</dd>
@@ -203,6 +233,15 @@ export class GroupViewPage extends AKElement {
</div>
</div>
</section>
<section
role="tabpanel"
tabindex="0"
slot="page-roles"
id="page-roles"
aria-label="${msg("Roles")}"
>
${this.renderTabRoles(this.group)}
</section>
<ak-rbac-object-permission-page
role="tabpanel"
tabindex="0"
@@ -216,6 +255,42 @@ export class GroupViewPage extends AKElement {
</main>`;
}
protected renderTabRoles(group: Group): TemplateResult {
return html`
<ak-tabs pageIdentifier="groupRoles" vertical>
<div
role="tabpanel"
tabindex="0"
slot="page-assigned-roles"
id="page-assigned-roles"
aria-label=${msg("Assigned Roles")}
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-role-related-list .targetGroup=${group}> </ak-role-related-list>
</div>
</div>
</div>
<div
role="tabpanel"
tabindex="0"
slot="page-all-roles"
id="page-all-roles"
aria-label=${msg("All Roles")}
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-role-related-list .targetGroup=${group} showInherited>
</ak-role-related-list>
</div>
</div>
</div>
</ak-tabs>
`;
}
updated(changed: PropertyValues<this>) {
super.updated(changed);
setPageDetails({
+93 -14
View File
@@ -14,14 +14,16 @@ import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
import { SlottedTemplateResult } from "#elements/types";
import { ifPresent } from "#elements/utils/attributes";
import { RbacApi, Role, User } from "@goauthentik/api";
import { Group, RbacApi, Role, User } from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { html, nothing, TemplateResult } from "lit";
import { html, nothing, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
@customElement("ak-role-related-add")
export class RelatedRoleAdd extends Form<{ roles: string[] }> {
#api = new RbacApi(DEFAULT_CONFIG);
@property({ attribute: false })
public user: User | null = null;
@@ -36,7 +38,7 @@ export class RelatedRoleAdd extends Form<{ roles: string[] }> {
await Promise.all(
data.roles.map((role) => {
if (!this.user) return Promise.resolve();
return new RbacApi(DEFAULT_CONFIG).rbacRolesAddUserCreate({
return this.#api.rbacRolesAddUserCreate({
uuid: role,
userAccountSerializerForRoleRequest: {
pk: this.user.pk,
@@ -86,6 +88,8 @@ export class RelatedRoleAdd extends Form<{ roles: string[] }> {
@customElement("ak-role-related-list")
export class RelatedRoleList extends Table<Role> {
#api = new RbacApi(DEFAULT_CONFIG);
checkbox = true;
clearOnRefresh = true;
protected override searchEnabled = true;
@@ -96,19 +100,54 @@ export class RelatedRoleList extends Table<Role> {
@property({ attribute: false })
public targetUser: User | null = null;
@property({ attribute: false })
public targetGroup: Group | null = null;
@property({ type: Boolean })
public showInherited = false;
willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (changedProperties.has("showInherited")) {
// Disable checkboxes in showInherited mode (view-only)
this.checkbox = !this.showInherited;
}
}
async apiEndpoint(): Promise<PaginatedResponse<Role>> {
return new RbacApi(DEFAULT_CONFIG).rbacRolesList({
...(await this.defaultEndpointConfig()),
users: this.targetUser ? [this.targetUser.pk] : [],
const config = await this.defaultEndpointConfig();
if (this.targetGroup) {
return this.#api.rbacRolesList({
...config,
akGroups: this.targetGroup.pk,
inherited: this.showInherited,
});
}
return this.#api.rbacRolesList({
...config,
users: this.targetUser?.pk,
inherited: this.showInherited,
});
}
protected columns: TableColumn[] = [
[msg("Name"), "name"],
[msg("Actions"), null, msg("Row Actions")],
];
protected get columns(): TableColumn[] {
// Hide actions column in showInherited mode (view-only)
if (this.showInherited) {
return [[msg("Name"), "name"]];
}
return [
[msg("Name"), "name"],
[msg("Actions"), null, msg("Row Actions")],
];
}
renderToolbarSelected(): TemplateResult {
renderToolbarSelected(): SlottedTemplateResult {
// Don't render Remove button in showInherited mode (view-only)
if (this.showInherited) {
return nothing;
}
const disabled = !this.selectedElements.length;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Role(s)")}
@@ -120,7 +159,7 @@ export class RelatedRoleList extends Table<Role> {
.objects=${this.selectedElements}
.delete=${(item: Role) => {
if (!this.targetUser) return;
return new RbacApi(DEFAULT_CONFIG).rbacRolesRemoveUserCreate({
return this.#api.rbacRolesRemoveUserCreate({
uuid: item.pk,
userAccountSerializerForRoleRequest: {
pk: this.targetUser.pk,
@@ -134,10 +173,46 @@ export class RelatedRoleList extends Table<Role> {
</ak-forms-delete-bulk>`;
}
protected isInherited(role: Role): boolean {
if (this.targetGroup) {
// For groups, check if role is in direct roles
if (!this.targetGroup.roles) return false;
return !this.targetGroup.roles.includes(role.pk);
}
if (this.targetUser) {
// For users, check if role is in direct roles
if (!this.targetUser.roles) return false;
return !this.targetUser.roles.includes(role.pk);
}
return false;
}
row(item: Role): SlottedTemplateResult[] {
const inherited = this.showInherited && this.isInherited(item);
const inheritedTooltip = this.targetGroup
? msg("Inherited from parent group")
: msg("Inherited from group");
const nameCell = html`<a href="#/identity/roles/${item.pk}">${item.name}</a> ${inherited
? html`<pf-tooltip position="top" content=${inheritedTooltip}>
<span class="pf-c-label pf-m-outline pf-m-cyan">
<span class="pf-c-label__content">&nbsp;${msg("Inherited")}</span>
</span>
</pf-tooltip>`
: nothing}`;
// Hide actions in showInherited mode (view-only)
if (this.showInherited) {
return [nameCell];
}
return [
html`<a href="#/identity/roles/${item.pk}">${item.name}</a>`,
html` <ak-forms-modal>
nameCell,
html`<ak-forms-modal>
<span slot="submit">${msg("Update")}</span>
<span slot="header">${msg("Update Role")}</span>
<ak-role-form slot="form" .instancePk=${item.pk}> </ak-role-form>
@@ -151,6 +226,10 @@ export class RelatedRoleList extends Table<Role> {
}
renderToolbar(): TemplateResult {
// Hide add buttons in showInherited mode (view-only)
if (this.showInherited || this.targetGroup) {
return html`${super.renderToolbar()}`;
}
return html`
${this.targetUser
? html`<ak-forms-modal>
+3 -1
View File
@@ -49,7 +49,9 @@ export class RoleSelectModal extends TableModal<Role> {
renderModalInner(): SlottedTemplateResult {
return html`<section class="pf-c-modal-box__header pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1 class="pf-c-title pf-m-2xl">${msg("Assign User to Groups")}</h1>
<h1 class="pf-c-title pf-m-2xl">
${msg("Select roles to attach to the user")}
</h1>
</div>
</section>
<section class="pf-c-modal-box__body pf-m-light">${this.renderTable()}</section>
+37 -6
View File
@@ -370,6 +370,42 @@ export class UserViewPage extends WithCapabilitiesConfig(WithSession(AKElement))
</div>`;
}
protected renderTabRoles(user: User): TemplateResult {
return html`
<ak-tabs pageIdentifier="userRoles" vertical>
<div
role="tabpanel"
tabindex="0"
slot="page-assigned-roles"
id="page-assigned-roles"
aria-label=${msg("Assigned Roles")}
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-role-related-list .targetUser=${user}> </ak-role-related-list>
</div>
</div>
</div>
<div
role="tabpanel"
tabindex="0"
slot="page-all-roles"
id="page-all-roles"
aria-label=${msg("All Roles")}
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-role-related-list .targetUser=${user} showInherited>
</ak-role-related-list>
</div>
</div>
</div>
</ak-tabs>
`;
}
render() {
if (!this.user) {
return nothing;
@@ -452,13 +488,8 @@ export class UserViewPage extends WithCapabilitiesConfig(WithSession(AKElement))
slot="page-roles"
id="page-roles"
aria-label=${msg("Roles")}
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-role-related-list .targetUser=${this.user}> </ak-role-related-list>
</div>
</div>
${this.renderTabRoles(this.user)}
</div>
<div
role="tabpanel"