Removed versioning from api-key/admin auth

refs: https://github.com/TryGhost/Toolbox/issues/229

- our api-key audience handling code is still relying on internal api version config
- the regex used is also buggy (it expects 3 parts, which isn't true without versions) and doesn't always match, in which case it can cause the tests to hang
- we already had some very similar code in the version-rewrite middleware which is also validates exact values for version and api type
- moved this code into a util inside api-version-compatibility-service
- using this code, all the tests still pass as is, but when I start to adjust them to cover more cases, none hang (test changes coming in a separate commit)
This commit is contained in:
Hannah Wolfe 2022-05-06 07:57:37 +01:00
parent 1afe52c657
commit 6dc3f1bf56
No known key found for this signature in database
GPG Key ID: AB586C3B5AE5C037
5 changed files with 100 additions and 9 deletions

View File

@ -49,5 +49,6 @@ module.exports.contentVersion = (req, res, next) => {
};
module.exports.versionRewrites = require('./mw-version-rewrites');
module.exports.legacyApiPathMatch = require('./legacy-api-path-match');
module.exports.init = init;

View File

@ -0,0 +1,21 @@
const pathMatch = require('path-match')();
module.exports = (url) => {
let apiRouteMatcher = '/:version(v2|v3|v4|canary)?/:api(admin|content)/*';
if (url.startsWith('/ghost/api')) {
apiRouteMatcher = `/ghost/api${apiRouteMatcher}`;
}
if (!url.endsWith('/')) {
url += '/';
}
let {version, api} = pathMatch(apiRouteMatcher)(url);
if (version === [null]) {
version = null;
}
return {version, api};
};

View File

@ -1,4 +1,4 @@
const routeMatch = require('path-match')();
const legacyApiPathMatch = require('./legacy-api-path-match');
const urlUtils = require('../../../shared/url-utils');
/**
@ -9,7 +9,7 @@ const urlUtils = require('../../../shared/url-utils');
* @param {import('express').NextFunction} next
*/
module.exports = (req, res, next) => {
let {version} = routeMatch('/:version(v2|v3|v4|canary)/:api(admin|content)/*')(req.url);
let {version} = legacyApiPathMatch(req.url);
// If we don't match a valid version, carry on
if (!version) {

View File

@ -3,7 +3,7 @@ const url = require('url');
const models = require('../../../models');
const errors = require('@tryghost/errors');
const limitService = require('../../../services/limits');
const config = require('../../../../shared/config');
const {legacyApiPathMatch} = require('../../../services/api-version-compatibility');
const tpl = require('@tryghost/tpl');
const _ = require('lodash');
@ -139,19 +139,20 @@ const authenticateWithToken = async (req, res, next, {token, JWT_OPTIONS}) => {
// https://github.com/auth0/node-jsonwebtoken/issues/208#issuecomment-231861138
const secret = Buffer.from(apiKey.get('secret'), 'hex');
const {pathname} = url.parse(req.originalUrl);
const [hasMatch, version, api] = pathname.match(/ghost\/api\/([^/]+)\/([^/]+)\/(.+)*/); // eslint-disable-line no-unused-vars
// Using req.originalUril means we get the right url even if version-rewrites have happened
const {version, api} = legacyApiPathMatch(req.originalUrl);
// ensure the token was meant for this api
let options;
if (!config.get('api:versions:all').includes(version)) {
// CASE: non-versioned api request
if (version) {
// CASE: legacy versioned api request
options = Object.assign({
audience: new RegExp(`\/?${version}\/?$`) // eslint-disable-line no-useless-escape
audience: new RegExp(`/?${version}/${api}/?$`)
}, JWT_OPTIONS);
} else {
options = Object.assign({
audience: new RegExp(`\/?${version}\/${api}\/?$`) // eslint-disable-line no-useless-escape
audience: new RegExp(`/?${api}/?$`)
}, JWT_OPTIONS);
}

View File

@ -0,0 +1,68 @@
const assert = require('assert');
const legacyApiPathMatch = require('../../../../../core/server/services/api-version-compatibility/legacy-api-path-match');
describe('Legacy Path Match', function () {
it('returns null, admin for all supported permutations', function () {
const permutations = [
'/ghost/api/admin/',
'/admin/',
'/ghost/api/admin',
'/admin',
'/ghost/api/admin/session/',
'/admin/session/',
'/ghost/api/admin/session',
'/admin/session',
'/ghost/api/admin/session/something/',
'/admin/session/something/',
'/ghost/api/admin/session/something',
'/admin/session/something'
];
permutations.forEach((url) => {
assert.deepEqual(legacyApiPathMatch(url), {version: null, api: 'admin'}, url);
});
});
it('returns canary, admin for all supported permutations', function () {
const permutations = [
'/ghost/api/canary/admin/',
'/canary/admin/',
'/ghost/api/canary/admin',
'/canary/admin',
'/ghost/api/canary/admin/session/',
'/canary/admin/session/',
'/ghost/api/canary/admin/session',
'/canary/admin/session',
'/ghost/api/canary/admin/session/something/',
'/canary/admin/session/something/',
'/ghost/api/canary/admin/session/something',
'/canary/admin/session/something'
];
permutations.forEach((url) => {
assert.deepEqual(legacyApiPathMatch(url), {version: 'canary', api: 'admin'}, url);
});
});
it('returns v4, admin for all permutations', function () {
const permutations = [
'/ghost/api/v4/admin/',
'/v4/admin/',
'/ghost/api/v4/admin',
'/v4/admin',
'/ghost/api/v4/admin/session/',
'/v4/admin/session/',
'/ghost/api/v4/admin/session',
'/v4/admin/session',
'/ghost/api/v4/admin/session/something/',
'/v4/admin/session/something/',
'/ghost/api/v4/admin/session/something',
'/v4/admin/session/something'
];
permutations.forEach((url) => {
assert.deepEqual(legacyApiPathMatch(url), {version: 'v4', api: 'admin'}, url);
});
});
});