Skip to content

Commit b9cef3a

Browse files
committed
feat: add system roles on project creation
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
1 parent 3ed193c commit b9cef3a

17 files changed

Lines changed: 171 additions & 12 deletions

File tree

apps/client/src/components/AdminRoleForm.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const props = withDefaults(defineProps<{
1212
permissions: bigint
1313
name: string
1414
oidcGroup: string
15+
type?: string
1516
}>(), {
1617
name: 'Nouveau rôle',
1718
oidcGroup: '',
@@ -139,6 +140,7 @@ function closeModal() {
139140
label-visible
140141
hint="Ne doit pas dépasser 30 caractères."
141142
class="mb-5"
143+
:disabled="role.type === 'system'"
142144
/>
143145
<p
144146
class="fr-h6"
@@ -174,6 +176,7 @@ function closeModal() {
174176
label-visible
175177
placeholder="/admin"
176178
class="mb-5"
179+
:disabled="role.type === 'system'"
177180
/>
178181
<DsfrButton
179182
data-testid="saveBtn"

apps/client/src/components/ProjectRoleForm.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const props = defineProps<{
1111
projectId: ProjectV2['id']
1212
isEveryone: boolean
1313
oidcGroup?: string
14+
type?: string
1415
}>()
1516
1617
defineEmits<{
@@ -25,6 +26,7 @@ const role = ref({
2526
permissions: props.permissions ?? 0n,
2627
allMembers: props.allMembers ?? [],
2728
oidcGroup: props.oidcGroup ?? '',
29+
type: props.type ?? 'custom',
2830
})
2931
3032
const isUpdated = computed(() => {
@@ -68,15 +70,15 @@ function updateChecked(checked: boolean, value: bigint) {
6870
data-testid="roleNameInput"
6971
label-visible
7072
class="mb-5"
71-
:disabled="role.isEveryone"
73+
:disabled="role.isEveryone || role.type === 'system'"
7274
/>
7375
<h6>Groupe OIDC</h6>
7476
<DsfrInput
7577
v-model="role.oidcGroup"
7678
data-testid="roleOidcGroupInput"
7779
label-visible
7880
class="mb-5"
79-
:disabled="role.isEveryone"
81+
:disabled="role.isEveryone || role.type === 'system'"
8082
/>
8183
<h6>Permissions</h6>
8284
<div
@@ -97,7 +99,7 @@ function updateChecked(checked: boolean, value: bigint) {
9799
:label="perm?.label"
98100
:hint="perm?.hint"
99101
:name="perm.key"
100-
:disabled="role.permissions & PROJECT_PERMS.MANAGE && perm.key !== 'MANAGE'"
102+
:disabled="(role.permissions & PROJECT_PERMS.MANAGE && perm.key !== 'MANAGE') || role.type === 'system'"
101103
@update:model-value="(checked: boolean) => updateChecked(checked, PROJECT_PERMS[perm.key])"
102104
/>
103105
</div>
@@ -110,7 +112,7 @@ function updateChecked(checked: boolean, value: bigint) {
110112
@click="$emit('save', role)"
111113
/>
112114
<DsfrButton
113-
v-if="!role.isEveryone"
115+
v-if="!role.isEveryone && role.type !== 'system'"
114116
data-testid="deleteBtn"
115117
label="Supprimer"
116118
secondary

apps/client/src/components/ProjectRoles.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ watch(props.project, reload, { immediate: true })
145145
:project-id="project.id"
146146
:is-everyone="selectedRole.isEveryone"
147147
:oidc-group="selectedRole.oidcGroup"
148+
:type="selectedRole.type"
148149
:all-members="project.members"
149150
@delete="deleteRole(selectedRole.id)"
150151
@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, projectId: string, oidcGroup?: string }[]
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

apps/client/src/views/admin/AdminRoles.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ onBeforeMount(async () => {
117117
:name="selectedRole.name"
118118
:permissions="BigInt(selectedRole.permissions)"
119119
:oidc-group="selectedRole.oidcGroup"
120+
:type="selectedRole.type"
120121
@delete="deleteRole(selectedRole.id)"
121122
@save="(role: Pick<AdminRole, 'name' | 'oidcGroup' | 'permissions'>) => saveRole(role)"
122123
@cancel="() => cancel()"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- AlterTable
2+
ALTER TABLE "AdminRole" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'custom';
3+
4+
-- AlterTable
5+
ALTER TABLE "ProjectRole" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'custom';
6+
7+
-- Update AdminRole system roles
8+
UPDATE "AdminRole" SET "type" = 'system' WHERE "name" IN ('Admin', 'Admin Locaux');
9+
10+
-- Update ProjectRole system roles
11+
UPDATE "ProjectRole" SET "type" = 'system' WHERE "name" IN ('Administrateur', 'DevOps', 'Développeur', 'Lecture seule');

apps/server/src/prisma/schema/admin.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ model AdminRole {
1212
permissions BigInt
1313
position Int @db.SmallInt
1414
oidcGroup String @default("")
15+
type String @default("custom")
1516
}
1617

1718
model SystemSetting {

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

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

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker'
22
import { describe, expect, it } from 'vitest'
33
import type { AdminRole, User } from '@prisma/client'
44
import prisma from '../../__mocks__/prisma.js'
5-
import { BadRequest400 } from '../../utils/errors.ts'
5+
import { BadRequest400, Forbidden403 } from '../../utils/errors.ts'
66
import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business.ts'
77

88
describe('test admin-role business', () => {
@@ -14,11 +14,12 @@ describe('test admin-role business', () => {
1414
permissions: 4n,
1515
position: 0,
1616
oidcGroup: '',
17+
type: 'custom',
1718
}
1819

1920
prisma.adminRole.findMany.mockResolvedValueOnce([dbRole])
2021
const response = await listRoles()
21-
expect(response).toContainEqual(expect.objectContaining({ permissions: '4' }))
22+
expect(response).toContainEqual(expect.objectContaining({ permissions: '4', type: 'custom' }))
2223
})
2324
})
2425

@@ -30,6 +31,7 @@ describe('test admin-role business', () => {
3031
permissions: 4n,
3132
position: 0,
3233
oidcGroup: '',
34+
type: 'custom',
3335
}
3436

3537
prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole)
@@ -47,6 +49,7 @@ describe('test admin-role business', () => {
4749
permissions: 4n,
4850
position: 50,
4951
oidcGroup: '',
52+
type: 'custom',
5053
}
5154

5255
prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole)
@@ -64,6 +67,7 @@ describe('test admin-role business', () => {
6467
permissions: 4n,
6568
position: 50,
6669
oidcGroup: '',
70+
type: 'custom',
6771
}
6872

6973
prisma.adminRole.findFirst.mockResolvedValueOnce(null)
@@ -105,6 +109,7 @@ describe('test admin-role business', () => {
105109
permissions: 4n,
106110
position: 50,
107111
oidcGroup: '',
112+
type: 'custom',
108113
}
109114

110115
prisma.user.findMany.mockResolvedValueOnce(users)
@@ -126,12 +131,14 @@ describe('test admin-role business', () => {
126131
oidcGroup: '',
127132
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
128133
position: 0,
134+
type: 'custom',
129135
}, {
130136
id: faker.string.uuid(),
131137
name: faker.string.alphanumeric(),
132138
oidcGroup: '',
133139
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
134140
position: 1,
141+
type: 'custom',
135142
}] as const satisfies AdminRole[]
136143

137144
const users = [{
@@ -170,14 +177,36 @@ describe('test admin-role business', () => {
170177
oidcGroup: '',
171178
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
172179
position: 0,
180+
type: 'custom',
173181
}, {
174182
id: faker.string.uuid(),
175183
name: faker.string.alphanumeric(),
176184
oidcGroup: '',
177185
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
178186
position: 1,
187+
type: 'custom',
179188
}]
180189

190+
it('should throw Forbidden403 when renaming a system role', async () => {
191+
const systemRole: AdminRole = {
192+
id: faker.string.uuid(),
193+
name: 'Admin',
194+
permissions: 10n,
195+
position: 0,
196+
oidcGroup: 'admin-group',
197+
type: 'system',
198+
}
199+
prisma.adminRole.findMany.mockResolvedValue([systemRole])
200+
201+
const updateRoles = [{
202+
id: systemRole.id,
203+
name: 'New Admin Name',
204+
}]
205+
206+
await expect(patchRoles(updateRoles)).rejects.toThrow(Forbidden403)
207+
expect(prisma.adminRole.update).toHaveBeenCalledTimes(0)
208+
})
209+
181210
it('should do nothing', async () => {
182211
prisma.adminRole.findMany.mockResolvedValue([])
183212
await patchRoles([])
@@ -233,6 +262,7 @@ describe('test admin-role business', () => {
233262
oidcGroup: dbRoles[1].oidcGroup,
234263
permissions: 0n,
235264
position: 1,
265+
type: 'custom',
236266
},
237267
where: {
238268
id: dbRoles[1].id,

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
listAdminRoles,
55
} from '@/resources/queries-index.js'
66
import type { ErrorResType } from '@/utils/errors.js'
7-
import { BadRequest400 } from '@/utils/errors.js'
7+
import { BadRequest400, Forbidden403 } from '@/utils/errors.js'
88
import prisma from '@/prisma.js'
99

1010
export async function listRoles() {
@@ -29,11 +29,15 @@ export async function patchRoles(roles: typeof adminRoleContract.patchAdminRoles
2929
permissions: matchingRole?.permissions ? BigInt(matchingRole?.permissions) : dbRole.permissions,
3030
position: matchingRole?.position ?? dbRole.position,
3131
oidcGroup: matchingRole?.oidcGroup ?? dbRole.oidcGroup,
32+
type: matchingRole?.type ?? dbRole.type,
3233
}
3334
})
3435

3536
if (positionsAvailable.length && positionsAvailable.length !== dbRoles.length) return new BadRequest400('Les numéros de position des rôles sont incohérentes')
3637
for (const { id, ...role } of updatedRoles) {
38+
if (role.type === 'system') {
39+
throw new Forbidden403('Ce rôle système ne peut pas être renommé')
40+
}
3741
await prisma.adminRole.update({ where: { id }, data: role })
3842
}
3943

@@ -74,6 +78,10 @@ export async function countRolesMembers() {
7478
}
7579

7680
export async function deleteRole(roleId: Project['id']) {
81+
const role = await prisma.adminRole.findFirst({ where: { id: roleId } })
82+
if (role?.type === 'system') {
83+
throw new Forbidden403('Ce rôle système ne peut pas être supprimé')
84+
}
7785
const allUsers = await prisma.user.findMany({
7886
where: {
7987
adminRoleIds: { has: roleId },

0 commit comments

Comments
 (0)