Skip to content

Commit

Permalink
Added admin page
Browse files Browse the repository at this point in the history
  • Loading branch information
rorylshanks committed Mar 22, 2024
1 parent f4b45b5 commit d5af731
Show file tree
Hide file tree
Showing 20 changed files with 421 additions and 82 deletions.
93 changes: 91 additions & 2 deletions lib/adminpage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,94 @@ 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'

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 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) {
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
sessions,
logo_image_src,
redirectBasePath
});
res.send(html)
}

async function renderUsersPage(req, res) {
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)
}

async function renderUserDetailsPage(req, res) {
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)
}
Expand All @@ -23,7 +103,16 @@ async function killSession(req, res) {
res.redirect(redirectBasePath + '/admin/sessions/')
}

async function checkUserGroupMembership(user, groups) {
// FIXME Change this to be a set in memory as part of the idpUpdate
let set = new Set(user.groups);
return groups.filter(item => set.has(item));
}

export default {
renderSessionsPage,
killSession
killSession,
verifyAdminJwt,
renderUsersPage,
renderUserDetailsPage
}
5 changes: 4 additions & 1 deletion lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,11 @@ app.get('/ping', (req, res) => {
})

if (config.enable_admin_panel !== false) {
app.get(redirectBasePath + '/admin/sessions/', admin.renderSessionsPage)
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)
}


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
};
7 changes: 6 additions & 1 deletion test/e2e/configs/dex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ staticPasswords:
# bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2)
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "test"
userID: "[email protected]"
userID: "[email protected]"
- email: "[email protected]"
# bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2)
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "denied"
userID: "[email protected]"
11 changes: 11 additions & 0 deletions test/e2e/configs/idp_output.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,16 @@
"All Users",
"test-header-group"
]
},
"[email protected]": {
"displayName": "Veriflow Denied User",
"givenName": "Denied",
"preferredLanguage": "en",
"surname": "User",
"userPrincipalName": "[email protected]",
"id": "[email protected]",
"groups": [
"No Users"
]
}
}
4 changes: 4 additions & 0 deletions test/e2e/configs/veriflow.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
---
admin:
enable: true
allowed_groups:
- All Users
data_listen_port: 80
metrics_listen_port: 9090
service_url: http://veriflow.localtest.me
Expand Down
17 changes: 17 additions & 0 deletions test/e2e/tests/admin_page_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Feature('Admin Page').retry(3);

Scenario('I can login to admin page', async ({ I }) => {
I.amOnPage('http://veriflow.localtest.me/.veriflow/admin/sessions');
I.login();
I.see("Sessions")
});

Scenario('I cant login to admin page as non admin', async ({ I }) => {
I.amOnPage('http://veriflow.localtest.me/.veriflow/admin/sessions');
I.fillField('login', '[email protected]');
I.fillField('password', 'password');
I.click('Login');
I.see("Unauthorized")
});


25 changes: 19 additions & 6 deletions util/caddyModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,18 +304,26 @@ async function saturateAllRoutesFromConfig(config) {
async function generateCaddyConfig() {
log.debug("Generating new caddy config")
var config = getConfig()
if (config.enable_admin_panel !== false) {

if (config?.admin?.enable !== false && config?.admin?.allowed_groups) {
log.info("Admin panel will be enabled")
var serviceUrl = config.service_url
var baseRedirectUrl = getRedirectBasepath()
var adminUrl = new URL(`https://${serviceUrl}${baseRedirectUrl}/admin`)
var adminUrl = new URL(`${serviceUrl}${baseRedirectUrl}/admin`)
var adminPanelRoute = {
from: {
host: adminUrl.hostname,
path: adminUrl.pathname
host: [adminUrl.hostname],
path: [getRedirectBasepath() + "/admin/*"]
},
to: "localhost:" + getAuthListenPort()
to: "http://localhost:" + getAuthListenPort(),
allowed_groups: config.admin.allowed_groups,
claims_headers: {
"X-Veriflow-Admin-Jwt": "jwt"
}
}
config.policy.unshift(adminPanelRoute)
}

var routes = await saturateAllRoutesFromConfig(config)

var requestIdRoute = {
Expand Down Expand Up @@ -381,7 +389,12 @@ async function generateCaddyConfig() {
serviceUrl.hostname
],
"path": [

"/ping",
getRedirectBasepath() + "/verify",
getRedirectBasepath() + "/set",
getRedirectBasepath() + "/logout",
getRedirectBasepath() + "/auth",
getRedirectBasepath() + "/callback"
]
}
],
Expand Down
Loading

0 comments on commit d5af731

Please sign in to comment.