Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use api-db for project and org group attributes instead of keycloak #3827

Merged
merged 1 commit into from
Oct 22, 2024
Merged
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
77 changes: 30 additions & 47 deletions services/api/src/models/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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'
Expand Down Expand Up @@ -156,7 +133,7 @@ export interface GroupModel {
loadGroupByName: (name: string) => Promise<Group>
loadGroupByIdOrName: (groupInput: GroupInput) => Promise<Group>
loadParentGroup: (groupInput: Group) => Promise<Group>
createGroupFromKeycloak: (group: KeycloakLagoonGroup) => SparseGroup
createGroupFromKeycloak: (group: KeycloakLagoonGroup, projectIdsArray: number[], organizationAttr: number) => SparseGroup
getProjectsFromGroupAndParents: (group: Group) => Promise<number[]>
getProjectsFromGroupAndSubgroups: (group: Group) => Promise<number[]>
getProjectsFromGroup: (group: Group) => Promise<number[]>
Expand Down Expand Up @@ -186,17 +163,17 @@ export const Group = (clients: {
keycloakGroups: GroupRepresentation[]
): Promise<Group[]> => {
// 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<Group> => ({
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 = [];

Expand All @@ -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');
Expand All @@ -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,
Expand All @@ -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);
};

/**
Expand Down Expand Up @@ -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);
};

/**
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -526,7 +506,8 @@ export const Group = (clients: {
const getProjectsFromGroupAndParents = async (
group: Group
): Promise<number[]> => {
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
Expand All @@ -545,7 +526,8 @@ export const Group = (clients: {
group: Group
): Promise<number[]> => {
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
Expand Down Expand Up @@ -577,7 +559,8 @@ export const Group = (clients: {
group: Group
): Promise<number[]> => {
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 = [];
Expand Down Expand Up @@ -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(',')}]}`,
Expand Down Expand Up @@ -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(',')}]}`,
Expand Down
12 changes: 8 additions & 4 deletions services/api/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
35 changes: 30 additions & 5 deletions services/api/src/resources/group/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@ import { logger } from '../../loggers/logger';

export const Helpers = (sqlClientPool: Pool) => {
return {
selectProjectIdsByGroupIDs: async (groupIds: string[]) => {
selectProjectIdsByGroupIDs: async (groupIds: string[]): Promise<number[]> => {
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<number[]> => {
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 []
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
Loading