generated from bastian/boilerplate
deploy
This commit is contained in:
139
apps/backend/src/auth/oidc-admin-synchronization.service.ts
Normal file
139
apps/backend/src/auth/oidc-admin-synchronization.service.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { AuditAction } from '../audit/entities/audit-log.entity';
|
||||
import { ApiError } from '../common/errors/api-error';
|
||||
import { ErrorCode } from '../common/errors/error-codes';
|
||||
import { AppConfigService } from '../config/config.service';
|
||||
import { RolesService } from '../roles/roles.service';
|
||||
import { UserEntity } from '../users/entities/user.entity';
|
||||
import {
|
||||
UserRoleAssignmentEntity,
|
||||
UserRoleAssignmentSource,
|
||||
} from '../users/entities/user-role-assignment.entity';
|
||||
import { OidcRoleExtractorService } from './oidc-role-extractor.service';
|
||||
|
||||
@Injectable()
|
||||
export class OidcAdminSynchronizationService {
|
||||
private readonly logger = new Logger(OidcAdminSynchronizationService.name);
|
||||
|
||||
constructor(
|
||||
private readonly config: AppConfigService,
|
||||
private readonly extractor: OidcRoleExtractorService,
|
||||
private readonly roles: RolesService,
|
||||
private readonly audit: AuditService,
|
||||
@InjectDataSource() private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async synchronize(
|
||||
userId: string,
|
||||
claims: Readonly<Record<string, unknown>>,
|
||||
): Promise<UserEntity> {
|
||||
const adminRoleName = this.config.oidc.adminRole;
|
||||
if (!adminRoleName) return this.reloadUser(userId);
|
||||
const claimPath = this.config.oidc.rolesClaim;
|
||||
if (!claimPath)
|
||||
throw new Error(
|
||||
'OIDC_ROLES_CLAIM fehlt trotz validierter Konfiguration.',
|
||||
);
|
||||
const extracted = this.extractor.extract(claims, claimPath);
|
||||
if (!extracted.readable) {
|
||||
this.logger.error(
|
||||
{ userId, claimPath, reason: extracted.reason },
|
||||
'OIDC-Administrator-Synchronisierung: Rollenclaim konnte nicht gelesen werden',
|
||||
);
|
||||
throw new ApiError(
|
||||
ErrorCode.Unauthorized,
|
||||
'Die OIDC-Rollen konnten nicht sicher validiert werden.',
|
||||
401,
|
||||
);
|
||||
}
|
||||
const normalize = (value: string) =>
|
||||
this.config.oidc.roleMatchCaseSensitive
|
||||
? value
|
||||
: value.toLocaleLowerCase('en-US');
|
||||
const hasAdminRole = extracted.roles.some(
|
||||
(role) => normalize(role) === normalize(adminRoleName),
|
||||
);
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const { admin } = await this.roles.ensureSystemRoles(manager);
|
||||
const assignments = manager.getRepository(UserRoleAssignmentEntity);
|
||||
const existing = await assignments.findOneBy({
|
||||
userId,
|
||||
roleId: admin.id,
|
||||
source: UserRoleAssignmentSource.Oidc,
|
||||
});
|
||||
if (hasAdminRole) {
|
||||
if (existing) {
|
||||
existing.lastSynchronizedAt = new Date();
|
||||
await assignments.save(existing);
|
||||
} else {
|
||||
await manager.query(
|
||||
`INSERT INTO user_role_assignments
|
||||
(id, user_id, role_id, source, last_synchronized_at, created_at, updated_at)
|
||||
VALUES (UUID(), ?, ?, ?, CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3))
|
||||
ON DUPLICATE KEY UPDATE last_synchronized_at = CURRENT_TIMESTAMP(3), updated_at = CURRENT_TIMESTAMP(3)`,
|
||||
[userId, admin.id, UserRoleAssignmentSource.Oidc],
|
||||
);
|
||||
await manager.query(
|
||||
'INSERT IGNORE INTO user_roles (user_id, role_id) VALUES (?, ?)',
|
||||
[userId, admin.id],
|
||||
);
|
||||
await this.audit.record(
|
||||
userId,
|
||||
AuditAction.OidcAdminAssigned,
|
||||
'user',
|
||||
userId,
|
||||
{
|
||||
roleId: admin.id,
|
||||
oidcRole: adminRoleName,
|
||||
source: UserRoleAssignmentSource.Oidc,
|
||||
},
|
||||
manager,
|
||||
);
|
||||
}
|
||||
} else if (existing) {
|
||||
await assignments.remove(existing);
|
||||
const remaining = await assignments.countBy({
|
||||
userId,
|
||||
roleId: admin.id,
|
||||
});
|
||||
if (remaining === 0) {
|
||||
await manager.query(
|
||||
'DELETE FROM user_roles WHERE user_id = ? AND role_id = ?',
|
||||
[userId, admin.id],
|
||||
);
|
||||
}
|
||||
await this.audit.record(
|
||||
userId,
|
||||
AuditAction.OidcAdminRemoved,
|
||||
'user',
|
||||
userId,
|
||||
{
|
||||
roleId: admin.id,
|
||||
oidcRole: adminRoleName,
|
||||
source: UserRoleAssignmentSource.Oidc,
|
||||
},
|
||||
manager,
|
||||
);
|
||||
}
|
||||
});
|
||||
return this.reloadUser(userId);
|
||||
}
|
||||
|
||||
private async reloadUser(userId: string): Promise<UserEntity> {
|
||||
const user = await this.dataSource.getRepository(UserEntity).findOne({
|
||||
where: { id: userId },
|
||||
relations: { roles: { permissions: true }, settings: true },
|
||||
});
|
||||
if (!user)
|
||||
throw new ApiError(
|
||||
ErrorCode.UserNotFound,
|
||||
'Der Benutzer wurde nicht gefunden.',
|
||||
404,
|
||||
);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user