From 43cf4b44fdd8f9867184a78ffe22396b056a3fe2 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Wed, 11 Feb 2026 11:09:41 +0100 Subject: [PATCH 1/3] fix: hooks config is always undefined For some reasons, the config is never passed down to the plugins hooks. Apparently the config is provided via the adminPlugin table of the database but is empty most of the time so for the moment let's just add default and fix the problem properly later. Signed-off-by: William Phetsinorath --- plugins/keycloak/src/functions.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/keycloak/src/functions.ts b/plugins/keycloak/src/functions.ts index c828ed679..f5940d00b 100644 --- a/plugins/keycloak/src/functions.ts +++ b/plugins/keycloak/src/functions.ts @@ -1,6 +1,6 @@ import type { AdminRole, Project, StepCall, UserEmail, ZoneObject, ProjectMemberPayload } from '@cpn-console/hooks' -import { ENABLED, type ProjectRole } from '@cpn-console/shared' -import { generateRandomPassword, parseError, PluginResultBuilder } from '@cpn-console/hooks' +import type { ProjectRole } from '@cpn-console/shared' +import { generateRandomPassword, parseError, PluginResultBuilder, specificallyEnabled } from '@cpn-console/hooks' import type GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation.js' import type ClientRepresentation from '@keycloak/keycloak-admin-client/lib/defs/clientRepresentation.js' import type { CustomGroup } from './group.js' @@ -65,7 +65,7 @@ export const upsertProject: StepCall = async ({ args: project, config } try { const kcClient = await getkcClient() const projectName = project.slug - const purgeEnabled = config.keycloak?.purge === ENABLED + const purge = config.keycloak?.purge const projectGroup = await getOrCreateProjectGroup(kcClient, projectName) const groupMembers = await kcClient.groups.listMembers({ id: projectGroup.id }) @@ -73,7 +73,7 @@ export const upsertProject: StepCall = async ({ args: project, config } await Promise.all([ ...groupMembers.map((member) => { if (!project.users.some(({ id }) => id === member.id)) { - if (purgeEnabled) { + if (specificallyEnabled(purge)) { return kcClient.users.delFromGroup({ // @ts-ignore id is present on user, bad typing in lib id: member.id, @@ -231,7 +231,7 @@ export const deleteZone: StepCall = async ({ args: zone }) => { export const upsertAdminRole: StepCall = async ({ args: role, config }) => { if (!role.oidcGroup) return { status: { result: 'OK', message: 'No OIDC Group defined' } } const pluginResult = new PluginResultBuilder('Up-to-date') - const purgeEnabled = config.keycloak?.purge === ENABLED + const purge = config.keycloak?.purge try { const kcClient = await getkcClient() const group = await getOrCreateGroupByPath(kcClient, role.oidcGroup) @@ -240,7 +240,7 @@ export const upsertAdminRole: StepCall = async ({ args: role, config await Promise.all([ ...groupMembers.map((member) => { if (member.id && !role.members.some(({ id }) => id === member.id)) { - if (purgeEnabled) { + if (specificallyEnabled(purge)) { return kcClient.users.delFromGroup({ id: member.id, groupId: group!.id!, @@ -388,7 +388,7 @@ export const deleteProjectRole: StepCall = async ({ args: role }) = export const upsertProjectMember: StepCall = async ({ args: member, config }) => { const pluginResult = new PluginResultBuilder('Synced') - const purgeEnabled = config.keycloak?.purge === ENABLED + const purge = config.keycloak?.purge try { const kcClient = await getkcClient() @@ -410,7 +410,7 @@ export const upsertProjectMember: StepCall = async ({ args if (shouldBeMember && !isMember) { await kcClient.users.addToGroup({ id: member.userId, groupId: roleGroup.id }) } else if (!shouldBeMember && isMember) { - if (purgeEnabled) { + if (specificallyEnabled(purge)) { await kcClient.users.delFromGroup({ id: member.userId, groupId: roleGroup.id }) } else { console.warn(`User ${member.email} is not in project ${member.project.slug} anymore, but purge is disabled`) From b43be2d3cd97535ab07acd9764d36bb07069e26d Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Wed, 11 Feb 2026 11:28:51 +0100 Subject: [PATCH 2/3] chore(keycloak): remove per project config We don't want drift to be an option in the future, so we'll keep the purge option as an admin only. Signed-off-by: William Phetsinorath --- plugins/keycloak/src/infos.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/plugins/keycloak/src/infos.ts b/plugins/keycloak/src/infos.ts index f6fa873fa..6162c251d 100644 --- a/plugins/keycloak/src/infos.ts +++ b/plugins/keycloak/src/infos.ts @@ -19,20 +19,7 @@ const infos: ServiceInfos = { description: 'Purger les utilisateurs non synchronisés de Keycloak lors de la synchronisation', }, ], - project: [ - { - kind: 'switch', - key: 'purge', - initialValue: DISABLED, - permissions: { - admin: { read: true, write: true }, - user: { read: false, write: false }, - }, - title: 'Purger les utilisateurs non synchronisés', - value: DISABLED, - description: 'Purger les utilisateurs non synchronisés de Keycloak lors de la synchronisation', - }, - ], + project: [], }, } From 31371f79153fd68be07e9bf89ea585c0a62c6b67 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Wed, 11 Feb 2026 11:09:41 +0100 Subject: [PATCH 3/3] feat(gitlab): add builtin admin roles to GitLab Signed-off-by: William Phetsinorath --- plugins/gitlab/src/functions.ts | 82 ++++++++++++++++++++++++++++++++- plugins/gitlab/src/index.ts | 6 +++ plugins/gitlab/src/infos.ts | 22 +++++++++ plugins/gitlab/src/user.ts | 5 +- 4 files changed, 111 insertions(+), 4 deletions(-) diff --git a/plugins/gitlab/src/functions.ts b/plugins/gitlab/src/functions.ts index 110ff01b8..7a06f7e13 100644 --- a/plugins/gitlab/src/functions.ts +++ b/plugins/gitlab/src/functions.ts @@ -1,8 +1,8 @@ import { okStatus, parseError, specificallyDisabled } from '@cpn-console/hooks' -import type { ClusterObject, PluginResult, Project, ProjectLite, StepCall, UniqueRepo, ZoneObject } from '@cpn-console/hooks' +import type { AdminRole, ClusterObject, PluginResult, Project, ProjectLite, StepCall, UniqueRepo, ZoneObject } from '@cpn-console/hooks' import { insert } from '@cpn-console/shared' import { deleteGroup } from './group.js' -import { createUsername, getUser } from './user.js' +import { createUsername, getUser, upsertUser } from './user.js' import { ensureMembers } from './members.js' import { ensureRepositories } from './repositories.js' import type { VaultSecrets } from './utils.js' @@ -232,3 +232,81 @@ export const commitFiles: StepCall = async (payload) => { + try { + const role = payload.args + const adminGroupPath = payload.config.gitlab?.adminGroupPath ?? DEFAULT_ADMIN_GROUP_PATH + const auditorGroupPath = payload.config.gitlab?.auditorGroupPath ?? DEFAULT_AUDITOR_GROUP_PATH + + const isAdmin = role.oidcGroup === adminGroupPath ? true : undefined + const isAuditor = role.oidcGroup === auditorGroupPath ? true : undefined + + if (isAdmin === undefined && isAuditor === undefined) { + return { + status: { + result: 'OK', + message: 'Not a managed role for GitLab plugin', + }, + } + } + + for (const member of role.members) { + await upsertUser(member, isAdmin, isAuditor) + } + + return { + status: { + result: 'OK', + message: 'Members synced', + }, + } + } catch (error) { + return { + error: parseError(cleanGitlabError(error)), + status: { + result: 'KO', + message: 'An error occured while syncing admin role', + }, + } + } +} + +export const deleteAdminRole: StepCall = async (payload) => { + try { + const role = payload.args + const adminGroupPath = payload.config.gitlab?.adminGroupPath ?? DEFAULT_ADMIN_GROUP_PATH + const auditorGroupPath = payload.config.gitlab?.auditorGroupPath ?? DEFAULT_AUDITOR_GROUP_PATH + + const isAdmin = role.oidcGroup === adminGroupPath ? false : undefined + const isAuditor = role.oidcGroup === auditorGroupPath ? false : undefined + + if (isAdmin === undefined && isAuditor === undefined) { + return { + status: { + result: 'OK', + message: 'Not a managed role for GitLab plugin', + }, + } + } + + for (const member of role.members) { + await upsertUser(member, isAdmin, isAuditor) + } + + return { + status: { + result: 'OK', + message: 'Admin role deleted and members synced', + }, + } + } catch (error) { + return { + error: parseError(cleanGitlabError(error)), + status: { + result: 'KO', + message: 'An error occured while deleting admin role', + }, + } + } +} diff --git a/plugins/gitlab/src/index.ts b/plugins/gitlab/src/index.ts index ef72f7dd5..44f857e18 100644 --- a/plugins/gitlab/src/index.ts +++ b/plugins/gitlab/src/index.ts @@ -6,6 +6,7 @@ import { deleteZone, getDsoProjectSecrets, syncRepository, + upsertAdminRole, upsertDsoProject, upsertZone, } from './functions.js' @@ -74,6 +75,11 @@ export const plugin: Plugin = { main: deleteZone, }, }, + upsertAdminRole: { + steps: { + main: upsertAdminRole, + }, + }, }, monitor, start, diff --git a/plugins/gitlab/src/infos.ts b/plugins/gitlab/src/infos.ts index af3b03869..98cfaa582 100644 --- a/plugins/gitlab/src/infos.ts +++ b/plugins/gitlab/src/infos.ts @@ -20,6 +20,28 @@ const infos = { title: 'Afficher l\'aide de trigger de pipeline', value: ENABLED, description: 'Afficher l\'aide de trigger de pipeline aux utilisateurs lorsqu\'ils souhaitent afficher les secrets du projet', + }, { + kind: 'text', + key: 'adminGroupPath', + permissions: { + admin: { read: true, write: true }, + user: { read: false, write: false }, + }, + title: 'Chemin du groupe OIDC Admin', + value: '/console/admin', + description: 'Le chemin du groupe OIDC qui donne les droits d\'administrateur GitLab', + placeholder: '/console/admin', + }, { + kind: 'text', + key: 'auditorGroupPath', + permissions: { + admin: { read: true, write: true }, + user: { read: false, write: false }, + }, + title: 'Chemin du groupe OIDC Auditeur', + value: '/console/readonly', + description: 'Le chemin du groupe OIDC qui donne les droits d\'auditeur GitLab', + placeholder: '/console/readonly', }], project: [], }, diff --git a/plugins/gitlab/src/user.ts b/plugins/gitlab/src/user.ts index cb984c036..2eed6599b 100644 --- a/plugins/gitlab/src/user.ts +++ b/plugins/gitlab/src/user.ts @@ -25,7 +25,7 @@ export async function getUser(user: { email: string, username: string, id: strin || allUsers.find(gitlabUser => gitlabUser.username === user.username) } -export async function upsertUser(user: UserObject): Promise { +export async function upsertUser(user: UserObject, isAdmin = false, isAuditor = false): Promise { const api = getApi() const username = createUsername(user.email) const existingUser = await getUser({ ...user, username }) @@ -38,6 +38,8 @@ export async function upsertUser(user: UserObject): Promise { // sso options externUid: user.id, provider: 'openid_connect', + admin: isAdmin, + auditor: isAuditor, } if (existingUser) { @@ -64,7 +66,6 @@ export async function upsertUser(user: UserObject): Promise { return api.Users.create({ ...userDefinitionBase, - admin: false, canCreateGroup: false, forceRandomPassword: true, projectsLimit: 0,