oidc: use better json based file store for objects

This commit is contained in:
Johannes Zellner
2023-03-24 20:08:17 +01:00
parent 44706b9c70
commit 99e0979c2e
7 changed files with 129 additions and 47 deletions

View File

@@ -3,6 +3,7 @@
exports = module.exports = {
start,
stop,
revokeByUserId,
clients: {
add: clientsAdd,
get: clientsGet,
@@ -97,6 +98,69 @@ async function clientsList() {
return results;
}
// -----------------------------
// Basic in-memory json file backed based data store
// -----------------------------
const DATA_STORE = {};
function load(modelName) {
assert.strictEqual(typeof modelName, 'string');
if (DATA_STORE[modelName]) return;
const filePath = path.join(paths.OIDC_STORE_DIR, `${modelName}.json`);
debug(`load: model ${modelName} based on ${filePath}.`);
let data = {};
try {
data = JSON.parse(fs.readFileSync(filePath), 'utf8');
} catch (e) {
debug(`load: failed to read ${filePath}, start with new one.`, e);
}
DATA_STORE[modelName] = data;
}
function save(modelName) {
assert.strictEqual(typeof modelName, 'string');
if (!DATA_STORE[modelName]) return;
const filePath = path.join(paths.OIDC_STORE_DIR, `${modelName}.json`);
debug(`save: model ${modelName} to ${filePath}.`);
try {
fs.writeFileSync(filePath, JSON.stringify(DATA_STORE[modelName]), 'utf8');
} catch (e) {
debug(`revokeByUserId: failed to write ${filePath}`, e);
}
}
// -----------------------------
// Session, Grant and Token management
// -----------------------------
// This is based on the same storage as the below CloudronAdapter
async function revokeByUserId(userId) {
assert.strictEqual(typeof userId, 'string');
debug(`revokeByUserId: userId:${userId}`);
function revokeObjects(modelName) {
load(modelName);
for (let id in DATA_STORE[modelName]) {
if (DATA_STORE[modelName][id].payload?.accountId === userId) delete DATA_STORE[modelName][id];
}
save(modelName);
}
revokeObjects('Session');
revokeObjects('Grant');
revokeObjects('AuthorizationCode');
revokeObjects('AccessToken');
}
// -----------------------------
// Generic oidc node module data store model
// -----------------------------
@@ -115,22 +179,10 @@ class CloudronAdapter {
constructor(name) {
this.name = name;
if (this.name === 'Client') {
this.store = null;
this.fileStorePath = null;
} else {
this.fileStorePath = path.join(paths.OIDC_STORE_DIR, `${name}.json`);
debug(`Creating storage adapter for ${name}`);
debug(`Creating storage adapter for ${name} backed by ${this.fileStorePath}`);
let data = {};
try {
data = JSON.parse(fs.readFileSync(this.fileStorePath), 'utf8');
} catch (e) {
debug(`filestore for adapter ${name} not found, start with new one`);
}
this.store = data;
if (this.name !== 'Client') {
load(name);
}
}
@@ -152,8 +204,8 @@ class CloudronAdapter {
if (this.name === 'Client') {
console.log('WARNING!! this should not happen as it is stored in our db');
} else {
this.store[id] = { id, expiresIn, payload, consumed: false };
if (this.fileStorePath) fs.writeFileSync(this.fileStorePath, JSON.stringify(this.store), 'utf8');
DATA_STORE[this.name][id] = { id, expiresIn, payload, consumed: false };
save(this.name);
}
}
@@ -191,11 +243,11 @@ class CloudronAdapter {
return tmp;
} else {
if (!this.store[id]) return null;
if (!DATA_STORE[this.name][id]) return null;
debug(`[${this.name}] find id:${id}`, this.store[id]);
debug(`[${this.name}] find id:${id}`, DATA_STORE[this.name][id]);
return this.store[id].payload;
return DATA_STORE[this.name][id].payload;
}
}
@@ -230,8 +282,8 @@ class CloudronAdapter {
if (this.name === 'Client') {
console.log('WARNING!! this should not happen as it is stored in our db');
} else {
for (let d in this.store) {
if (this.store[d].payload.uid === uid) return this.store[d].payload;
for (let d in DATA_STORE[this.name]) {
if (DATA_STORE[this.name][d].payload.uid === uid) return DATA_STORE[this.name][d].payload;
}
return false;
@@ -255,9 +307,8 @@ class CloudronAdapter {
if (this.name === 'Client') {
console.log('WARNING!! this should not happen as it is stored in our db');
} else {
if (this.store[id]) this.store[id].consumed = true;
if (this.fileStorePath) fs.writeFileSync(this.fileStorePath, JSON.stringify(this.store), 'utf8');
if (DATA_STORE[this.name][id]) DATA_STORE[this.name][id].consumed = true;
save(this.name);
}
}
@@ -277,9 +328,8 @@ class CloudronAdapter {
if (this.name === 'Client') {
console.log('WARNING!! this should not happen as it is stored in our db');
} else {
delete this.store[id];
if (this.fileStorePath) fs.writeFileSync(this.fileStorePath, JSON.stringify(this.store), 'utf8');
delete DATA_STORE[this.name][id];
save(this.name);
}
}
@@ -299,10 +349,10 @@ class CloudronAdapter {
if (this.name === 'Client') {
console.log('WARNING!! this should not happen as it is stored in our db');
} else {
for (let d in this.store) {
if (this.store[d].grantId === grantId) {
delete this.store[d];
return;
for (let d in DATA_STORE[this.name]) {
if (DATA_STORE[this.name][d].grantId === grantId) {
delete DATA_STORE[this.name][d];
return save(this.name);
}
}
}

View File

@@ -17,7 +17,7 @@ exports = module.exports = {
mailserver: require('./mailserver.js'),
network: require('./network.js'),
notifications: require('./notifications.js'),
oidcclients: require('./oidcclients.js'),
oidc: require('./oidc.js'),
profile: require('./profile.js'),
provision: require('./provision.js'),
services: require('./services.js'),

View File

@@ -1,11 +1,15 @@
'use strict';
exports = module.exports = {
get,
list,
add,
update,
remove
clients: {
get,
list,
add,
update,
del
},
destroyUserSession
};
const assert = require('assert'),
@@ -78,7 +82,7 @@ async function list(req, res, next) {
next(new HttpSuccess(200, { clients: result }));
}
async function remove(req, res, next) {
async function del(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
const [error] = await safe(oidc.clients.del(req.params.clientId));
@@ -86,3 +90,12 @@ async function remove(req, res, next) {
next(new HttpSuccess(204));
}
async function destroyUserSession(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
const [error] = await safe(oidc.revokeByUserId(req.user.id));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
}

View File

@@ -369,11 +369,14 @@ async function initializeExpressSync() {
router.get ('/well-known-handler/*', routes.wellknown.get);
// OpenID connect clients
router.get ('/api/v1/oidc/clients', token, authorizeAdmin, routes.oidcclients.list);
router.post('/api/v1/oidc/clients', json, token, authorizeAdmin, routes.oidcclients.add);
router.get ('/api/v1/oidc/clients/:clientId', token, authorizeAdmin, routes.oidcclients.get);
router.post('/api/v1/oidc/clients/:clientId', json, token, authorizeAdmin, routes.oidcclients.update);
router.del ('/api/v1/oidc/clients/:clientId', token, authorizeAdmin, routes.oidcclients.remove);
router.get ('/api/v1/oidc/clients', token, authorizeAdmin, routes.oidc.clients.list);
router.post('/api/v1/oidc/clients', json, token, authorizeAdmin, routes.oidc.clients.add);
router.get ('/api/v1/oidc/clients/:clientId', token, authorizeAdmin, routes.oidc.clients.get);
router.post('/api/v1/oidc/clients/:clientId', json, token, authorizeAdmin, routes.oidc.clients.update);
router.del ('/api/v1/oidc/clients/:clientId', token, authorizeAdmin, routes.oidc.clients.del);
// OpenID connect sessions
router.del ('/api/v1/oidc/sessions', token, authorizeUser, routes.oidc.destroyUserSession);
// disable server socket "idle" timeout. we use the timeout middleware to handle timeouts on a route level
// we rely on nginx for timeouts on the TCP level (see client_header_timeout)