diff --git a/services/api/src/apolloServer.js b/services/api/src/apolloServer.js index 7b451f6195..edafed1fff 100644 --- a/services/api/src/apolloServer.js +++ b/services/api/src/apolloServer.js @@ -25,6 +25,7 @@ const { userActivityLogger } = require('./loggers/userActivityLogger'); const typeDefs = require('./typeDefs'); const resolvers = require('./resolvers'); const { keycloakGrantManager } = require('./clients/keycloakClient'); +const { getRedisKeycloakCache, saveRedisKeycloakCache } = require('./clients/redisClient'); const User = require('./models/user'); const Group = require('./models/group'); @@ -171,12 +172,30 @@ const apolloServer = new ApolloServer({ // get all keycloak groups, do this early to reduce the number of times this is called otherwise // but doing this early and once is pretty cheap - let allGroups = await Group.Group(modelClients).loadAllGroups(); - let keycloakGroups = await Group.Group(modelClients).transformKeycloakGroups(allGroups); + let keycloakGroups = [] + try { + // check redis for the allgroups cache value + const data = await getRedisKeycloakCache("allgroups"); + let buff = new Buffer(data, 'base64'); + keycloakGroups = JSON.parse(buff.toString('utf-8')); + } catch (err) { + logger.warn(`Couldn't check redis keycloak cache: ${err.message}`); + // if it can't be recalled from redis, get the data from keycloak + const allGroups = await Group.Group(modelClients).loadAllGroups(); + keycloakGroups = await Group.Group(modelClients).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}`); + } + } let currentUser = {}; let serviceAccount = {}; // if this is a user request, get the users keycloak groups too, do this one to reduce the number of times it is called elsewhere + // legacy accounts don't do this let keycloakUsersGroups = [] let groupRoleProjectIds = [] const keycloakGrant = req.kauth ? req.kauth.grant : null diff --git a/services/api/src/clients/redisClient.ts b/services/api/src/clients/redisClient.ts index 6b8a5643cc..7694c88ff7 100644 --- a/services/api/src/clients/redisClient.ts +++ b/services/api/src/clients/redisClient.ts @@ -65,6 +65,23 @@ export const saveRedisCache = async ( ); }; +export const getRedisKeycloakCache = async (key: string) => { + const redisHash = await hgetall(`cache:keycloak`); + + return R.prop(key, redisHash); +}; + +export const saveRedisKeycloakCache = async ( + key: string, + value: number | string +) => { + await redisClient.hmset( + `cache:keycloak`, + key, + value + ); +}; + export const deleteRedisUserCache = userId => del(`cache:authz:${userId}`); export const getProjectGroupsCache = async projectId => @@ -77,6 +94,8 @@ export const deleteProjectGroupsCache = async projectId => export default { getRedisCache, saveRedisCache, + getRedisKeycloakCache, + saveRedisKeycloakCache, deleteRedisUserCache, getProjectGroupsCache, saveProjectGroupsCache diff --git a/services/api/src/models/group.ts b/services/api/src/models/group.ts index 018c83b9c5..c7bd3da8f6 100644 --- a/services/api/src/models/group.ts +++ b/services/api/src/models/group.ts @@ -5,6 +5,7 @@ import pickNonNil from '../util/pickNonNil'; import { logger } from '../loggers/logger'; import GroupRepresentation from 'keycloak-admin/lib/defs/groupRepresentation'; import { User } from './user'; +import { saveRedisKeycloakCache } from '../clients/redisClient'; interface IGroupAttributes { 'lagoon-projects'?: [string]; @@ -437,6 +438,16 @@ export const Group = (clients: { } } + 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 group; }; @@ -477,6 +488,16 @@ export const Group = (clients: { }); } } + + 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 newGroup; }; @@ -490,6 +511,16 @@ export const Group = (clients: { throw new Error(`Error deleting group ${id}: ${err}`); } } + + 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}`); + } }; const addUserToGroup = async ( @@ -531,6 +562,15 @@ export const Group = (clients: { throw new Error(`Could not add user to group: ${err.message}`); } + 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); }; @@ -556,6 +596,16 @@ export const Group = (clients: { } + 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); }; @@ -591,6 +641,16 @@ export const Group = (clients: { `Error setting projects for group ${group.name}: ${err.message}` ); } + + 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}`); + } }; const removeProjectFromGroup = async ( @@ -624,6 +684,16 @@ export const Group = (clients: { `Error setting projects for group ${group.name}: ${err.message}` ); } + + 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 { diff --git a/services/api/src/resources/group/resolvers.ts b/services/api/src/resources/group/resolvers.ts index a2ed181657..4c1dc57b61 100644 --- a/services/api/src/resources/group/resolvers.ts +++ b/services/api/src/resources/group/resolvers.ts @@ -55,7 +55,7 @@ export const getAllGroups: ResolverFn = async ( }; // TODO: recursive lookups for groups in groups? -export const getGroupFromGroups = async (id, groups) => { +export const getGroupFromGroupsById = async (id, groups) => { const d = R.filter(R.propEq('id', id), groups); if (d.length) { return d[0]; @@ -71,6 +71,22 @@ export const getGroupFromGroups = async (id, groups) => { return {}; } +export const getGroupFromGroupsByName = async (id, groups) => { + const d = R.filter(R.propEq('name', id), groups); + if (d.length) { + return d[0]; + } + for (const group in groups) { + if (groups[group].groups.length) { + const d = R.filter(R.propEq('name', id), groups[group].groups) + if (d.length) { + return d[0]; + } + } + } + return {}; +} + export const getMembersByGroupId: ResolverFn = async ( { id }, _input, @@ -79,7 +95,7 @@ export const getMembersByGroupId: ResolverFn = async ( try { // members resolver is only called by group, no need to check the permissions on the group // as the group resolver will have already checked permission - const group = await getGroupFromGroups(id, keycloakGroups); + const group = await getGroupFromGroupsById(id, keycloakGroups); const members = await models.GroupModel.getGroupMembership(group); return members; } catch (err) { @@ -492,7 +508,13 @@ export const getAllProjectsInGroup: ResolverFn = async ( if (adminScopes.groupViewAll) { try { // get group from all keycloak groups apollo context - const group = await getGroupFromGroups(groupInput.id, keycloakGroups) + let group = []; + if (groupInput.name) { + group = await getGroupFromGroupsByName(groupInput.name, keycloakGroups); + } + if (groupInput.id) { + group = await getGroupFromGroupsById(groupInput.id, keycloakGroups); + } const projectIdsArray = await getProjectsFromGroupAndSubgroups(group); return projectIdsArray.map(async id => projectHelpers(sqlClientPool).getProjectByProjectInput({ id }) @@ -514,7 +536,13 @@ export const getAllProjectsInGroup: ResolverFn = async ( return []; } else { // get group from all keycloak groups apollo context - const group = await getGroupFromGroups(groupInput.id, keycloakGroups) + let group = []; + if (groupInput.name) { + group = await getGroupFromGroupsByName(groupInput.name, keycloakGroups); + } + if (groupInput.id) { + group = await getGroupFromGroupsById(groupInput.id, keycloakGroups); + } // get users groups from users keycloak groups apollo context const userGroups = keycloakUsersGroups; diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 9c8da4b109..52ee36c141 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -6,6 +6,8 @@ import { isNotNil } from './func'; import { keycloakGrantManager } from '../clients/keycloakClient'; const { userActivityLogger } = require('../loggers/userActivityLogger'); import { Group } from '../models/group'; +import { User } from '../models/user'; +import { saveRedisKeycloakCache } from '../clients/redisClient'; interface ILegacyToken { iat: string; @@ -147,6 +149,7 @@ export class KeycloakUnauthorizedError extends Error { export const keycloakHasPermission = (grant, requestCache, modelClients, serviceAccount, currentUser, groupRoleProjectIds) => { const GroupModel = Group(modelClients); + const UserModel = User(modelClients); return async (resource, scope, attributes: IKeycloakAuthAttributes = {}) => { @@ -187,7 +190,18 @@ export const keycloakHasPermission = (grant, requestCache, modelClients, service projectQuery: [`${projectId}`] }; - const [highestRoleForProject, upids] = getUserRoleForProjectFromRoleProjectIds(groupRoleProjectIds, projectId) + let [highestRoleForProject, upids] = getUserRoleForProjectFromRoleProjectIds(groupRoleProjectIds, projectId) + + if (!highestRoleForProject) { + // if no role is detected, fall back to checking the slow way. this is usually only going to be on project creation + // but could happen elsewhere + const keycloakUsersGroups = await UserModel.getAllGroupsForUser(currentUser.id); + // grab the users project ids and roles in the first request + groupRoleProjectIds = await UserModel.getAllProjectsIdsForUser(currentUser, keycloakUsersGroups); + + [highestRoleForProject, upids] = getUserRoleForProjectFromRoleProjectIds(groupRoleProjectIds, projectId) + } + if (upids.length) { claims = { ...claims, @@ -214,12 +228,14 @@ export const keycloakHasPermission = (grant, requestCache, modelClients, service R.prop('group', attributes) ); + const groupMembers = await GroupModel.getGroupMembership(group) + const groupRoles = R.pipe( R.filter(membership => R.pathEq(['user', 'id'], currentUser.id, membership) ), R.pluck('role') - )(group.members); + )(groupMembers); const highestRoleForGroup = R.pipe( R.uniq,