Skip to content

Commit f465df9

Browse files
committed
feat: add OIDC group to project groups
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
1 parent cf5ba00 commit f465df9

17 files changed

Lines changed: 315 additions & 88 deletions

File tree

apps/client/src/components/ProjectRoleForm.vue

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts" setup>
22
import { computed, ref } from 'vue'
3-
import type { Member, ProjectV2, RoleBigint } from '@cpn-console/shared'
3+
import type { Member, ProjectRoleBigint, ProjectV2 } from '@cpn-console/shared'
44
import { PROJECT_PERMS, projectPermsDetails, shallowEqual } from '@cpn-console/shared'
55
66
const props = defineProps<{
@@ -10,19 +10,21 @@ const props = defineProps<{
1010
allMembers: Member[]
1111
projectId: ProjectV2['id']
1212
isEveryone: boolean
13+
oidcGroup?: string
1314
}>()
1415
1516
defineEmits<{
1617
delete: []
1718
updateMemberRoles: [checked: boolean, userId: Member['userId']]
18-
save: [value: Omit<RoleBigint, 'position'>]
19+
save: [value: Omit<ProjectRoleBigint, 'position' | 'projectId'>]
1920
cancel: []
2021
}>()
2122
const router = useRouter()
2223
const role = ref({
2324
...props,
2425
permissions: props.permissions ?? 0n,
2526
allMembers: props.allMembers ?? [],
27+
oidcGroup: props.oidcGroup ?? '',
2628
})
2729
2830
const isUpdated = computed(() => {
@@ -68,6 +70,14 @@ function updateChecked(checked: boolean, value: bigint) {
6870
class="mb-5"
6971
:disabled="role.isEveryone"
7072
/>
73+
<h6>Groupe OIDC</h6>
74+
<DsfrInput
75+
v-model="role.oidcGroup"
76+
data-testid="roleOidcGroupInput"
77+
label-visible
78+
class="mb-5"
79+
:disabled="role.isEveryone"
80+
/>
7181
<h6>Permissions</h6>
7282
<div
7383
v-for="scope in projectPermsDetails"

apps/client/src/components/ProjectRoles.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts" setup>
2-
import type { Member, Role, RoleBigint } from '@cpn-console/shared'
2+
import type { Member, ProjectRole, ProjectRoleBigint, Role, RoleBigint } from '@cpn-console/shared'
33
import { useSnackbarStore } from '@/stores/snackbar.js'
44
import type { Project } from '@/utils/project-utils.js'
55
@@ -11,7 +11,7 @@ const snackbarStore = useSnackbarStore()
1111
1212
const selectedId = ref<string>()
1313
14-
type RoleItem = Omit<Role, 'permissions'> & { permissions: bigint, memberCounts: number, isEveryone: boolean }
14+
type RoleItem = Omit<ProjectRole, 'permissions'> & { permissions: bigint, memberCounts: number, isEveryone: boolean }
1515
1616
const roleList = ref<RoleItem[]>([])
1717
@@ -56,7 +56,7 @@ async function saveEveryoneRole(role: { permissions: bigint }) {
5656
snackbarStore.setMessage('Rôle mis à jour', 'success')
5757
}
5858
59-
async function saveRole(role: Omit<RoleBigint, 'position'>) {
59+
async function saveRole(role: Omit<ProjectRoleBigint, 'position' | 'projectId'>) {
6060
if (role.id === 'everyone') {
6161
await saveEveryoneRole(role)
6262
snackbarStore.setMessage('Rôle mis à jour', 'success')
@@ -67,6 +67,7 @@ async function saveRole(role: Omit<RoleBigint, 'position'>) {
6767
id: selectedRole.value.id,
6868
permissions: role.permissions.toString(),
6969
name: role.name,
70+
oidcGroup: role.oidcGroup,
7071
}])
7172
reload()
7273
snackbarStore.setMessage('Rôle mis à jour', 'success')
@@ -86,6 +87,7 @@ function reload() {
8687
permissions: BigInt(props.project.everyonePerms),
8788
position: 1000,
8889
isEveryone: true,
90+
projectId: props.project.id,
8991
})
9092
roleList.value = roles
9193
}
@@ -142,6 +144,7 @@ watch(props.project, reload, { immediate: true })
142144
:permissions="BigInt(selectedRole.permissions)"
143145
:project-id="project.id"
144146
:is-everyone="selectedRole.isEveryone"
147+
:oidc-group="selectedRole.oidcGroup"
145148
:all-members="project.members"
146149
@delete="deleteRole(selectedRole.id)"
147150
@update-member-roles="(checked: boolean, userId: Member['userId']) => updateMember(checked, userId)"

apps/client/src/utils/project-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class Project implements ProjectV2 {
6464
locked: boolean
6565
owner: Omit<User, 'adminRoleIds'>
6666
ownerId: string
67-
roles: { id: string, name: string, permissions: string, position: number }[]
67+
roles: { id: string, name: string, permissions: string, position: number, projectId: string, oidcGroup?: string, type?: string }[]
6868
members: ({ userId: string, firstName: string, lastName: string, email: string, roleIds: string[] } | { updatedAt: string, createdAt: string, firstName: string, lastName: string, email: string, userId: string, roleIds: string[] })[]
6969
createdAt: string
7070
updatedAt: string
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "ProjectRole" ADD COLUMN "oidcGroup" TEXT NOT NULL DEFAULT '';
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Please do not edit this file manually
22
# It should be added in your version-control system (e.g., Git)
3-
provider = "postgresql"
3+
provider = "postgresql"

apps/server/src/prisma/schema/project.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ model ProjectRole {
6666
permissions BigInt
6767
projectId String @db.Uuid
6868
position Int @db.SmallInt
69+
oidcGroup String @default("")
6970
project Project @relation(fields: [projectId], references: [id])
7071
}
7172

apps/server/src/resources/admin-role/business.spec.ts

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,74 @@
1+
import { faker } from '@faker-js/faker'
12
import { describe, expect, it } from 'vitest'
23
import type { AdminRole, User } from '@prisma/client'
3-
import { faker } from '@faker-js/faker'
44
import prisma from '../../__mocks__/prisma.js'
55
import { BadRequest400 } from '../../utils/errors.ts'
66
import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business.ts'
77

88
describe('test admin-role business', () => {
99
describe('listRoles', () => {
1010
it('should stringify bigint', async () => {
11-
const partialRole: Partial<AdminRole> = {
11+
const dbRole: AdminRole = {
12+
id: faker.string.uuid(),
13+
name: faker.string.alphanumeric(),
1214
permissions: 4n,
15+
position: 0,
16+
oidcGroup: '',
1317
}
1418

15-
prisma.adminRole.findMany.mockResolvedValueOnce([partialRole])
19+
prisma.adminRole.findMany.mockResolvedValueOnce([dbRole])
1620
const response = await listRoles()
17-
expect(response).toEqual([{ permissions: '4' }])
21+
expect(response).toContainEqual(expect.objectContaining({ permissions: '4' }))
1822
})
1923
})
2024

2125
describe('createRole', () => {
2226
it('should create role with incremented position when position 0 is the highest', async () => {
23-
const dbRole: Partial<AdminRole> = {
27+
const dbRole: AdminRole = {
28+
id: faker.string.uuid(),
29+
name: faker.string.alphanumeric(),
2430
permissions: 4n,
2531
position: 0,
32+
oidcGroup: '',
2633
}
2734

2835
prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole)
2936
prisma.adminRole.findMany.mockResolvedValueOnce([dbRole])
30-
prisma.adminRole.create.mockResolvedValue(null)
37+
prisma.adminRole.create.mockResolvedValue(dbRole)
3138
await createRole({ name: 'test' })
3239

3340
expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 1 } })
3441
})
3542

3643
it('should create role with incremented position with bigger position', async () => {
37-
const dbRole: Partial<AdminRole> = {
44+
const dbRole: AdminRole = {
45+
id: faker.string.uuid(),
46+
name: faker.string.alphanumeric(),
3847
permissions: 4n,
3948
position: 50,
49+
oidcGroup: '',
4050
}
4151

4252
prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole)
4353
prisma.adminRole.findMany.mockResolvedValueOnce([dbRole])
44-
prisma.adminRole.create.mockResolvedValue(null)
54+
prisma.adminRole.create.mockResolvedValue(dbRole)
4555
await createRole({ name: 'test' })
4656

4757
expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 51 } })
4858
})
4959

5060
it('should create role with incremented position with no role in db', async () => {
51-
const dbRole: Partial<AdminRole> = {
61+
const dbRole: AdminRole = {
62+
id: faker.string.uuid(),
63+
name: faker.string.alphanumeric(),
5264
permissions: 4n,
5365
position: 50,
66+
oidcGroup: '',
5467
}
5568

56-
prisma.adminRole.findFirst.mockResolvedValueOnce(undefined)
69+
prisma.adminRole.findFirst.mockResolvedValueOnce(null)
5770
prisma.adminRole.findMany.mockResolvedValueOnce([dbRole])
58-
prisma.adminRole.create.mockResolvedValue(null)
71+
prisma.adminRole.create.mockResolvedValue(dbRole)
5972
await createRole({ name: 'test' })
6073

6174
expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 0 } })
@@ -65,16 +78,39 @@ describe('test admin-role business', () => {
6578
const roleId = faker.string.uuid()
6679
it('should delete role and remove id from concerned users', async () => {
6780
const users = [{
68-
adminRoleIds: [roleId],
6981
id: faker.string.uuid(),
82+
type: 'human',
83+
firstName: faker.person.firstName(),
84+
lastName: faker.person.lastName(),
85+
email: faker.internet.email(),
86+
adminRoleIds: [roleId],
87+
createdAt: faker.date.past(),
88+
updatedAt: faker.date.recent(),
89+
lastLogin: faker.date.past(),
7090
}, {
71-
adminRoleIds: [roleId, faker.string.uuid()],
7291
id: faker.string.uuid(),
73-
}] as const satisfies Partial<User>[]
92+
type: 'human',
93+
firstName: faker.person.firstName(),
94+
lastName: faker.person.lastName(),
95+
email: faker.internet.email(),
96+
adminRoleIds: [roleId, faker.string.uuid()],
97+
createdAt: faker.date.past(),
98+
updatedAt: faker.date.recent(),
99+
lastLogin: faker.date.past(),
100+
}] as const satisfies User[]
101+
102+
const dbRole: AdminRole = {
103+
name: 'Admin',
104+
id: roleId,
105+
permissions: 4n,
106+
position: 50,
107+
oidcGroup: '',
108+
}
74109

75110
prisma.user.findMany.mockResolvedValueOnce(users)
76111
prisma.adminRole.findMany.mockResolvedValueOnce([])
77-
prisma.adminRole.create.mockResolvedValue(null)
112+
prisma.adminRole.findUnique.mockResolvedValueOnce(dbRole)
113+
prisma.adminRole.create.mockResolvedValue(dbRole)
78114
await deleteRole(roleId)
79115

80116
expect(prisma.user.update).toHaveBeenNthCalledWith(1, { where: { id: users[0].id }, data: { adminRoleIds: [] } })
@@ -84,35 +120,59 @@ describe('test admin-role business', () => {
84120
})
85121
describe('countRolesMembers', () => {
86122
it('should return aggregated role member counts', async () => {
87-
const partialRoles = [{
123+
const roles = [{
88124
id: faker.string.uuid(),
125+
name: faker.string.alphanumeric(),
126+
oidcGroup: '',
127+
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
128+
position: 0,
89129
}, {
90130
id: faker.string.uuid(),
91-
}] as const satisfies Partial<AdminRole>[]
131+
name: faker.string.alphanumeric(),
132+
oidcGroup: '',
133+
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
134+
position: 1,
135+
}] as const satisfies AdminRole[]
92136

93137
const users = [{
94-
adminRoleIds: [partialRoles[0].id, partialRoles[1].id],
138+
id: faker.string.uuid(),
139+
type: 'human',
140+
firstName: faker.person.firstName(),
141+
lastName: faker.person.lastName(),
142+
email: faker.internet.email(),
143+
adminRoleIds: [roles[0].id, roles[1].id],
144+
createdAt: faker.date.past(),
145+
updatedAt: faker.date.recent(),
146+
lastLogin: faker.date.past(),
95147
}, {
96-
adminRoleIds: [partialRoles[1].id],
97-
}] as const satisfies Partial<User>[]
98-
prisma.adminRole.findMany.mockResolvedValue(partialRoles)
148+
id: faker.string.uuid(),
149+
type: 'human',
150+
firstName: faker.person.firstName(),
151+
lastName: faker.person.lastName(),
152+
email: faker.internet.email(),
153+
adminRoleIds: [roles[1].id],
154+
createdAt: faker.date.past(),
155+
updatedAt: faker.date.recent(),
156+
lastLogin: faker.date.past(),
157+
}] as const satisfies User[]
158+
prisma.adminRole.findMany.mockResolvedValue(roles)
99159
prisma.user.findMany.mockResolvedValue(users)
100160

101161
const response = await countRolesMembers()
102162

103-
expect(response).toEqual({ [partialRoles[0].id]: 1, [partialRoles[1].id]: 2 })
163+
expect(response).toEqual({ [roles[0].id]: 1, [roles[1].id]: 2 })
104164
})
105165
})
106166
describe('patchRoles', () => {
107167
const dbRoles: AdminRole[] = [{
108168
id: faker.string.uuid(),
109-
name: faker.company.name(),
169+
name: faker.string.alphanumeric(),
110170
oidcGroup: '',
111171
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
112172
position: 0,
113173
}, {
114174
id: faker.string.uuid(),
115-
name: faker.company.name(),
175+
name: faker.string.alphanumeric(),
116176
oidcGroup: '',
117177
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
118178
position: 1,
@@ -125,7 +185,7 @@ describe('test admin-role business', () => {
125185
})
126186

127187
it('should return 400 if incoherent positions', async () => {
128-
const updateRoles: Pick<AdminRole, 'id' | 'position'> = [
188+
const updateRoles: Pick<AdminRole, 'id' | 'position'>[] = [
129189
{ id: dbRoles[0].id, position: 1 },
130190
{ id: dbRoles[1].id, position: 1 },
131191
]
@@ -137,7 +197,7 @@ describe('test admin-role business', () => {
137197
expect(prisma.adminRole.update).toHaveBeenCalledTimes(0)
138198
})
139199
it('should return 400 if incoherent positions (missing roles)', async () => {
140-
const updateRoles: Pick<AdminRole, 'id' | 'position'> = [
200+
const updateRoles: Pick<AdminRole, 'id' | 'position'>[] = [
141201
{ id: dbRoles[1].id, position: 1 },
142202
]
143203
prisma.adminRole.findMany.mockResolvedValue(dbRoles)
@@ -148,7 +208,7 @@ describe('test admin-role business', () => {
148208
expect(prisma.adminRole.update).toHaveBeenCalledTimes(0)
149209
})
150210
it('should update positions', async () => {
151-
const updateRoles: Pick<AdminRole, 'id' | 'position'> = [
211+
const updateRoles: Pick<AdminRole, 'id' | 'position'>[] = [
152212
{ id: dbRoles[0].id, position: 1 },
153213
{ id: dbRoles[1].id, position: 0 },
154214
]
@@ -159,7 +219,7 @@ describe('test admin-role business', () => {
159219
expect(prisma.adminRole.update).toHaveBeenCalledTimes(2)
160220
})
161221
it('should update permissions', async () => {
162-
const updateRoles: Pick<AdminRole, 'id' | 'position'> = [
222+
const updateRoles: (Pick<AdminRole, 'id'> & { permissions?: string })[] = [
163223
{ id: dbRoles[1].id, permissions: '0' },
164224
]
165225
prisma.adminRole.findMany.mockResolvedValue(dbRoles)

0 commit comments

Comments
 (0)