Skip to content

Commit 25677c2

Browse files
committed
Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/swagger-ui-express-5.0.1
2 parents af74637 + 252d1f5 commit 25677c2

File tree

4 files changed

+108
-28
lines changed

4 files changed

+108
-28
lines changed

src/api/v1/users/{userId}.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ const debug = Debug('otomi:api:v1:users')
66

77
export default function (): OperationHandlerArray {
88
const get: Operation = [
9-
({ otomi, params: { userId } }: OpenApiRequestExt, res): void => {
9+
({ otomi, user: sessionUser, params: { userId } }: OpenApiRequestExt, res): void => {
1010
debug(`getUser(${userId})`)
11-
const data = otomi.getUser(decodeURIComponent(userId))
11+
const data = otomi.getUser(decodeURIComponent(userId), sessionUser)
1212
res.json(data)
1313
},
1414
]

src/error.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable max-classes-per-file */
21
import { debug } from 'console'
32
import { CustomError } from 'ts-custom-error'
43

@@ -13,6 +12,12 @@ export class OtomiError extends CustomError {
1312
this.publicMessage = msg
1413
}
1514
}
15+
export class ForbiddenError extends OtomiError {
16+
public constructor(err?: string) {
17+
super('Forbidden', err)
18+
this.code = 403
19+
}
20+
}
1621
export class NotExistError extends OtomiError {
1722
public constructor(err?: string) {
1823
super('Not Found', err)

src/otomi-stack.test.ts

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,26 @@ describe('Users tests', () => {
259259

260260
const domainSuffix = 'dev.linode-apl.net'
261261

262+
const platformAdminSession: SessionUser = {
263+
name: 'Platform Admin',
264+
email: `platform-admin@${domainSuffix}`,
265+
isPlatformAdmin: true,
266+
isTeamAdmin: false,
267+
authz: {},
268+
teams: [],
269+
roles: [],
270+
sub: 'platform-admin',
271+
}
272+
const teamAdminSession: SessionUser = {
273+
name: 'Team Admin',
274+
email: `team-admin@${domainSuffix}`,
275+
isPlatformAdmin: false,
276+
isTeamAdmin: true,
277+
authz: {},
278+
teams: ['team1'],
279+
roles: [],
280+
sub: 'team-admin',
281+
}
262282
const sessionUser: SessionUser = {
263283
name: 'Session User',
264284
email: `session@${domainSuffix}`,
@@ -269,7 +289,6 @@ describe('Users tests', () => {
269289
roles: [],
270290
sub: 'session-user',
271291
}
272-
273292
const defaultPlatformAdmin: User = {
274293
id: '1',
275294
email: `platform-admin@${domainSuffix}`,
@@ -342,14 +361,75 @@ describe('Users tests', () => {
342361
test('should not allow deleting the default platform admin user', async () => {
343362
await expect(otomiStack.deleteUser('1')).rejects.toMatchObject({
344363
code: 403,
345-
publicMessage: 'Cannot delete the default platform admin user',
364+
publicMessage: 'Forbidden',
346365
})
347366
})
348367

349368
test('should allow deleting any other platform admin user', async () => {
350369
expect(await otomiStack.deleteUser('2')).toBeUndefined()
351370
})
352371

372+
describe('User Retrieve Validation', () => {
373+
beforeEach(async () => {
374+
otomiStack = new OtomiStack()
375+
await otomiStack.init()
376+
otomiStack.git = mockDeep<Git>()
377+
await otomiStack.initRepo()
378+
otomiStack.repoService.createUser(teamMember1)
379+
})
380+
381+
it('should return full user for platform admin', () => {
382+
const result = otomiStack.getUser(teamMember1.id!, platformAdminSession)
383+
expect(result).toMatchObject(teamMember1)
384+
})
385+
386+
it('should return limited user info for team admin', () => {
387+
const result = otomiStack.getUser(teamMember1.id!, teamAdminSession)
388+
expect(result).toEqual({
389+
id: teamMember1.id,
390+
email: teamMember1.email,
391+
isPlatformAdmin: teamMember1.isPlatformAdmin,
392+
isTeamAdmin: teamMember1.isTeamAdmin,
393+
teams: teamMember1.teams,
394+
})
395+
})
396+
397+
it('should throw 403 for regular user', () => {
398+
try {
399+
otomiStack.getUser(teamMember1.id!, { ...sessionUser, isPlatformAdmin: false, isTeamAdmin: false })
400+
fail('Expected error was not thrown')
401+
} catch (err: any) {
402+
expect(err).toHaveProperty('code', 403)
403+
}
404+
})
405+
406+
it('should return all users for platform admin in getAllUsers', () => {
407+
const users = otomiStack.getAllUsers(platformAdminSession)
408+
expect(users.some((u) => u.id === teamMember1.id)).toBe(true)
409+
})
410+
411+
it('should return limited info for team admin in getAllUsers', () => {
412+
const users = otomiStack.getAllUsers(teamAdminSession)
413+
expect(users[0]).toHaveProperty('id')
414+
expect(users[0]).toHaveProperty('email')
415+
expect(users[0]).toHaveProperty('isPlatformAdmin')
416+
expect(users[0]).toHaveProperty('isTeamAdmin')
417+
expect(users[0]).toHaveProperty('teams')
418+
// Should not have firstName/lastName
419+
expect(users[0]).not.toHaveProperty('firstName')
420+
expect(users[0]).not.toHaveProperty('lastName')
421+
})
422+
423+
it('should throw 403 for regular user in getAllUsers', () => {
424+
try {
425+
otomiStack.getAllUsers({ ...sessionUser, isPlatformAdmin: false, isTeamAdmin: false })
426+
fail('Expected error was not thrown')
427+
} catch (err: any) {
428+
expect(err).toHaveProperty('code', 403)
429+
}
430+
})
431+
})
432+
353433
describe('User Creation Validation', () => {
354434
describe('Username Length Validation', () => {
355435
it('should not create a user with less than 3 characters', async () => {
@@ -436,8 +516,6 @@ describe('Users tests', () => {
436516
await otomiStack.createUser(user)
437517
const updated = { ...user, firstName: 'edited' }
438518
jest.spyOn(otomiStack.repoService, 'updateUser').mockReturnValue(updated)
439-
// Use a platform admin session user
440-
const platformAdminSession = { ...sessionUser, isPlatformAdmin: true }
441519
const result = await otomiStack.editUser(user.id, updated, platformAdminSession)
442520
expect(result.firstName).toBe('edited')
443521
})
@@ -536,8 +614,7 @@ describe('Users tests', () => {
536614
const data = [{ ...teamMember2, teams: ['team3'] }]
537615
await expect(otomiStack.editTeamUsers(data, sessionUser)).rejects.toMatchObject({
538616
code: 403,
539-
publicMessage:
540-
'Team admins are permitted to add or remove users only within the teams they manage. However, they cannot remove themselves or other team admins from those teams.',
617+
publicMessage: 'Forbidden',
541618
})
542619
})
543620

@@ -569,7 +646,7 @@ describe('Users tests', () => {
569646
const data = [{ ...teamMember2, teams: ['team1'] }]
570647
await expect(otomiStack.editTeamUsers(data, regularUser)).rejects.toMatchObject({
571648
code: 403,
572-
publicMessage: "Only platform admins or team admins can modify a user's team memberships.",
649+
publicMessage: 'Forbidden',
573650
})
574651
})
575652
})

src/otomi-stack.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { readdir, readFile, writeFile } from 'fs/promises'
77
import { generate as generatePassword } from 'generate-password'
88
import { cloneDeep, filter, isEmpty, map, mapValues, merge, omit, pick, set, unset } from 'lodash'
99
import { getAppList, getAppSchema, getSpec } from 'src/app'
10-
import { AlreadyExists, HttpError, OtomiError, PublicUrlExists, ValidationError } from 'src/error'
10+
import { AlreadyExists, ForbiddenError, HttpError, OtomiError, PublicUrlExists, ValidationError } from 'src/error'
1111
import getRepo, { Git } from 'src/git'
1212
import { cleanSession, getSessionStack } from 'src/middleware'
1313
import {
@@ -1032,9 +1032,8 @@ export default class OtomiStack {
10321032
return { id, email, isPlatformAdmin, isTeamAdmin, teams }
10331033
})
10341034
return usersWithBasicInfo as Array<User>
1035-
} else {
1036-
return []
10371035
}
1036+
throw new ForbiddenError()
10381037
}
10391038

10401039
async createUser(data: User): Promise<User> {
@@ -1081,15 +1080,21 @@ export default class OtomiStack {
10811080
}
10821081
}
10831082

1084-
getUser(id: string): User {
1085-
return this.repoService.getUser(id)
1083+
getUser(id: string, sessionUser: SessionUser): User {
1084+
const user = this.repoService.getUser(id)
1085+
if (sessionUser.isPlatformAdmin) {
1086+
return user
1087+
}
1088+
if (sessionUser.isTeamAdmin) {
1089+
const { id: userId, email, isPlatformAdmin, isTeamAdmin, teams } = user
1090+
return { id: userId, email, isPlatformAdmin, isTeamAdmin, teams } as User
1091+
}
1092+
throw new ForbiddenError()
10861093
}
10871094

10881095
async editUser(id: string, data: User, sessionUser: SessionUser): Promise<User> {
10891096
if (!sessionUser.isPlatformAdmin) {
1090-
const error = new OtomiError('Only platform admins can modify user details.')
1091-
error.code = 403
1092-
throw error
1097+
throw new ForbiddenError('Only platform admins can modify user details.')
10931098
}
10941099
const user = this.repoService.updateUser(id, data)
10951100
await this.saveUser(user)
@@ -1106,10 +1111,7 @@ export default class OtomiStack {
11061111
async deleteUser(id: string): Promise<void> {
11071112
const user = this.repoService.getUser(id)
11081113
if (user.email === env.DEFAULT_PLATFORM_ADMIN_EMAIL) {
1109-
const error = new OtomiError('Forbidden')
1110-
error.code = 403
1111-
error.publicMessage = 'Cannot delete the default platform admin user'
1112-
throw error
1114+
throw new ForbiddenError('Cannot delete the default platform admin user')
11131115
}
11141116
await this.deleteUserFile(user)
11151117
await this.doRepoDeployment((repoService) => {
@@ -1150,21 +1152,17 @@ export default class OtomiStack {
11501152
sessionUser: SessionUser,
11511153
): Promise<Pick<User, 'id' | 'teams'>[]> {
11521154
if (!sessionUser.isPlatformAdmin && !sessionUser.isTeamAdmin) {
1153-
const error = new OtomiError("Only platform admins or team admins can modify a user's team memberships.")
1154-
error.code = 403
1155-
throw error
1155+
throw new ForbiddenError("Only platform admins or team admins can modify a user's team memberships.")
11561156
}
11571157
for (const user of data) {
11581158
const existingUser = this.repoService.getUser(user.id!)
11591159
if (
11601160
!sessionUser.isPlatformAdmin &&
11611161
!this.canTeamAdminUpdateUserTeams(sessionUser, existingUser, user.teams as string[])
11621162
) {
1163-
const error = new OtomiError(
1163+
throw new ForbiddenError(
11641164
'Team admins are permitted to add or remove users only within the teams they manage. However, they cannot remove themselves or other team admins from those teams.',
11651165
)
1166-
error.code = 403
1167-
throw error
11681166
}
11691167
const updateUser = this.repoService.updateUser(user.id!, { ...existingUser, teams: user.teams })
11701168
await this.saveUser(updateUser)

0 commit comments

Comments
 (0)