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
111 changes: 55 additions & 56 deletions apps/client/src/components/AdminRoleForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ const props = withDefaults(defineProps<{
permissions: bigint
name: string
oidcGroup: string
type: string
}>(), {
name: 'Nouveau rôle',
oidcGroup: '',
type: 'custom',
})
const emits = defineEmits<{
delete: []
save: [{ name: string, permissions: string, oidcGroup: string }]
save: [{ name: string, permissions: string, oidcGroup: string, type: string }]
cancel: []
}>()
const usersStore = useUsersStore()
Expand All @@ -33,6 +35,8 @@ const isUpdated = computed(() => {
return !shallowEqual(props, role.value)
})

const isSystem = computed(() => props.type === 'system')

const errorSchema = computed<SharedZodError | undefined>(() => {
const schemaValidation = RoleSchema.partial().safeParse(role.value)
return schemaValidation.success ? undefined : schemaValidation.error
Expand Down Expand Up @@ -139,6 +143,7 @@ function closeModal() {
label-visible
hint="Ne doit pas dépasser 30 caractères."
class="mb-5"
:disabled="isSystem"
/>
<p
class="fr-h6"
Expand All @@ -163,7 +168,7 @@ function closeModal() {
:label="perm.label"
:hint="perm?.hint"
:name="perm.key"
:disabled="role.permissions & ADMIN_PERMS.MANAGE && perm.key !== 'MANAGE'"
:disabled="isSystem || (role.permissions & ADMIN_PERMS.MANAGE && perm.key !== 'MANAGE')"
@update:model-value="(checked: boolean) => updateChecked(checked, perm.key)"
/>
</div>
Expand All @@ -174,8 +179,10 @@ function closeModal() {
label-visible
placeholder="/admin"
class="mb-5"
:disabled="isSystem"
/>
<DsfrButton
v-if="!isSystem"
data-testid="saveBtn"
label="Enregistrer"
secondary
Expand All @@ -184,6 +191,7 @@ function closeModal() {
@click="$emit('save', { ...role, permissions: role.permissions.toString() })"
/>
<DsfrButton
v-if="!isSystem"
data-testid="deleteBtn"
label="Supprimer"
secondary
Expand All @@ -194,63 +202,54 @@ function closeModal() {
panel-id="members"
tab-id="members"
>
<template
v-if="!props.oidcGroup"
<DsfrCheckbox
v-for="user in users"
:id="`${user.id}-cbx`"
:key="user.email"
:label="`${user.lastName} ${user.firstName}`"
:hint="user.email"
:name="`checkbox-${user.id}`"
value="user.adminRoleIds.includes(role.id)"
:model-value="user.adminRoleIds.includes(role.id)"
@update:model-value="(checked: boolean) => switchUserMembership(checked, user)"
/>
<DsfrNotice
v-if="!users.length"
class="mb-5"
data-testid="noUserNotice"
title="Aucun utilisateur ne dispose actuellement de ce rôle."
/>
<div
class="w-max"
>
<DsfrCheckbox
v-for="user in users"
:id="`${user.id}-cbx`"
:key="user.email"
:label="`${user.lastName} ${user.firstName}`"
:hint="user.email"
:name="`checkbox-${user.id}`"
value="user.adminRoleIds.includes(role.id)"
:model-value="user.adminRoleIds.includes(role.id)"
@update:model-value="(checked: boolean) => switchUserMembership(checked, user)"
<SuggestionInput
:key="newUserInputKey"
v-model="newUserInput"
data-testid="addUserSuggestionInput"
label="Nom, prénom ou adresse mail de l'utilisateur à rechercher"
label-visible
hint="Adresse e-mail de l'utilisateur"
placeholder="prenom.nom@interieur.gouv.fr"
:suggestions="usersToSuggest"
@update:model-value="(value: string) => retrieveUsersToAdd(value)"
/>
<DsfrNotice
v-if="!users.length"
class="mb-5"
data-testid="noUserNotice"
title="Aucun utilisateur ne dispose actuellement de ce rôle."
<DsfrAlert
v-if="isUserAlreadyInTeam"
data-testid="userErrorInfo"
description="L'utilisateur est déjà détenteur de ce rôle."
small
type="error"
class="w-max fr-mb-2w"
/>
<div
class="w-max"
>
<SuggestionInput
:key="newUserInputKey"
v-model="newUserInput"
data-testid="addUserSuggestionInput"
label="Nom, prénom ou adresse mail de l'utilisateur à rechercher"
label-visible
hint="Adresse e-mail de l'utilisateur"
placeholder="prenom.nom@interieur.gouv.fr"
:suggestions="usersToSuggest"
@update:model-value="(value: string) => retrieveUsersToAdd(value)"
/>
<DsfrAlert
v-if="isUserAlreadyInTeam"
data-testid="userErrorInfo"
description="L'utilisateur est déjà détenteur de ce rôle."
small
type="error"
class="w-max fr-mb-2w"
/>
<DsfrButton
data-testid="addUserBtn"
label="Ajouter l'utilisateur"
secondary
icon="ri:user-add-line"
:disabled="!newUserInput || isUserAlreadyInTeam || !newUser"
@click="() => newUser && switchUserMembership(true, newUser, true)"
/>
</div>
</template>
<template
v-else
>
Les groupes ayant une liaison OIDC ne peuvent pas gérer leurs membres.
</template>
<DsfrButton
data-testid="addUserBtn"
label="Ajouter l'utilisateur"
secondary
icon="ri:user-add-line"
:disabled="!newUserInput || isUserAlreadyInTeam || !newUser"
@click="() => newUser && switchUserMembership(true, newUser, true)"
/>
</div>
</DsfrTabContent>
<DsfrTabContent
panel-id="close"
Expand Down
25 changes: 20 additions & 5 deletions apps/client/src/components/ProjectRoleForm.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import type { Member, ProjectV2, RoleBigint } from '@cpn-console/shared'
import type { Member, ProjectRoleBigint, ProjectV2 } from '@cpn-console/shared'
import { PROJECT_PERMS, projectPermsDetails, shallowEqual } from '@cpn-console/shared'

const props = defineProps<{
Expand All @@ -10,25 +10,32 @@ const props = defineProps<{
allMembers: Member[]
projectId: ProjectV2['id']
isEveryone: boolean
oidcGroup?: string
type?: string
}>()

defineEmits<{
delete: []
updateMemberRoles: [checked: boolean, userId: Member['userId']]
save: [value: Omit<RoleBigint, 'position'>]
save: [value: Omit<ProjectRoleBigint, 'position' | 'projectId'>]
cancel: []
}>()
const router = useRouter()
const role = ref({
...props,
permissions: props.permissions ?? 0n,
allMembers: props.allMembers ?? [],
oidcGroup: props.oidcGroup ?? '',
type: props.type ?? 'custom',
})

const isUpdated = computed(() => {
if (role.value.isEveryone) return props.permissions !== role.value.permissions
return !shallowEqual(props, role.value)
})

const isSystem = computed(() => props.type === 'system')

const tabListName = 'Liste d’onglet'
const tabTitles = [
{ title: 'Général', icon: 'ri:checkbox-circle-line', tabId: 'general' },
Expand Down Expand Up @@ -66,7 +73,15 @@ function updateChecked(checked: boolean, value: bigint) {
data-testid="roleNameInput"
label-visible
class="mb-5"
:disabled="role.isEveryone"
:disabled="role.isEveryone || isSystem"
/>
<h6>Groupe OIDC</h6>
<DsfrInput
v-model="role.oidcGroup"
data-testid="roleOidcGroupInput"
label-visible
class="mb-5"
:disabled="role.isEveryone || isSystem"
/>
<h6>Permissions</h6>
<div
Expand All @@ -87,7 +102,7 @@ function updateChecked(checked: boolean, value: bigint) {
:label="perm?.label"
:hint="perm?.hint"
:name="perm.key"
:disabled="role.permissions & PROJECT_PERMS.MANAGE && perm.key !== 'MANAGE'"
:disabled="(role.permissions & PROJECT_PERMS.MANAGE && perm.key !== 'MANAGE') || role.type === 'system'"
@update:model-value="(checked: boolean) => updateChecked(checked, PROJECT_PERMS[perm.key])"
/>
</div>
Expand All @@ -100,7 +115,7 @@ function updateChecked(checked: boolean, value: bigint) {
@click="$emit('save', role)"
/>
<DsfrButton
v-if="!role.isEveryone"
v-if="!role.isEveryone && role.type !== 'system'"
data-testid="deleteBtn"
label="Supprimer"
secondary
Expand Down
10 changes: 7 additions & 3 deletions apps/client/src/components/ProjectRoles.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { Member, Role, RoleBigint } from '@cpn-console/shared'
import type { Member, ProjectRole, ProjectRoleBigint, Role, RoleBigint } from '@cpn-console/shared'

Check notice on line 2 in apps/client/src/components/ProjectRoles.vue

View check run for this annotation

cloud-pi-native-sonarqube / SonarQube Code Analysis

apps/client/src/components/ProjectRoles.vue#L2

Remove this unused import of 'RoleBigint'.
import { useSnackbarStore } from '@/stores/snackbar.js'
import type { Project } from '@/utils/project-utils.js'

Expand All @@ -11,7 +11,7 @@

const selectedId = ref<string>()

type RoleItem = Omit<Role, 'permissions'> & { permissions: bigint, memberCounts: number, isEveryone: boolean }
type RoleItem = Omit<ProjectRole, 'permissions'> & { permissions: bigint, memberCounts: number, isEveryone: boolean }

const roleList = ref<RoleItem[]>([])

Expand Down Expand Up @@ -56,7 +56,7 @@
snackbarStore.setMessage('Rôle mis à jour', 'success')
}

async function saveRole(role: Omit<RoleBigint, 'position'>) {
async function saveRole(role: Omit<ProjectRoleBigint, 'position' | 'projectId'>) {
if (role.id === 'everyone') {
await saveEveryoneRole(role)
snackbarStore.setMessage('Rôle mis à jour', 'success')
Expand All @@ -67,6 +67,7 @@
id: selectedRole.value.id,
permissions: role.permissions.toString(),
name: role.name,
oidcGroup: role.oidcGroup,
}])
reload()
snackbarStore.setMessage('Rôle mis à jour', 'success')
Expand All @@ -86,6 +87,7 @@
permissions: BigInt(props.project.everyonePerms),
position: 1000,
isEveryone: true,
projectId: props.project.id,
})
roleList.value = roles
}
Expand Down Expand Up @@ -142,6 +144,8 @@
:permissions="BigInt(selectedRole.permissions)"
:project-id="project.id"
:is-everyone="selectedRole.isEveryone"
:oidc-group="selectedRole.oidcGroup"
:type="selectedRole.type"
:all-members="project.members"
@delete="deleteRole(selectedRole.id)"
@update-member-roles="(checked: boolean, userId: Member['userId']) => updateMember(checked, userId)"
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/utils/project-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class Project implements ProjectV2 {
locked: boolean
owner: Omit<User, 'adminRoleIds'>
ownerId: string
roles: { id: string, name: string, permissions: string, position: number }[]
roles: { id: string, name: string, permissions: string, position: number, projectId: string, oidcGroup?: string, type?: string }[]
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[] })[]
createdAt: string
updatedAt: string
Expand Down
6 changes: 4 additions & 2 deletions apps/client/src/views/admin/AdminRoles.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ async function deleteRole(roleId: Role['id']) {
selectedId.value = undefined
}

async function saveRole(role: Pick<AdminRole, 'name' | 'oidcGroup' | 'permissions'>) {
async function saveRole(role: Pick<AdminRole, 'name' | 'oidcGroup' | 'permissions' | 'type'>) {
if (!selectedRole.value) return
await adminRoleStore.patchRoles(
[{
id: selectedRole.value.id,
permissions: role.permissions.toString(),
name: role.name,
oidcGroup: role.oidcGroup,
type: role.type,
}],
)
snackbarStore.setMessage('Rôle mis à jour', 'success')
Expand Down Expand Up @@ -117,8 +118,9 @@ onBeforeMount(async () => {
:name="selectedRole.name"
:permissions="BigInt(selectedRole.permissions)"
:oidc-group="selectedRole.oidcGroup"
:type="selectedRole.type"
@delete="deleteRole(selectedRole.id)"
@save="(role: Pick<AdminRole, 'name' | 'oidcGroup' | 'permissions'>) => saveRole(role)"
@save="(role: Pick<AdminRole, 'name' | 'oidcGroup' | 'permissions' | 'type'>) => saveRole(role)"
@cancel="() => cancel()"
/>
</div>
Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/__mocks__/utils/hook-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { mockDeep, mockReset } from 'vitest-mock-extended'
vi.mock('../utils/hook-wrapper.ts')

export const hook = {
adminRole: {
delete: vi.fn(),
upsert: vi.fn(),
},
cluster: {
delete: vi.fn(),
upsert: vi.fn(),
Expand All @@ -17,6 +21,10 @@ export const hook = {
delete: vi.fn(),
getSecrets: vi.fn(),
},
projectRole: {
upsert: vi.fn(),
delete: vi.fn(),
},
user: {
retrieveUserByEmail: vi.fn(),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ProjectRole" ADD COLUMN "oidcGroup" TEXT NOT NULL DEFAULT '';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- AlterTable
ALTER TABLE "AdminRole" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'custom';

-- AlterTable
ALTER TABLE "ProjectRole" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'custom';

-- Update AdminRole system roles
UPDATE "AdminRole" SET "type" = 'system' WHERE "name" IN ('Admin', 'Admin Locaux');

-- Update ProjectRole system roles
UPDATE "ProjectRole" SET "type" = 'system' WHERE "name" IN ('Administrateur', 'DevOps', 'Développeur', 'Lecture seule');
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- Update existing Admin role to be system role 'Administrateur Plateforme'
UPDATE "AdminRole"
SET
"name" = 'Administrateur Plateforme',
"type" = 'system',
"permissions" = 3, -- Assuming 3n means bit 0 and 1 (1 | 2 = 3)
"oidcGroup" = '/admin',
"position" = 0
WHERE "id" = '76229c96-4716-45bc-99da-00498ec9018c'::uuid;

-- Insert 'Lecture Seule Plateforme' system role if it doesn't exist
INSERT INTO "AdminRole" ("id", "name", "permissions", "position", "oidcGroup", "type")
VALUES (
'35848aa2-e881-4770-9844-0c5c3693e506'::uuid,
'Lecture Seule Plateforme',
1, -- Assuming 1n means bit 0
2,
'/readonly',
'system'
)
ON CONFLICT ("id") DO UPDATE
SET
"name" = 'Lecture Seule Plateforme',
"type" = 'system',
"permissions" = 1,
"oidcGroup" = '/readonly';
Loading
Loading