diff --git a/.gitignore b/.gitignore index 0d630a1..3f03d8e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ yarn-error.log npm-error.log /.vscode /.idea +.env diff --git a/impersonate.js b/impersonate.js new file mode 100644 index 0000000..9a60fa8 --- /dev/null +++ b/impersonate.js @@ -0,0 +1,100 @@ +const fetchPromise = import("node-fetch"); + +async function getSaToken() { + const fetch = (await fetchPromise).default; + + const resp = await fetch( + "https://sso.csh.rit.edu/auth/realms/master/protocol/openid-connect/token", + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${Buffer.from( + process.env.GK_SA_USERNAME + ":" + process.env.GK_SA_PASSWORD + ).toString("base64")}`, + }, + body: "grant_type=client_credentials", + } + ); + const json = await resp.json(); + + return json["access_token"]; +} + +async function getUidFromUsername(username, saToken) { + const fetch = (await fetchPromise).default; + + const resp = await fetch( + "https://sso.csh.rit.edu/auth/admin/realms/csh/users?username=" + username, + { + headers: { + Authorization: `Bearer ${saToken}`, + }, + } + ); + const json = await resp.json(); + + return json[0]["id"]; +} + +async function getImpersonationSession(userId, saToken) { + const fetch = (await fetchPromise).default; + + const resp = await fetch( + `https://sso.csh.rit.edu/auth/admin/realms/csh/users/${userId}/impersonation`, + { + method: "POST", + headers: { + Authorization: `Bearer ${saToken}`, + }, + } + ); + const headers = resp.headers; + const cookies = headers.get("set-cookie"); + + const identityRegex = /KEYCLOAK_IDENTITY=\S+/; + const sessionRegex = /KEYCLOAK_SESSION=\S+/; + + const identity = identityRegex + .exec(cookies)[0] + .split("=")[1] + .replace(";", ""); + const session = sessionRegex.exec(cookies)[0].split("=")[1].replace(";", ""); + + return {identity, session}; +} + +async function getUserToken(identity, session) { + const fetch = (await fetchPromise).default; + + const resp = await fetch( + "https://sso.csh.rit.edu/auth/realms/csh/protocol/openid-connect/auth?client_id=gatekeeper&response_type=token&response_mode=fragment&redirect_uri=https%3A%2F%2Fgatekeeper.csh.rit.edu%2Fcallback", + { + method: "GET", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Cookie: `KEYCLOAK_SESSION=${session}; KEYCLOAK_IDENTITY=${identity};`, + }, + redirect: "manual", + } + ); + + const headers = resp.headers; + const location = new URL(headers.get("location").replace("#", "?")); + + let accessToken = location.searchParams.get("access_token"); + + return accessToken; +} + +async function getImpersonationToken(userId) { + const saToken = await getSaToken(); + const uid = await getUidFromUsername(userId, saToken); + const {identity, session} = await getImpersonationSession(uid, saToken); + const accessToken = await getUserToken(identity, session); + return {uid, userId, accessToken}; +} + +module.exports = { + getImpersonationToken, +}; diff --git a/package.json b/package.json index 82fff09..63dee2b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "ldapjs": "^2.3.1", "mongodb": "^3.6.9", "morgan": "^1.10.0", - "mqtt": "^4.2.6" + "mqtt": "^4.2.6", + "node-fetch": "^3.2.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93e2f59..2d0dc13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ specifiers: mongodb: ^3.6.9 morgan: ^1.10.0 mqtt: ^4.2.6 + node-fetch: ^3.2.3 dependencies: body-parser: 1.19.0 @@ -15,6 +16,7 @@ dependencies: mongodb: 3.6.9 morgan: 1.10.0 mqtt: 4.2.6 + node-fetch: 3.2.3 packages: @@ -179,6 +181,11 @@ packages: resolution: {integrity: sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=} dev: false + /data-uri-to-buffer/4.0.0: + resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==} + engines: {node: '>= 12'} + dev: false + /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} dependencies: @@ -294,6 +301,14 @@ packages: engines: {'0': node >=0.6.0} dev: false + /fetch-blob/3.1.5: + resolution: {integrity: sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==} + engines: {node: ^12.20 || >= 14.13} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.2.0 + dev: false + /finalhandler/1.1.2: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} @@ -307,6 +322,13 @@ packages: unpipe: 1.0.0 dev: false + /formdata-polyfill/4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + dependencies: + fetch-blob: 3.1.5 + dev: false + /forwarded/0.1.2: resolution: {integrity: sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=} engines: {node: '>= 0.6'} @@ -637,6 +659,20 @@ packages: engines: {node: '>= 0.6'} dev: false + /node-domexception/1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: false + + /node-fetch/3.2.3: + resolution: {integrity: sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + data-uri-to-buffer: 4.0.0 + fetch-blob: 3.1.5 + formdata-polyfill: 4.0.10 + dev: false + /on-finished/2.3.0: resolution: {integrity: sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=} engines: {node: '>= 0.8'} @@ -945,6 +981,11 @@ packages: extsprintf: 1.4.0 dev: false + /web-streams-polyfill/3.2.0: + resolution: {integrity: sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==} + engines: {node: '>= 8'} + dev: false + /wrappy/1.0.2: resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} dev: false diff --git a/routes/memberProjects.js b/routes/memberProjects.js index 1d4a223..ec7bfc7 100644 --- a/routes/memberProjects.js +++ b/routes/memberProjects.js @@ -1,5 +1,6 @@ const router = require("express").Router(); const ldap = require("../ldap"); +const impersonate = require("../impersonate"); function findUser(id) { return new Promise((resolve, reject) => { @@ -88,4 +89,53 @@ router.get("/by-key/:associationId", async (req, res) => { }); }); +router.get("/impersonate/:associationId", async (req, res) => { + const key = await req.ctx.db.collection("keys").findOne({ + [req.associationType]: {$eq: req.params.associationId}, + }); + + if (!key) { + res.status(404).json({message: "Not found"}); + return; + } + + const userDocument = await req.ctx.db.collection("users").findOne({ + id: {$eq: key.userId}, + disabled: {$ne: true}, + }); + if (!userDocument) { + res.status(404).json({message: "User not found or disabled"}); + return; + } + + let user; + try { + user = await findUser(key.userId); + } catch (err) { + res.status(500).json({message: "Internal server error"}); + return; + } + + const response = {}; + for (const attribute of user.attributes) { + if (attribute.type == "jpegPhoto") { + response[attribute.type] = attribute._vals[0].toString("base64"); + } else { + const values = attribute._vals.map((value) => value.toString("utf8")); + if (ARRAYS.has(attribute.type)) { + response[attribute.type] = values; + } else { + if (values.length > 1) { + console.warn(`${attribute.type} has many values!!`); + } + response[attribute.type] = values.join(","); + } + } + } + + const uid = response["uid"]; + + res.json(await impersonate.getImpersonationToken(uid)); +}); + module.exports = router;