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

Add group listing API queries/resolvers #1228

Merged
merged 5 commits into from
Sep 7, 2019
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
7 changes: 3 additions & 4 deletions services/api/src/helpers/sync-groups-searchguard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,10 @@ const refreshToken = async keycloakAdminClient => {

for (const group of groups) {
await refreshToken(keycloakAdminClient);
const loadedGroup = await GroupModel.loadGroupById(group.id);
logger.debug(`Processing ${loadedGroup.name}`);
const projectIdsArray = await GroupModel.getProjectsFromGroupAndSubgroups(loadedGroup)
logger.debug(`Processing ${group.name}`);
const projectIdsArray = await GroupModel.getProjectsFromGroupAndSubgroups(group)
const projectIds = R.join(',')(projectIdsArray)
await SearchguardOperations(sqlClient, GroupModel).syncGroup(loadedGroup.name, projectIds);
await SearchguardOperations(sqlClient, GroupModel).syncGroup(group.name, projectIds);
}

logger.info('Migration completed');
Expand Down
54 changes: 53 additions & 1 deletion services/api/src/models/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,18 @@ interface GroupEdit {
attributes?: object;
}

interface AttributeFilterFn {
(attribute: { name: string, value: string[] }, group: Group): boolean;
}

interface GroupModel {
loadAllGroups: () => Promise<Group[]>;
loadGroupById: (id: string) => Promise<Group>;
loadGroupByName: (name: string) => Promise<Group>;
loadGroupByIdOrName: (groupInput: GroupEdit) => Promise<Group>;
loadParentGroup: (groupInput: Group) => Promise<Group>;
loadGroupsByAttribute: (filterFn: AttributeFilterFn) => Promise<Group[]>;
loadGroupsByProjectId: (projectId: number) => Promise<Group[]>;
getProjectsFromGroupAndParents: (group: Group) => Promise<number[]>,
getProjectsFromGroupAndSubgroups: (group: Group) => Promise<number[]>,
addGroup: (groupInput: Group) => Promise<Group>;
Expand Down Expand Up @@ -160,7 +166,17 @@ const loadGroupByIdOrName = async (groupInput: GroupEdit): Promise<Group> => {
const loadAllGroups = async (): Promise<Group[]> => {
const keycloakGroups = await keycloakAdminClient.groups.find();

const groups = await transformKeycloakGroups(keycloakGroups);
let fullGroups: Group[] = [];
for (const group of keycloakGroups) {
const fullGroup = await loadGroupById(group.id);

fullGroups = [
...fullGroups,
fullGroup,
];
}

const groups = await transformKeycloakGroups(fullGroups);

return groups;
};
Expand All @@ -175,6 +191,40 @@ const loadParentGroup = async (groupInput: Group): Promise<Group> => asyncPipe(
]),
)(groupInput);

const loadGroupsByAttribute = async (filterFn: AttributeFilterFn): Promise<Group[]> => {
const allGroups = await loadAllGroups();

const filteredGroups = R.filter((group: Group) => R.pipe(
R.toPairs,
R.reduce((isMatch: boolean, attribute: [string, string[]]): boolean => {
if (!isMatch) {
return filterFn({
name: attribute[0],
value: attribute[1],
}, group);
}

return isMatch;
}, false),
)(group.attributes)
)(allGroups);

return filteredGroups;
};

const loadGroupsByProjectId = async (projectId: number): Promise<Group[]> => {
const filterFn = (attribute) => {
if (attribute.name === 'lagoon-projects') {
const value = R.is(Array, attribute.value) ? R.path(['value', 0], attribute) : attribute.value;
return R.test(new RegExp(`\\b${projectId}\\b`), value);
}

return false;
}

return loadGroupsByAttribute(filterFn);
}

// Recursive function to load projects "up" the group chain
const getProjectsFromGroupAndParents = async (
group: Group,
Expand Down Expand Up @@ -494,6 +544,8 @@ export const Group = (): GroupModel => ({
loadGroupByName,
loadGroupByIdOrName,
loadParentGroup,
loadGroupsByAttribute,
loadGroupsByProjectId,
getProjectsFromGroupAndParents,
getProjectsFromGroupAndSubgroups,
addGroup,
Expand Down
42 changes: 22 additions & 20 deletions services/api/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface UserModel {
loadUserById: (id: string) => Promise<User>;
loadUserByUsername: (username: string) => Promise<User>;
loadUserByIdOrUsername: (userInput: UserEdit) => Promise<User>;
getAllGroupsForUser: (userInput: User) => Promise<Group[]>;
getAllProjectsIdsForUser: (userInput: User) => Promise<number[]>;
getUserRolesForProject: (
userInput: User,
Expand Down Expand Up @@ -201,11 +202,9 @@ const loadAllUsers = async (): Promise<User[]> => {
return users;
};



const getAllProjectsIdsForUser = async (userInput: User): Promise<number[]> => {
const getAllGroupsForUser = async (userInput: User): Promise<Group[]> => {
const GroupModel = Group();
let projects = [];
let groups = [];

const roleSubgroups = await keycloakAdminClient.users.listGroups({
id: userInput.id,
Expand All @@ -221,7 +220,20 @@ const getAllProjectsIdsForUser = async (userInput: User): Promise<number[]> => {
fullRoleSubgroup,
);

const projectIds = await GroupModel.getProjectsFromGroupAndSubgroups(roleSubgroupParent);
groups.push(roleSubgroupParent);
}

return groups;
}

const getAllProjectsIdsForUser = async (userInput: User): Promise<number[]> => {
const GroupModel = Group();
let projects = [];

const userGroups = await getAllGroupsForUser(userInput);

for (const group of userGroups) {
const projectIds = await GroupModel.getProjectsFromGroupAndSubgroups(group);
projects = [...projects, ...projectIds];
}

Expand All @@ -234,30 +246,19 @@ const getUserRolesForProject = async (
): Promise<string[]> => {
const GroupModel = Group();

const roleSubgroups = await keycloakAdminClient.users.listGroups({
id: userInput.id,
});
const userGroups = await getAllGroupsForUser(userInput);

let roles = [];
for (const roleSubgroup of roleSubgroups) {
const fullRoleSubgroup = await GroupModel.loadGroupById(roleSubgroup.id);
if (!isRoleSubgroup(fullRoleSubgroup)) {
continue;
}

const roleSubgroupParent = await GroupModel.loadParentGroup(
fullRoleSubgroup,
);

const projectIds = await GroupModel.getProjectsFromGroupAndSubgroups(roleSubgroupParent);
for (const group of userGroups) {
const projectIds = await GroupModel.getProjectsFromGroupAndSubgroups(group);

if (projectIds.includes(projectId)) {
const groupRoles = R.pipe(
R.filter(membership =>
R.pathEq(['user', 'id'], userInput.id, membership),
),
R.pluck('role'),
)(roleSubgroupParent.members);
)(group.members);

roles = [...roles, ...groupRoles];
}
Expand Down Expand Up @@ -354,6 +355,7 @@ export const User = (): UserModel => ({
loadUserById,
loadUserByUsername,
loadUserByIdOrUsername,
getAllGroupsForUser,
getAllProjectsIdsForUser,
getUserRolesForProject,
addUser,
Expand Down
6 changes: 6 additions & 0 deletions services/api/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ const {
} = require('./resources/user/resolvers');

const {
getAllGroups,
getGroupsByProjectId,
getGroupsByUserId,
addGroup,
updateGroup,
deleteGroup,
Expand Down Expand Up @@ -156,6 +159,7 @@ const resolvers /* : { [string]: ResolversObj | typeof GraphQLDate } */ = {
openshift: getOpenshiftByProjectId,
environments: getEnvironmentsByProjectId,
envVariables: getEnvVarsByProjectId,
groups: getGroupsByProjectId,
},
Environment: {
project: getProjectByEnvironmentId,
Expand Down Expand Up @@ -191,6 +195,7 @@ const resolvers /* : { [string]: ResolversObj | typeof GraphQLDate } */ = {
},
User: {
sshKeys: getUserSshKeys,
groups: getGroupsByUserId,
},
Backup: {
restore: getRestoreByBackupId,
Expand All @@ -207,6 +212,7 @@ const resolvers /* : { [string]: ResolversObj | typeof GraphQLDate } */ = {
allProjects: getAllProjects,
allOpenshifts: getAllOpenshifts,
allEnvironments: getAllEnvironments,
allGroups: getAllGroups,
},
Mutation: {
addOrUpdateEnvironment,
Expand Down
96 changes: 96 additions & 0 deletions services/api/src/resources/group/resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,105 @@
import * as R from 'ramda';
import validator from 'validator';
import * as logger from '../../logger';
import { isPatchEmpty } from '../../util/db';
import { GroupNotFoundError } from '../../models/group';
import * as projectHelpers from '../project/helpers';
import { SearchguardOperations } from './searchguard';

export const getAllGroups = async (
root,
{ name },
{ hasPermission, dataSources, keycloakGrant },
) => {
try {
await hasPermission('group', 'viewAll');

if (name) {
const group = await dataSources.GroupModel.loadGroupByName(name);
return [group];
} else {
return await dataSources.GroupModel.loadAllGroups();
}
} catch (err) {
if (err instanceof GroupNotFoundError) {
return [];
}

if (!keycloakGrant) {
logger.warn('No grant available for getAllGroups');
return [];
}

const user = await dataSources.UserModel.loadUserById(
keycloakGrant.access_token.content.sub,
);
const userGroups = await dataSources.UserModel.getAllGroupsForUser(user);

if (name) {
return R.filter(R.propEq('name', name), userGroups);
} else {
return userGroups;
}
}
};

export const getGroupsByProjectId = async (
{ id: pid },
_input,
{ hasPermission, dataSources, keycloakGrant },
) => {
const projectGroups = await dataSources.GroupModel.loadGroupsByProjectId(pid);

try {
await hasPermission('group', 'viewAll');

return projectGroups;
} catch (err) {
if (!keycloakGrant) {
logger.warn('No grant available for getGroupsByProjectId');
return [];
}

const user = await dataSources.UserModel.loadUserById(
keycloakGrant.access_token.content.sub,
);
const userGroups = await dataSources.UserModel.getAllGroupsForUser(user);
const userProjectGroups = R.intersection(projectGroups, userGroups);

return userProjectGroups;
}
};

export const getGroupsByUserId = async (
{ id: uid },
_input,
{ hasPermission, dataSources, keycloakGrant },
) => {
const queryUser = await dataSources.UserModel.loadUserById(
uid,
);
const queryUserGroups = await dataSources.UserModel.getAllGroupsForUser(queryUser);

try {
await hasPermission('group', 'viewAll');

return queryUserGroups;
} catch (err) {
if (!keycloakGrant) {
logger.warn('No grant available for getGroupsByUserId');
return [];
}

const currentUser = await dataSources.UserModel.loadUserById(
keycloakGrant.access_token.content.sub,
);
const currentUserGroups = await dataSources.UserModel.getAllGroupsForUser(currentUser);
const bothUserGroups = R.intersection(queryUserGroups, currentUserGroups);

return bothUserGroups;
}
};

export const addGroup = async (_root, { input }, { dataSources, sqlClient, hasPermission }) => {
await hasPermission('group', 'add');

Expand Down
9 changes: 9 additions & 0 deletions services/api/src/typeDefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ const typeDefs = gql`
comment: String
gitlabId: Int
sshKeys: [SshKey]
groups: [Group]
}

type GroupMembership {
Expand Down Expand Up @@ -272,6 +273,10 @@ const typeDefs = gql`
Environment variables available during build-time and run-time
"""
envVariables: [EnvKeyValue]
"""
Which groups are directly linked to project
"""
groups: [Group]
}

"""
Expand Down Expand Up @@ -481,6 +486,10 @@ const typeDefs = gql`
Returns all Environments matching given filter (all if no filter defined)
"""
allEnvironments(createdAfter: String, type: EnvType, order: EnvOrderType): [Environment]
"""
Returns all Groups matching given filter (all if no filter defined)
"""
allGroups(name: String): [Group]
}

# Must provide id OR name
Expand Down
Loading