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

Add admin page for session and user overview #32

Merged
merged 7 commits into from
Mar 22, 2024
Merged
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
2 changes: 1 addition & 1 deletion docker/supervisord-dev.conf
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ startsecs=2
priority=1

[program:auth]
command=npx nodemon app.js
command=npx nodemon app.js -e ejs,js,css,html,jpg,png,scss
autostart=true
autorestart=true
redirect_stderr=true
Expand Down
132 changes: 132 additions & 0 deletions lib/adminpage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import ejs from 'ejs';
import path from 'path';
import { getConfig, getRedirectBasepath } from '../util/config.js';
import redisHelper from '../util/redis.js'
import idp from './idp.js';
import errorpages from '../util/errorpage.js'
import { decodeJWT, createJWT } from '../util/jwt.js';
import log from '../util/logging.js'
import authz from './authz.js'

var redirectBasePath = getRedirectBasepath()

const route = {
from: "ADMIN_INTERNAL",
to: "ADMIN_INTERNAL"
}

async function verifyAdminJwt(req, res, next) {
try {
var jwt = req.get("X-Veriflow-Admin-Jwt")
var decodedJwt = await decodeJWT(jwt)
if (!jwt || !decodedJwt) {
var html = await errorpages.renderErrorPage(403, null, req)
log.access("invalidJwtUsedToAccessAdminPage", route, jwt, req)
res.status(403).send(html)
return
}
var allowedGroups = getConfig().admin.allowed_groups
var foundGroups = await authz.checkUserGroupMembership(decodedJwt, allowedGroups)
if (foundGroups.length > 0) {
next()
} else {
var html = await errorpages.renderErrorPage(403, null, req)
log.access("userIsDeniedToAdminPage", route, jwt, req)
res.status(403).send(html)
return
}
} catch (error) {
var html = await errorpages.renderErrorPage(403, null, req)
log.error({ message: "Unknown error occurred in verifyAdminJwt", context: { error: error.message, stack: error.stack } })
res.status(403).send(html)
return
}
}

async function renderSessionsPage(req, res) {
try {
var sessions = await redisHelper.getAllSessions()
var config = getConfig()
var logo_image_src = config?.ui?.logo_image_src || false
var html = await ejs.renderFile(path.join(process.cwd(), '/views/admin_sessions.ejs'),
{
sessions,
logo_image_src,
redirectBasePath
});
res.send(html)
} catch (error) {
var html = await errorpages.renderErrorPage(500, null, req)
log.error({ message: "Unknown error occurred in renderSessionsPage", context: { error: error.message, stack: error.stack } })
res.status(500).send(html)
}

}

async function renderUsersPage(req, res) {
try {
var usersObj = await idp.getAllUsers()
console.log(usersObj)
var users = []
for (var user of Object.keys(usersObj)) {
users.push(usersObj[user])
}
var config = getConfig()
var logo_image_src = config?.ui?.logo_image_src || false
var html = await ejs.renderFile(path.join(process.cwd(), '/views/admin_users.ejs'),
{
users,
logo_image_src,
redirectBasePath
});
res.send(html)
} catch (error) {
var html = await errorpages.renderErrorPage(500, null, req)
log.error({ message: "Unknown error occurred in renderUsersPage", context: { error: error.message, stack: error.stack } })
res.status(500).send(html)
}
}

async function renderUserDetailsPage(req, res) {
try {
var userId = req.query.id
if (!userId) {
res.redirect(redirectBasePath + "/asmin/users")
return
}
var user = await idp.getUserById(userId)
var userGroups = user.groups
delete user.groups
var config = getConfig()
var logo_image_src = config?.ui?.logo_image_src || false
var html = await ejs.renderFile(path.join(process.cwd(), '/views/admin_user_details.ejs'),
{
user,
userGroups,
logo_image_src,
redirectBasePath
});
res.send(html)
} catch (error) {
var html = await errorpages.renderErrorPage(500, null, req)
log.error({ message: "Unknown error occurred in renderUserDetailsPage", context: { error: error.message, stack: error.stack } })
res.status(500).send(html)
}

}

async function killSession(req, res) {
var sessionId = req.query.id
if (sessionId) {
await redisHelper.deleteSession(sessionId)
}
res.redirect(redirectBasePath + '/admin/sessions/')
}

export default {
renderSessionsPage,
killSession,
verifyAdminJwt,
renderUsersPage,
renderUserDetailsPage
}
3 changes: 2 additions & 1 deletion lib/authz.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,6 @@ async function getRequestHeaderMapConfig(user, route) {
}

export default {
authZRequest
authZRequest,
checkUserGroupMembership
}
28 changes: 21 additions & 7 deletions lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import crypto from 'crypto';
import errorpages from '../util/errorpage.js'
import metrics from '../util/metrics.js'
import { randomUUID } from 'node:crypto'
import admin from './adminpage.js'

var trusted_ranges = ["loopback"].concat(getConfig().trusted_ranges || [])
const config = getConfig()

var trusted_ranges = ["loopback"].concat(config.trusted_ranges || [])
log.debug({ message: `Setting trusted proxies to ${trusted_ranges}` })
app.set('trust proxy', trusted_ranges)

