diff --git a/local-dev/api-data-watcher-pusher/api-data/04-populate-api-data-organizations.gql b/local-dev/api-data-watcher-pusher/api-data/04-populate-api-data-organizations.gql index d0016054ad..3d45db9137 100644 --- a/local-dev/api-data-watcher-pusher/api-data/04-populate-api-data-organizations.gql +++ b/local-dev/api-data-watcher-pusher/api-data/04-populate-api-data-organizations.gql @@ -94,7 +94,9 @@ mutation PopulateApi { UILocalKubernetesOrganization: addDeployTargetToOrganization(input:{ deployTarget: 2001 organization: 1 - }) + }) { + id + } UIProject1: addProject( input: { diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index df980c78e5..3f5e30f991 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -135,7 +135,8 @@ export const Group = (clients: { type: attributeKVOrNull('type', keycloakGroup), path: keycloakGroup.path, attributes: keycloakGroup.attributes, - subGroups: keycloakGroup.subGroups + subGroups: keycloakGroup.subGroups, + organization: parseInt(attributeKVOrNull('lagoon-organization', keycloakGroup)), }) ); @@ -784,6 +785,90 @@ export const Group = (clients: { } }; + // helper to remove project from groups + const removeProjectFromGroups = async ( + projectId: number, + groups: Group[] + ): Promise => { + for (const g in groups) { + const group = groups[g] + const groupProjectIds = getProjectIdsFromGroup(group) + const newGroupProjects = R.pipe( + R.without([projectId]), + R.uniq, + R.join(',') + // @ts-ignore + )(groupProjectIds); + + try { + await keycloakAdminClient.groups.update( + { + id: group.id + }, + { + name: group.name, + attributes: { + ...group.attributes, + 'lagoon-projects': [newGroupProjects], + 'group-lagoon-project-ids': [`{${JSON.stringify(group.name)}:[${newGroupProjects}]}`] + } + } + ); + } catch (err) { + throw new Error( + `Error setting projects for group ${group.name}: ${err.message}` + ); + } + } + + // once the project is remove from the groups, update the cache + const allGroups = await loadAllGroups(); + const keycloakGroups = await transformKeycloakGroups(allGroups); + const data = Buffer.from(JSON.stringify(keycloakGroups)).toString('base64') + try { + // then attempt to save it to redis + await saveRedisKeycloakCache("allgroups", data); + } catch (err) { + logger.warn(`Couldn't save redis keycloak cache: ${err.message}`); + } + }; + + // helper to remove all non default-users from project + const removeNonProjectDefaultUsersFromGroup = async ( + group: Group, + project: String, + ): Promise => { + const members = await getGroupMembership(group); + + for (const u in members) { + if (members[u].user.email != "default-user@"+project) { + try { + await keycloakAdminClient.users.delFromGroup({ + // @ts-ignore + id: members[u].user.id, + // @ts-ignore + groupId: members[u].roleSubgroupId + }); + } catch (err) { + throw new Error(`Could not remove user from group: ${err.message}`); + } + } + } + + // once the users are removed from the group, update the cache + const allGroups = await loadAllGroups(); + const keycloakGroups = await transformKeycloakGroups(allGroups); + const data = Buffer.from(JSON.stringify(keycloakGroups)).toString('base64') + try { + // then attempt to save it to redis + await saveRedisKeycloakCache("allgroups", data); + } catch (err) { + logger.warn(`Couldn't save redis keycloak cache: ${err.message}`); + } + + return await loadGroupById(group.id); + }; + return { loadAllGroups, loadGroupById, @@ -804,7 +889,9 @@ export const Group = (clients: { removeUserFromGroup, addProjectToGroup, removeProjectFromGroup, + removeProjectFromGroups, transformKeycloakGroups, - getGroupMembership + getGroupMembership, + removeNonProjectDefaultUsersFromGroup }; }; diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index 2ff45aa4a3..97ba963f8a 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -245,8 +245,10 @@ const { addOrganization, getAllOrganizations, updateOrganization, + deleteOrganization, getOrganizationById, addDeployTargetToOrganization, + removeDeployTargetFromOrganization, getDeployTargetsByOrganizationId, getGroupsByOrganizationId, getUsersByOrganizationId, @@ -256,6 +258,7 @@ const { getOwnersByOrganizationId, getProjectsByOrganizationId, addProjectToOrganization, + removeProjectFromOrganization, addGroupToOrganization, getGroupsByOrganizationsProject, getProjectGroupOrganizationAssociation, // WIP resolver @@ -684,9 +687,12 @@ const resolvers = { updateDeployTargetConfig, addOrganization, updateOrganization, + deleteOrganization, addGroupToOrganization, addProjectToOrganization, + removeProjectFromOrganization, addDeployTargetToOrganization, + removeDeployTargetFromOrganization, updateEnvironmentDeployTarget, }, Subscription: { diff --git a/services/api/src/resources/notification/helpers.ts b/services/api/src/resources/notification/helpers.ts index faf538bfd3..fcc525fb98 100644 --- a/services/api/src/resources/notification/helpers.ts +++ b/services/api/src/resources/notification/helpers.ts @@ -14,4 +14,46 @@ export const Helpers = (sqlClientPool: Pool) => ({ return R.map(R.prop('nid'), result); }, + selectNotificationsByProjectId: async ( + { project }: { project: number }, + ) => { + let input = {pid: project, type: "slack"} + // get all the notifications for the projects + const slacks = await query( + sqlClientPool, + Sql.selectNotificationsByTypeByProjectId(input) + ); + input.type = "rocketchat" + const rcs = await query( + sqlClientPool, + Sql.selectNotificationsByTypeByProjectId(input) + ); + input.type = "microsoftteams" + const teams = await query( + sqlClientPool, + Sql.selectNotificationsByTypeByProjectId(input) + ); + input.type = "email" + const email = await query( + sqlClientPool, + Sql.selectNotificationsByTypeByProjectId(input) + ); + input.type = "webhook" + const webhook = await query( + sqlClientPool, + Sql.selectNotificationsByTypeByProjectId(input) + ); + let result = [...slacks, ...rcs, ...teams, ...email, ...webhook] + + return result + }, + removeAllNotificationsFromProject: async ( + { project }: { project: number }, + ) => { + await query(sqlClientPool, Sql.deleteProjectNotificationByProjectId(project, "slack")); + await query(sqlClientPool, Sql.deleteProjectNotificationByProjectId(project, "rocketchat")); + await query(sqlClientPool, Sql.deleteProjectNotificationByProjectId(project, "microsoftteams")); + await query(sqlClientPool, Sql.deleteProjectNotificationByProjectId(project, "email")); + await query(sqlClientPool, Sql.deleteProjectNotificationByProjectId(project, "webhook")); + }, }); diff --git a/services/api/src/resources/notification/resolvers.ts b/services/api/src/resources/notification/resolvers.ts index d753d8a0cf..493f5a9dd6 100644 --- a/services/api/src/resources/notification/resolvers.ts +++ b/services/api/src/resources/notification/resolvers.ts @@ -454,15 +454,7 @@ export const getNotificationsByOrganizationId: ResolverFn = async ( const results = await Promise.all( types.map(type => - query( - sqlClientPool, - Sql.selectNotificationsByTypeByOrganizationId({ - type, - oid, - contentType, - notificationSeverityThreshold - }) - ) + organizationHelpers(sqlClientPool).getNotificationsByTypeForOrganizationId(oid, type) ) ); diff --git a/services/api/src/resources/notification/sql.ts b/services/api/src/resources/notification/sql.ts index bc3b1a9a1d..75c2c4731f 100644 --- a/services/api/src/resources/notification/sql.ts +++ b/services/api/src/resources/notification/sql.ts @@ -53,6 +53,22 @@ export const Sql = { return deleteQuery.toString(); }, + deleteProjectNotificationByProjectId: (id: number, type: string) => { + const deleteQuery = knex.raw( + `DELETE pn + FROM project_notification as pn + LEFT JOIN :notificationTable: AS nt ON pn.nid = nt.id AND pn.type = :notificationType + LEFT JOIN project as p on pn.pid = p.id + WHERE p.id = :pid`, + { + pid: id, + notificationType: type, + notificationTable: `notification_${type}` + } + ); + + return deleteQuery.toString(); + }, selectProjectById: input => knex('project') .select('*') diff --git a/services/api/src/resources/organization/helpers.ts b/services/api/src/resources/organization/helpers.ts index 1ed34391a2..6647b52ab9 100644 --- a/services/api/src/resources/organization/helpers.ts +++ b/services/api/src/resources/organization/helpers.ts @@ -18,6 +18,14 @@ export const Helpers = (sqlClientPool: Pool) => { const rows = await query(sqlClientPool, Sql.selectOrganizationEnvironments(id)); return rows; }; + const getNotificationsByTypeForOrganizationId = async (id: number, type: string) => { + let input = {id: id, type: type} + const result = await query( + sqlClientPool, + Sql.selectNotificationsByTypeByOrganizationId(input) + ); + return result + }; const getNotificationsForOrganizationId = async (id: number) => { let input = {id: id, type: "slack"} // get all the notifications for the projects @@ -57,6 +65,7 @@ export const Helpers = (sqlClientPool: Pool) => { getProjectsByOrganizationId, getDeployTargetsByOrganizationId, getNotificationsForOrganizationId, + getNotificationsByTypeForOrganizationId, getEnvironmentsByOrganizationId, } }; \ No newline at end of file diff --git a/services/api/src/resources/organization/resolvers.ts b/services/api/src/resources/organization/resolvers.ts index 8e9b6196a8..aaa10bfa95 100644 --- a/services/api/src/resources/organization/resolvers.ts +++ b/services/api/src/resources/organization/resolvers.ts @@ -7,6 +7,8 @@ import { Helpers as projectHelpers } from '../project/helpers'; import { Helpers} from './helpers'; import { Sql } from './sql'; import { arrayDiff } from '../../util/func'; +import { Helpers as openshiftHelpers } from '../openshift/helpers'; +import { Helpers as notificationHelpers } from '../notification/helpers'; import validator from 'validator'; const isValidName = value => { @@ -23,7 +25,7 @@ const isValidName = value => { export const addOrganization: ResolverFn = async ( args, { input }, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, userActivityLogger } ) => { // check if the name is valid @@ -33,6 +35,18 @@ export const addOrganization: ResolverFn = async ( await hasPermission('organization', 'add'); const { insertId } = await query(sqlClientPool, Sql.insertOrganization(input)); const rows = await query(sqlClientPool, Sql.selectOrganization(insertId)); + + userActivityLogger(`User added an organization ${R.prop(0, rows).name}`, { + project: '', + organization: input.organization, + event: 'api:addOrganization', + payload: { + data: { + input + } + } + }); + return R.prop(0, rows); } catch (err) { throw new Error(`There was an error creating the organization ${input.name} ${err}`); @@ -43,15 +57,78 @@ export const addOrganization: ResolverFn = async ( export const addDeployTargetToOrganization: ResolverFn = async ( args, { input }, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, userActivityLogger } ) => { - try { - await hasPermission('organization', 'add'); - const { insertId } = await query(sqlClientPool, Sql.addDeployTarget({dtid: input.deployTarget, orgid: input.organization})); - return insertId - } catch (err) { - throw new Error(`There was an error adding the deployTarget: ${err}`); + await hasPermission('organization', 'add'); + + const org = await query(sqlClientPool, Sql.selectOrganization(input.organization)); + if (R.length(org) == 0) { + throw new Error( + `Organization doesn't exist` + ); + } + try { + await openshiftHelpers(sqlClientPool).getOpenshiftByOpenshiftInput({id: input.deployTarget}) + } catch (err) { + throw new Error(`There was an error adding the deployTarget: ${err}`); + } + const result = await query(sqlClientPool, Sql.selectDeployTargetsByOrganizationAndDeployTarget(input.organization, input.deployTarget)) + if (R.length(result) >= 1) { + throw new Error( + `Already added to organization` + ); + } + try { + await query(sqlClientPool, Sql.addDeployTarget({dtid: input.deployTarget, orgid: input.organization})); + } catch (err) { + throw new Error(`There was an error adding the deployTarget: ${err}`); + } + userActivityLogger(`User added a deploytarget to organization ${R.prop(0, org).name}`, { + project: '', + organization: input.organization, + event: 'api:addDeployTargetToOrganization', + payload: { + data: { + input + } } + }); + + return org[0]; +}; + +export const removeDeployTargetFromOrganization: ResolverFn = async ( + args, + { input }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + await hasPermission('organization', 'add'); + + const org = await query(sqlClientPool, Sql.selectOrganization(input.organization)); + if (R.length(org) == 0) { + throw new Error( + `Organization doesn't exist` + ); + } + + try { + await query(sqlClientPool, Sql.removeDeployTarget(input.organization, input.deployTarget)); + } catch (err) { + throw new Error(`There was an error removing the deployTarget: ${err}`); + } + + userActivityLogger(`User removed a deploytarget from organization ${R.prop(0, org).name}`, { + project: '', + organization: input.organization, + event: 'api:removeDeployTargetFromOrganization', + payload: { + data: { + input + } + } + }); + + return org[0]; }; @@ -103,7 +180,7 @@ export const getEnvironmentsByOrganizationId: ResolverFn = async ( export const updateOrganization: ResolverFn = async ( root, { input }, - { sqlClientPool, hasPermission } + { sqlClientPool, hasPermission, userActivityLogger } ) => { if (input.patch.quotaProject || input.patch.quotaGroup || input.patch.quotaNotification || input.patch.quotaEnvironment || input.patch.quotaRoute) { @@ -126,6 +203,17 @@ export const updateOrganization: ResolverFn = async ( await query(sqlClientPool, Sql.updateOrganization(input)); const rows = await query(sqlClientPool, Sql.selectOrganization(oid)); + userActivityLogger(`User updated organization ${R.prop(0, rows).name}`, { + project: '', + organization: input.organization, + event: 'api:updateOrganization', + payload: { + data: { + input + } + } + }); + return R.prop(0, rows); }; @@ -214,36 +302,7 @@ export const getNotificationsForOrganizationProjectId: ResolverFn = async ( pid = organization.id; } - let input = {oid: oid, pid: pid, type: "slack"} - // get all the notifications for the projects - const slacks = await query( - sqlClientPool, - Sql.selectNotificationsByTypeByProjectId(input) - ); - input.type = "rocketchat" - const rcs = await query( - sqlClientPool, - Sql.selectNotificationsByTypeByProjectId(input) - ); - input.type = "microsoftteams" - const teams = await query( - sqlClientPool, - Sql.selectNotificationsByTypeByProjectId(input) - ); - input.type = "email" - const email = await query( - sqlClientPool, - Sql.selectNotificationsByTypeByProjectId(input) - ); - input.type = "webhook" - const webhook = await query( - sqlClientPool, - Sql.selectNotificationsByTypeByProjectId(input) - ); - - let result = [...slacks, ...rcs, ...teams, ...email, ...webhook] - - return result; + return await notificationHelpers(sqlClientPool).selectNotificationsByProjectId({project: pid}) }; // gets owners of an organization by id @@ -536,6 +595,94 @@ export const getProjectGroupOrganizationAssociation: ResolverFn = async ( return "success"; }; +// remove project from an organization +// this removes all notifications and groups from the project and resets all the access to the project to only +// the default project group and the default-user of the project +export const removeProjectFromOrganization: ResolverFn = async ( + root, + { input }, + { sqlClientPool, hasPermission, models, keycloakGroups, userActivityLogger } +) => { + // platform admin only + await hasPermission('organization', 'add'); + + let pid = input.project; + const project = await projectHelpers(sqlClientPool).getProjectById(pid) + if (project.organization != input.organization) { + throw new Error( + `Project is not in organization` + ); + } + + try { + const projectGroups = await models.GroupModel.loadGroupsByProjectIdFromGroups(pid, keycloakGroups); + + let removeGroups = [] + for (const g in projectGroups) { + if (projectGroups[g].attributes["type"] == "project-default-group") { + // remove all users from the project default group except the `default-user@project` + await models.GroupModel.removeNonProjectDefaultUsersFromGroup(projectGroups[g], project.name) + // update group + await models.GroupModel.updateGroup({ + id: projectGroups[g].id, + name: projectGroups[g].name, + attributes: { + ...projectGroups[g].attributes, + "lagoon-organization": [""] + } + }); + } else { + removeGroups.push(projectGroups[g]) + } + } + // remove groups from project + await models.GroupModel.removeProjectFromGroups(pid, removeGroups); + } catch (err) { + throw new Error( + `Unable to remove all groups from the project` + ) + } + try { + // remove all notifications from project + await notificationHelpers(sqlClientPool).removeAllNotificationsFromProject({project: pid}) + } catch (err) { + throw new Error( + `Unable to remove all notifications from the project` + ) + } + + try { + // remove the project from the organization + await query( + sqlClientPool, + Sql.updateProjectOrganization({ + pid, + patch:{ + organization: null, + } + }) + ); + } catch (err) { + throw new Error( + `Unable to remove project from organization` + ) + } + + const org = await query(sqlClientPool, Sql.selectOrganization(input.organization)); + userActivityLogger(`User removed project ${project.name} from an organization ${R.prop(0, org).name}`, { + project: '', + organization: input.organization, + event: 'api:removeProjectFromOrganization', + payload: { + data: { + input + } + } + }); + + return projectHelpers(sqlClientPool).getProjectById(pid); +} + // add existing project to an organization export const addProjectToOrganization: ResolverFn = async ( root, @@ -614,18 +761,8 @@ export const addProjectToOrganization: ResolverFn = async ( return projectHelpers(sqlClientPool).getProjectById(pid); } -// check an existing group to see if it can be added to an organization -// this function will return errors if there are projects in the group that are not in the organization -// if there are no projects in the organization, and no projects in the group then it will succeed -// this is a helper function that is a WIP, not fully flushed out -export const getGroupProjectOrganizationAssociation: ResolverFn = async ( - _root, - { input }, - { models, sqlClientPool, hasPermission, userActivityLogger } -) => { - // platform admin only as it potentially reveals information about projects/orgs/groups - await hasPermission('organization', 'add'); +const checkOrgProjectGroup = async (sqlClientPool, input, models) => { // check the organization exists const organizationData = await Helpers(sqlClientPool).getOrganizationById(input.organization); if (organizationData === undefined) { @@ -642,15 +779,17 @@ export const getGroupProjectOrganizationAssociation: ResolverFn = async ( const projectsByOrg = await projectHelpers(sqlClientPool).getProjectByOrganizationId(input.organization); const projectIdsByOrg = [] for (const project of projectsByOrg) { - projectIdsByOrg.push(project.id) + projectIdsByOrg.push(parseInt(project.id)) } // get the project ids const groupProjectIds = [] if (R.prop('lagoon-projects', group.attributes)) { const groupProjects = R.prop('lagoon-projects', group.attributes).toString().split(',') - for (const project of groupProjects) { - groupProjectIds.push(project) + if (groupProjects.length > 0) { + for (const project of groupProjects) { + groupProjectIds.push(parseInt(project)) + } } } @@ -668,6 +807,23 @@ export const getGroupProjectOrganizationAssociation: ResolverFn = async ( } } + return group +} + +// check an existing group to see if it can be added to an organization +// this function will return errors if there are projects in the group that are not in the organization +// if there are no projects in the organization, and no projects in the group then it will succeed +// this is a helper function that is a WIP, not fully flushed out +export const getGroupProjectOrganizationAssociation: ResolverFn = async ( + _root, + { input }, + { models, sqlClientPool, hasPermission } +) => { + // platform admin only as it potentially reveals information about projects/orgs/groups + await hasPermission('organization', 'add'); + + await checkOrgProjectGroup(sqlClientPool, input, models) + return "success" }; @@ -688,42 +844,7 @@ export const addGroupToOrganization: ResolverFn = async ( throw new Error(`Organization does not exist`) } - // check the requested group exists - const group = await models.GroupModel.loadGroupByIdOrName(input); - if (group === undefined) { - throw new Error(`Group does not exist`) - } - - - // check the organization for projects currently attached to it - const projectsByOrg = await projectHelpers(sqlClientPool).getProjectByOrganizationId(input.organization); - const projectIdsByOrg = [] - for (const project of projectsByOrg) { - projectIdsByOrg.push(project.id) - } - - // get the project ids - const groupProjectIds = [] - if (R.prop('lagoon-projects', group.attributes)) { - const groupProjects = R.prop('lagoon-projects', group.attributes).toString().split(',') - for (const project of groupProjects) { - groupProjectIds.push(project) - } - } - - if (projectIdsByOrg.length > 0 && groupProjectIds.length > 0) { - if (projectIdsByOrg.length == 0) { - let filters = arrayDiff(groupProjectIds, projectIdsByOrg) - throw new Error(`This organization has no projects associated to it, the following projects that are not part of the requested organization: [${filters}]`) - } else { - if (groupProjectIds.length > 0) { - let filters = arrayDiff(groupProjectIds, projectIdsByOrg) - if (filters.length > 0) { - throw new Error(`This group has the following projects that are not part of the requested organization: [${filters}]`) - } - } - } - } + const group = await checkOrgProjectGroup(sqlClientPool, input, models) // update the group to be in the organization const updatedGroup = await models.GroupModel.updateGroup({ @@ -747,5 +868,78 @@ export const addGroupToOrganization: ResolverFn = async ( } }); - return "success" -}; \ No newline at end of file + return updatedGroup +}; + +// delete an organization, only if it has no projects, notifications, or groups +export const deleteOrganization: ResolverFn = async ( + _root, + { input }, + { sqlClientPool, hasPermission, userActivityLogger, models, keycloakGroups } +) => { + await hasPermission('organization', 'delete', { + organization: input.id + }); + + const rows = await query(sqlClientPool, Sql.selectOrganization(input.id)); + if (R.length(rows) == 0) { + throw new Error( + `Organization doesn't exist` + ); + } + const orgResult = rows[0]; + + const projects = await query( + sqlClientPool, Sql.selectOrganizationProjects(orgResult.id) + ); + + if (projects.length > 0) { + // throw error if there are any existing environments + throw new Error( + 'Unable to delete organization, there are existing projects that need to be removed first' + ); + } + + const notifications = await Helpers(sqlClientPool).getNotificationsForOrganizationId(orgResult.id) + if (notifications.length > 0) { + // throw error if there are any existing environments + throw new Error( + 'Unable to delete organization, there are existing notifications that need to be removed first' + ); + } + + const orgGroups = await models.GroupModel.loadGroupsByOrganizationIdFromGroups(orgResult.id, keycloakGroups); + if (orgGroups.length > 0) { + // throw error if there are any existing environments + throw new Error( + 'Unable to delete organization, there are existing groups that need to be removed first' + ); + } + + try { + await query( + sqlClientPool, + Sql.deleteOrganizationDeployTargets(orgResult.id) + ); + + await query( + sqlClientPool, + Sql.deleteOrganization(orgResult.id) + ); + } catch (err) { + throw new Error( + `Unable to delete organization` + ) + } + + userActivityLogger(`User deleted an organization '${orgResult.name}'`, { + project: '', + event: 'api:deleteOrganization', + payload: { + input: { + orgResult + } + } + }); + return 'success'; +}; diff --git a/services/api/src/resources/organization/sql.ts b/services/api/src/resources/organization/sql.ts index 22d3b91ced..273558fd62 100644 --- a/services/api/src/resources/organization/sql.ts +++ b/services/api/src/resources/organization/sql.ts @@ -86,40 +86,16 @@ export const Sql = { .where(knex.raw('organization.id = ?', id)) .andWhere('e.deleted', '0000-00-00 00:00:00') .toString(), - selectNotificationsByTypeByProjectId: (input) => { - const { - type, - pid, - oid, - contentType = 'deployment', - notificationSeverityThreshold = 0 - } = input; - let selectQuery = knex('project_notification AS pn').join('project', 'project.id', 'pn.pid').joinRaw( - `JOIN notification_${type} AS nt ON pn.nid = nt.id AND pn.type = :type AND pn.content_type = :contentType`, - { type, contentType } - ); - - return selectQuery - .where('pn.pid', '=', pid) - .where( - 'pn.notification_severity_threshold', - '>=', - notificationSeverityThreshold - ) - .andWhere('project.organization', '=', oid) - .andWhere('project.id', '=', pid) - .select( - 'nt.*', - 'pn.type', - ) - .toString(); - }, selectNotificationsByTypeByOrganizationId: (input) =>{ const { type, id, } = input; return knex(`notification_${type}`) + .select( + '*', + knex.raw(`'${type}' as type`) + ) .where('organization', '=', id) .toString(); }, @@ -133,5 +109,28 @@ export const Sql = { .select('dt.*') .join('organization_deploy_target as odt', 'dt.id', '=', 'odt.dtid') .where('odt.orgid', '=', id) - .toString() + .toString(), + selectDeployTargetsByOrganizationAndDeployTarget: (id: number, dtid: number) => + knex('openshift as dt') + .select('dt.*') + .join('organization_deploy_target as odt', 'dt.id', '=', 'odt.dtid') + .where('odt.orgid', '=', id) + .andWhere('odt.dtid', '=', dtid) + .toString(), + deleteOrganization: (id: number) => + knex('organization') + .where('id', '=', id) + .delete() + .toString(), + deleteOrganizationDeployTargets: (id: number) => + knex('organization_deploy_target') + .where('orgid', '=', id) + .delete() + .toString(), + removeDeployTarget: (orgid: number, dtid: number) => + knex('organization_deploy_target') + .where('orgid', '=', orgid) + .andWhere('dtid', '=', dtid) + .delete() + .toString(), }; diff --git a/services/api/src/resources/project/resolvers.ts b/services/api/src/resources/project/resolvers.ts index fb14df47e7..1806354238 100644 --- a/services/api/src/resources/project/resolvers.ts +++ b/services/api/src/resources/project/resolvers.ts @@ -17,6 +17,7 @@ import { generatePrivateKey, getSshKeyFingerprint } from '../sshKey'; import { Sql as sshKeySql } from '../sshKey/sql'; import { createHarborOperations } from './harborSetup'; import { Helpers as organizationHelpers } from '../organization/helpers'; +import { Helpers as notificationHelpers } from '../notification/helpers'; import { getUserProjectIdsFromRoleProjectIds } from '../../util/auth'; import GitUrlParse from 'git-url-parse'; @@ -567,11 +568,22 @@ export const deleteProject: ResolverFn = async ( ); } + try { + // remove all notifications from project + await notificationHelpers(sqlClientPool).removeAllNotificationsFromProject({project: pid}) + } catch (err) { + logger.error( + `Could not remove notifications from project ${project.name}: ${err.message}` + ); + } + await Helpers(sqlClientPool).deleteProjectById(pid); // Remove the project from all groups it is associated to try { const projectGroups = await models.GroupModel.loadGroupsByProjectIdFromGroups(pid, keycloakGroups); + // @TODO: use the new helper instead in the following for loop, once the `opendistrosecurityoperations` stuff goes away + // await models.GroupModel.removeProjectFromGroups(pid, projectGroups); for (const groupInput of projectGroups) { const group = await models.GroupModel.loadGroupByIdOrName(groupInput); await models.GroupModel.removeProjectFromGroup(project.id, group); diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 3a75d76af0..06e1bc47c8 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -484,6 +484,7 @@ const typeDefs = gql` groups: [GroupInterface] members: [GroupMembership] projects: [Project] + organization: Int } type Group implements GroupInterface { @@ -493,6 +494,7 @@ const typeDefs = gql` groups: [GroupInterface] members: [GroupMembership] projects: [Project] + organization: Int } type Openshift { @@ -1068,6 +1070,10 @@ const typeDefs = gql` quotaRoute: Int } + input DeleteOrganizationInput { + id: Int! + } + input UpdateOrganizationPatchInput { name: String friendlyName: String @@ -1883,11 +1889,21 @@ const typeDefs = gql` organization: Int! } + input RemoveProjectFromOrganizationInput { + project: Int! + organization: Int! + } + input AddDeployTargetToOrganizationInput { deployTarget: Int! organization: Int! } + input RemoveDeployTargetFromOrganizationInput { + deployTarget: Int! + organization: Int! + } + input UpdateOpenshiftPatchInput { name: String consoleUrl: String @@ -2337,9 +2353,6 @@ const typeDefs = gql` addUserToGroup(input: UserGroupRoleInput!): GroupInterface removeUserFromGroup(input: UserGroupInput!): GroupInterface addGroupsToProject(input: ProjectGroupsInput): Project - addGroupToOrganization(input: AddGroupInput!): String - addProjectToOrganization(input: AddProjectToOrganizationInput): Project - addDeployTargetToOrganization(input: AddDeployTargetToOrganizationInput): String removeGroupsFromProject(input: ProjectGroupsInput!): Project updateProjectMetadata(input: UpdateMetadataInput!): Project removeProjectMetadataByKey(input: RemoveMetadataInput!): Project @@ -2347,15 +2360,39 @@ const typeDefs = gql` updateDeployTargetConfig(input: UpdateDeployTargetConfigInput!): DeployTargetConfig @deprecated(reason: "Unstable API, subject to breaking changes in any release. Use at your own risk") deleteDeployTargetConfig(input: DeleteDeployTargetConfigInput!): String @deprecated(reason: "Unstable API, subject to breaking changes in any release. Use at your own risk") deleteAllDeployTargetConfigs: String @deprecated(reason: "Unstable API, subject to breaking changes in any release. Use at your own risk") + updateEnvironmentDeployTarget(environment: Int!, deployTarget: Int!): Environment """ Add an organization """ - addOrganization(input: AddOrganizationInput!): Organization @deprecated(reason: "Unstable API, subject to breaking changes in any release. Use at your own risk") + addOrganization(input: AddOrganizationInput!): Organization """ Update an organization """ - updateOrganization(input: UpdateOrganizationInput!): Organization @deprecated(reason: "Unstable API, subject to breaking changes in any release. Use at your own risk") - updateEnvironmentDeployTarget(environment: Int!, deployTarget: Int!): Environment + updateOrganization(input: UpdateOrganizationInput!): Organization + """ + Delete an organization + """ + deleteOrganization(input: DeleteOrganizationInput!): String + """ + Add a group to an organization + """ + addGroupToOrganization(input: AddGroupInput!): GroupInterface + """ + Add a project to an organization, will return an error if it can't easily do it + """ + addProjectToOrganization(input: AddProjectToOrganizationInput): Project + """ + Remove a project from an organization, this will return the project to a state where it has no groups or notifications associated to it + """ + removeProjectFromOrganization(input: RemoveProjectFromOrganizationInput): Project + """ + Add a deploytarget to an organization + """ + addDeployTargetToOrganization(input: AddDeployTargetToOrganizationInput): Organization + """ + Remove a deploytarget from an organization + """ + removeDeployTargetFromOrganization(input: RemoveDeployTargetFromOrganizationInput): Organization } type Subscription {