diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index 34b5033a8c..fc302740ad 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -81,10 +81,6 @@ interface GroupEdit { attributes?: object; } -interface AttributeFilterFn { - (attribute: { name: string; value: string[] }): boolean; -} - export class GroupExistsError extends Error { constructor(message: string) { super(message); @@ -102,25 +98,6 @@ export class GroupNotFoundError extends Error { // Group types that are for internal use only. const internalGroupTypes = [GroupType.ROLE_SUBGROUP]; -const attrLens = R.lensPath(['attributes']); -const lagoonProjectsLens = R.lensPath(['lagoon-projects']); - -const attrLagoonProjectsLens = R.compose( - // @ts-ignore - attrLens, - lagoonProjectsLens, - R.lensPath([0]) -); - -const getProjectIdsFromGroup = R.pipe( - // @ts-ignore - R.view(attrLagoonProjectsLens), - R.defaultTo(''), - R.split(','), - R.reject(R.isEmpty), - R.map(id => parseInt(id, 10)) -); - export const isRoleSubgroup = R.pathEq( ['attributes', 'type', 0], 'role-subgroup' @@ -156,7 +133,7 @@ export interface GroupModel { loadGroupByName: (name: string) => Promise loadGroupByIdOrName: (groupInput: GroupInput) => Promise loadParentGroup: (groupInput: Group) => Promise - createGroupFromKeycloak: (group: KeycloakLagoonGroup) => SparseGroup + createGroupFromKeycloak: (group: KeycloakLagoonGroup, projectIdsArray: number[], organizationAttr: number) => SparseGroup getProjectsFromGroupAndParents: (group: Group) => Promise getProjectsFromGroupAndSubgroups: (group: Group) => Promise getProjectsFromGroup: (group: Group) => Promise @@ -186,17 +163,17 @@ export const Group = (clients: { keycloakGroups: GroupRepresentation[] ): Promise => { // Map from keycloak object to group object - const groups = keycloakGroups.map( - (keycloakGroup: GroupRepresentation): Group => ({ + const groups = await Promise.all(keycloakGroups.map( + async (keycloakGroup: GroupRepresentation): Promise => ({ id: keycloakGroup.id, name: keycloakGroup.name, type: attributeKVOrNull('type', keycloakGroup), path: keycloakGroup.path, attributes: keycloakGroup.attributes, subGroups: keycloakGroup.subGroups, - organization: parseInt(attributeKVOrNull('lagoon-organization', keycloakGroup), 10) || null, // if it exists set it or null + organization: await groupHelpers(sqlClientPool).selectOrganizationIdByGroupId(keycloakGroup.id), // if it exists set it or null }) - ); + )); let groupsWithGroupsAndMembers = []; @@ -216,17 +193,8 @@ export const Group = (clients: { return groupsWithGroupsAndMembers; }; - const createGroupFromKeycloak = (group: KeycloakLagoonGroup): SparseGroup => { + const createGroupFromKeycloak = (group: KeycloakLagoonGroup, projectsArray: number[], organizationAttr: number): SparseGroup => { const groupAttr = group.attributes?.['type']?.[0]; - const projectsAttr = group.attributes?.['lagoon-projects']?.[0]; - const projectsArray = projectsAttr - ? projectsAttr - .split(',') - .map((id) => id.trim()) - .filter((id) => !!id) - .map(toNumber) - : []; - const organizationAttr = group.attributes?.['lagoon-organization']?.[0]; if (!group.id) { throw new Error('Missing group id'); @@ -239,7 +207,7 @@ export const Group = (clients: { return { id: group.id, name: group.name, - organization: organizationAttr ? toNumber(organizationAttr) : null, + organization: organizationAttr, parentGroupId: group.parentId ?? null, projects: new Set(projectsArray), subGroupCount: group.subGroupCount ?? 0, @@ -257,7 +225,11 @@ export const Group = (clients: { throw new GroupNotFoundError(`Group not found: ${id}`); } - return createGroupFromKeycloak(keycloakGroup); + // get the projectids from the group_project database table instead of group attributes + const projectIds = await groupHelpers(sqlClientPool).selectProjectIdsByGroupID(keycloakGroup.id); + const organizationId = await groupHelpers(sqlClientPool).selectOrganizationIdByGroupId(keycloakGroup.id); + + return createGroupFromKeycloak(keycloakGroup, projectIds, organizationId); }; /** @@ -324,7 +296,11 @@ export const Group = (clients: { ); } - return createGroupFromKeycloak(keycloakGroups[0]); + // get the projectids from the group_project database table instead of group attributes + const projectIds = await groupHelpers(sqlClientPool).selectProjectIdsByGroupID(keycloakGroups[0].id); + const organizationId = await groupHelpers(sqlClientPool).selectOrganizationIdByGroupId(keycloakGroups[0].id); + + return createGroupFromKeycloak(keycloakGroups[0], projectIds, organizationId); }; /** @@ -456,7 +432,11 @@ export const Group = (clients: { let subGroups: HieararchicalGroup[] = []; for (const keycloakGroup of keycloakGroups) { - const subGroup = createGroupFromKeycloak(keycloakGroup); + // get the projectids from the group_project database table instead of group attributes + const projectIds = await groupHelpers(sqlClientPool).selectProjectIdsByGroupID(keycloakGroup.id); + const organizationId = await groupHelpers(sqlClientPool).selectOrganizationIdByGroupId(keycloakGroup.id); + + const subGroup = createGroupFromKeycloak(keycloakGroup, projectIds, organizationId); const groupWithSubgroups = await loadSubGroups(subGroup, depth ? depth -1 : null); subGroups.push({ @@ -526,7 +506,8 @@ export const Group = (clients: { const getProjectsFromGroupAndParents = async ( group: Group ): Promise => { - const projectIds = getProjectIdsFromGroup(group); + // get the projectids from the group_project database table instead of group attributes + const projectIds = await groupHelpers(sqlClientPool).selectProjectIdsByGroupID(group.id); const parentGroup = await loadParentGroup(group); const parentProjectIds = parentGroup @@ -545,7 +526,8 @@ export const Group = (clients: { group: Group ): Promise => { try { - const groupProjectIds = getProjectIdsFromGroup(group); + // get the projectids from the group_project database table instead of group attributes + const groupProjectIds = await groupHelpers(sqlClientPool).selectProjectIdsByGroupID(group.id); let subGroupProjectIds = []; // @TODO: check is `groups.groups` ever used? it always appears to be empty @@ -577,7 +559,8 @@ export const Group = (clients: { group: Group ): Promise => { try { - const groupProjectIds = getProjectIdsFromGroup(group); + // get the projectids from the group_project database table instead of group attributes + const groupProjectIds = await groupHelpers(sqlClientPool).selectProjectIdsByGroupID(group.id); // remove deleted projects from the result to prevent null errors in user queries const existingProjects = await projectHelpers(sqlClientPool).getAllProjectsIn(groupProjectIds); let existingProjectsIds = []; @@ -913,10 +896,10 @@ export const Group = (clients: { if (group.type) { attributes.type = [group.type]; } + // lagoon-organization and lagoon-projects/project-ids attributes are added for legacy reasons only, theses values are stored in the api-db now if (group.organization) { attributes['lagoon-organization'] = [group.organization.toString()]; } - attributes['lagoon-projects'] = [Array.from(group.projects).join(',')]; attributes['group-lagoon-project-ids'] = [ `{${JSON.stringify(group.name)}:[${Array.from(group.projects).join(',')}]}`, @@ -957,10 +940,10 @@ export const Group = (clients: { if (group.type) { attributes.type = [group.type]; } + // lagoon-organization and lagoon-projects/project-ids attributes are added for legacy reasons only, theses values are stored in the api-db now if (group.organization) { attributes['lagoon-organization'] = [group.organization.toString()]; } - attributes['lagoon-projects'] = [Array.from(group.projects).join(',')]; attributes['group-lagoon-project-ids'] = [ `{${JSON.stringify(group.name)}:[${Array.from(group.projects).join(',')}]}`, diff --git a/services/api/src/models/user.ts b/services/api/src/models/user.ts index 74f30d9d0a..a0b21b296f 100644 --- a/services/api/src/models/user.ts +++ b/services/api/src/models/user.ts @@ -7,6 +7,7 @@ import { toNumber } from '../util/func'; import { Group, GroupType, KeycloakLagoonGroup } from './group'; import { Sql } from '../resources/user/sql'; import { getConfigFromEnv } from '../util/config'; +import { Helpers as groupHelpers } from '../resources/group/helpers'; interface IUserAttributes { comment?: [string]; @@ -356,9 +357,12 @@ export const User = (clients: { id: userId, briefRepresentation: false, })) as KeycloakLagoonGroup[]; - const roleSubgroups = keycloakGroups.map( - GroupModel.createGroupFromKeycloak, - ); + let roleSubgroups = []; + for (const keycloakGroup of keycloakGroups){ + const projectIds = await groupHelpers(sqlClientPool).selectProjectIdsByGroupID(keycloakGroup.id); + const organizationId = await groupHelpers(sqlClientPool).selectOrganizationIdByGroupId(keycloakGroup.id); + roleSubgroups.push(GroupModel.createGroupFromKeycloak(keycloakGroup, projectIds, organizationId)) + } let userGroups: { [key: string]: Group; @@ -370,7 +374,7 @@ export const User = (clients: { if (!userGroups[ug.parentGroupId]) { const parentGroup = await GroupModel.loadGroupById(ug.parentGroupId); - const parentOrg = parentGroup.attributes?.['lagoon-organization']?.[0]; + const parentOrg = await groupHelpers(sqlClientPool).selectOrganizationIdByGroupId(parentGroup.id); if (organization && parentOrg && toNumber(parentOrg) != organization) { continue; } diff --git a/services/api/src/resources/group/helpers.ts b/services/api/src/resources/group/helpers.ts index 0307a75383..036367f2b7 100644 --- a/services/api/src/resources/group/helpers.ts +++ b/services/api/src/resources/group/helpers.ts @@ -7,24 +7,24 @@ import { logger } from '../../loggers/logger'; export const Helpers = (sqlClientPool: Pool) => { return { - selectProjectIdsByGroupIDs: async (groupIds: string[]) => { + selectProjectIdsByGroupIDs: async (groupIds: string[]): Promise => { const projectIdsArray = await query( sqlClientPool, Sql.selectProjectIdsByGroupIDs(groupIds) ) if (projectIdsArray[0]["projectIds"] != null) { - const values = projectIdsArray[0]["projectIds"].split(','); + const values = projectIdsArray[0]["projectIds"].split(',').map(Number); return values } return [] }, - selectProjectIdsByGroupID: async (groupId: string) => { + selectProjectIdsByGroupID: async (groupId: string): Promise => { const projectIdsArray = await query( sqlClientPool, Sql.selectProjectIdsByGroupID(groupId) ) if (projectIdsArray[0]["projectIds"] != null) { - const values = projectIdsArray[0]["projectIds"].split(','); + const values = projectIdsArray[0]["projectIds"].split(',').map(Number); return values } return [] @@ -68,10 +68,24 @@ export const Helpers = (sqlClientPool: Pool) => { return [] }, selectOrganizationByGroupId: async (groupId: string) => { - return await query( + const organization = await query( sqlClientPool, Sql.selectOrganizationByGroupId(groupId) ) + if (organization[0] != null) { + return organization[0] + } + return null + }, + selectOrganizationIdByGroupId: async (groupId: string) => { + const organization = await query( + sqlClientPool, + Sql.selectOrganizationByGroupId(groupId) + ) + if (organization[0] != null) { + return organization[0].organizationId + } + return null }, removeProjectFromGroup: async (projectId: number, groupId: string) => { await query( @@ -99,6 +113,17 @@ export const Helpers = (sqlClientPool: Pool) => { return null } }, + removeGroupFromOrganization: async (groupId: string) => { + try { + // delete the reference for the group from the group_organization table + await query( + sqlClientPool, + Sql.deleteOrganizationGroup(groupId) + ); + } catch (err) { + return null + } + }, deleteGroup: async (groupId: string) => { await query( sqlClientPool, diff --git a/services/api/src/resources/group/resolvers.ts b/services/api/src/resources/group/resolvers.ts index 47c96cf163..41f17b3380 100644 --- a/services/api/src/resources/group/resolvers.ts +++ b/services/api/src/resources/group/resolvers.ts @@ -108,8 +108,9 @@ export const getGroupRolesByUserId: ResolverFn =async ( if (queryUserGroups[g].attributes["type"]) { group.groupType = queryUserGroups[g].attributes["type"][0] } - if (queryUserGroups[g].attributes["lagoon-organization"]) { - group.organization = queryUserGroups[g].attributes["lagoon-organization"] + const org = await Helpers(sqlClientPool).selectOrganizationIdByGroupId(group.id); + if (org) { + group.organization = org } groups.push(group) } @@ -128,8 +129,9 @@ export const getGroupRolesByUserId: ResolverFn =async ( if (keycloakUsersGroups[g].attributes["type"]) { group.groupType = keycloakUsersGroups[g].attributes["type"][0] } - if (keycloakUsersGroups[g].attributes["lagoon-organization"]) { - group.organization = keycloakUsersGroups[g].attributes["lagoon-organization"] + const org = await Helpers(sqlClientPool).selectOrganizationIdByGroupId(keycloakUsersGroups[g].id); + if (org) { + group.organization = org } groups.push(group) } @@ -349,6 +351,7 @@ export const addGroup: ResolverFn = async ( attributes = { attributes: { + // lagoon-organization attribute is added for legacy reasons only, theses values are stored in the api-db now "lagoon-organization": [input.organization] } } @@ -431,11 +434,11 @@ export const updateGroup: ResolverFn = async ( { models, hasPermission, userActivityLogger } ) => { const group = await models.GroupModel.loadGroupByIdOrName(groupInput); - - if (R.prop('lagoon-organization', group.attributes)) { + const org = await Helpers(sqlClientPool).selectOrganizationIdByGroupId(group.id); + if (org) { // if this is a group in an organization, check that the user updating it has permission to do so before deleting the group await hasPermission('organization', 'addGroup', { - organization: R.prop('lagoon-organization', group.attributes) + organization: org }); } else { await hasPermission('group', 'update', { @@ -480,11 +483,11 @@ export const deleteGroup: ResolverFn = async ( { models, sqlClientPool, hasPermission, userActivityLogger } ) => { const group = await models.GroupModel.loadGroupByIdOrName(groupInput); - - if (R.prop('lagoon-organization', group.attributes)) { + const org = await Helpers(sqlClientPool).selectOrganizationIdByGroupId(group.id); + if (org) { // if this is a group in an organization, check that the user deleting it has permission to do so before deleting the group await hasPermission('organization', 'removeGroup', { - organization: R.prop('lagoon-organization', group.attributes) + organization: org }); } else { await hasPermission('group', 'delete', { @@ -542,11 +545,11 @@ export const addUserToGroup: ResolverFn = async ( } } - - if (R.prop('lagoon-organization', group.attributes)) { + const org = await Helpers(sqlClientPool).selectOrganizationIdByGroupId(group.id); + if (org) { // if this is a group in an organization, check that the user adding members to the group in this org is in the org await hasPermission('organization', 'addGroup', { - organization: R.prop('lagoon-organization', group.attributes) + organization: org }); // only organization:addGroup will be able to "invite users" this way if (createUserErr && createUserErr.message.includes("User not found") && inviteUser) { @@ -622,11 +625,11 @@ export const removeUserFromGroup: ResolverFn = async ( } const group = await models.GroupModel.loadGroupByIdOrName(groupInput); - - if (R.prop('lagoon-organization', group.attributes)) { + const org = await Helpers(sqlClientPool).selectOrganizationIdByGroupId(group.id); + if (org) { // if this is a group in an organization, check that the user removing members from the group in this org is in the org await hasPermission('organization', 'addGroup', { - organization: R.prop('lagoon-organization', group.attributes) + organization: org }); } else { await hasPermission('group', 'removeUser', { @@ -681,14 +684,15 @@ export const addGroupsToProject: ResolverFn = async ( for (const groupInput of groupsInput) { const group = await models.GroupModel.loadGroupByIdOrName(groupInput); - if (R.prop('lagoon-organization', group.attributes) === undefined && project.organization != null) { + const org = await Helpers(sqlClientPool).selectOrganizationIdByGroupId(group.id); + if (org == null && project.organization != null) { throw new Error('Group must be in same organization as the project'); } - if (R.prop('lagoon-organization', group.attributes) && project.organization != null) { - if (project.organization == R.prop('lagoon-organization', group.attributes)) { + if (org && project.organization != null) { + if (project.organization == org) { // if this is a group in an organization, check that the user removing members from the group in this org is in the org await hasPermission('organization', 'addGroup', { - organization: R.prop('lagoon-organization', group.attributes) + organization: org }); } else { throw new Error('Group must be in same organization as the project'); diff --git a/services/api/src/resources/organization/resolvers.ts b/services/api/src/resources/organization/resolvers.ts index 7eeeced9a4..52f3770145 100644 --- a/services/api/src/resources/organization/resolvers.ts +++ b/services/api/src/resources/organization/resolvers.ts @@ -435,17 +435,17 @@ export const getUserByEmailAndOrganizationId: ResolverFn = async ( user.owner = false user.admin = false user.comment = null - if (user.attributes["comment"]) { + if (user.attributes?.["comment"]) { user.comment = user.attributes["comment"][0] } - if (user.attributes["lagoon-organizations"]) { + if (user.attributes?.["lagoon-organizations"]) { for (const a in user.attributes["lagoon-organizations"]) { if (parseInt(user.attributes["lagoon-organizations"][a]) == organization) { user.owner = true } } } - if (user.attributes["lagoon-organizations-admin"]) { + if (user.attributes?.["lagoon-organizations-admin"]) { for (const a in user.attributes["lagoon-organizations-admin"]) { if (parseInt(user.attributes["lagoon-organizations-admin"][a]) == organization) { user.admin = true @@ -467,18 +467,19 @@ export const getUserByEmailAndOrganizationId: ResolverFn = async ( export const getGroupRolesByUserIdAndOrganization: ResolverFn =async ( { id: uid, organization }, _input, - { hasPermission, models, adminScopes } + { models, sqlClientPool } ) => { if (organization) { const queryUserGroups = await models.UserModel.getAllGroupsForUser(uid, organization); let groups = [] for (const g in queryUserGroups) { let group = {id: queryUserGroups[g].id, name: queryUserGroups[g].name, role: queryUserGroups[g].subGroups[0].realmRoles[0], groupType: null, organization: null} - if (queryUserGroups[g].attributes["type"]) { + if (queryUserGroups[g].attributes?.["type"]) { group.groupType = queryUserGroups[g].attributes["type"][0] } - if (queryUserGroups[g].attributes["lagoon-organization"]) { - group.organization = queryUserGroups[g].attributes["lagoon-organization"][0] + const org = await groupHelpers(sqlClientPool).selectOrganizationIdByGroupId(queryUserGroups[g].id) + if (org) { + group.organization = org } groups.push(group) } @@ -492,7 +493,7 @@ export const getGroupRolesByUserIdAndOrganization: ResolverFn =async ( export const getGroupsByNameAndOrganizationId: ResolverFn = async ( root, { name, organization }, - { hasPermission, models, keycloakGrant } + { hasPermission, models, sqlClientPool } ) => { try { await hasPermission('organization', 'viewGroup', { @@ -500,8 +501,8 @@ export const getGroupsByNameAndOrganizationId: ResolverFn = async ( }); const group = await models.GroupModel.loadGroupByName(name); - const groupOrg = group.attributes?.['lagoon-organization']?.[0]; - if (groupOrg && toNumber(groupOrg) == organization) { + const org = await groupHelpers(sqlClientPool).selectOrganizationIdByGroupId(group.id) + if (org == organization) { return group } } catch (err) { @@ -543,19 +544,17 @@ const checkProjectGroupAssociation = async (oid, projectGroups, projectGroupName // get all the groups the requested project is in for (const group of projectGroups) { // for each group the project is in, check if it has an organization - if (R.prop('lagoon-organization', group.attributes)) { + + const groupOrganization = await groupHelpers(sqlClientPool).selectOrganizationIdByGroupId(group.id); // if it has an organization that is not the requested organization, add it to a list - if (R.prop('lagoon-organization', group.attributes) != oid) { - projectGroupNames.push({group: group.name, organization: R.prop('lagoon-organization', group.attributes).toString()}) - otherOrgs.push(R.prop('lagoon-organization', group.attributes).toString()) - } + if (groupOrganization != null && groupOrganization != oid) { + projectGroupNames.push({group: group.name, organization: groupOrganization.toString()}) + otherOrgs.push(groupOrganization.toString()) } // for each group the project is in, get the list of projects that are also in this group - if (R.prop('lagoon-projects', group.attributes)) { - const groupProjects = R.prop('lagoon-projects', group.attributes).toString().split(',') - for (const project of groupProjects) { - groupProjectIds.push({group: group.name, project: project}) - } + const groupProjects = await groupHelpers(sqlClientPool).selectProjectIdsByGroupID(group.id); + for (const project of groupProjects) { + groupProjectIds.push({group: group.name, project: project}) } } @@ -649,9 +648,13 @@ export const removeProjectFromOrganization: ResolverFn = async ( name: projectGroups[g].name, attributes: { ...projectGroups[g].attributes, + // lagoon-organization attribute is removed for legacy reasons only, theses values are stored in the api-db now "lagoon-organization": [""] } }); + // remove the default project group from the group_organization association table + // when the project is removed from the organization + await groupHelpers(sqlClientPool).removeGroupFromOrganization(projectGroups[g].id) } else { removeGroups.push(projectGroups[g]) } @@ -735,9 +738,13 @@ export const addExistingProjectToOrganization: ResolverFn = async ( name: projectGroups[g].name, attributes: { ...projectGroups[g].attributes, + // lagoon-organization attribute is removed for legacy reasons only, theses values are stored in the api-db now "lagoon-organization": [""] } }); + // remove the default project group from the group_organization association table + // when the project is about to be added to the new organization + await groupHelpers(sqlClientPool).removeGroupFromOrganization(projectGroups[g].id) } else { removeGroups.push(projectGroups[g]) } @@ -768,6 +775,7 @@ export const addExistingProjectToOrganization: ResolverFn = async ( name: group.name, attributes: { ...group.attributes, + // lagoon-organization attribute is added for legacy reasons only, theses values are stored in the api-db now "lagoon-organization": [input.organization] } }); @@ -839,12 +847,10 @@ const checkOrgProjectGroup = async (sqlClientPool, input, models) => { // get the project ids const groupProjectIds = [] - if (R.prop('lagoon-projects', group.attributes)) { - const groupProjects = R.prop('lagoon-projects', group.attributes).toString().split(',') - if (groupProjects.length > 0) { - for (const project of groupProjects) { - groupProjectIds.push(parseInt(project)) - } + const groupProjects = await groupHelpers(sqlClientPool).selectProjectIdsByGroupID(group.id); + if (groupProjects.length > 0) { + for (const project of groupProjects) { + groupProjectIds.push(project) } } @@ -907,9 +913,11 @@ export const addExistingGroupToOrganization: ResolverFn = async ( name: group.name, attributes: { ...group.attributes, + // lagoon-organization attribute is added for legacy reasons only, theses values are stored in the api-db now "lagoon-organization": [input.organization] } }); + await groupHelpers(sqlClientPool).addOrganizationToGroup(input.organization, group.id) // log this activity userActivityLogger(`User added a group to organization`, { @@ -958,7 +966,8 @@ export const removeUserFromOrganizationGroups: ResolverFn = async ( let groupsRemoved = [] for (const group in orgGroups) { // if the groups organization is the one to remove from, push it to a new array - if (R.prop('lagoon-organization', orgGroups[group].attributes) == organizationInput) { + const org = await groupHelpers(sqlClientPool).selectOrganizationIdByGroupId(orgGroups[group].id) + if (org == organizationInput) { groupsRemoved.push(orgGroups[group]); } } @@ -1218,6 +1227,7 @@ export const bulkImportProjectsAndGroupsToOrganization: ResolverFn = async ( name: group.name, attributes: { ...group.attributes, + // lagoon-organization attribute is added for legacy reasons only, theses values are stored in the api-db now "lagoon-organization": [input.organization] } }); diff --git a/services/api/src/resources/project/resolvers.ts b/services/api/src/resources/project/resolvers.ts index b4217a1498..33e7fad500 100644 --- a/services/api/src/resources/project/resolvers.ts +++ b/services/api/src/resources/project/resolvers.ts @@ -394,11 +394,13 @@ export const addProject = async ( let group; let attributes = { type: ['project-default-group'], + // lagoon-projects and group-lagoon-project-ids attributes are added for legacy reasons only, theses values are stored in the api-db now 'lagoon-projects': [project.id], 'group-lagoon-project-ids': [`{${JSON.stringify(`project-${project.name}`)}:[${project.id}]}`] }; // add the organization attribute if this exists if (input.organization != null) { + // lagoon-organization attribute is added for legacy reasons only, theses values are stored in the api-db now attributes['lagoon-organization'] = [input.organization]; } try {