180 lines
5.6 KiB
JavaScript
180 lines
5.6 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
add,
|
|
get,
|
|
update,
|
|
del,
|
|
|
|
delByAccessToken,
|
|
delExpired,
|
|
delByUserIdAndType,
|
|
|
|
listByUserId,
|
|
getByAccessToken,
|
|
|
|
validateTokenType,
|
|
|
|
hasScope,
|
|
|
|
// token client ids. we categorize them so we can have different restrictions based on the client
|
|
ID_WEBADMIN: 'cid-webadmin', // dashboard
|
|
ID_DEVELOPMENT: 'cid-development', // dashboard development
|
|
ID_CLI: 'cid-cli', // cloudron cli
|
|
ID_SDK: 'cid-sdk', // created by user via dashboard
|
|
|
|
SCOPES: ['*']//, 'apps', 'domains'],
|
|
};
|
|
|
|
const TOKENS_FIELDS = [ 'id', 'accessToken', 'identifier', 'clientId', 'scopeJson', 'expires', 'name', 'lastUsedTime' ].join(',');
|
|
|
|
const assert = require('assert'),
|
|
BoxError = require('./boxerror.js'),
|
|
database = require('./database.js'),
|
|
hat = require('./hat.js'),
|
|
safe = require('safetydance'),
|
|
uuid = require('uuid');
|
|
|
|
function postProcess(result) {
|
|
assert.strictEqual(typeof result, 'object');
|
|
|
|
result.scope = safe.JSON.parse(result.scopeJson) || {};
|
|
delete result.scopeJson;
|
|
|
|
return result;
|
|
}
|
|
|
|
function validateTokenName(name) {
|
|
assert.strictEqual(typeof name, 'string');
|
|
|
|
if (name.length > 64) return new BoxError(BoxError.BAD_FIELD, 'name too long');
|
|
|
|
return null;
|
|
}
|
|
|
|
function validateTokenType(type) {
|
|
assert.strictEqual(typeof type, 'string');
|
|
|
|
const types = [ exports.ID_WEBADMIN, exports.ID_SDK, exports.ID_DEVELOPMENT, exports.ID_CLI ];
|
|
if (types.indexOf(type) === -1) return new BoxError(BoxError.BAD_FIELD, `type must be one of ${types.join(',')}`);
|
|
|
|
return null;
|
|
}
|
|
|
|
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'`);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
async function add(token) {
|
|
assert.strictEqual(typeof token, 'object');
|
|
|
|
const { clientId, identifier, expires } = token;
|
|
const name = token.name || '';
|
|
const scope = token.scope || { '*': 'rw' };
|
|
|
|
let error = validateTokenName(name);
|
|
if (error) throw error;
|
|
|
|
error = validateScope(scope);
|
|
if (error) throw error;
|
|
|
|
const id = 'tid-' + uuid.v4();
|
|
const accessToken = token.accessToken || hat(8 * 32);
|
|
|
|
await database.query('INSERT INTO tokens (id, accessToken, identifier, clientId, expires, scopeJson, name) VALUES (?, ?, ?, ?, ?, ?, ?)', [ id, accessToken, identifier, clientId, expires, JSON.stringify(scope), name ]);
|
|
|
|
return { id, accessToken, scope, clientId, identifier, expires, name };
|
|
}
|
|
|
|
async function get(id) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
|
|
const result = await database.query(`SELECT ${TOKENS_FIELDS} FROM tokens WHERE id = ?`, [ id ]);
|
|
if (result.length === 0) return null;
|
|
|
|
return postProcess(result[0]);
|
|
}
|
|
|
|
async function del(id) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
|
|
const result = await database.query('DELETE FROM tokens WHERE id = ?', [ id ]);
|
|
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Token not found');
|
|
}
|
|
|
|
async function listByUserId(userId) {
|
|
assert.strictEqual(typeof userId, 'string');
|
|
|
|
const results = await database.query(`SELECT ${TOKENS_FIELDS} FROM tokens WHERE identifier = ?`, [ userId ]);
|
|
results.forEach(postProcess);
|
|
|
|
return results;
|
|
}
|
|
|
|
async function getByAccessToken(accessToken) {
|
|
assert.strictEqual(typeof accessToken, 'string');
|
|
|
|
const result = await database.query('SELECT ' + TOKENS_FIELDS + ' FROM tokens WHERE accessToken = ? AND expires > ?', [ accessToken, Date.now() ]);
|
|
if (result.length === 0) return null;
|
|
return postProcess(result[0]);
|
|
}
|
|
|
|
async function delByAccessToken(accessToken) {
|
|
assert.strictEqual(typeof accessToken, 'string');
|
|
|
|
const result = await database.query('DELETE FROM tokens WHERE accessToken = ?', [ accessToken ]);
|
|
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Token not found');
|
|
}
|
|
|
|
async function delExpired() {
|
|
const result = await database.query('DELETE FROM tokens WHERE expires <= ?', [ Date.now() ]);
|
|
return result.affectedRows;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
async function update(id, values) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
assert.strictEqual(typeof values, 'object');
|
|
|
|
const args = [ ];
|
|
const fields = [ ];
|
|
for (const k in values) {
|
|
fields.push(k + ' = ?');
|
|
args.push(values[k]);
|
|
}
|
|
args.push(id);
|
|
|
|
const result = await database.query('UPDATE tokens SET ' + fields.join(', ') + ' WHERE id = ?', args);
|
|
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Token not found');
|
|
}
|