fix: Rechteausweitung ueber Rollen-Rechtezuweisung geschlossen

RolesService.create/update uebernahm eine explizit uebergebene
permissionKeys-Liste ungeprueft. Ein Nutzer mit nur roles:update
konnte damit JEDER Rolle - auch der eigenen, ohne users:assign-role -
beliebige weitere Rechte (bis hin zu allen 92 Katalog-Eintraegen)
hinzufuegen, da das Bearbeiten der eigenen Rollen-Rechte nicht ueber
User.roleId laeuft und daher vom bereits gefixten
users:assign-role-Check nicht erfasst wurde.

Neue Regel (Standard-RBAC-Prinzip "keine Rechte ueber das eigene
Rechte-Set hinaus vergeben"): create/update pruefen jetzt, dass der
aufrufende Nutzer jedes zu vergebende Recht selbst bereits besitzt,
bevor die Rolle angelegt/geaendert wird.

roles.service.ts hatte bislang keine Unit-Tests; neue Spec-Datei deckt
den Fix sowie die uebrigen Methoden (create/update/deactivate/
reactivate) ab.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-02 14:58:44 +02:00
1 parent 8b8616ce03
commit f57cffe6a8
2 files changed
+317

No files matched your search

@@ -81,6 +81,31 @@ export class RolesService {
return this.toSummary(await this.findById(id));
}
/**
* Nobody can grant a role a permission they do not hold themselves - a
* caller with only roles:update could otherwise turn any role (including
* one they belong to) into a shadow-admin by adding every catalog key, no
* users:assign-role needed at all since editing a role's own grants
* doesn't touch User.roleId. This is the standard "can't escalate beyond
* what you have" RBAC rule.
*/
private async assertCanGrantPermissions(
permissionKeys: string[],
actingUserId?: string,
): Promise<void> {
if (!actingUserId) {
return;
}
const callerPermissions =
await this.permissionResolver.getPermissions(actingUserId);
const missing = permissionKeys.filter((key) => !callerPermissions.has(key));
if (missing.length > 0) {
throw new ForbiddenException(
`Cannot grant permissions you do not hold yourself: ${missing.join(', ')}`,
);
}
}
async create(
dto: CreateRoleDto,
actingUserId?: string,
@@ -91,6 +116,7 @@ export class RolesService {
if (existing) {
throw new ConflictException('Role key already in use');
}
await this.assertCanGrantPermissions(dto.permissionKeys, actingUserId);
const role = await this.prisma.$transaction(async (tx) => {
const created = await tx.role.create({
data: {
@@ -126,6 +152,9 @@ export class RolesService {
if (before.isSystem) {
throw new ForbiddenException('System roles cannot be edited');
}
if (dto.permissionKeys) {
await this.assertCanGrantPermissions(dto.permissionKeys, actingUserId);
}
const role = await this.prisma.$transaction(async (tx) => {
await tx.role.update({
where: { id },