Skip to content

Commit

Permalink
Add admin page for session and user overview (#32)
Browse files Browse the repository at this point in the history
* Temp

* Added admin page

* Used standard exported

* Fixed margin on user details page

* Removed useless config

* Added trycatch to all admin page

* Updated getConfig
  • Loading branch information
rorylshanks authored Mar 22, 2024
1 parent b106778 commit e3ba027
Show file tree
Hide file tree
Showing 26 changed files with 569 additions and 27 deletions.
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

0 comments on commit e3ba027

Please sign in to comment.