groups und impersination
This commit is contained in:
@@ -24,6 +24,8 @@ import {
|
||||
import type { TaskDigestPreference } from '../tasks/task-digest.types';
|
||||
import { OidcProfile, OidcService } from './oidc.service';
|
||||
import { RefreshTokenEntity } from './refresh-token.entity';
|
||||
import { UserKeycloakGroupEntity } from './user-keycloak-group.entity';
|
||||
import { UserImpersonationEntity } from './user-impersonation.entity';
|
||||
import { UserEntity } from './user.entity';
|
||||
|
||||
@Injectable()
|
||||
@@ -42,6 +44,10 @@ export class AuthService {
|
||||
private readonly usersRepository: Repository<UserEntity>,
|
||||
@InjectRepository(RefreshTokenEntity)
|
||||
private readonly refreshTokensRepository: Repository<RefreshTokenEntity>,
|
||||
@InjectRepository(UserKeycloakGroupEntity)
|
||||
private readonly userKeycloakGroupsRepository: Repository<UserKeycloakGroupEntity>,
|
||||
@InjectRepository(UserImpersonationEntity)
|
||||
private readonly userImpersonationsRepository: Repository<UserImpersonationEntity>,
|
||||
@Optional()
|
||||
private readonly auditLogService?: AuditLogService,
|
||||
) {}
|
||||
@@ -89,9 +95,12 @@ export class AuthService {
|
||||
where: { email: this.normalizeEmail(profile.email) },
|
||||
}));
|
||||
const user = await this.syncOidcUser(profile, existingUser);
|
||||
await this.syncKeycloakGroups(user.id, profile.groups);
|
||||
const response = {
|
||||
...(await this.createAuthTokens(user)),
|
||||
user: this.toPublicUser(user),
|
||||
user: await this.toPublicUserWithGroups(
|
||||
await this.resolveEffectiveUser(user),
|
||||
),
|
||||
};
|
||||
|
||||
await this.auditLogService?.record({
|
||||
@@ -136,7 +145,9 @@ export class AuthService {
|
||||
|
||||
const response = {
|
||||
...(await this.createAuthTokens(user)),
|
||||
user: this.toPublicUser(user),
|
||||
user: await this.toPublicUserWithGroups(
|
||||
await this.resolveEffectiveUser(user),
|
||||
),
|
||||
};
|
||||
|
||||
await this.auditLogService?.record({
|
||||
@@ -168,7 +179,19 @@ export class AuthService {
|
||||
throw new UnauthorizedException('Access token is invalid.');
|
||||
}
|
||||
|
||||
return payload;
|
||||
const effectiveUser = await this.resolveEffectiveUser(user);
|
||||
|
||||
if (effectiveUser.id === user.id) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return {
|
||||
...payload,
|
||||
sub: effectiveUser.id,
|
||||
email: effectiveUser.email,
|
||||
impersonatorSub: user.id,
|
||||
impersonatorEmail: user.email,
|
||||
};
|
||||
} catch {
|
||||
throw new UnauthorizedException('Access token is invalid.');
|
||||
}
|
||||
@@ -195,7 +218,7 @@ export class AuthService {
|
||||
throw new UnauthorizedException('Authenticated user is required.');
|
||||
}
|
||||
|
||||
return this.toPublicUser(user);
|
||||
return this.toPublicUserWithGroups(user);
|
||||
}
|
||||
|
||||
async searchUsers(
|
||||
@@ -255,7 +278,7 @@ export class AuthService {
|
||||
metadata: { completed },
|
||||
});
|
||||
|
||||
return this.toPublicUser(savedUser);
|
||||
return this.toPublicUserWithGroups(savedUser);
|
||||
}
|
||||
|
||||
async updateTaskDigestPreference(
|
||||
@@ -283,7 +306,7 @@ export class AuthService {
|
||||
metadata: { taskDigestPreference: savedUser.taskDigestPreference },
|
||||
});
|
||||
|
||||
return this.toPublicUser(savedUser);
|
||||
return this.toPublicUserWithGroups(savedUser);
|
||||
}
|
||||
|
||||
private normalizeEmail(email?: string): string {
|
||||
@@ -333,6 +356,77 @@ export class AuthService {
|
||||
return this.usersRepository.save(user);
|
||||
}
|
||||
|
||||
private async resolveEffectiveUser(user: UserEntity): Promise<UserEntity> {
|
||||
const now = Date.now();
|
||||
const impersonations = await this.userImpersonationsRepository.find({
|
||||
where: { impersonatorUserId: user.id, enabled: true },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
const activeImpersonation = impersonations.find(
|
||||
(impersonation) =>
|
||||
!impersonation.expiresAt || impersonation.expiresAt.getTime() > now,
|
||||
);
|
||||
|
||||
if (!activeImpersonation) {
|
||||
return user;
|
||||
}
|
||||
|
||||
const targetUser = await this.usersRepository.findOne({
|
||||
where: { id: activeImpersonation.targetUserId },
|
||||
});
|
||||
|
||||
if (!targetUser) {
|
||||
throw new UnauthorizedException('Impersonated user does not exist.');
|
||||
}
|
||||
|
||||
return targetUser;
|
||||
}
|
||||
|
||||
private async syncKeycloakGroups(
|
||||
userId: string,
|
||||
groups: string[],
|
||||
): Promise<void> {
|
||||
const normalizedGroups = this.normalizeGroupPaths(groups);
|
||||
|
||||
await this.userKeycloakGroupsRepository.delete({ userId });
|
||||
|
||||
if (!normalizedGroups.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userKeycloakGroupsRepository.save(
|
||||
normalizedGroups.map((groupPath) =>
|
||||
this.userKeycloakGroupsRepository.create({
|
||||
id: randomUUID(),
|
||||
userId,
|
||||
groupPath,
|
||||
groupName: this.groupNameFromPath(groupPath),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private normalizeGroupPaths(groups: string[]): string[] {
|
||||
return [...new Set(groups)]
|
||||
.map((group) => group.trim())
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
private groupNameFromPath(groupPath: string): string {
|
||||
return groupPath.split('/').filter(Boolean).at(-1) ?? groupPath;
|
||||
}
|
||||
|
||||
private async getUserGroupPaths(userId: string): Promise<string[]> {
|
||||
const groups = await this.userKeycloakGroupsRepository.find({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
return groups
|
||||
.map((group) => group.groupPath)
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
private secretMatches(secret: string, storedSecretHash: string): boolean {
|
||||
const [salt, storedHash] = storedSecretHash.split(':');
|
||||
|
||||
@@ -419,13 +513,18 @@ export class AuthService {
|
||||
return this.secretMatches(token, tokenHash);
|
||||
}
|
||||
|
||||
private toPublicUser(user: UserEntity): PublicUser {
|
||||
private async toPublicUserWithGroups(user: UserEntity): Promise<PublicUser> {
|
||||
return this.toPublicUser(user, await this.getUserGroupPaths(user.id));
|
||||
}
|
||||
|
||||
private toPublicUser(user: UserEntity, groups: string[] = []): PublicUser {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name ?? undefined,
|
||||
onboardingCompleted: user.onboardingCompleted === true,
|
||||
taskDigestPreference: user.taskDigestPreference ?? 'both',
|
||||
groups,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user