Skip to content
Open
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
1 change: 0 additions & 1 deletion packages/shared/src/utils/const.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export const adminGroupPath = '/admin'
export const deleteValidationInput = 'DELETE'
export const forbiddenRepoNames = ['mirror', 'infra-apps', 'infra-observability']

Expand Down
193 changes: 130 additions & 63 deletions plugins/sonarqube/src/functions.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,169 @@
import { adminGroupPath } from '@cpn-console/shared'
import type { Project, StepCall } from '@cpn-console/hooks'
import { generateProjectKey, parseError } from '@cpn-console/hooks'
import type { AdminRole, Project, StepCall } from '@cpn-console/hooks'
import { generateProjectKey, parseError, specificallyEnabled } from '@cpn-console/hooks'
import type { VaultProjectApi } from '@cpn-console/vault-plugin/types/vault-project-api.js'
import { ensureGroupExists, findGroupByName } from './group.js'
import { addUserToGroup, ensureGroupExists, getGroupMembers, removeUserFromGroup } from './group.js'
import { formatGroupName } from './utils.js'
import type { VaultSonarSecret } from './tech.js'
import { getAxiosInstance } from './tech.js'
import type { SonarUser } from './user.js'
import { ensureUserExists } from './user.js'
import type { SonarPaging } from './project.js'
import { ensureUserExists, getUser } from './user.js'
import { createDsoRepository, deleteDsoRepository, ensureRepositoryConfiguration, files, findSonarProjectsForDsoProjects } from './project.js'
import { DEFAULT_ADMIN_GROUP_PATH, DEFAULT_READONLY_GROUP_PATH } from './infos.js'

const globalPermissions = [
'admin',
'profileadmin',
'gateadmin',
'scan',
'provisioning',
]
const PLATFORM_ADMIN_TEMPLATE_NAME = 'Default platform admin template'
const PLATFORM_READONLY_TEMPLATE_NAME = 'Default platform readonly template'

const projectPermissions = [
const platformAdminPermissions = [
'admin',
'codeviewer',
'issueadmin',
'securityhotspotadmin',
'scan',
'securityhotspotadmin',
'user',
]
] as const

export async function initSonar() {
await setTemplatePermisions()
await createAdminGroup()
await setAdminPermisions()
}
const platformReadonlyPermissions = [
'codeviewer',
'user',
] as const

