Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/server-nestjs/src/modules/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,13 @@ describe('authController', () => {
expect(await controller.auth({ userId: user.id })).toEqual(toContractUser(user))
expect(service.getUser).toHaveBeenCalledWith(user.id)
})

it('merges the oidc-resolved admin role ids into the session payload', async () => {
const user: User = makeUser({ adminRoleIds: ['persisted-role'] })
service.getUser.mockResolvedValue(user)

const payload = await controller.auth({ userId: user.id, adminRoleIds: ['persisted-role', 'oidc-role'] })

expect(payload).toEqual({ ...toContractUser(user), adminRoleIds: ['persisted-role', 'oidc-role'] })
})
})
5 changes: 4 additions & 1 deletion apps/server-nestjs/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export class AuthController {
@Get()
@UseGuards(UserGuard)
async auth(@AuthUser() user: UserContext): Promise<User> {
return toContractUser(await this.authUserService.getUser(user.userId))
const contractUser = toContractUser(await this.authUserService.getUser(user.userId))
return user.adminRoleIds === undefined
? contractUser
: { ...contractUser, adminRoleIds: user.adminRoleIds }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createParamDecorator } from '@nestjs/common'
export interface UserContext {
userId: string
adminPermissions?: bigint
adminRoleIds?: string[]
userType?: User['type']
}

Expand All @@ -15,6 +16,7 @@ export const AuthUser = createParamDecorator(
return {
userId: request.userId,
adminPermissions: request.adminPermissions,
adminRoleIds: request.adminRoleIds,
userType: request.userType,
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ describe('keycloakJwtService', () => {
expect(result).toEqual({
userId: payload.sub,
adminPermissions: 60n,
adminRoleIds: ['stale-oidc-role', 'manual-role', 'current-oidc-role'],
userType: 'human',
})
})
Expand Down Expand Up @@ -131,6 +132,7 @@ describe('keycloakJwtService', () => {
expect(result).toEqual({
userId: payload.sub,
adminPermissions: 0n,
adminRoleIds: [],
userType: 'human',
})
})
Expand Down Expand Up @@ -165,6 +167,7 @@ describe('keycloakJwtService', () => {
expect(result).toEqual({
userId: payload.sub,
adminPermissions: undefined,
adminRoleIds: undefined,
userType: 'human',
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,29 @@ export class KeycloakJwtService implements AuthProvider {
): Promise<UserContext> {
return this.prisma.$transaction(async (tx) => {
const user = await upsertUser(tx, payload, makeUserSelect(requirements))
const adminContext = await this.maybeAdminContext(tx, payload, user.adminRoleIds ?? [], requirements)

return {
userId: payload.sub,
adminPermissions: await this.maybeAdminPermissions(tx, payload, user.adminRoleIds ?? [], requirements),
adminPermissions: adminContext?.adminPermissions,
adminRoleIds: adminContext?.adminRoleIds,
userType: this.maybeUserType(user, requirements),
}
})
}

private async maybeAdminPermissions(tx: Prisma.TransactionClient, payload: KeycloakPayload, adminRoleIds: string[], requirements?: AuthRequirements) {
private async maybeAdminContext(tx: Prisma.TransactionClient, payload: KeycloakPayload, adminRoleIds: string[], requirements?: AuthRequirements) {
if (!(requirements?.includeAdminRoleIds ?? true)) {
return undefined
}
const matchingAdminRoles = await listMatchingAdminRoles(tx, payload.groups, adminRoleIds)
return matchingAdminRoles.reduce((acc, curr) => acc | curr.permissions, 0n)
const oidcRoleIds = matchingAdminRoles
.filter(({ oidcGroup }) => oidcGroup && payload.groups.includes(oidcGroup))
.map(({ id }) => id)
return {
adminPermissions: matchingAdminRoles.reduce((acc, curr) => acc | curr.permissions, 0n),
adminRoleIds: [...new Set([...adminRoleIds, ...oidcRoleIds])],
}
}

private maybeUserType(user: UserRecord, requirements?: AuthRequirements) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface RequestWithProjectContext extends FastifyRequest {
type RequestWithUserContext = FastifyRequest & {
userId?: string
adminPermissions?: bigint
adminRoleIds?: string[]
userType?: string
}

Expand Down Expand Up @@ -70,6 +71,7 @@ export class ProjectGuard implements CanActivate {

request.userId = user.userId
if (user.adminPermissions !== undefined) request.adminPermissions = user.adminPermissions
if (user.adminRoleIds !== undefined) request.adminRoleIds = user.adminRoleIds
if (user.userType !== undefined) request.userType = user.userType

return user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ describe('userGuard', () => {
expect(request.userId).toBe('u1')
})

it('propagates the oidc-resolved admin role ids onto the request', async () => {
authService.authenticate.mockResolvedValue({ userId: 'u1', adminRoleIds: ['oidc-role'] })
userPolicy.build.mockReturnValue({ adminPermissions: [], userTypes: [] })
const ctx = makeExecutionContext({ 'x-dso-token': 'tok' })

const result = await guard.canActivate(ctx)
const request = ctx.switchToHttp().getRequest()

expect(result).toBe(true)
expect(request.adminRoleIds).toEqual(['oidc-role'])
})

it('validates required admin permissions', async () => {
authService.authenticate.mockResolvedValue({ userId: 'u1', adminPermissions: 32768n })
const policy: UserPolicyConfig = { adminPermissions: ['ListSystem'], userTypes: [] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { UserPermissionService } from './user.service'
type RequestWithAuthContext = FastifyRequest & {
userId?: string
adminPermissions?: bigint
adminRoleIds?: string[]
userType?: string
}

Expand Down Expand Up @@ -45,6 +46,7 @@ export class UserGuard implements CanActivate {

request.userId = user.userId
if (user.adminPermissions !== undefined) request.adminPermissions = user.adminPermissions
if (user.adminRoleIds !== undefined) request.adminRoleIds = user.adminRoleIds
if (user.userType !== undefined) request.userType = user.userType

return user
Expand Down
Loading