diff --git a/api/source/package-lock.json b/api/source/package-lock.json index 317a0fc34..b64ae9b01 100644 --- a/api/source/package-lock.json +++ b/api/source/package-lock.json @@ -25,7 +25,6 @@ "jsonwebtoken": "^9.0.2", "jszip": "^3.10.1", "jwks-rsa": "^3.1.0", - "lodash": "^4.17.23", "multer": "^2.1.1", "mysql2": "^3.11.2", "net-keepalive": "^4.0.17", @@ -1965,9 +1964,9 @@ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.clonedeep": { diff --git a/api/source/package.json b/api/source/package.json index f1f6312d7..df37b5a46 100644 --- a/api/source/package.json +++ b/api/source/package.json @@ -29,7 +29,6 @@ "jsonwebtoken": "^9.0.2", "jszip": "^3.10.1", "jwks-rsa": "^3.1.0", - "lodash": "^4.17.23", "multer": "^2.1.1", "mysql2": "^3.11.2", "net-keepalive": "^4.0.17", diff --git a/api/source/service/MetricsService.js b/api/source/service/MetricsService.js index 70e2e2cf0..3725e05f2 100644 --- a/api/source/service/MetricsService.js +++ b/api/source/service/MetricsService.js @@ -1,4 +1,3 @@ -const { rest } = require('lodash') const dbUtils = require('./utils') function genLabelPredicates ({labelNames, labelIds, labelMatch, collectionLabelTableAlias = 'cl'}) { diff --git a/api/source/utils/auth.js b/api/source/utils/auth.js index 0d25c7752..fbbab37b3 100644 --- a/api/source/utils/auth.js +++ b/api/source/utils/auth.js @@ -2,7 +2,6 @@ const config = require('./config') const logger = require('./logger') const jwt = require('jsonwebtoken') const retry = require('async-retry') -const _ = require('lodash') const UserService = require(`../service/UserService`) const SmError = require('./error') const state = require('./state') @@ -170,17 +169,14 @@ const validateOauthSecurity = function (req, requiredScopes) { const grantedScopes = typeof tokenPayload[config.oauth.claims.scope] === 'string' ? tokenPayload[config.oauth.claims.scope].split(' ') : tokenPayload[config.oauth.claims.scope] - const commonScopes = _.intersectionWith(grantedScopes, requiredScopes, function(gs, rs) { - if (gs === rs) return gs - let gsTokens = gs.split(":").filter(i => i.length) - let rsTokens = rs.split(":").filter(i => i.length) - if (gsTokens.length === 0) { - return false - } - else { - return gsTokens.every((t, i) => rsTokens[i] === t) - } - }) + const commonScopes = grantedScopes.filter(gs => + requiredScopes.some(rs => { + if (gs === rs) return true + const gsTokens = gs.split(':').filter(i => i.length) + const rsTokens = rs.split(':').filter(i => i.length) + return gsTokens.length > 0 && gsTokens.every((t, i) => rsTokens[i] === t) + }) + ) if (commonScopes.length == 0) { throw new SmError.OutOfScopeError() } diff --git a/test/state/mocha/tokenValidation.test.js b/test/state/mocha/tokenValidation.test.js index 0495db235..dcdf318bf 100644 --- a/test/state/mocha/tokenValidation.test.js +++ b/test/state/mocha/tokenValidation.test.js @@ -109,4 +109,101 @@ describe('Token validation', function () { expect(res.body.error).to.equal(`Required scopes were not found in token.`) }) }) + + describe('Token scope validation', function () { + const {apiPort, dbPort, oidcPort, apiOrigin, oidcOrigin} = getPorts(54080) + + before(async function () { + this.timeout(60000) + oidc = new MockOidc({keyCount: 1, includeInsecureKid: false}) + await oidc.start({port: oidcPort}) + console.log(' ✔ oidc started') + console.log(' try mysql start') + mysql = await spawnMySQL({tag:'8.0.24', port: dbPort}) + console.log(' ✔ mysql started') + console.log(' try api start') + api = await spawnApiPromise({ + resolveOnType: 'started', + resolveOnClose: false, + env: { + STIGMAN_API_PORT: apiPort, + STIGMAN_DEPENDENCY_RETRIES: 2, + STIGMAN_DB_PASSWORD: 'stigman', + STIGMAN_DB_HOST: '127.0.0.1', + STIGMAN_DB_PORT: dbPort, + STIGMAN_OIDC_PROVIDER: oidcOrigin, + STIGMAN_LOG_LEVEL: '4' + } + }) + console.log(' ✔ api started') + }) + + after(async function () { + await api.stop() + await mysql.stop() + await oidc.stop() + addContext(this, {title: 'api-log', value: api.logRecords}) + }) + + it('should accept top-level scope "stig-manager"', async function () { + const res = await bearerRequest({ + url: `${apiOrigin}/api/user`, + method: 'GET', + token: oidc.getToken({username: 'user01', scope: 'stig-manager'}) + }) + expect(res.status).to.equal(200) + }) + it('should accept parent scope "stig-manager:user"', async function () { + const res = await bearerRequest({ + url: `${apiOrigin}/api/user`, + method: 'GET', + token: oidc.getToken({username: 'user01', scope: 'stig-manager:user'}) + }) + expect(res.status).to.equal(200) + }) + it('should accept exact scope "stig-manager:user:read"', async function () { + const res = await bearerRequest({ + url: `${apiOrigin}/api/user`, + method: 'GET', + token: oidc.getToken({username: 'user01', scope: 'stig-manager:user:read'}) + }) + expect(res.status).to.equal(200) + }) + it('should reject wrong branch scope "stig-manager:stig"', async function () { + const res = await bearerRequest({ + url: `${apiOrigin}/api/user`, + method: 'GET', + token: oidc.getToken({username: 'user01', scope: 'stig-manager:stig'}) + }) + expect(res.status).to.equal(403) + expect(res.body.error).to.equal('Required scopes were not found in token.') + }) + it('should reject wrong branch scope "stig-manager:collection:read"', async function () { + const res = await bearerRequest({ + url: `${apiOrigin}/api/user`, + method: 'GET', + token: oidc.getToken({username: 'user01', scope: 'stig-manager:collection:read'}) + }) + expect(res.status).to.equal(403) + expect(res.body.error).to.equal('Required scopes were not found in token.') + }) + it('should reject unrelated scopes "openid profile"', async function () { + const res = await bearerRequest({ + url: `${apiOrigin}/api/user`, + method: 'GET', + token: oidc.getToken({username: 'user01', scope: 'openid profile'}) + }) + expect(res.status).to.equal(403) + expect(res.body.error).to.equal('Required scopes were not found in token.') + }) + it('should reject empty scope', async function () { + const res = await bearerRequest({ + url: `${apiOrigin}/api/user`, + method: 'GET', + token: oidc.getToken({username: 'user01', scope: ''}) + }) + expect(res.status).to.equal(403) + expect(res.body.error).to.equal('Required scopes were not found in token.') + }) + }) })