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

refactor: store project-group association in api instead of keycloak #3612

Merged
merged 21 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c13afd2
refactor: use apidb to store project-group and org-group association
shreddedbacon Dec 3, 2023
629e4d1
chore: merge branch 'main' into project-groups-db-cache
shreddedbacon Dec 4, 2023
4681c5d
fix: remove unused keycloakgroups attribute
shreddedbacon Dec 4, 2023
9d1933b
chore: merge branch 'main' into project-groups-db-cache
shreddedbacon Dec 4, 2023
5cd4871
fix: re-add buildimage filter to project query
shreddedbacon Dec 4, 2023
2835fb4
feat: add a custom token mapper to get project ids from api-db
shreddedbacon Dec 5, 2023
d2d3253
chore: merge main and fix conflicts
shreddedbacon Dec 12, 2023
035cc6d
chore: merge main fix conflicts
shreddedbacon Jan 12, 2024
1db0014
chore: use updated keycloak-migrations fix chart
shreddedbacon Jan 12, 2024
aefe084
chore: merge branch 'main' into project-groups-db-cache
shreddedbacon Jan 17, 2024
d637b6c
chore: rename migration file to be last in list
shreddedbacon Jan 17, 2024
8a86dc4
chore: merge main and fix conflicts
shreddedbacon Feb 8, 2024
143186c
chore: merge main and fix conflicts
shreddedbacon Feb 14, 2024
b5d533d
fix: use new stream endpoint for collecting group/role information
shreddedbacon Feb 15, 2024
1071058
fix: prefix db tables with kc_ and add droptables in rollback
shreddedbacon Mar 7, 2024
6359eb8
fix: missing id on currentuser
shreddedbacon Mar 7, 2024
fae0a2a
fix: empty group check
shreddedbacon Mar 7, 2024
d121d35
refactor: adjust group migration to use flattened groups
shreddedbacon Mar 8, 2024
97577c1
Merge branch 'main' into project-groups-db-cache
tobybellwood Mar 12, 2024
0789d17
chore: reorder db migration
tobybellwood Mar 12, 2024
bf3794d
chore: rename keycloak migration to match api-db
tobybellwood Mar 12, 2024
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ STERN_VERSION = v2.6.1
CHART_TESTING_VERSION = v3.10.1
K3D_IMAGE = docker.io/rancher/k3s:v1.28.6-k3s2
TESTS = [nginx,api,features-kubernetes,bulk-deployment,features-kubernetes-2,features-variables,active-standby-kubernetes,tasks,drush,python,gitlab,github,bitbucket,services,workflows]
CHARTS_TREEISH = prerelease/lagoon_v218
CHARTS_TREEISH = keycloak-migrations
TASK_IMAGES = task-activestandby