Expand All @@ -25,10 +28,10 @@ app.use(
store: redisHelper.getRedisStore(),
resave: false,
saveUninitialized: false,
secret: getConfig().cookie_secret,
secret: config.cookie_secret,
cookie: {
...defaultCookieOptions,
...getConfig().cookie_settings
...config.cookie_settings
}
})
)
Expand All @@ -41,13 +44,13 @@ const getPublicKeyFromPrivateKey = (privateKeyPem) => {
};

function generateJwks() {
let signing_key = getConfig().signing_key
let signing_key = config.signing_key
let buff = Buffer.from(signing_key, 'base64');
let pemPrivateKey = buff.toString('ascii');
const pemPublicKey = getPublicKeyFromPrivateKey(pemPrivateKey);
jwks = pem2jwk(pemPublicKey);
jwks.kid = getConfig().kid_override || "0"
jwks.alg = getConfig().signing_key_algorithm || "RS256"
jwks.kid = config.kid_override || "0"
jwks.alg = config.signing_key_algorithm || "RS256"
jwks.use = "sig"
}

Expand All @@ -73,21 +76,32 @@ app.get('/ping', (req, res) => {
res.sendStatus(200)
})

if (config.enable_admin_panel !== false) {
app.use(redirectBasePath + '/admin', admin.verifyAdminJwt)
app.get(redirectBasePath + '/admin/sessions', admin.renderSessionsPage)
app.get(redirectBasePath + '/admin/sessions/kill', admin.killSession)
app.get(redirectBasePath + '/admin/users', admin.renderUsersPage)
app.get(redirectBasePath + '/admin/users/details', admin.renderUserDetailsPage)
}



app.get(redirectBasePath + '/verify', ssoController.verifyAuth)
app.get(redirectBasePath + '/set', ssoController.setSessionCookie)

app.get(redirectBasePath + '/logout', async (req, res) => {
if (req.session.loggedin) {
await redisHelper.logUserOutAllSessions(req.session.userId)
}
req.session.destroy()
var html = await errorpages.renderErrorPage(200, "LOGOUT_SUCCESS", req)
res.send(html)
})

app.get(redirectBasePath + '/auth', ssoController.redirectToSsoProvider)
app.get(redirectBasePath + '/callback', ssoController.verifySsoCallback)

app.get(getConfig().jwks_path, (req, res) => {
app.get(config.jwks_path, (req, res) => {
res.json({
keys: [jwks],
});
Expand Down
8 changes: 7 additions & 1 deletion lib/idp.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ async function getUserById(userId) {
return user
}

async function getAllUsers() {
var users = await adapter.getAllUsers()
return users
}

async function addNewUserFromClaims(userClaims) {
if (!adapter.addNewUserFromClaims) {
log.debug({ message: `Adapter ${currentConfig.idp_provider} does not support adding new users via claims, returning`, context: { claims: userClaims } })
Expand All @@ -67,5 +72,6 @@ async function addNewUserFromClaims(userClaims) {
export default {
getUserById,
scheduleUpdate,
addNewUserFromClaims
addNewUserFromClaims,
getAllUsers
}
8 changes: 7 additions & 1 deletion lib/idp_adapters/googleworkspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,13 @@ async function getUserById(id) {
return config[id]
}

async function getAllUsers() {
var config = await getIdpConfig()
return config
}

export default {
runUpdate,
getUserById
getUserById,
getAllUsers
};
8 changes: 7 additions & 1 deletion lib/idp_adapters/localfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ async function getUserById(id) {
return config[id]
}

async function getAllUsers() {
var config = await getLocalIdpConfig()
return config
}

export default {
runUpdate,
getUserById
getUserById,
getAllUsers
};
7 changes: 6 additions & 1 deletion lib/idp_adapters/msgraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,13 @@ async function getUserById(id) {
return config[id]
}

async function getAllUsers() {
var config = await getIdpConfig()
return config
}

export default {
runUpdate,
getUserById
getUserById,
getAllUsers
};
31 changes: 30 additions & 1 deletion lib/idp_adapters/tokenclaims.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,37 @@ async function addNewUserFromClaims(claims) {
await redisClient.expire(`veriflow:users:${userId}`, 87000); // expire in 24 hours
}

async function getAllUsers() {
const keys = await scanKeys('veriflow:users:*');
const users = [];
for (const key of keys) {
const userDataRaw = await redisClient.get(key);
if (userDataRaw) {
let userData = JSON.parse(userDataRaw);
// Extract session ID from key and add it to the session object
const userId = key.replace('veriflow:users:', '');
userData.userId = userId;
users.push(userData);
}
}
return users;
}

// Utility function to use SCAN for fetching keys without blocking the server
async function scanKeys(pattern) {
let cursor = '0';
const keys = [];
do {
const reply = await redisClient.scan(cursor, 'MATCH', pattern, 'COUNT', '100');
cursor = reply[0];
keys.push(...reply[1]);
} while (cursor !== '0');
return keys;
}

export default {
runUpdate,
getUserById,
addNewUserFromClaims
addNewUserFromClaims,
getAllUsers
};
Loading
Loading