Files
cloudron-box/src/tokens.js

210 lines
6.5 KiB
JavaScript
Raw Normal View History

2020-02-06 16:57:33 +01:00
'use strict';
exports = module.exports = {
2021-03-15 12:47:57 -07:00
add,
get,
update,
del,
2021-06-04 09:28:40 -07:00
delByAccessToken,
delExpired,
delByUserIdAndType,
2021-06-04 09:28:40 -07:00
listByUserId,
2021-03-15 12:47:57 -07:00
getByAccessToken,
2020-02-06 16:57:33 +01:00
2022-09-23 12:57:13 +02:00
hasScope,
2025-03-07 11:53:03 +01:00
isIpAllowedSync,
2022-09-23 12:57:13 +02:00
SCOPES: ['*']//, 'apps', 'domains'],
2020-02-06 16:57:33 +01:00
};
2025-03-07 11:53:03 +01:00
const TOKENS_FIELDS = [ 'id', 'accessToken', 'identifier', 'clientId', 'scopeJson', 'expires', 'name', 'lastUsedTime', 'allowedIpRanges' ].join(',');
2021-06-04 09:28:40 -07:00
const assert = require('node:assert'),
2020-02-06 16:57:33 +01:00
BoxError = require('./boxerror.js'),
crypto = require('node:crypto'),
2021-06-04 09:28:40 -07:00
database = require('./database.js'),
2020-02-06 16:57:33 +01:00
hat = require('./hat.js'),
2025-05-06 16:16:33 +02:00
ipaddr = require('./ipaddr.js'),
safe = require('safetydance');
2020-02-06 16:57:33 +01:00
2025-03-07 11:53:03 +01:00
const gParsedRangesCache = new Map(); // indexed by token.id
2022-09-22 21:58:56 +02:00
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
result.scope = safe.JSON.parse(result.scopeJson) || {};
delete result.scopeJson;
2022-09-22 21:58:56 +02:00
return result;
}
2020-02-06 16:57:33 +01:00
function validateTokenName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length > 64) return new BoxError(BoxError.BAD_FIELD, 'name too long');
2020-02-06 16:57:33 +01:00
return null;
}
2022-09-22 21:58:56 +02:00
function validateScope(scope) {
assert.strictEqual(typeof scope, 'object');
for (const key in scope) {
if (exports.SCOPES.indexOf(key) === -1) return new BoxError(BoxError.BAD_FIELD, `Unkown token scope ${key}. Valid scopes are ${exports.SCOPES.join(',')}`);
if (scope[key] !== 'rw' && scope[key] !== 'r') return new BoxError(BoxError.BAD_FIELD, `Unkown token scope value ${scope[key]} for ${key}. Valid values are 'rw' or 'r'`);
2022-09-22 21:58:56 +02:00
}
return null;
}
2022-09-23 12:57:13 +02:00
function hasScope(token, method, path) {
assert.strictEqual(typeof token, 'object');
assert.strictEqual(typeof method, 'string');
assert.strictEqual(typeof path, 'string');
// TODO path checking in the future
const matchAll = token.scope['*'];
if (matchAll === 'rw') {
return true;
} else if (matchAll === 'r' && (method === 'GET' || method === 'HEAD' || method === 'OPTIONS')) {
return true;
} else {
return false;
}
}
2025-03-07 11:53:03 +01:00
function parseIpRanges(ipRanges) {
assert.strictEqual(typeof ipRanges, 'string');
if (ipRanges.length === 0) return ['0.0.0.0/0', '::/0'];
const result = [];
for (const line of ipRanges.split('\n')) {
if (!line || line.startsWith('#')) continue;
// each line can have comma separated list. this complexity is because we changed the UI to take a line input instead of textarea
for (const entry of line.split(',')) {
const rangeOrIP = entry.trim();
2025-09-08 18:59:47 +02:00
if (!ipaddr.isValid(rangeOrIP) && !ipaddr.isValidCIDR(rangeOrIP)) throw new BoxError(BoxError.BAD_FIELD, `'${rangeOrIP}' is not a valid IP or range`);
2025-03-07 11:53:03 +01:00
result.push(rangeOrIP);
}
}
return result;
}
2021-06-04 09:28:40 -07:00
async function add(token) {
assert.strictEqual(typeof token, 'object');
2025-03-07 11:53:03 +01:00
const { clientId, identifier, expires, allowedIpRanges } = token;
2021-06-04 09:28:40 -07:00
const name = token.name || '';
2022-09-22 21:58:56 +02:00
const scope = token.scope || { '*': 'rw' };
let error = validateTokenName(name);
if (error) throw error;
error = validateScope(scope);
2021-06-04 09:28:40 -07:00
if (error) throw error;
2025-03-07 11:53:03 +01:00
parseIpRanges(allowedIpRanges); // validate
const id = 'tid-' + crypto.randomUUID();
2023-06-02 20:47:36 +02:00
const accessToken = token.accessToken || hat(8 * 32);
2021-06-04 09:28:40 -07:00
2025-03-07 11:53:03 +01:00
await database.query('INSERT INTO tokens (id, accessToken, identifier, clientId, expires, scopeJson, name, allowedIpRanges) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [ id, accessToken, identifier, clientId, expires, JSON.stringify(scope), name, allowedIpRanges ]);
2021-06-04 09:28:40 -07:00
2025-03-07 11:53:03 +01:00
return { id, accessToken, scope, clientId, identifier, expires, name, allowedIpRanges };
2020-02-06 16:57:33 +01:00
}
2020-02-07 16:20:05 +01:00
2021-06-04 09:28:40 -07:00
async function get(id) {
2020-02-07 16:20:05 +01:00
assert.strictEqual(typeof id, 'string');
2021-06-04 09:28:40 -07:00
const result = await database.query(`SELECT ${TOKENS_FIELDS} FROM tokens WHERE id = ?`, [ id ]);
if (result.length === 0) return null;
2020-02-07 16:20:05 +01:00
2022-09-22 21:58:56 +02:00
return postProcess(result[0]);
2020-02-07 16:20:05 +01:00
}
2021-06-04 09:28:40 -07:00
async function del(id) {
2020-02-07 16:20:05 +01:00
assert.strictEqual(typeof id, 'string');
2021-06-04 09:28:40 -07:00
const result = await database.query('DELETE FROM tokens WHERE id = ?', [ id ]);
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Token not found');
2020-02-07 16:20:05 +01:00
}
2021-06-04 09:28:40 -07:00
async function listByUserId(userId) {
2020-02-07 16:20:05 +01:00
assert.strictEqual(typeof userId, 'string');
2022-09-22 21:58:56 +02:00
const results = await database.query(`SELECT ${TOKENS_FIELDS} FROM tokens WHERE identifier = ?`, [ userId ]);
results.forEach(postProcess);
return results;
2021-06-04 09:28:40 -07:00
}
async function getByAccessToken(accessToken) {
assert.strictEqual(typeof accessToken, 'string');
2020-02-07 16:20:05 +01:00
2025-03-07 11:53:03 +01:00
const result = await database.query(`SELECT ${TOKENS_FIELDS} FROM tokens WHERE accessToken = ? AND expires > ?`, [ accessToken, Date.now() ]);
2021-06-04 09:28:40 -07:00
if (result.length === 0) return null;
2022-09-22 21:58:56 +02:00
return postProcess(result[0]);
2020-02-07 16:20:05 +01:00
}
2021-03-15 12:47:57 -07:00
2021-06-04 09:28:40 -07:00
async function delByAccessToken(accessToken) {
assert.strictEqual(typeof accessToken, 'string');
2021-03-15 12:47:57 -07:00
2021-06-04 09:28:40 -07:00
const result = await database.query('DELETE FROM tokens WHERE accessToken = ?', [ accessToken ]);
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Token not found');
}
2021-03-15 12:47:57 -07:00
2021-06-04 09:28:40 -07:00
async function delExpired() {
const result = await database.query('DELETE FROM tokens WHERE expires <= ?', [ Date.now() ]);
return result.affectedRows;
2021-03-15 12:47:57 -07:00
}
async function delByUserIdAndType(userId, type) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof type, 'string');
const result = await database.query('DELETE FROM tokens WHERE identifier=? AND clientId=?', [ userId, type ]);
return result.affectedRows;
}
2021-06-04 09:28:40 -07:00
async function update(id, values) {
2021-03-15 12:47:57 -07:00
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof values, 'object');
2025-02-02 11:45:48 +01:00
const args = [ ];
const fields = [ ];
for (const k in values) {
2021-06-04 09:28:40 -07:00
fields.push(k + ' = ?');
args.push(values[k]);
}
args.push(id);
2025-03-07 11:53:03 +01:00
const result = await database.query(`UPDATE tokens SET ${fields.join(', ')} WHERE id = ?`, args);
2021-06-04 09:28:40 -07:00
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Token not found');
}
2025-03-07 11:53:03 +01:00
function isIpAllowedSync(token, ip) {
assert.strictEqual(typeof token, 'object');
assert.strictEqual(typeof ip, 'string');
let allowedIpRanges = gParsedRangesCache.get(token.id); // returns undefined if not found
if (!allowedIpRanges) {
allowedIpRanges = parseIpRanges(token.allowedIpRanges || '');
gParsedRangesCache.set(token.id, allowedIpRanges);
}
for (const ipOrRange of allowedIpRanges) {
if (!ipOrRange.includes('/')) {
2025-05-06 16:16:33 +02:00
if (ipaddr.isEqual(ipOrRange, ip)) return true;
2025-03-07 11:53:03 +01:00
} else {
2025-05-06 16:16:33 +02:00
if (ipaddr.includes(ipOrRange, ip)) return true;
2025-03-07 11:53:03 +01:00
}
}
return false;
}