Skip to content

Commit

Permalink
Merge pull request #3428 from uselagoon/reintroduce-redis
Browse files Browse the repository at this point in the history
refactor: add redis to the allgroups query to reduce load on keycloak
  • Loading branch information
tobybellwood authored Apr 11, 2023
2 parents c5708ed + 753ef08 commit 1908bda
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 8 deletions.
23 changes: 21 additions & 2 deletions services/api/src/apolloServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions services/api/src/clients/redisClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -77,6 +94,8 @@ export const deleteProjectGroupsCache = async projectId =>
export default {
getRedisCache,
saveRedisCache,
getRedisKeycloakCache,
saveRedisKeycloakCache,
deleteRedisUserCache,
getProjectGroupsCache,
saveProjectGroupsCache
Expand Down
70 changes: 70 additions & 0 deletions services/api/src/models/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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;
};

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

Expand All @@ -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 (
Expand Down Expand Up @@ -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);
};

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

Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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 {
Expand Down
36 changes: 32 additions & 4 deletions services/api/src/resources/group/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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 })
Expand All @@ -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;

Expand Down
20 changes: 18 additions & 2 deletions services/api/src/util/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {}) => {

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit 1908bda

Please sign in to comment.