From 447e1cdff9618ebb8ec6c031cb7103e71a3c2aca Mon Sep 17 00:00:00 2001 From: Eleanor Lewis Date: Sat, 20 Jul 2024 12:44:17 -0700 Subject: [PATCH 1/9] commit to save --- app/api/keycloak.js | 120 ++++++++++++++++++++++++++++++++++++++++++++ app/config.js | 4 ++ 2 files changed, 124 insertions(+) diff --git a/app/api/keycloak.js b/app/api/keycloak.js index 1cd89716a..541954a4e 100644 --- a/app/api/keycloak.js +++ b/app/api/keycloak.js @@ -25,4 +25,124 @@ $keycloak.getToken = async (username, password) => { return request(options); }; +$keycloak.createPORIUser = async (token, newUsername, newEmail) => { + const {clientId, baseuri, enableUserCreate} = nconf.get('keycloak'); + if (!enableUserCreate) { + return + } + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `bearer ${token}` + }; + const newUserOptions = { + method: 'POST', + url: `${baseuri}/auth/admin/realms/GSC/users`, + json: true, + headers: headers, + data: { + "username": newUsername, + "email": newEmail, + "enabled": true, + "emailVerified": true + } + }; + const newUserSuccess = request(newUserOptions); + + const userslist = request({method: "GET", + url: `${baseuri}/auth/admin/realms/GSC/users`, + headers: headers}); + const newUser = (userslist).filter(item => item.username===newUsername)[0] + console.dir(newUser) + const roleslist = request({method: "GET", + url: `${baseuri}/auth/admin/realms/GSC/roles`, headers: headers}); + console.dir(roleslist); + const iprRoleId = (roleslist).filter(item => item.name==='IPR')[0]['id'] + const graphkbRoleId = (roleslist).filter(item => item.name==='IPR')[0]['id'] + console.dir(iprRoleId); + console.dir(graphkbRoleId); + + const roleMappingSuccess = request({ + method: "POST", + url: `${baseuri}/auth/admin/realms/GSC/users/${user['id']}/role-mappings/realm`, + headers: headers, + data: [{ + 'id': gkbRoleId, + 'name': "GraphKB" + }, { + 'id': iprRoleId, + "name": "IPR" + }], +}); + console.dir(roleMappingSuccess); + return roleMappingSuccess.status_code; + +}; + +// aka grantRealmAdmin +$keycloak.addUserCreateRoles = async (token, editUsername, editUseremail) => { + const headers = { + 'Content-Type': 'application/json', + Authorization: `bearer ${token}`, + }; + const userslist = request({method: 'GET', + url: `${REALMS_URL}/${REALM}/users`, + headers}); + const currUser = (userslist).filter((item) => {return item.username === editUsername && item.email === editUseremail;})[0]; + console.dir(currUser); + const currUserId = currUser.id; + + const clients = request({method: 'GET', + url: `${REALMS_URL}/${REALM}/clients`, + headers}); + const rmClient = (clients).filter((item) => {return item.clientId === 'realm-management';})[0]; + const rmclientId = rmClient.id; + + const clientRoleMappings = request({ + method: 'GET', + url: `${REALMS_URL}/${REALM}/users/${currUserId}/role-mappings/clients/${rmClientId}`, + headers, + }); + const clientRM = (clientRoleMappings).filter((item) => {return item.name === 'realm-management';})[0]; + const clientRMid = clientRM.id; + console.dir(clientRM); + + resp2 = requests.request({ + method: "GET", + url: `${REALM_URL}/clients/${rm_client['id']}/roles`, + headers} + ) + const realm_admin = (resp2).filter((item) => {return item.name === 'realm-admin';})[0]; + + const postclientroles = request({method: 'POST', + url: `${REALMS_URL}/${REALM}/users/${currUserId}/role-mappings/clients/${rmclientId}`, + headers, + data: [{ + id: realm_admin['id'], + name: 'realm-admin', + }]}); + + console.dir(postclientroles); +}; + + +$keycloak.ungrantRealmAdmin = async (token, editUser) => { + const {clientId, baseuri, enableUserCreate} = nconf.get('keycloak'); + if (!enableUserCreate) { + return + } + + const options = { + method: 'DELETE', + url: baseuri, + json: true, + + headers: { + 'Content-Type': 'application/json', + 'Authorization': `bearer ${token}` + }, + }; + logger.debug(`Requesting token from ${uri}`); + return request(options); +}; + module.exports = $keycloak; diff --git a/app/config.js b/app/config.js index 69d598429..81245331f 100644 --- a/app/config.js +++ b/app/config.js @@ -34,6 +34,10 @@ const DEFAULTS = { uri: ENV === 'production' ? 'https://sso.bcgsc.ca/auth/realms/GSC/protocol/openid-connect/token' : 'https://keycloakdev01.bcgsc.ca/auth/realms/GSC/protocol/openid-connect/token', + baseuri: ENV === 'production' + ? 'https://sso.bcgsc.ca' + : 'https://keycloakdev01.bcgsc.ca', + enableUserCreate: false, clientId: 'IPR', role: 'IPR', keyfile: ENV === 'production' From 97cb9ae384a8d92013964c496522716183568eaa Mon Sep 17 00:00:00 2001 From: Eleanor Lewis Date: Sat, 20 Jul 2024 13:47:43 -0700 Subject: [PATCH 2/9] commit to save --- app/api/keycloak.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/app/api/keycloak.js b/app/api/keycloak.js index 541954a4e..bf18ddc9a 100644 --- a/app/api/keycloak.js +++ b/app/api/keycloak.js @@ -131,18 +131,26 @@ $keycloak.ungrantRealmAdmin = async (token, editUser) => { return } - const options = { - method: 'DELETE', - url: baseuri, - json: true, + const clientRoleMappings = request({ + method: 'GET', + url: `${REALMS_URL}/${REALM}/users/${currUserId}/role-mappings/clients/${rmClientId}`, + headers, + }); + const clientRM = (clientRoleMappings).filter((item) => {return item.name === 'realm-management';})[0]; + const clientRMid = clientRM.id; + console.dir(clientRM); - headers: { - 'Content-Type': 'application/json', - 'Authorization': `bearer ${token}` - }, - }; - logger.debug(`Requesting token from ${uri}`); - return request(options); + const deleteOutcome = request({ + method: "DELETE", + url: `${REALMS_URL}/${REALM}/users/${currUserId}/role-mappings/clients/${rmClientId}`, + headers: headers, + data: [{ + id: clientRMid, + name: 'realm-admin', + }] + }) + console.dir(deleteOutcome); + return deleteOutcome; }; module.exports = $keycloak; From 324219406dd7cbe36fd9fc25297db7365fdb1a14 Mon Sep 17 00:00:00 2001 From: Eleanor Lewis Date: Sun, 4 Aug 2024 22:19:27 -0700 Subject: [PATCH 3/9] commit to save --- app/api/keycloak.js | 81 ++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/app/api/keycloak.js b/app/api/keycloak.js index e587f4bfa..718ea6c7f 100644 --- a/app/api/keycloak.js +++ b/app/api/keycloak.js @@ -28,58 +28,52 @@ $keycloak.getToken = async (username, password) => { $keycloak.createKeycloakUser = async (token, newUsername, newEmail) => { const {clientId, baseuri, enableUserCreate} = nconf.get('keycloak'); if (!enableUserCreate) { - return + return; } const headers = { 'Content-Type': 'application/json', - 'Authorization': `bearer ${token}` + Authorization: `bearer ${token}`, }; const newUserOptions = { method: 'POST', url: `${baseuri}/auth/admin/realms/GSC/users`, json: true, - headers: headers, + headers, data: { - "username": newUsername, - "email": newEmail, - "enabled": true, - "emailVerified": true - } + username: newUsername, + email: newEmail, + enabled: true, + emailVerified: true, + }, }; const newUserSuccess = request(newUserOptions); - const userslist = request({method: "GET", - url: `${baseuri}/auth/admin/realms/GSC/users`, - headers: headers}); - const newUser = (userslist).filter(item => item.username===newUsername)[0] - console.dir(newUser) - const roleslist = request({method: "GET", - url: `${baseuri}/auth/admin/realms/GSC/roles`, headers: headers}); - console.dir(roleslist); - const iprRoleId = (roleslist).filter(item => item.name==='IPR')[0]['id'] - const graphkbRoleId = (roleslist).filter(item => item.name==='IPR')[0]['id'] - console.dir(iprRoleId); - console.dir(graphkbRoleId); + const userslist = request({method: 'GET', + url: `${baseuri}/auth/admin/realms/GSC/users`, + headers}); + const newUser = (userslist).filter((item) => {return item.username === newUsername;})[0]; + const roleslist = request({method: 'GET', + url: `${baseuri}/auth/admin/realms/GSC/roles`, + headers}); + const iprRoleId = (roleslist).filter((item) => {return item.name === 'IPR';})[0].id; + const graphkbRoleId = (roleslist).filter((item) => {return item.name === 'IPR';})[0].id; const roleMappingSuccess = request({ - method: "POST", - url: `${baseuri}/auth/admin/realms/GSC/users/${user['id']}/role-mappings/realm`, - headers: headers, + method: 'POST', + url: `${baseuri}/auth/admin/realms/GSC/users/${user.id}/role-mappings/realm`, + headers, data: [{ - 'id': gkbRoleId, - 'name': "GraphKB" + id: gkbRoleId, + name: 'GraphKB', }, { - 'id': iprRoleId, - "name": "IPR" + id: iprRoleId, + name: 'IPR', }], -}); - console.dir(roleMappingSuccess); + }); return roleMappingSuccess.status_code; - }; -// aka grantRealmAdmin -$keycloak.grantKeycloakUserCreateRole = async (token, editUsername, editUseremail) => { +$keycloak.grantRealmAdmin = async (token, editUsername, editUseremail) => { const headers = { 'Content-Type': 'application/json', Authorization: `bearer ${token}`, @@ -106,29 +100,26 @@ $keycloak.grantKeycloakUserCreateRole = async (token, editUsername, editUseremai const clientRMid = clientRM.id; console.dir(clientRM); - resp2 = requests.request({ - method: "GET", - url: `${REALM_URL}/clients/${rm_client['id']}/roles`, - headers} - ) + resp2 = requests.request({method: 'GET', + url: `${REALM_URL}/clients/${rm_client.id}/roles`, + headers}); const realm_admin = (resp2).filter((item) => {return item.name === 'realm-admin';})[0]; const postclientroles = request({method: 'POST', url: `${REALMS_URL}/${REALM}/users/${currUserId}/role-mappings/clients/${rmclientId}`, headers, data: [{ - id: realm_admin['id'], + id: realm_admin.id, name: 'realm-admin', }]}); console.dir(postclientroles); }; - -$keycloak.ungrantKeycloakUserCreateRole = async (token, editUser) => { +$keycloak.ungrantRealmAdmin = async (token, editUser) => { const {clientId, baseuri, enableUserCreate} = nconf.get('keycloak'); if (!enableUserCreate) { - return + return; } const clientRoleMappings = request({ @@ -141,14 +132,14 @@ $keycloak.ungrantKeycloakUserCreateRole = async (token, editUser) => { console.dir(clientRM); const deleteOutcome = request({ - method: "DELETE", + method: 'DELETE', url: `${REALMS_URL}/${REALM}/users/${currUserId}/role-mappings/clients/${rmClientId}`, - headers: headers, + headers, data: [{ id: clientRMid, name: 'realm-admin', - }] - }) + }], + }); console.dir(deleteOutcome); return deleteOutcome; }; From 768ff8a0d35ad1db3509c2de766d0682079b4715 Mon Sep 17 00:00:00 2001 From: Eleanor Lewis Date: Wed, 7 Aug 2024 15:23:21 -0700 Subject: [PATCH 4/9] no underscores in config name --- app/config.js | 2 +- app/queue.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config.js b/app/config.js index 39c3dd71c..dc7f7f4ac 100644 --- a/app/config.js +++ b/app/config.js @@ -101,7 +101,7 @@ const DEFAULTS = { ? 6380 : 6379, }, - redis_queue: REDIS_QUEUE_CONFIG, + redisqueue: REDIS_QUEUE_CONFIG, paths: { data: { POGdata: '/projects/tumour_char/pog/reports/genomic', diff --git a/app/queue.js b/app/queue.js index 050ef77f4..834dc2c1c 100644 --- a/app/queue.js +++ b/app/queue.js @@ -3,7 +3,7 @@ const nodemailer = require('nodemailer'); const conf = require('./config'); const createReport = require('./libs/createReport'); -const {host, port, enableQueue} = conf.get('redis_queue'); +const {host, port, enableQueue} = conf.get('redisqueue'); const logger = require('./log'); // Load logging library const CONFIG = require('./config'); From 19961027210db4eaefad728743413c568f195ee9 Mon Sep 17 00:00:00 2001 From: Eleanor Lewis Date: Mon, 26 Aug 2024 15:31:55 -0700 Subject: [PATCH 5/9] structure for kc user management --- app/config.js | 4 +- app/index.js | 5 +++ app/libs/getAdminCliToken.js | 53 ++++++++++++++++++++++++ app/middleware/keycloakUserManagement.js | 19 +++++++++ app/routes/index.js | 6 +++ app/routes/user/member.js | 29 ++++++++++++- app/routes/user/user.js | 33 +++++++++++---- 7 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 app/libs/getAdminCliToken.js create mode 100644 app/middleware/keycloakUserManagement.js diff --git a/app/config.js b/app/config.js index dc7f7f4ac..1c14b2ad9 100644 --- a/app/config.js +++ b/app/config.js @@ -53,8 +53,9 @@ const DEFAULTS = { baseuri: ENV === 'production' ? 'https://sso.bcgsc.ca' : 'https://keycloakdev01.bcgsc.ca', - enableUserCreate: false, + enableV16UserManagerment: false, // keycloak 16 clientId: 'IPR', + realm: 'PORI', role: 'IPR', keyfile: ENV === 'production' ? 'keys/prodkey.pem' @@ -123,7 +124,6 @@ const processEnvVariables = (env = process.env, opt = {}) => { for (const [key, value] of Object.entries(env)) { let newKey = key; - if (/^ipr_\w+$/i.exec(key)) { if (lowerCase) { newKey = newKey.toLowerCase(); diff --git a/app/index.js b/app/index.js index 4e0e378f1..ca9e94ca2 100644 --- a/app/index.js +++ b/app/index.js @@ -115,6 +115,11 @@ const listen = async (port = null) => { // ensure the db connection is ready await sequelize.authenticate(); + const enableV16UserManagement = conf.get('keycloak:enablev16usermanagement'); + if (enableV16UserManagement) { + logger.info(`user management with Keycloak 16 is enabled`); + } + // set up the routing const routing = new Routing(); try { diff --git a/app/libs/getAdminCliToken.js b/app/libs/getAdminCliToken.js new file mode 100644 index 000000000..676702b27 --- /dev/null +++ b/app/libs/getAdminCliToken.js @@ -0,0 +1,53 @@ +const HTTP_STATUS = require('http-status-codes'); +const jwt = require('jsonwebtoken'); +const fs = require('fs'); +const db = require('../models'); +const keycloak = require('../api/keycloak'); +const nconf = require('../config'); +const cache = require('../cache'); + +const logger = require('../log'); + +const pubKey = fs.readFileSync(nconf.get('keycloak:keyfile')).toString(); + +const getAdminCliToken = async (req, res) => { + const {enableV16UserManagement} = nconf.get('keycloak'); + if (!enableV16UserManagement) { + return; + } + let token = req.header('Authorization') || ''; + + // Check for basic authorization header + if (token.includes('Basic')) { + let credentials; + try { + credentials = Buffer.from(token.split(' ')[1], 'base64').toString('utf-8').split(':'); + } catch (err) { + return res.status(HTTP_STATUS.BAD_REQUEST).json({message: 'The authentication header you provided was not properly formatted.'}); + } + try { + const adminCliToken = await keycloak.getAdminCliToken(credentials[0], credentials[1]); + const adminToken = adminCliToken.access_token; + } catch (error) { + let errorDescription; + try { + errorDescription = JSON.parse(error.error).error_description; + } catch (parseError) { + // if the error is propagated from upstread of the keycloak server it will not have the error.error_description format (ex. certificate failure) + errorDescription = error; + } + console.dir(credentials); + logger.error(`Authentication failed ${error.name} ${error.statusCode} - ${errorDescription}`); + return res.status(HTTP_STATUS.BAD_REQUEST).json({error: {message: `Authentication failed ${error.name} ${error.statusCode} - ${errorDescription}`}}); + } + } + if (!token) { + return res.status(HTTP_STATUS.FORBIDDEN).json({message: 'Missing required Authorization token'}); + } + + return adminToken; +}; + +module.exports = { + getAdminCliToken, +}; diff --git a/app/middleware/keycloakUserManagement.js b/app/middleware/keycloakUserManagement.js new file mode 100644 index 000000000..eb9adef0b --- /dev/null +++ b/app/middleware/keycloakUserManagement.js @@ -0,0 +1,19 @@ +const nconf = require('../config'); + +const { + getUser, + } = require('../libs/getAdminCliToken'); + + // Require Active Session Middleware + module.exports = async (req, res, next) => { + const {enableV16UserManagement} = nconf.get('keycloak'); + if (!enableV16UserManagement) { + return next(); + } + + // Get Authorization Header + const adminCliToken = await getAdminCliToken(req, res); + req.adminCliToken = adminCliToken; + return next(); + }; + \ No newline at end of file diff --git a/app/routes/index.js b/app/routes/index.js index 8faafb9f9..d8026c62b 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -59,6 +59,12 @@ class Routing extends RouterInterface { // To every route except for specification routes (/spec and /spec.json) this.router.use(/^((?!^\/spec).)*$/i, authMiddleware); + // Add keycloak 16 admin-cli token getter + // To every route that uses that token (user management routes) + // If keycloak user management is enabled + this.router.use('/user', userManagementMiddleware) + this.router.use('/user/member', userManagementMiddleware) + // Acl middleware // To every route except for specification and report routes this.router.use(/^(?!^\/spec|\/germline-small-mutation-reports(?:\/([^\\?]+?))|\/reports(?:\/([^\\?]+?))[\\?]?.*)/i, aclMiddleware); diff --git a/app/routes/user/member.js b/app/routes/user/member.js index 3c197318b..3e1a0da09 100644 --- a/app/routes/user/member.js +++ b/app/routes/user/member.js @@ -9,6 +9,8 @@ const {isAdmin} = require('../../libs/helperFunctions'); const schemaGenerator = require('../../schemas/schemaGenerator'); const validateAgainstSchema = require('../../libs/validateAgainstSchema'); const {BASE_EXCLUDE} = require('../../schemas/exclude'); +const {grantRealmAdmin, ungrantRealmAdmin} = require('../../api/keycloak'); + // Generate schema const memberSchema = schemaGenerator(db.models.userGroupMember, { @@ -83,6 +85,18 @@ router.route('/') // Add user to group await db.models.userGroupMember.create({group_id: req.group.id, user_id: user.id}); await user.reload(); + const {enableV16UserManagement} = nconf.get('keycloak'); + if ((groupIsAdmin || req.group.name === 'manager') && enableV16UserManagement) { + try { + const token = req.adminCliToken; + await grantRealmAdmin(token, user.username, user.email); + } catch (error) { + logger.error(`Error while trying to add user to keycloak realm-management`); + return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ + error: {message: 'User creation succeeded but error granting user realm-management role in keycloak'} + }) + } + } return res.status(HTTP_STATUS.CREATED).json(user.view('public')); } catch (error) { logger.error(`Error while trying to add user to group ${error}`); @@ -153,9 +167,20 @@ router.route('/') error: {message: 'User doesn\'t belong to group'}, }); } - - // Remove membership await membership.destroy(); + const {enableV16UserManagement} = nconf.get('keycloak'); + if ((groupIsAdmin || req.group.name === 'manager') && enableV16UserManagement) { + try { + const token = req.adminCliToken; + await ungrantRealmAdmin(token, req.body.username, req.body.email); + } catch (error) { + logger.error(`Error while trying to ungrant user keycloak realm-management`); + return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ + error: {message: 'User group removal succeeded but error removing user realm-management role in keycloak'} + }) + } + } + // Remove membership return res.status(HTTP_STATUS.NO_CONTENT).send(); } catch (error) { logger.error(`Error while trying to remove user from group ${error}`); diff --git a/app/routes/user/user.js b/app/routes/user/user.js index ceca3bf8e..a2ef64a6d 100644 --- a/app/routes/user/user.js +++ b/app/routes/user/user.js @@ -5,7 +5,7 @@ const {Op} = require('sequelize'); const db = require('../../models'); const logger = require('../../log'); const {isAdmin, isManager} = require('../../libs/helperFunctions'); -const {createKeycloakUser} = require('../api/keycloak'); +const {createKeycloakUser, deleteKeycloakUser} = require('../../api/keycloak'); const validateAgainstSchema = require('../../libs/validateAgainstSchema'); const {createSchema, updateSchema, notificationUpdateSchema} = require('../../schemas/user'); @@ -175,6 +175,18 @@ router.route('/:userByIdent([A-z0-9-]{36})') } try { + const {enableV16UserManagement} = nconf.get('keycloak'); + if (enableV16UserManagement) { + try { + const token = req.adminCliToken; + await deleteKeycloakUser(token, req.userByIdent.username, req.userByIdent.email); + } catch (error) { + logger.error(`Error while trying to delete user from keycloak`); + return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ + error: {message: 'User deletion succeeded but error removing user from keycloak'} + }) + } + } await req.userByIdent.destroy(); return res.status(HTTP_STATUS.NO_CONTENT).send(); } catch (error) { @@ -266,6 +278,18 @@ router.route('/') // Commit changes await transaction.commit(); // Return new user + const {enableV16UserManagement} = nconf.get('keycloak'); + if (enableV16UserManagement) { + try { + const token = req.adminCliToken; + await createKeycloakUser(token, req.userByIdent.username, req.userByIdent.email); + } catch (error) { + logger.error(`Error while trying to create user in keycloak`); + return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ + error: {message: 'User deletion succeeded but error creating user in keycloak'} + }) + } + } return res.status(HTTP_STATUS.CREATED).json(createdUser.view('public')); } catch (error) { await transaction.rollback(); @@ -281,13 +305,6 @@ router.route('/me') return res.json(req.user.view('public')); }); - -router.route('/createkeycloak') - .get((req, res) => { - //createKeycloakUser - return res.json(req.user.view('public')); - }); - // User Search router.route('/search') .get(async (req, res) => { From f5be593f1a4b94977c5090e3f1f3fb0d48eb51c4 Mon Sep 17 00:00:00 2001 From: Eleanor Lewis Date: Mon, 26 Aug 2024 15:57:26 -0700 Subject: [PATCH 6/9] user management funcs --- app/api/keycloak.js | 218 +++++++++++++++++++++++++++++--------------- 1 file changed, 143 insertions(+), 75 deletions(-) diff --git a/app/api/keycloak.js b/app/api/keycloak.js index 718ea6c7f..a61466ec2 100644 --- a/app/api/keycloak.js +++ b/app/api/keycloak.js @@ -5,6 +5,19 @@ const logger = require('../log'); const $keycloak = {}; +const headers = (token) => { + return { + 'Accept': 'application/json', + "Content-Encoding": "deflate", + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token['access_token']}`, + }; +} + +const realmUri = (baseuri, realm) => { + return `${baseuri}/auth/admin/realms/${realm}`; +} + $keycloak.getToken = async (username, password) => { const {clientId, uri} = nconf.get('keycloak'); const options = { @@ -25,122 +38,177 @@ $keycloak.getToken = async (username, password) => { return request(options); }; +$keycloak.getAdminCliToken = async (username, password) => { + const {uri} = nconf.get('keycloak'); + const options = { + method: 'POST', + url: uri, + json: true, + body: form({ + client_id: 'admin-cli', + grant_type: 'password', + username, + password, + }), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }; + logger.debug(`Requesting admin-cli token from ${uri}`); + return request(options); +}; + $keycloak.createKeycloakUser = async (token, newUsername, newEmail) => { - const {clientId, baseuri, enableUserCreate} = nconf.get('keycloak'); - if (!enableUserCreate) { - return; + const {enableV16UserManagement, baseuri, realm} = nconf.get('keycloak'); + if (!enableV16UserManagement) { + return {}; } - const headers = { - 'Content-Type': 'application/json', - Authorization: `bearer ${token}`, - }; - const newUserOptions = { + const headers = headers(token); + const realmUri = realmUri(baseuri, realm); + + // create the user + await request({ method: 'POST', - url: `${baseuri}/auth/admin/realms/GSC/users`, - json: true, + url: `${realmUri}/users`, headers, - data: { + body: JSON.stringify({ username: newUsername, email: newEmail, - enabled: true, emailVerified: true, - }, - }; - const newUserSuccess = request(newUserOptions); + enabled: true, + }), + }); - const userslist = request({method: 'GET', - url: `${baseuri}/auth/admin/realms/GSC/users`, + // get the id of the newly created user + const userslist = await request({method: 'GET', + json: true, + url: `${realmUri}/users`, headers}); const newUser = (userslist).filter((item) => {return item.username === newUsername;})[0]; - const roleslist = request({method: 'GET', - url: `${baseuri}/auth/admin/realms/GSC/roles`, + + // get the ids of the ipr and graphkb roles + const roleslist = await request({method: 'GET', + json: true, + url: `${realmUri}/roles`, headers}); const iprRoleId = (roleslist).filter((item) => {return item.name === 'IPR';})[0].id; - const graphkbRoleId = (roleslist).filter((item) => {return item.name === 'IPR';})[0].id; + const graphkbRoleId = (roleslist).filter((item) => {return item.name === 'GraphKB';})[0].id; - const roleMappingSuccess = request({ + // add the roles + const roleMappingResult = await request({ method: 'POST', - url: `${baseuri}/auth/admin/realms/GSC/users/${user.id}/role-mappings/realm`, + url: `${realmUri}/users/${newUser.id}/role-mappings/realm`, headers, - data: [{ - id: gkbRoleId, - name: 'GraphKB', - }, { - id: iprRoleId, - name: 'IPR', - }], + body: JSON.stringify([ + {id: graphkbRoleId, name: 'GraphKB'}, + {id: iprRoleId, name: 'IPR'} + ]), }); - return roleMappingSuccess.status_code; + return roleMappingResult.status_code; +}; + + +$keycloak.deleteKeycloakUser = async (token, username, email) => { + const {enableV16UserManagement, baseuri, realm} = nconf.get('keycloak'); + if (!enableV16UserManagement) { + return {}; + } + const headers = headers(token); + const realmUri = realmUri(baseuri, realm); + const userslist = await request({method: 'GET', + json: true, + url: `${realmUri}/users`, + headers}); + const currUser = (userslist).filter((item) => {return item.username === username;})[0]; + const deleteUserSuccess = await request({ + method: 'DELETE', + json: true, + headers, + url: `${realmUri}/users/${currUser.id}` + }) + return deleteUserSuccess.status_code; }; $keycloak.grantRealmAdmin = async (token, editUsername, editUseremail) => { - const headers = { - 'Content-Type': 'application/json', - Authorization: `bearer ${token}`, - }; - const userslist = request({method: 'GET', - url: `${REALMS_URL}/${REALM}/users`, + const {enableV16UserManagement, baseuri, realm} = nconf.get('keycloak'); + if (!enableV16UserManagement) { + return {}; + } + const headers = headers(token); + const realmUri = realmUri(baseuri, realm); + + // get the id of the user to be updated + const userslist = await request({method: 'GET', + url: `${realmUri}/users`, + json: true, headers}); const currUser = (userslist).filter((item) => {return item.username === editUsername && item.email === editUseremail;})[0]; - console.dir(currUser); - const currUserId = currUser.id; - const clients = request({method: 'GET', - url: `${REALMS_URL}/${REALM}/clients`, + // get the id of the realm-management client + const clients = await request({method: 'GET', + url: `${realmUri}/clients`, + json: true, headers}); const rmClient = (clients).filter((item) => {return item.clientId === 'realm-management';})[0]; - const rmclientId = rmClient.id; - const clientRoleMappings = request({ - method: 'GET', - url: `${REALMS_URL}/${REALM}/users/${currUserId}/role-mappings/clients/${rmClientId}`, - headers, - }); - const clientRM = (clientRoleMappings).filter((item) => {return item.name === 'realm-management';})[0]; - const clientRMid = clientRM.id; - console.dir(clientRM); - - resp2 = requests.request({method: 'GET', - url: `${REALM_URL}/clients/${rm_client.id}/roles`, + // get the id of the realm-admin role in the realm-management client + const clientRoles = await request({method: 'GET', + url: `${realmUri}/clients/${rmClient.id}/roles`, + json: true, headers}); - const realm_admin = (resp2).filter((item) => {return item.name === 'realm-admin';})[0]; + const realmAdmin = (clientRoles).filter((item) => {return item.name === 'realm-admin';})[0]; - const postclientroles = request({method: 'POST', - url: `${REALMS_URL}/${REALM}/users/${currUserId}/role-mappings/clients/${rmclientId}`, + // add the realm-admin role in the realm-management client to the user + const postclientroles = await request({method: 'POST', + url: `${realmUri}/users/${currUser.id}/role-mappings/clients/${rmClient.id}`, headers, - data: [{ - id: realm_admin.id, + body: JSON.stringify([{ + id: realmAdmin.id, name: 'realm-admin', - }]}); - - console.dir(postclientroles); + }]), + }); + return postclientroles; }; -$keycloak.ungrantRealmAdmin = async (token, editUser) => { - const {clientId, baseuri, enableUserCreate} = nconf.get('keycloak'); - if (!enableUserCreate) { - return; +$keycloak.ungrantRealmAdmin = async (token, editUsername, editUseremail) => { + const {enableV16UserManagement, baseuri, realm} = nconf.get('keycloak'); + if (!enableV16UserManagement) { + return {}; } - - const clientRoleMappings = request({ + const headers = headers(token); + const realmUri = realmUri(baseuri, realm); + + // get the record for the current user + const userslist = await request({method: 'GET', + url: `${realmUri}/users`, + headers}); + const currUser = (userslist).filter((item) => {return item.username === editUsername && item.email === editUseremail;})[0]; + + // get the record for the realm-management client + const clients = await request({method: 'GET', + url: `${realmUri}/clients`, + headers}); + const rmClient = (clients).filter((item) => {return item.clientId === 'realm-management';})[0]; + + // get the record connecting the user to the role + const clientRoleMappings = await request({ method: 'GET', - url: `${REALMS_URL}/${REALM}/users/${currUserId}/role-mappings/clients/${rmClientId}`, + url: `${realmUri}/users/${currUser.id}/role-mappings/clients/${rmClient.id}`, headers, }); + // TODO - double check this, should be realm-admin not realm-management const clientRM = (clientRoleMappings).filter((item) => {return item.name === 'realm-management';})[0]; - const clientRMid = clientRM.id; - console.dir(clientRM); - const deleteOutcome = request({ + // delete the role + const deleteOutcome = await request({ method: 'DELETE', - url: `${REALMS_URL}/${REALM}/users/${currUserId}/role-mappings/clients/${rmClientId}`, + url: `${realmUri}/users/${currUser.id}/role-mappings/clients/${rmClient.id}`, headers, - data: [{ - id: clientRMid, + body: JSON.stringify([{ + id: clientRM.id, name: 'realm-admin', - }], + }]), }); - console.dir(deleteOutcome); return deleteOutcome; }; From 0bbcc76f82c1a2a3de108831e8e53649b4e7c813 Mon Sep 17 00:00:00 2001 From: Eleanor Lewis Date: Mon, 26 Aug 2024 16:59:45 -0700 Subject: [PATCH 7/9] lint --- app/api/keycloak.js | 52 ++++++++++++------------ app/libs/getAdminCliToken.js | 21 ++++------ app/middleware/keycloakUserManagement.js | 27 ++++++------ app/routes/user/member.js | 15 +++---- app/routes/user/user.js | 15 +++---- 5 files changed, 61 insertions(+), 69 deletions(-) diff --git a/app/api/keycloak.js b/app/api/keycloak.js index a61466ec2..65c732fdc 100644 --- a/app/api/keycloak.js +++ b/app/api/keycloak.js @@ -5,18 +5,18 @@ const logger = require('../log'); const $keycloak = {}; -const headers = (token) => { +const getHeaders = (token) => { return { - 'Accept': 'application/json', - "Content-Encoding": "deflate", + Accept: 'application/json', + 'Content-Encoding': 'deflate', 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token['access_token']}`, + Authorization: `Bearer ${token.access_token}`, }; -} +}; -const realmUri = (baseuri, realm) => { +const getRealmUri = (baseuri, realm) => { return `${baseuri}/auth/admin/realms/${realm}`; -} +}; $keycloak.getToken = async (username, password) => { const {clientId, uri} = nconf.get('keycloak'); @@ -63,8 +63,8 @@ $keycloak.createKeycloakUser = async (token, newUsername, newEmail) => { if (!enableV16UserManagement) { return {}; } - const headers = headers(token); - const realmUri = realmUri(baseuri, realm); + const headers = getHeaders(token); + const realmUri = getRealmUri(baseuri, realm); // create the user await request({ @@ -101,20 +101,19 @@ $keycloak.createKeycloakUser = async (token, newUsername, newEmail) => { headers, body: JSON.stringify([ {id: graphkbRoleId, name: 'GraphKB'}, - {id: iprRoleId, name: 'IPR'} + {id: iprRoleId, name: 'IPR'}, ]), }); return roleMappingResult.status_code; }; - -$keycloak.deleteKeycloakUser = async (token, username, email) => { +$keycloak.deleteKeycloakUser = async (token, username) => { const {enableV16UserManagement, baseuri, realm} = nconf.get('keycloak'); if (!enableV16UserManagement) { return {}; } - const headers = headers(token); - const realmUri = realmUri(baseuri, realm); + const headers = getHeaders(token); + const realmUri = getRealmUri(baseuri, realm); const userslist = await request({method: 'GET', json: true, url: `${realmUri}/users`, @@ -124,8 +123,8 @@ $keycloak.deleteKeycloakUser = async (token, username, email) => { method: 'DELETE', json: true, headers, - url: `${realmUri}/users/${currUser.id}` - }) + url: `${realmUri}/users/${currUser.id}`, + }); return deleteUserSuccess.status_code; }; @@ -134,9 +133,9 @@ $keycloak.grantRealmAdmin = async (token, editUsername, editUseremail) => { if (!enableV16UserManagement) { return {}; } - const headers = headers(token); - const realmUri = realmUri(baseuri, realm); - + const headers = getHeaders(token); + const realmUri = getRealmUri(baseuri, realm); + // get the id of the user to be updated const userslist = await request({method: 'GET', url: `${realmUri}/users`, @@ -147,7 +146,7 @@ $keycloak.grantRealmAdmin = async (token, editUsername, editUseremail) => { // get the id of the realm-management client const clients = await request({method: 'GET', url: `${realmUri}/clients`, - json: true, + json: true, headers}); const rmClient = (clients).filter((item) => {return item.clientId === 'realm-management';})[0]; @@ -165,8 +164,7 @@ $keycloak.grantRealmAdmin = async (token, editUsername, editUseremail) => { body: JSON.stringify([{ id: realmAdmin.id, name: 'realm-admin', - }]), - }); + }])}); return postclientroles; }; @@ -175,21 +173,21 @@ $keycloak.ungrantRealmAdmin = async (token, editUsername, editUseremail) => { if (!enableV16UserManagement) { return {}; } - const headers = headers(token); - const realmUri = realmUri(baseuri, realm); - + const headers = getHeaders(token); + const realmUri = getRealmUri(baseuri, realm); + // get the record for the current user const userslist = await request({method: 'GET', url: `${realmUri}/users`, headers}); const currUser = (userslist).filter((item) => {return item.username === editUsername && item.email === editUseremail;})[0]; - + // get the record for the realm-management client const clients = await request({method: 'GET', url: `${realmUri}/clients`, headers}); const rmClient = (clients).filter((item) => {return item.clientId === 'realm-management';})[0]; - + // get the record connecting the user to the role const clientRoleMappings = await request({ method: 'GET', diff --git a/app/libs/getAdminCliToken.js b/app/libs/getAdminCliToken.js index 676702b27..0b4bde902 100644 --- a/app/libs/getAdminCliToken.js +++ b/app/libs/getAdminCliToken.js @@ -1,22 +1,16 @@ const HTTP_STATUS = require('http-status-codes'); -const jwt = require('jsonwebtoken'); -const fs = require('fs'); -const db = require('../models'); const keycloak = require('../api/keycloak'); const nconf = require('../config'); -const cache = require('../cache'); const logger = require('../log'); -const pubKey = fs.readFileSync(nconf.get('keycloak:keyfile')).toString(); - const getAdminCliToken = async (req, res) => { - const {enableV16UserManagement} = nconf.get('keycloak'); - if (!enableV16UserManagement) { - return; - } - let token = req.header('Authorization') || ''; - + const {enableV16UserManagement} = nconf.get('keycloak'); + if (!enableV16UserManagement) { + return null; + } + const token = req.header('Authorization') || ''; + let adminToken; // Check for basic authorization header if (token.includes('Basic')) { let credentials; @@ -27,7 +21,7 @@ const getAdminCliToken = async (req, res) => { } try { const adminCliToken = await keycloak.getAdminCliToken(credentials[0], credentials[1]); - const adminToken = adminCliToken.access_token; + adminToken = adminCliToken.access_token; } catch (error) { let errorDescription; try { @@ -36,7 +30,6 @@ const getAdminCliToken = async (req, res) => { // if the error is propagated from upstread of the keycloak server it will not have the error.error_description format (ex. certificate failure) errorDescription = error; } - console.dir(credentials); logger.error(`Authentication failed ${error.name} ${error.statusCode} - ${errorDescription}`); return res.status(HTTP_STATUS.BAD_REQUEST).json({error: {message: `Authentication failed ${error.name} ${error.statusCode} - ${errorDescription}`}}); } diff --git a/app/middleware/keycloakUserManagement.js b/app/middleware/keycloakUserManagement.js index eb9adef0b..30771209c 100644 --- a/app/middleware/keycloakUserManagement.js +++ b/app/middleware/keycloakUserManagement.js @@ -1,19 +1,18 @@ const nconf = require('../config'); const { - getUser, - } = require('../libs/getAdminCliToken'); - - // Require Active Session Middleware - module.exports = async (req, res, next) => { - const {enableV16UserManagement} = nconf.get('keycloak'); - if (!enableV16UserManagement) { - return next(); - } + getAdminCliToken, +} = require('../libs/getAdminCliToken'); - // Get Authorization Header - const adminCliToken = await getAdminCliToken(req, res); - req.adminCliToken = adminCliToken; +// Require Active Session Middleware +module.exports = async (req, res, next) => { + const {enableV16UserManagement} = nconf.get('keycloak'); + if (!enableV16UserManagement) { return next(); - }; - \ No newline at end of file + } + + // Get Authorization Header + const adminCliToken = await getAdminCliToken(req, res); + req.adminCliToken = adminCliToken; + return next(); +}; diff --git a/app/routes/user/member.js b/app/routes/user/member.js index 909452edc..eea9daf02 100644 --- a/app/routes/user/member.js +++ b/app/routes/user/member.js @@ -4,6 +4,8 @@ const express = require('express'); const db = require('../../models'); const logger = require('../../log'); +const nconf = require('../../config'); + const router = express.Router({mergeParams: true}); const {isAdmin} = require('../../libs/helperFunctions'); const schemaGenerator = require('../../schemas/schemaGenerator'); @@ -11,7 +13,6 @@ const validateAgainstSchema = require('../../libs/validateAgainstSchema'); const {BASE_EXCLUDE} = require('../../schemas/exclude'); const {grantRealmAdmin, ungrantRealmAdmin} = require('../../api/keycloak'); - // Generate schema const memberSchema = schemaGenerator(db.models.userGroupMember, { baseUri: '/create-delete', @@ -91,10 +92,10 @@ router.route('/') const token = req.adminCliToken; await grantRealmAdmin(token, user.username, user.email); } catch (error) { - logger.error(`Error while trying to add user to keycloak realm-management`); + logger.error('Error while trying to add user to keycloak realm-management'); return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ - error: {message: 'User creation succeeded but error granting user realm-management role in keycloak'} - }) + error: {message: 'User creation succeeded but error granting user realm-management role in keycloak'}, + }); } } return res.status(HTTP_STATUS.CREATED).json(user.view('public')); @@ -174,10 +175,10 @@ router.route('/') const token = req.adminCliToken; await ungrantRealmAdmin(token, req.body.username, req.body.email); } catch (error) { - logger.error(`Error while trying to ungrant user keycloak realm-management`); + logger.error('Error while trying to ungrant user keycloak realm-management'); return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ - error: {message: 'User group removal succeeded but error removing user realm-management role in keycloak'} - }) + error: {message: 'User group removal succeeded but error removing user realm-management role in keycloak'}, + }); } } // Remove membership diff --git a/app/routes/user/user.js b/app/routes/user/user.js index 03f6d25c7..4249cb510 100644 --- a/app/routes/user/user.js +++ b/app/routes/user/user.js @@ -3,6 +3,7 @@ const express = require('express'); const bcrypt = require('bcryptjs'); const {Op} = require('sequelize'); const db = require('../../models'); +const nconf = require('../../config'); const logger = require('../../log'); const {isAdmin, isManager} = require('../../libs/helperFunctions'); const {createKeycloakUser, deleteKeycloakUser} = require('../../api/keycloak'); @@ -179,12 +180,12 @@ router.route('/:userByIdent([A-z0-9-]{36})') if (enableV16UserManagement) { try { const token = req.adminCliToken; - await deleteKeycloakUser(token, req.userByIdent.username, req.userByIdent.email); + await deleteKeycloakUser(token, req.userByIdent.username); } catch (error) { - logger.error(`Error while trying to delete user from keycloak`); + logger.error('Error while trying to delete user from keycloak'); return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ - error: {message: 'User deletion succeeded but error removing user from keycloak'} - }) + error: {message: 'User deletion succeeded but error removing user from keycloak'}, + }); } } await req.userByIdent.destroy(); @@ -284,10 +285,10 @@ router.route('/') const token = req.adminCliToken; await createKeycloakUser(token, req.userByIdent.username, req.userByIdent.email); } catch (error) { - logger.error(`Error while trying to create user in keycloak`); + logger.error('Error while trying to create user in keycloak'); return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ - error: {message: 'User deletion succeeded but error creating user in keycloak'} - }) + error: {message: 'User deletion succeeded but error creating user in keycloak'}, + }); } } return res.status(HTTP_STATUS.CREATED).json(createdUser.view('public')); From e5ca6110360c0af45bbbf405e8397a28d444c1da Mon Sep 17 00:00:00 2001 From: Eleanor Lewis Date: Mon, 26 Aug 2024 17:08:06 -0700 Subject: [PATCH 8/9] fix redisqueue name --- app/queue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/queue.js b/app/queue.js index 050ef77f4..834dc2c1c 100644 --- a/app/queue.js +++ b/app/queue.js @@ -3,7 +3,7 @@ const nodemailer = require('nodemailer'); const conf = require('./config'); const createReport = require('./libs/createReport'); -const {host, port, enableQueue} = conf.get('redis_queue'); +const {host, port, enableQueue} = conf.get('redisqueue'); const logger = require('./log'); // Load logging library const CONFIG = require('./config'); From eece36815f5bea3b6929ce958f5424fd05351ea5 Mon Sep 17 00:00:00 2001 From: Eleanor Lewis Date: Tue, 27 Aug 2024 10:44:18 -0700 Subject: [PATCH 9/9] fix middleware name --- app/routes/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/routes/index.js b/app/routes/index.js index d8026c62b..d3223e621 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -5,6 +5,7 @@ const reportMiddleware = require('../middleware/report'); const germlineMiddleware = require('../middleware/germlineSmallMutation/reports'); const authMiddleware = require('../middleware/auth'); const aclMiddleware = require('../middleware/acl'); +const keycloakUserManagement = require('../middleware/keycloakUserManagement'); // Get route files const APIVersion = require('./version'); @@ -62,8 +63,8 @@ class Routing extends RouterInterface { // Add keycloak 16 admin-cli token getter // To every route that uses that token (user management routes) // If keycloak user management is enabled - this.router.use('/user', userManagementMiddleware) - this.router.use('/user/member', userManagementMiddleware) + this.router.use('/user', keycloakUserManagement) + this.router.use('/user/:user/member', keycloakUserManagement) // Acl middleware // To every route except for specification and report routes