diff --git a/migrations/20220922194751-tokens-add-scopeJson.js b/migrations/20220922194751-tokens-add-scopeJson.js new file mode 100644 index 000000000..e674a84e2 --- /dev/null +++ b/migrations/20220922194751-tokens-add-scopeJson.js @@ -0,0 +1,13 @@ +'use strict'; + +exports.up = async function (db) { + await db.runSql('ALTER TABLE tokens DROP COLUMN scope'); + await db.runSql('ALTER TABLE tokens ADD COLUMN scopeJson TEXT'); + + await db.runSql('UPDATE tokens SET scopeJson = ?', [ JSON.stringify({'*':'rw'})]); +}; + +exports.down = async function (db) { + await db.runSql('ALTER TABLE tokens ADD COLUMN scope VARCHAR(512) NOT NULL DEFAULT ""'); + await db.runSql('ALTER TABLE tokens DROP COLUMN scopeJson'); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index aa62973d9..394079df6 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -58,7 +58,7 @@ CREATE TABLE IF NOT EXISTS tokens( accessToken VARCHAR(128) NOT NULL UNIQUE, identifier VARCHAR(128) NOT NULL, // resourceId: app id or user id clientId VARCHAR(128), - scope VARCHAR(512) NOT NULL, + scopeJson TEXT, expires BIGINT NOT NULL, // FIXME: make this a timestamp lastUsedTime TIMESTAMP NULL, PRIMARY KEY(accessToken)); diff --git a/src/routes/tokens.js b/src/routes/tokens.js index 2d91a8836..8df7406f2 100644 --- a/src/routes/tokens.js +++ b/src/routes/tokens.js @@ -50,10 +50,12 @@ async function add(req, res, next) { if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string')); if ('expiresAt' in req.body && typeof req.body.expiresAt !== 'number') return next(new HttpError(400, 'expiresAt must be number')); + if ('scope' in req.body && typeof req.body.scope !== 'object') return next(new HttpError(400, 'scope must be an object')); const expiresAt = req.body.expiresAt || (Date.now() + (100 * 365 * 24 * 60 * 60 * 1000)); // forever - 100 years TODO maybe we should allow 0 or -1 to make that explicit + const scope = req.body.scope || null; - const [error, result] = await safe(tokens.add({ clientId: tokens.ID_SDK, identifier: req.user.id, expires: expiresAt, name: req.body.name })); + const [error, result] = await safe(tokens.add({ clientId: tokens.ID_SDK, identifier: req.user.id, expires: expiresAt, name: req.body.name, scope })); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(201, result)); diff --git a/src/server.js b/src/server.js index 6be8f6ea8..8ec5a2e61 100644 --- a/src/server.js +++ b/src/server.js @@ -164,7 +164,7 @@ function initializeExpressSync() { router.get ('/api/v1/app_passwords/:id', token, routes.appPasswords.get); router.del ('/api/v1/app_passwords/:id', token, routes.appPasswords.del); - // access tokens + // access tokenss router.get ('/api/v1/tokens', token, routes.tokens.list); router.post('/api/v1/tokens', json, token, routes.tokens.add); router.get ('/api/v1/tokens/:id', token, routes.tokens.verifyOwnership, routes.tokens.get); diff --git a/src/tokens.js b/src/tokens.js index 9fd80d7b1..1afe6b13c 100644 --- a/src/tokens.js +++ b/src/tokens.js @@ -18,16 +18,27 @@ exports = module.exports = { // token client ids. we categorize them so we can have different restrictions based on the client ID_WEBADMIN: 'cid-webadmin', // dashboard ID_SDK: 'cid-sdk', // created by user via dashboard + + SCOPES: ['*', 'apps', 'domains'], }; -const TOKENS_FIELDS = [ 'id', 'accessToken', 'identifier', 'clientId', 'scope', 'expires', 'name', 'lastUsedTime' ].join(','); +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) || {}; + + return result; +} + function validateTokenName(name) { assert.strictEqual(typeof name, 'string'); @@ -45,19 +56,34 @@ function validateTokenType(type) { return null; } +function validateScope(scope) { + assert.strictEqual(typeof scope, 'object'); + + for (const key in scope) { + if (exports.SCOPES.indexOf(key) === -1) return BoxError(BoxError.BAD_FIELD, `Unkown token scope ${key}. Valid scopes are ${exports.SCOPES.join(',')}`); + if (scope[key] !== 'rw' && scope[key] !== 'r') return BoxError(BoxError.BAD_FIELD, `Unkown token scope value ${scope[key]} for ${key}. Valid values are 'rw' or 'r'`); + } + + return null; +} + async function add(token) { assert.strictEqual(typeof token, 'object'); const { clientId, identifier, expires } = token; const name = token.name || ''; - const error = validateTokenName(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 = hat(8 * 32); - const scope = 'unused'; - await database.query('INSERT INTO tokens (id, accessToken, identifier, clientId, expires, scope, name) VALUES (?, ?, ?, ?, ?, ?, ?)', [ id, accessToken, identifier, clientId, expires, scope, name ]); + 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 }; } @@ -68,7 +94,7 @@ async function get(id) { const result = await database.query(`SELECT ${TOKENS_FIELDS} FROM tokens WHERE id = ?`, [ id ]); if (result.length === 0) return null; - return result[0]; + return postProcess(result[0]); } async function del(id) { @@ -81,7 +107,10 @@ async function del(id) { async function listByUserId(userId) { assert.strictEqual(typeof userId, 'string'); - return await database.query(`SELECT ${TOKENS_FIELDS} FROM tokens WHERE identifier = ?`, [ userId ]); + const results = await database.query(`SELECT ${TOKENS_FIELDS} FROM tokens WHERE identifier = ?`, [ userId ]); + results.forEach(postProcess); + + return results; } async function getByAccessToken(accessToken) { @@ -89,7 +118,7 @@ async function getByAccessToken(accessToken) { const result = await database.query('SELECT ' + TOKENS_FIELDS + ' FROM tokens WHERE accessToken = ? AND expires > ?', [ accessToken, Date.now() ]); if (result.length === 0) return null; - return result[0]; + return postProcess(result[0]); } async function delByAccessToken(accessToken) {