generated from bastian/boilerplate
141 lines
4.8 KiB
TypeScript
141 lines
4.8 KiB
TypeScript
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.',
|
|
);
|
|
console.log(claims, claimPath)
|
|
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;
|
|
}
|
|
}
|