oidc: use better json based file store for objects
This commit is contained in:
114
src/oidc.js
114
src/oidc.js
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user