Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ yarn-error.log
npm-error.log
/.vscode
/.idea
.env
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/.env

100 changes: 100 additions & 0 deletions impersonate.js
Original file line number Diff line number Diff line change
@@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be cool if this didn't always use the same client ID (think: audiophiler)

{
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,
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
41 changes: 41 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions routes/memberProjects.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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;
}
Comment on lines +102 to +109
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary query to DB


let user;
try {
user = await findUser(key.userId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pulls every attribute, which will be MUCH slower than just fetching attributes we want. Look at findUser in routes/users.js, notice how it only enumerates a few attributes we care about. I would make another function that just fetches a uid from a ipaUniqueID... It should be much faster

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add an optional attributes parameter to findUser? Would be nice if that got moved out into another file because I was being lazy and stuck the same function in routes/users.js and routes/memberProjects.js

} 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"];
Comment on lines +119 to +136
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having my jetbrains arc

💡 Loop can be simplified:

const uid = user.attributes.find(attribute => attribute.type == "uid")._vals[0].toString("utf8");

We should really use a different client for drink vs normal... One should grant read/write drink credits scope to /drink but no others. req.associationType will give you a hint for this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disappointed there's no way to search by ipaUniqueID on keycloak... 😢


res.json(await impersonate.getImpersonationToken(uid));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we added ipaUniqueID to the response too, I really want to encourage people to use that attribute where possible

});

module.exports = router;