async function createAdminGroup() {
const axiosInstance = getAxiosInstance()
const adminGroup = await findGroupByName(adminGroupPath)
if (!adminGroup) {
await axiosInstance({
method: 'post',
params: {
name: adminGroupPath,
description: 'DSO platform admins',
export const upsertAdminRole: StepCall<AdminRole> = async (payload) => {
try {
const role = payload.args
const adminGroupPath = payload.config.sonarqube?.adminGroupPath ?? DEFAULT_ADMIN_GROUP_PATH
const readonlyGroupPath = payload.config.sonarqube?.readonlyGroupPath ?? DEFAULT_READONLY_GROUP_PATH

let managedGroupPath: string | undefined

if (role.oidcGroup === adminGroupPath) {
managedGroupPath = formatGroupName(adminGroupPath)
await ensureAdminTemplateExists()
await ensureGroupExists(managedGroupPath)
await setTemplateGroupPermissions(managedGroupPath, platformAdminPermissions, PLATFORM_ADMIN_TEMPLATE_NAME)
} else if (role.oidcGroup === readonlyGroupPath) {
managedGroupPath = formatGroupName(readonlyGroupPath)
await ensureReadonlyTemplateExists()
await ensureGroupExists(managedGroupPath)
await setTemplateGroupPermissions(managedGroupPath, platformReadonlyPermissions, PLATFORM_READONLY_TEMPLATE_NAME)
}

if (!managedGroupPath) {
return {
status: {
result: 'OK',
message: 'Not a managed role for SonarQube plugin',
},
}
}

const groupMembers = await getGroupMembers(managedGroupPath)

await Promise.all([
...role.members.map((member) => {
if (!groupMembers.includes(member.email)) {
return addUserToGroup(managedGroupPath, member.email)
.catch((error) => {
console.warn(`Failed to add user ${member.email} to group ${managedGroupPath}`, error)
})
}
return undefined
}),
...groupMembers.map((memberEmail) => {
if (!role.members.some(m => m.email === memberEmail)) {
if (specificallyEnabled(payload.config.sonarqube?.purge)) {
return removeUserFromGroup(managedGroupPath, memberEmail)
.catch((error) => {
console.warn(`Failed to remove user ${memberEmail} from group ${managedGroupPath}`, error)
})
}
}
return undefined
}),
])

return {
status: {
result: 'OK',
message: 'Admin role synced',
},
url: 'user_groups/create',
})
}
} catch (error) {
return {
error: parseError(error),
status: {
result: 'KO',
message: 'An error occured while syncing admin role',
},
}
}
}

async function setAdminPermisions() {
async function setTemplateGroupPermissions(groupName: string, permissions: readonly string[], templateName: string) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: permissions c'est une permission au sens défini ici ?

Ça me paraît louche que ce soit "simplement" un string[] à l'entrée comme ça 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oui tout a fait, c'est une fonction plus generique

const axiosInstance = getAxiosInstance()
for (const permission of globalPermissions) {
await axiosInstance({
await Promise.all(permissions.map(permission =>
axiosInstance({
method: 'post',
params: {
groupName: adminGroupPath,
groupName,
templateName,
permission,
},
url: 'permissions/add_group',
})
}
url: 'permissions/add_group_to_template',
}),
))
}

async function setTemplatePermisions() {
async function ensureAdminTemplateExists() {
const axiosInstance = getAxiosInstance()

// Create Admin Template
await axiosInstance({
method: 'post',
params: { name: 'Forge Default' },
params: { name: PLATFORM_ADMIN_TEMPLATE_NAME },
url: 'permissions/create_template',
validateStatus: code => [200, 400].includes(code),
})
for (const permission of projectPermissions) {
await axiosInstance({

// Add Project Creator and sonar-administrators to Admin Template
await Promise.all(platformAdminPermissions.map(permission =>
axiosInstance({
method: 'post',
params: {
templateName: 'Forge Default',
templateName: PLATFORM_ADMIN_TEMPLATE_NAME,
permission,
},
url: 'permissions/add_project_creator_to_template',
})
await axiosInstance({
}),
))
}

async function ensureReadonlyTemplateExists() {
const axiosInstance = getAxiosInstance()

// Create Readonly Template
await axiosInstance({
method: 'post',
params: { name: PLATFORM_READONLY_TEMPLATE_NAME },
url: 'permissions/create_template',
validateStatus: code => [200, 400].includes(code),
})

// Add Project Creator and sonar-administrators to Readonly Template
await Promise.all(platformReadonlyPermissions.map(permission =>
axiosInstance({
method: 'post',
params: {
groupName: 'sonar-administrators',
templateName: 'Forge Default',
templateName: PLATFORM_READONLY_TEMPLATE_NAME,
permission,
},
url: 'permissions/add_group_to_template',
})
}
url: 'permissions/add_project_creator_to_template',
}),
))

// Set Readonly Template as Default
await axiosInstance({
method: 'post',
params: {
templateName: 'Forge Default',
templateName: PLATFORM_READONLY_TEMPLATE_NAME,
},
url: 'permissions/set_default_template',
})
Expand All @@ -111,11 +181,12 @@ export const upsertProject: StepCall<Project> = async (payload) => {
} = project
const username = project.slug
const keycloakGroupPath = await keycloakApi.getProjectGroupPath()
const sonarGroupPath = formatGroupName(keycloakGroupPath)
const sonarRepositories = await findSonarProjectsForDsoProjects(projectSlug)

await Promise.all([
ensureUserAndVault(vaultApi, username, projectSlug),
ensureGroupExists(keycloakGroupPath),
ensureGroupExists(sonarGroupPath),

// Remove excess repositories
...sonarRepositories
Expand All @@ -128,7 +199,7 @@ export const upsertProject: StepCall<Project> = async (payload) => {
if (!sonarRepositories.some(sonarRepository => sonarRepository.repository === repository.internalRepoName)) {
await createDsoRepository(projectSlug, repository.internalRepoName)
}
await ensureRepositoryConfiguration(projectKey, username, keycloakGroupPath)
await ensureRepositoryConfiguration(projectKey, username, sonarGroupPath)
}),
])

Expand Down Expand Up @@ -166,7 +237,9 @@ export const setVariables: StepCall<Project> = async (payload) => {
...project.repositories.map(async (repo) => {
const projectKey = generateProjectKey(projectSlug, repo.internalRepoName)
const repoId = await payload.apis.gitlab.getProjectId(repo.internalRepoName)
if (!repoId) return
if (!repoId) {
throw new Error(`Unable to find GitLab project for repository ${repo.internalRepoName}`)
}
const listVars = await gitlabApi.getGitlabRepoVariables(repoId)
return [
await gitlabApi.setGitlabRepoVariable(repoId, listVars, {
Expand Down Expand Up @@ -231,13 +304,7 @@ export const deleteProject: StepCall<Project> = async (payload) => {
try {
const sonarRepositories = await findSonarProjectsForDsoProjects(projectSlug)
await Promise.all(sonarRepositories.map(repo => deleteRepo(repo.key)))
const users: { paging: SonarPaging, users: SonarUser[] } = (await axiosInstance({
url: 'users/search',
params: {
q: username,
},
}))?.data
const user = users.users.find(u => u.login === username)
const user = await getUser(username)
if (!user) {
return {
status: {
Expand Down
66 changes: 58 additions & 8 deletions plugins/sonarqube/src/group.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
import { getAxiosInstance } from './tech.js'
import type { SonarPaging } from './project.js'
import { find, getAll, pagePaginate } from './utils.js'

export async function getGroupMembers(groupName: string): Promise<string[]> {
const axiosInstance = getAxiosInstance()
const users = await getAll<{ login: string }>(pagePaginate(async (params) => {
const response = await axiosInstance({
url: 'user_groups/users',
params: {
...params,
name: groupName,
},
})
const data: { paging: SonarPaging, users: { login: string }[] } = response.data
return {
items: data.users,
paging: data.paging,
}
}))
return users.map(u => u.login)
}
export interface SonarGroup {
id: string
name: string
Expand All @@ -9,15 +28,22 @@ export interface SonarGroup {
default: boolean
}

export async function findGroupByName(name: string): Promise<void | SonarGroup> {
export async function findGroupByName(name: string): Promise<SonarGroup | undefined> {
const axiosInstance = getAxiosInstance()
const groupsSearch: { paging: SonarPaging, groups: SonarGroup[] } = (await axiosInstance({
url: 'user_groups/search',
params: {
q: name,
},
}))?.data
return groupsSearch.groups.find(g => g.name === name)
return find<SonarGroup>(pagePaginate(async (params) => {
const response = await axiosInstance({
url: 'user_groups/search',
params: {
...params,
q: name,
},
})
const data: { paging: SonarPaging, groups: SonarGroup[] } = response.data
return {
items: data.groups,
paging: data.paging,
}
}), group => group.name === name)
}

export async function ensureGroupExists(groupName: string) {
Expand All @@ -33,3 +59,27 @@ export async function ensureGroupExists(groupName: string) {
})
}
}

export async function addUserToGroup(groupName: string, login: string) {
const axiosInstance = getAxiosInstance()
await axiosInstance({
url: 'user_groups/add_user',
method: 'post',
params: {
name: groupName,
login,
},
})
}

export async function removeUserFromGroup(groupName: string, login: string) {
const axiosInstance = getAxiosInstance()
await axiosInstance({
url: 'user_groups/remove_user',
method: 'post',
params: {
name: groupName,
login,
},
})
}
13 changes: 9 additions & 4 deletions plugins/sonarqube/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import type { HookStepsNames, Plugin } from '@cpn-console/hooks'
import type { DeclareModuleGenerator, HookStepsNames, Plugin } from '@cpn-console/hooks'
import { getStatus } from './check.js'
import { deleteProject, initSonar, setVariables, upsertProject } from './functions.js'
import { deleteProject, setVariables, upsertAdminRole, upsertProject } from './functions.js'
import infos from './infos.js'
import monitor from './monitor.js'

function start(_options: unknown) {
function start() {
try {
initSonar()
getStatus()
} catch (_error) {}
}

export const plugin: Plugin = {
infos,
subscribedHooks: {
upsertAdminRole: {
steps: {
main: upsertAdminRole,
},
},
upsertProject: {
steps: {
main: upsertProject,
Expand All @@ -34,4 +38,5 @@ declare module '@cpn-console/hooks' {
interface PluginResult {
errors?: Partial<Record<HookStepsNames, unknown>>
}
interface Config extends DeclareModuleGenerator<typeof infos, 'global'> {}
}
Loading
Loading