# Symlink the installed kubectl client if the correct version is already
Expand Down
28 changes: 23 additions & 5 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ services:
- ./node-packages:/app/node-packages:delegated
environment:
- CONSOLE_LOGGING_LEVEL=trace
api-init:
api-db-init:
image: ${IMAGE_REPO:-lagoon}/api
command: >
sh -c "./node_modules/.bin/knex migrate:list --cwd /app/services/api/database
Expand All @@ -53,8 +53,24 @@ services:
- ./node-packages:/app/node-packages:delegated
- /app/node-packages/commons/dist
depends_on:
- api-db
- keycloak
api-db:
condition: service_started
keycloak:
condition: service_started
api-lagoon-migrations:
image: ${IMAGE_REPO:-lagoon}/api
command: sh -c "./node_modules/.bin/tsc && node -r dotenv-extended/config dist/migrations/lagoon/migration.js"
volumes:
- ./services/api/src:/app/services/api/src
environment:
- KEYCLOAK_URL=http://172.17.0.1:8088
- NODE_ENV=development
- CONSOLE_LOGGING_LEVEL=trace
depends_on:
api-db-init:
condition: service_completed_successfully # don't start the lagoon migrations until the db migrations is completed
keycloak:
condition: service_started
api:
image: ${IMAGE_REPO:-lagoon}/api
command: ./node_modules/.bin/tsc-watch --build --incremental --onSuccess "node -r dotenv-extended/config dist/index"
Expand All @@ -74,8 +90,10 @@ services:
- S3_BAAS_SECRET_ACCESS_KEY=minio123
- CONSOLE_LOGGING_LEVEL=debug
depends_on:
- api-init
- keycloak
api-lagoon-migrations:
condition: service_started
keycloak:
condition: service_started
ports:
- '3000:3000'
# Uncomment for local new relic tracking
Expand Down
40 changes: 40 additions & 0 deletions services/api/database/migrations/20240312000000_group_projects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = async function(knex) {
group_projects = await knex.schema.hasTable('kc_group_projects');
if (!group_projects) {
return knex.schema
// this table holds the main group to organization id association
.createTable('kc_group_organization', function (table) {
table.increments('id').notNullable().primary();
table.string('group_id', 50).notNullable();
table.integer('organization_id').notNullable();
table.unique(['group_id', 'organization_id'], {indexName: 'group_organization'});
})
// this table holds the main group to organization id association
.createTable('kc_group_projects', function (table) {
table.increments('id').notNullable().primary();
table.string('group_id', 50).notNullable();
table.integer('project_id').notNullable();
table.unique(['group_id', 'project_id'], {indexName: 'group_project'});
})
}
else {
return knex.schema
}
};

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function(knex) {
// caveats around this are that the rollback can only work while data is still saved in keycloak attributes
// once we remove that duplication of attribute into keycloak, this rollback would result in data loss for group>project associations
// for any group project associations made after the attribute removal
return knex.schema
.dropTable('kc_group_organization')
.dropTable('kc_group_projects')
};
5 changes: 3 additions & 2 deletions services/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"sync:gitlab:all": "yarn run sync:gitlab:users && yarn run sync:gitlab:groups && yarn run sync:gitlab:projects",
"sync:opendistro-security": "node --max-http-header-size=80000 dist/helpers/sync-groups-opendistro-security",
"sync:bitbucket:repo-permissions": "node dist/bitbucket-sync/repo-permissions",
"sync:harbor:projects": "node dist/migrations/2-harbor/harborSync.js"
"sync:harbor:projects": "node dist/migrations/2-harbor/harborSync.js",
"migrations:lagoon": "node dist/migrations/lagoon/migrations.js"
},
"keywords": [],
"author": "amazee.io <hello@amazee.io> (http://www.amazee.io)",
Expand Down Expand Up @@ -49,7 +50,7 @@
"jsonwebtoken": "^8.0.1",
"keycloak-admin": "https://github.com/amazeeio/keycloak-admin.git#bd015d2e34634f262c0827f00620657427e3c252",
"keycloak-connect": "^5.0.0",
"knex": "^0.95.15",
"knex": "^3.0.1",
"mariadb": "^2.5.2",
"moment": "^2.24.0",
"morgan": "^1.9.0",
Expand Down
52 changes: 4 additions & 48 deletions services/api/src/apolloServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,28 +120,6 @@ const apolloServer = new ApolloServer({
esClient,
};

// 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 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
Expand All @@ -151,11 +129,12 @@ const apolloServer = new ApolloServer({
const keycloakGrant = grant
let legacyGrant = legacyCredentials ? legacyCredentials : null
if (keycloakGrant) {
// get all the users keycloak groups, do this early to reduce the number of times this is called otherwise
keycloakUsersGroups = await User.User(modelClients).getAllGroupsForUser(keycloakGrant.access_token.content.sub);
serviceAccount = await keycloakGrantManager.obtainFromClientCredentials();
currentUser = await User.User(modelClients).loadUserById(keycloakGrant.access_token.content.sub);
// grab the users project ids and roles in the first request
groupRoleProjectIds = await User.User(modelClients).getAllProjectsIdsForUser(currentUser, keycloakUsersGroups);
groupRoleProjectIds = await User.User(modelClients).getAllProjectsIdsForUser(currentUser.id, keycloakUsersGroups);
}

return {
Expand All @@ -173,7 +152,6 @@ const apolloServer = new ApolloServer({
ProjectModel: ProjectModel.ProjectModel(modelClients),
EnvironmentModel: EnvironmentModel.EnvironmentModel(modelClients)
},
keycloakGroups,
keycloakUsersGroups,
};
},
Expand Down Expand Up @@ -206,28 +184,6 @@ const apolloServer = new ApolloServer({
esClient,
};

// 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 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
Expand All @@ -237,11 +193,12 @@ const apolloServer = new ApolloServer({
const keycloakGrant = req.kauth ? req.kauth.grant : null
let legacyGrant = req.legacyCredentials ? req.legacyCredentials : null
if (keycloakGrant) {
// get all the users keycloak groups, do this early to reduce the number of times this is called otherwise
keycloakUsersGroups = await User.User(modelClients).getAllGroupsForUser(keycloakGrant.access_token.content.sub);
serviceAccount = await keycloakGrantManager.obtainFromClientCredentials();
currentUser = await User.User(modelClients).loadUserById(keycloakGrant.access_token.content.sub);
// grab the users project ids and roles in the first request
groupRoleProjectIds = await User.User(modelClients).getAllProjectsIdsForUser(currentUser, keycloakUsersGroups);
groupRoleProjectIds = await User.User(modelClients).getAllProjectsIdsForUser(currentUser.id, keycloakUsersGroups);
}

// do a permission check to see if the user is platform admin/owner, or has permission for `viewAll` on certain resources
Expand Down Expand Up @@ -308,7 +265,6 @@ const apolloServer = new ApolloServer({
ProjectModel: ProjectModel.ProjectModel(modelClients),
EnvironmentModel: EnvironmentModel.EnvironmentModel(modelClients)
},
keycloakGroups,
keycloakUsersGroups,
adminScopes: {
projectViewAll: projectViewAll,
Expand Down
2 changes: 2 additions & 0 deletions services/api/src/clients/redisClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ redisClient.on('error', function(error) {
console.error(error);
});

export const groupCacheExpiry = toNumber(getConfigFromEnv('REDIS_GROUP_CACHE_EXPIRY', '172800'))

export const get = promisify(redisClient.get).bind(redisClient);
const hgetall = promisify(redisClient.hgetall).bind(redisClient);
const smembers = promisify(redisClient.smembers).bind(redisClient);
Expand Down
30 changes: 30 additions & 0 deletions services/api/src/migrations/lagoon/migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { waitForKeycloak } from '../../util/waitForKeycloak';
import { envHasConfig } from '../../util/config';
import { logger } from '../../loggers/logger';
import { migrate } from '../../util/db'


(async () => {
await waitForKeycloak();

// run any migrations that need keycloak before starting the api
try {
// run the migrations
logger.info('previous migrations:');
const before = await migrate.migrate.list();
for (const l of before) {
if (l.length) {
logger.info(`- ${l.name}`)
}
}
logger.info('performing migrations if required');
// actually run the migrations
await migrate.migrate.latest();
logger.info('migrations completed');
} catch (e) {
logger.fatal(`Couldn't run migrations: ${e.message}`);
process.exit(1)
}

process.exit()
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as R from 'ramda';
import { logger } from '@lagoon/commons/dist/logs/local-logger';
import { getKeycloakAdminClient } from '../../../clients/keycloak-admin';
import { sqlClientPool } from '../../../clients/sqlClient';
import { esClient } from '../../../clients/esClient';
import redisClient from '../../../clients/redisClient';
import { Group } from '../../../models/group';
import { Helpers } from '../../../resources/group/helpers';

export const up = async (migrate) => {
const keycloakAdminClient = await getKeycloakAdminClient();

const GroupModel = Group({
sqlClientPool,
keycloakAdminClient,
esClient,
redisClient
});

// load all groups from keycloak
const allGroups = await GroupModel.loadAllGroups();
// flatten them out
const flattenGroups = (groups, group) => {
groups.push(R.omit(['subGroups'], group));
const flatSubGroups = group.subGroups.reduce(flattenGroups, []);
return groups.concat(flatSubGroups);
};
const fgs = R.pipe(
R.reduce(flattenGroups, []),
)(allGroups)
// loop over the groups ignoring `role-subgroup` groups
for (const fg of fgs) {
if (fg.attributes['type'] != "role-subgroup") {
const groupProjects = await GroupModel.getProjectsFromGroup(fg);
for (const pid of groupProjects) {
logger.info(`Migrating project ${pid} and group ${fg.name}/${fg.id} to database`)
// add the project group association to the database
await Helpers(sqlClientPool).addProjectToGroup(pid, fg.id)
}
// if the group is in an organization
if (R.prop('lagoon-organization', fg.attributes)) {
// add the organization group association to the database
logger.info(`Migrating group ${fg.name}/${fg.id} in organization ${R.prop('lagoon-organization', fg.attributes)} to database`)
await Helpers(sqlClientPool).addOrganizationToGroup(parseInt(R.prop('lagoon-organization', fg.attributes)[0], 10), fg.id)
}
}
}

return migrate.schema
}

export const down = async (migrate) => {
return migrate.schema
}
Loading