From 9490d534f17f46300182ff93b7b1feca3662c98a Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 20 Dec 2024 17:26:24 -0600 Subject: [PATCH] feat: add organization environment variables --- node-packages/commons/src/api.ts | 5 + node-packages/commons/src/tasks.ts | 42 +- .../migrations/20241216000000_org_env_var.js | 24 + services/api/src/resolvers.js | 4 +- .../src/resources/env-variables/resolvers.ts | 554 +++++++++++------- .../api/src/resources/env-variables/sql.ts | 12 + .../api/src/resources/organization/helpers.ts | 22 + services/api/src/typeDefs.js | 13 +- .../keycloak/lagoon-realm-base-import.json | 31 +- .../startup-scripts/00-configure-lagoon.sh | 47 ++ 10 files changed, 512 insertions(+), 242 deletions(-) create mode 100644 services/api/database/migrations/20241216000000_org_env_var.js diff --git a/node-packages/commons/src/api.ts b/node-packages/commons/src/api.ts index b7c4e24be5..86ed72d984 100644 --- a/node-packages/commons/src/api.ts +++ b/node-packages/commons/src/api.ts @@ -1032,6 +1032,11 @@ export async function getOrganizationById(id: number): Promise { quotaGroup quotaRoute quotaNotification + envVariables { + name + value + scope + } } } `); diff --git a/node-packages/commons/src/tasks.ts b/node-packages/commons/src/tasks.ts index 01f5ad44a0..116d5efed6 100644 --- a/node-packages/commons/src/tasks.ts +++ b/node-packages/commons/src/tasks.ts @@ -591,7 +591,7 @@ export const getControllerBuildData = async function(deployData: any) { // encode some values so they get sent to the controllers nicely const sshKeyBase64 = new Buffer(deployPrivateKey.replace(/\\n/g, "\n")).toString('base64') - const [routerPattern, envVars, projectVars] = await getEnvironmentsRouterPatternAndVariables( + const [routerPattern, envVars, projectVars, orgVars] = await getEnvironmentsRouterPatternAndVariables( lagoonProjectData, environment.addOrUpdateEnvironment, deployTarget.openshift, @@ -653,6 +653,7 @@ export const getControllerBuildData = async function(deployData: any) { statuspageID: uptimeRobotStatusPageId, }, variables: { + organization: orgVars, project: projectVars, environment: envVars, }, @@ -682,7 +683,7 @@ export const getEnvironmentsRouterPatternAndVariables = async function name( buildPriority: number, buildVariables: any, bulkTask: bulkType -): Promise<[string, string, string]> { +): Promise<[string, string, string, string]> { let projectVars: Array & { scope: EnvVariableScope | InternalEnvVariableScope; }> = [...project.envVariables]; @@ -738,9 +739,17 @@ export const getEnvironmentsRouterPatternAndVariables = async function name( } } + let orgVars: Array & { + scope: EnvVariableScope | InternalEnvVariableScope; + }> = []; if (project.organization) { - // check the environment quota, this prevents environments being deployed by the api or webhooks const curOrg = await getOrganizationById(project.organization); + + orgVars = [ + ...curOrg.envVariables + ] + + // check the environment quota, this prevents environments being deployed by the api or webhooks projectVars = [ ...projectVars, { @@ -749,6 +758,20 @@ export const getEnvironmentsRouterPatternAndVariables = async function name( scope: InternalEnvVariableScope.INTERNAL_SYSTEM } ]; + + // @TODO + // For backwards compatibility, also add org vars with project vars until + // the remote side is checking standalone org vars data + for (const orgVar of orgVars) { + const index = projectVars.findIndex((projectVar) => + projectVar.name === orgVar.name); + + // Project vars take precedence, so don't add if one with the same name + // already exists + if (index == -1) { + projectVars.push(orgVar); + } + } } // handle any bulk deploy related injections here @@ -822,8 +845,9 @@ export const getEnvironmentsRouterPatternAndVariables = async function name( // encode some values so they get sent to the controllers nicely const envVarsEncoded = new Buffer(JSON.stringify(lagoonEnvironmentVariables)).toString('base64') const projectVarsEncoded = new Buffer(JSON.stringify(projectVars)).toString('base64') + const orgVarsEncoded = new Buffer(JSON.stringify(orgVars)).toString('base64') - return [routerPattern, envVarsEncoded, projectVarsEncoded] + return [routerPattern, envVarsEncoded, projectVarsEncoded, orgVarsEncoded] } /* @@ -1120,13 +1144,13 @@ export const getTaskProjectEnvironmentVariables =async (projectName: string, env // needing to trigger a full deployment const result = await getOpenShiftInfoForProject(projectName); const environment = await getEnvironmentByIdWithVariables(environmentId); - const [_, envVars, projectVars] = await getEnvironmentsRouterPatternAndVariables( + const [_, envVars, projectVars, orgVars] = await getEnvironmentsRouterPatternAndVariables( result.project, environment.environmentById, environment.environmentById.openshift, null, null, null, null, bulkType.Task // bulk deployments don't apply to tasks yet, but this is future proofing the function call ) - return [projectVars, envVars] + return [projectVars, envVars, orgVars] } export const getBaasBucketName = async ( @@ -1165,11 +1189,12 @@ export const createTaskTask = async function(taskData: any) { const { project } = taskData; // inject variables into tasks the same way it is in builds - const [_, envVars, projectVars] = await getTaskProjectEnvironmentVariables( + const [_, envVars, projectVars, orgVars] = await getTaskProjectEnvironmentVariables( project.name, taskData.environment.id ) taskData.project.variables = { + organization: orgVars, project: projectVars, environment: envVars, } @@ -1355,11 +1380,12 @@ export const createMiscTask = async function(taskData: any) { break; case 'deploytarget:task:advanced': // inject variables into advanced tasks the same way it is in builds and standard tasks - const [_, envVars, projectVars] = await getTaskProjectEnvironmentVariables( + const [_, envVars, projectVars, orgVars] = await getTaskProjectEnvironmentVariables( taskData.data.project.name, taskData.data.environment.id ) miscTaskData.project.variables = { + organization: orgVars, project: projectVars, environment: envVars, } diff --git a/services/api/database/migrations/20241216000000_org_env_var.js b/services/api/database/migrations/20241216000000_org_env_var.js new file mode 100644 index 0000000000..b5530f23a6 --- /dev/null +++ b/services/api/database/migrations/20241216000000_org_env_var.js @@ -0,0 +1,24 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = async function(knex) { + return knex.schema + .alterTable('env_vars', (table) => { + table.integer('organization'); + table.unique(['name', 'organization'], {indexName: 'name_organization'}); + table.index('organization', 'organization_index'); + }) +}; + +/** +* @param { import("knex").Knex } knex +* @returns { Promise } +*/ +exports.down = async function(knex) { + return knex.schema + .alterTable('env_vars', (table) => { + table.dropUnique(['name', 'organization'], 'name_organization'); + table.dropColumn('organization'); + }) +}; diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index 0ea710d047..8aa058b328 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -279,6 +279,7 @@ const { } = require('./resources/backup/resolvers'); const { + getEnvVarsByOrganizationId, getEnvVarsByProjectId, getEnvVarsByEnvironmentId, addEnvVariable, @@ -456,7 +457,8 @@ const resolvers = { environments: getEnvironmentsByOrganizationId, owners: getOwnersByOrganizationId, deployTargets: getDeployTargetsByOrganizationId, - notifications: getNotificationsByOrganizationId + notifications: getNotificationsByOrganizationId, + envVariables: getEnvVarsByOrganizationId, }, OrgProject: { groups: getGroupsByOrganizationsProject, diff --git a/services/api/src/resources/env-variables/resolvers.ts b/services/api/src/resources/env-variables/resolvers.ts index cddd1e6620..adf3870e3f 100644 --- a/services/api/src/resources/env-variables/resolvers.ts +++ b/services/api/src/resources/env-variables/resolvers.ts @@ -4,8 +4,44 @@ import { query, knex } from '../../util/db'; import { Sql } from './sql'; import { Helpers as environmentHelpers } from '../environment/helpers'; import { Helpers as projectHelpers } from '../project/helpers'; -import { Sql as projectSql } from '../project/sql'; +import { Helpers as orgHelpers } from '../organization/helpers'; +export enum EnvVarType { + ORGANIZATION = 'organization', + PROJECT = 'project', + ENVIRONMENT = 'environment', +} + +const getEnvVarType = (envVars: { + organization?: String; + project?: String; + environment?: String; +}): EnvVarType | Error => { + if (envVars.organization && !envVars.project && !envVars.environment) { + return EnvVarType.ORGANIZATION; + } else if (!envVars.organization && envVars.project && !envVars.environment) { + return EnvVarType.PROJECT; + } else if (!envVars.organization && envVars.project && envVars.environment) { + return EnvVarType.ENVIRONMENT; + } + + return new Error( + 'Error determining type. Submit an organization name, a project name, or a project name and environment name.', + ); +}; + +export const getEnvVarsByOrganizationId: ResolverFn = async ( + { id: oid }, + _, + { sqlClientPool, hasPermission }, +) => { + await hasPermission('organization', 'viewEnvVar', { + organization: oid, + }); + const rows = await query(sqlClientPool, Sql.selectEnvVarsByOrgId(oid)); + + return rows; +}; export const getEnvVarsByProjectId: ResolverFn = async ( { id: pid }, @@ -100,189 +136,108 @@ export const getEnvVarsByEnvironmentId: ResolverFn = async ( } } -export const addEnvVariable: ResolverFn = async (obj, args, context) => { - const { - input: { type } - } = args; - - if (type === 'project') { - return addEnvVariableToProject(obj, args, context); - } else if (type === 'environment') { - return addEnvVariableToEnvironment(obj, args, context); - } -}; - -const addEnvVariableToProject = async ( +export const deleteEnvVariableByName: ResolverFn = async ( root, - { input: { id, typeId, name, value, scope } }, - { sqlClientPool, hasPermission, userActivityLogger } -) => { - await hasPermission('env_var', 'project:add', { - project: `${typeId}` - }); - - const { insertId } = await query( - sqlClientPool, - Sql.insertEnvVariable({ - id, - name, - value, - scope, - project: typeId - }) - ); - - const rows = await query(sqlClientPool, Sql.selectEnvVariable(insertId)); - - userActivityLogger(`User added environment variable '${name}' with scope '${scope}' to project '${typeId}'`, { - project: '', - event: 'api:addEnvVariableToProject', - payload: { - id, + { + input: { + project: projectName, + environment: environmentName, + organization: orgName, name, - scope, - typeId - } - }); - - return R.prop(0, rows); -}; - -const addEnvVariableToEnvironment = async ( - root, - { input: { id, typeId, name, value, scope } }, - { sqlClientPool, hasPermission, userActivityLogger } + }, + }, + { sqlClientPool, hasPermission, userActivityLogger }, ) => { - const environment = await environmentHelpers( - sqlClientPool - ).getEnvironmentById(typeId); - - await hasPermission( - 'env_var', - `environment:add:${environment.environmentType}`, - { - project: environment.project - } - ); - - const { insertId } = await query( - sqlClientPool, - Sql.insertEnvVariable({ - id, - name, - value, - scope, - environment: typeId - }) - ); - - const rows = await query(sqlClientPool, Sql.selectEnvVariable(insertId)); - - userActivityLogger(`User added environment variable '${name}' with scope '${scope}' to environment '${environment.name}' on '${environment.project}'`, { - project: '', - event: 'api:addEnvVariableToEnvironment', - payload: { - id, - name, - scope, - typeId, - environment - } + const envVarType = getEnvVarType({ + organization: orgName, + project: projectName, + environment: environmentName, }); - return R.prop(0, rows); -}; + if (envVarType instanceof Error) { + throw envVarType; + } -export const deleteEnvVariable: ResolverFn = async ( - root, - { input: { id } }, - { sqlClientPool, hasPermission, userActivityLogger } -) => { - const perms = await query(sqlClientPool, Sql.selectPermsForEnvVariable(id)); + let envVarTypeName = ''; - await hasPermission('env_var', 'delete', { - project: R.path(['0', 'pid'], perms) - }); - - await query(sqlClientPool, Sql.deleteEnvVariable(id)); + if (envVarType == EnvVarType.ORGANIZATION) { + envVarTypeName = orgName; + const orgId = + await orgHelpers(sqlClientPool).getOrganizationIdByName(orgName); + const orgVariable = await query( + sqlClientPool, + Sql.selectEnvVarByNameAndOrgId(name, orgId), + ); - userActivityLogger(`User deleted environment variable`, { - project: '', - event: 'api:deleteEnvVariable', - payload: { - id + await hasPermission('organization', 'deleteEnvVar', { + organization: orgId, + }); + if (orgVariable[0]) { + await query(sqlClientPool, Sql.deleteEnvVariable(orgVariable[0].id)); + } else { + // variable doesn't exist, just return success + return 'success'; } - }); - - return 'success'; -}; - -// delete an environment variable by name -// if the environment name is provided, it will delete them from the environment, otherwise project -export const deleteEnvVariableByName: ResolverFn = async ( - root, - { input: { project: projectName, environment: environmentName, name } }, - { sqlClientPool, hasPermission, userActivityLogger } -) => { - const projectId = await projectHelpers(sqlClientPool).getProjectIdByName( - projectName - ); + } else { + const projectId = + await projectHelpers(sqlClientPool).getProjectIdByName(projectName); - let envVarType = "project" - let envVarTypeName = projectName - if (environmentName) { - // is environment - const environmentRows = await query( - sqlClientPool, - Sql.selectEnvironmentByNameAndProject(environmentName, projectId) - ); - const environment = environmentRows[0]; - if (environment) { - const environmentVariable = await query( + if (envVarType == EnvVarType.PROJECT) { + envVarTypeName = projectName; + const projectVariable = await query( sqlClientPool, - Sql.selectEnvVarByNameAndEnvironmentId(name, environment.id) - ); - await hasPermission( - 'env_var', - `environment:delete:${environment.environmentType}`, - { - project: projectId - } + Sql.selectEnvVarByNameAndProjectId(name, projectId), ); - if (environmentVariable[0]) { - envVarType = "environment" - envVarTypeName = environmentName - await query(sqlClientPool, Sql.deleteEnvVariable(environmentVariable[0].id)); + await hasPermission('env_var', 'project:delete', { + project: projectId, + }); + if (projectVariable[0]) { + await query( + sqlClientPool, + Sql.deleteEnvVariable(projectVariable[0].id), + ); } else { // variable doesn't exist, just return success - return "success" + return 'success'; } - } else { - // if the environment doesn't exist, check the user has permission to delete on the project - // before throwing an error that the environment doesn't exist - await hasPermission('project', 'view', { - project: projectId - }); - throw new Error( - `environment ${environmentName} doesn't exist` + } else if (envVarType == EnvVarType.ENVIRONMENT) { + const environmentRows = await query( + sqlClientPool, + Sql.selectEnvironmentByNameAndProject(environmentName, projectId), ); - } - } else { - // is project - const projectVariable = await query( - sqlClientPool, - Sql.selectEnvVarByNameAndProjectId(name, projectId) - ); + const environment = environmentRows[0]; + if (environment) { + const environmentVariable = await query( + sqlClientPool, + Sql.selectEnvVarByNameAndEnvironmentId(name, environment.id), + ); + await hasPermission( + 'env_var', + `environment:delete:${environment.environmentType}`, + { + project: projectId, + }, + ); - await hasPermission('env_var', 'project:delete', { - project: projectId - }); - if (projectVariable[0]) { - await query(sqlClientPool, Sql.deleteEnvVariable(projectVariable[0].id)); - } else { - // variable doesn't exist, just return success - return "success" + if (environmentVariable[0]) { + envVarTypeName = environmentName; + await query( + sqlClientPool, + Sql.deleteEnvVariable(environmentVariable[0].id), + ); + } else { + // variable doesn't exist, just return success + return 'success'; + } + } else { + // if the environment doesn't exist, check the user has permission to delete on the project + // before throwing an error that the environment doesn't exist + await hasPermission('project', 'view', { + project: projectId, + }); + throw new Error(`environment ${environmentName} doesn't exist`); + } } } @@ -292,8 +247,8 @@ export const deleteEnvVariableByName: ResolverFn = async ( payload: { name, envVarType, - envVarTypeName - } + envVarTypeName, + }, }); return 'success'; @@ -301,89 +256,113 @@ export const deleteEnvVariableByName: ResolverFn = async ( export const addOrUpdateEnvVariableByName: ResolverFn = async ( root, - { input: { project: projectName, environment: environmentName, name, scope, value } }, - { sqlClientPool, hasPermission, userActivityLogger } + { + input: { + project: projectName, + environment: environmentName, + organization: orgName, + name, + scope, + value, + }, + }, + { sqlClientPool, hasPermission, userActivityLogger }, ) => { - const projectId = await projectHelpers(sqlClientPool).getProjectIdByName( - projectName - ); - - const projectRows = await query( - sqlClientPool, - projectSql.selectProject(projectId) - ); - if (name.trim().length == 0) { - throw new Error( - 'A variable name must be provided.' - ); + throw new Error('A variable name must be provided.'); } - const project = projectRows[0]; + const envVarType = getEnvVarType({ + organization: orgName, + project: projectName, + environment: environmentName, + }); + + if (envVarType instanceof Error) { + throw envVarType; + } let updateData = {}; - let envVarType = "project" - let envVarTypeName = projectName - if (environmentName) { - const environmentRows = await query( - sqlClientPool, - Sql.selectEnvironmentByNameAndProject(environmentName, projectId) - ); - const environment = environmentRows[0]; - await hasPermission( - 'env_var', - `environment:add:${environment.environmentType}`, - { - project: projectId - } - ); - updateData = { - name: name.trim(), - value, - scope, - environment: environment.id, - } - envVarType = "environment" - envVarTypeName = environmentName - } else { - // this is a project - await hasPermission('env_var', 'project:add', { - project: projectId + let envVarTypeName = ''; + + if (envVarType == EnvVarType.ORGANIZATION) { + envVarTypeName = orgName; + const orgId = + await orgHelpers(sqlClientPool).getOrganizationIdByName(orgName); + await hasPermission('organization', 'addEnvVar', { + organization: orgId, }); updateData = { name: name.trim(), value, scope, - project: project.id, + organization: orgId, + }; + } else { + const projectId = + await projectHelpers(sqlClientPool).getProjectIdByName(projectName); + + if (envVarType === EnvVarType.PROJECT) { + envVarTypeName = projectName; + await hasPermission('env_var', 'project:add', { + project: projectId, + }); + updateData = { + name: name.trim(), + value, + scope, + project: projectId, + }; + } else if (envVarType == EnvVarType.ENVIRONMENT) { + envVarTypeName = environmentName; + const environmentRows = await query( + sqlClientPool, + Sql.selectEnvironmentByNameAndProject(environmentName, projectId), + ); + const environment = environmentRows[0]; + await hasPermission( + 'env_var', + `environment:add:${environment.environmentType}`, + { + project: projectId, + }, + ); + updateData = { + name: name.trim(), + value, + scope, + environment: environment.id, + }; } } - const createOrUpdateSql = knex('env_vars') .insert({ ...updateData, }) .onConflict('id') .merge({ - ...updateData - }).toString(); + ...updateData, + }) + .toString(); - const { insertId } = await query( - sqlClientPool, - createOrUpdateSql); + const { insertId } = await query(sqlClientPool, createOrUpdateSql); const rows = await query(sqlClientPool, Sql.selectEnvVariable(insertId)); - userActivityLogger(`User added environment variable to ${envVarType} '${envVarTypeName}'`, { - project: projectName, - event: 'api:addOrUpdateEnvVariableByName', - payload: { - name, - scope, - envVarType, - envVarTypeName - } - }); + userActivityLogger( + `User added environment variable to ${envVarType} '${envVarTypeName}'`, + { + project: projectName, + event: 'api:addOrUpdateEnvVariableByName', + payload: { + name, + scope, + envVarType, + envVarTypeName, + }, + }, + ); return R.prop(0, rows); }; @@ -480,3 +459,124 @@ export const getEnvVariablesByProjectEnvironmentName: ResolverFn = async ( } return []; }; + +// Deprecated +export const addEnvVariable: ResolverFn = async (obj, args, context) => { + const { + input: { type } + } = args; + + if (type === 'project') { + return addEnvVariableToProject(obj, args, context); + } else if (type === 'environment') { + return addEnvVariableToEnvironment(obj, args, context); + } +}; + +// Deprecated +const addEnvVariableToProject = async ( + root, + { input: { id, typeId, name, value, scope } }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + await hasPermission('env_var', 'project:add', { + project: `${typeId}` + }); + + const { insertId } = await query( + sqlClientPool, + Sql.insertEnvVariable({ + id, + name, + value, + scope, + project: typeId + }) + ); + + const rows = await query(sqlClientPool, Sql.selectEnvVariable(insertId)); + + userActivityLogger(`User added environment variable '${name}' with scope '${scope}' to project '${typeId}'`, { + project: '', + event: 'api:addEnvVariableToProject', + payload: { + id, + name, + scope, + typeId + } + }); + + return R.prop(0, rows); +}; + +// Deprecated +const addEnvVariableToEnvironment = async ( + root, + { input: { id, typeId, name, value, scope } }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + const environment = await environmentHelpers( + sqlClientPool + ).getEnvironmentById(typeId); + + await hasPermission( + 'env_var', + `environment:add:${environment.environmentType}`, + { + project: environment.project + } + ); + + const { insertId } = await query( + sqlClientPool, + Sql.insertEnvVariable({ + id, + name, + value, + scope, + environment: typeId + }) + ); + + const rows = await query(sqlClientPool, Sql.selectEnvVariable(insertId)); + + userActivityLogger(`User added environment variable '${name}' with scope '${scope}' to environment '${environment.name}' on '${environment.project}'`, { + project: '', + event: 'api:addEnvVariableToEnvironment', + payload: { + id, + name, + scope, + typeId, + environment + } + }); + + return R.prop(0, rows); +}; + +// Deprecated +export const deleteEnvVariable: ResolverFn = async ( + root, + { input: { id } }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + const perms = await query(sqlClientPool, Sql.selectPermsForEnvVariable(id)); + + await hasPermission('env_var', 'delete', { + project: R.path(['0', 'pid'], perms) + }); + + await query(sqlClientPool, Sql.deleteEnvVariable(id)); + + userActivityLogger(`User deleted environment variable`, { + project: '', + event: 'api:deleteEnvVariable', + payload: { + id + } + }); + + return 'success'; +}; diff --git a/services/api/src/resources/env-variables/sql.ts b/services/api/src/resources/env-variables/sql.ts index 43a682b22f..cc9d187bf6 100644 --- a/services/api/src/resources/env-variables/sql.ts +++ b/services/api/src/resources/env-variables/sql.ts @@ -42,6 +42,12 @@ export const Sql = { .andWhere('project', '=', projectId) .andWhere('deleted', '0000-00-00 00:00:00') .toString(), + selectEnvVarByNameAndOrgId: (name: string, orgId: number) => + knex('env_vars') + .select('env_vars.*') + .where('env_vars.name', '=', name) + .andWhere('env_vars.organization', '=', orgId) + .toString(), selectEnvVarByNameAndProjectId: (name: string, projectId: number) => knex('env_vars') .select('env_vars.*') @@ -54,6 +60,12 @@ export const Sql = { .where('env_vars.name', '=', name) .andWhere('env_vars.environment', '=', environmentId) .toString(), + selectEnvVarsByOrgId: (orgId: number) => + knex('env_vars') + .select('env_vars.*') + .where('env_vars.organization', '=', orgId) + .orderBy('env_vars.name', 'asc') + .toString(), selectEnvVarsByProjectId: (projectId: number) => knex('env_vars') .select('env_vars.*') diff --git a/services/api/src/resources/organization/helpers.ts b/services/api/src/resources/organization/helpers.ts index 8b208ad361..12da18d7b2 100644 --- a/services/api/src/resources/organization/helpers.ts +++ b/services/api/src/resources/organization/helpers.ts @@ -1,6 +1,7 @@ import * as R from 'ramda'; import { Pool } from 'mariadb'; import { query } from '../../util/db'; +import { toNumber } from '../../util/func'; import { asyncPipe } from '@lagoon/commons/dist/util/func'; import { Sql } from './sql'; @@ -97,5 +98,26 @@ export const Helpers = (sqlClientPool: Pool) => { ] ])(organizationInput); }, + getOrganizationIdByName: async (name: string): Promise => { + const idResult = await query( + sqlClientPool, + Sql.selectOrganizationByName(name) + ); + + const amount = R.length(idResult); + if (amount > 1) { + throw new Error( + `Multiple organization candidates for '${name}' (${amount} found). Do nothing.` + ); + } + + if (amount === 0) { + throw new Error(`Not found: '${name}'`); + } + + const id = R.path(['0', 'id'], idResult) as string; + + return toNumber(id); + }, } }; diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 6693c162e7..3f8aed3deb 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -1106,6 +1106,7 @@ const typeDefs = gql` owners: [OrgUser] notifications(type: NotificationType): [Notification] created: String + envVariables: [EnvKeyValue] } input AddOrganizationInput { @@ -2195,17 +2196,19 @@ const typeDefs = gql` } input DeleteEnvVariableByNameInput { - environment: String - project: String! name: String! + organization: String + project: String + environment: String } input EnvVariableByNameInput { - environment: String - project: String! - scope: EnvVariableScope name: String! value: String! + scope: EnvVariableScope + organization: String + project: String + environment: String } input SetEnvironmentServicesInput { diff --git a/services/keycloak/lagoon-realm-base-import.json b/services/keycloak/lagoon-realm-base-import.json index 7b39a203e0..f25d1c7316 100644 --- a/services/keycloak/lagoon-realm-base-import.json +++ b/services/keycloak/lagoon-realm-base-import.json @@ -1290,6 +1290,15 @@ }, { "name": "addGroup" + }, + { + "name": "addEnvVar" + }, + { + "name": "deleteEnvVar" + }, + { + "name": "viewEnvVar" } ] }, @@ -1804,10 +1813,21 @@ "decisionStrategy": "AFFIRMATIVE", "config": { "resources": "[\"organization\"]", - "scopes": "[\"view\",\"viewProject\",\"viewGroup\",\"viewNotification\",\"viewUser\",\"viewUsers\"]", + "scopes": "[\"view\",\"viewProject\",\"viewGroup\",\"viewNotification\",\"viewUser\",\"viewUsers\",\"viewEnvVar\"]", "applyPolicies": "[\"[Lagoon] User is admin of organization\",\"[Lagoon] User is owner of organization\",\"[Lagoon] Users role for realm is Platform Organization Owner\",\"[Lagoon] Users role for realm is Platform Viewer\",\"[Lagoon] Users role for realm is Platform Owner\",\"[Lagoon] User is viewer of organization\"]" } }, + { + "name": "Manage Organization Environmnet Variables", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "resources": "[\"organization\"]", + "scopes": "[\"addEnvVar\",\"deleteEnvVar\",\"viewEnvVar\"]", + "applyPolicies": "[\"[Lagoon] User is admin of organization\",\"[Lagoon] User is owner of organization\",\"[Lagoon] Users role for realm is Platform Organization Owner\",\"[Lagoon] Users role for realm is Platform Owner\"]" + } + }, { "name": "Update Project", "type": "scope", @@ -2902,6 +2922,15 @@ }, { "name": "addNoExec" + }, + { + "name": "addEnvVar" + }, + { + "name": "deleteEnvVar" + }, + { + "name": "viewEnvVar" } ], "decisionStrategy": "UNANIMOUS" diff --git a/services/keycloak/startup-scripts/00-configure-lagoon.sh b/services/keycloak/startup-scripts/00-configure-lagoon.sh index 3f2ba56d17..528583dc63 100755 --- a/services/keycloak/startup-scripts/00-configure-lagoon.sh +++ b/services/keycloak/startup-scripts/00-configure-lagoon.sh @@ -869,6 +869,52 @@ EOF } +function add_org_env_vars { + local api_client_id=$(/opt/keycloak/bin/kcadm.sh get -r lagoon clients?clientId=api --config $CONFIG_PATH | jq -r '.[0]["id"]') + local manage_org_env_var=$(/opt/keycloak/bin/kcadm.sh get -r lagoon clients/$api_client_id/authz/resource-server/permission?name=Manage+Organization+Environmnet+Variables --config $CONFIG_PATH) + + + if [ "$manage_org_env_var" != "[ ]" ]; then + echo "Organization env vars already configured" + return 0 + fi + + echo adding permissions for organization env vars + + # Add scopes to organization resource + ORGANIZATION_RESOURCE_ID=$(/opt/keycloak/bin/kcadm.sh get -r lagoon clients/$CLIENT_ID/authz/resource-server/resource?name=organization --config $CONFIG_PATH | jq -r '.[0]["_id"]') + /opt/keycloak/bin/kcadm.sh update clients/$CLIENT_ID/authz/resource-server/resource/$ORGANIZATION_RESOURCE_ID --config $CONFIG_PATH -r ${KEYCLOAK_REALM:-master} -s 'scopes=[{"name":"updateNotification"},{"name":"addUser"},{"name":"add"},{"name":"removeNotification"},{"name":"viewNotification"},{"name":"addOwner"},{"name":"updateOrganization"},{"name":"update"},{"name":"viewUser"},{"name":"viewAll"},{"name":"updateProject"},{"name":"delete"},{"name":"viewProject"},{"name":"addNotification"},{"name":"viewUsers"},{"name":"view"},{"name":"viewGroup"},{"name":"deleteProject"},{"name":"removeGroup"},{"name":"addViewer"},{"name":"addProject"},{"name":"addGroup"},{"name":"addEnvVar"},{"name":"deleteEnvVar"},{"name":"viewEnvVar"}]' + + # Create "Manage Organization Environmnet Variables" permission + /opt/keycloak/bin/kcadm.sh create clients/$api_client_id/authz/resource-server/permission/scope --config $CONFIG_PATH -r lagoon -f - <