diff --git a/.env.example b/.env.example index 02da5b63..e8f63beb 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,7 @@ NODE_ENV=development # EdgeOps Console static embed (npm run build:console → dev/console/build) EDGEOPS_CONSOLE_PATH=dev/console/build # must be absolute path -EDGEOPS_CONSOLE_VERSION=v1.0.8 +EDGEOPS_CONSOLE_VERSION=v1.0.9 # EDGEOPS_CONSOLE_REPO=https://github.com/Datasance/edgeops-console # EDGEOPS_CONSOLE_FLAVOR=datasance diff --git a/.github/actions/set-build-env/action.yml b/.github/actions/set-build-env/action.yml index 5d0cfee2..9bb807a8 100644 --- a/.github/actions/set-build-env/action.yml +++ b/.github/actions/set-build-env/action.yml @@ -8,7 +8,7 @@ runs: shell: bash run: | VERSION="${{ env.EDGEOPS_CONSOLE_VERSION }}" - if [ -z "$VERSION" ]; then VERSION="1.0.8"; fi + if [ -z "$VERSION" ]; then VERSION="1.0.9"; fi echo "EDGEOPS_CONSOLE_VERSION=$VERSION" >> "${GITHUB_ENV}" REPO="${{ env.EDGEOPS_CONSOLE_REPO }}" diff --git a/Dockerfile b/Dockerfile index c3232d23..d1b62197 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ FROM node:24-bookworm@sha256:fdddfb3e688158251943d52eba361de991548f6814007acba4917ae6b512d6be AS console-builder ARG EDGEOPS_CONSOLE_REPO=https://github.com/Datasance/edgeops-console -ARG EDGEOPS_CONSOLE_VERSION=v1.0.8 +ARG EDGEOPS_CONSOLE_VERSION=v1.0.9 ARG EDGEOPS_CONSOLE_FLAVOR=datasance RUN apt-get update \ @@ -50,7 +50,7 @@ RUN npm pack # ubi9/nodejs-24-minimal:latest — pin manifest list digest for reproducible multi-arch builds FROM registry.access.redhat.com/ubi9/nodejs-24-minimal@sha256:5f1ac8eab93c93eb2227f4ee7822668b312ee292d122dddd580bee8f17359c2f -ARG EDGEOPS_CONSOLE_VERSION=v1.0.8 +ARG EDGEOPS_CONSOLE_VERSION=v1.0.9 ARG IMAGE_REGISTRY ARG OCI_SOURCE_REPO ARG CONTROLLER_DISTRIBUTION=iofog diff --git a/Makefile b/Makefile index c02f42b7..025aaf29 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Local Docker build — mirrors CI/release build-args (see .github/actions/set-build-env). -# Override any variable: make build FLAVOR=iofog EDGEOPS_CONSOLE_VERSION=v1.0.8 +# Override any variable: make build FLAVOR=iofog EDGEOPS_CONSOLE_VERSION=v1.0.9 FLAVOR ?= datasance IMAGE_NAME ?= controller @@ -25,7 +25,7 @@ else $(error FLAVOR must be "datasance" or "iofog", got "$(FLAVOR)") endif -EDGEOPS_CONSOLE_VERSION ?= v1.0.8 +EDGEOPS_CONSOLE_VERSION ?= v1.0.9 IMAGE_REF = $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(DOCKER_TAG) diff --git a/scripts/build-console-dev.js b/scripts/build-console-dev.js index 45235b97..5367f7cd 100644 --- a/scripts/build-console-dev.js +++ b/scripts/build-console-dev.js @@ -9,7 +9,7 @@ const CONSOLE_DIR = path.join(DEV_DIR, 'console') const BUILD_OUT = path.join(CONSOLE_DIR, 'build') const REPO = process.env.EDGEOPS_CONSOLE_REPO || 'https://github.com/Datasance/edgeops-console' -const VERSION = process.env.EDGEOPS_CONSOLE_VERSION || 'v1.0.8' +const VERSION = process.env.EDGEOPS_CONSOLE_VERSION || 'v1.0.9' const FLAVOR = process.env.EDGEOPS_CONSOLE_FLAVOR || 'datasance' function normalizeTag (version) { diff --git a/src/data/managers/rbac-cache-version-manager.js b/src/data/managers/rbac-cache-version-manager.js index b92fbd07..97616963 100644 --- a/src/data/managers/rbac-cache-version-manager.js +++ b/src/data/managers/rbac-cache-version-manager.js @@ -16,6 +16,14 @@ class RbacCacheVersionManager extends BaseManager { return cacheVersion.version || 0 } + async getVersionWithoutTransaction () { + const cacheVersion = await this.getEntity().findOne({ where: { id: 1 } }) + if (!cacheVersion) { + return 0 + } + return cacheVersion.version || 0 + } + _getModelOptions (transaction) { return { transaction } } diff --git a/src/lib/rbac/authorizer.js b/src/lib/rbac/authorizer.js index c2427b03..1efd5268 100644 --- a/src/lib/rbac/authorizer.js +++ b/src/lib/rbac/authorizer.js @@ -1,17 +1,19 @@ const RbacRoleBindingManager = require('../../data/managers/rbac-role-binding-manager') const RbacRoleManager = require('../../data/managers/rbac-role-manager') const RbacCacheVersionManager = require('../../data/managers/rbac-cache-version-manager') +const transactionRunner = require('../../helpers/transaction-runner') const logger = require('../../logger') - // Simple in-memory cache for authorization decisions // Key format: `${subjectKind}:${subjectName}:${apiGroup}:${resource}:${verb}:${resourceName}` const authCache = new Map() const CACHE_TTL = 5 * 60 * 1000 // 5 minutes const MAX_CACHE_SIZE = 10000 +const VERSION_CHECK_INTERVAL_MS = 1000 // Track last known cache version to detect changes across instances let lastKnownVersion = null +let lastVersionCheckAt = 0 /** * Check if a value matches a pattern (supports wildcard *) @@ -38,6 +40,44 @@ function matchesArray (value, array) { return array.some(item => matchesPattern(value, item)) } +function buildCacheKey (subjects, apiGroup, resource, verb, resourceName) { + return `${JSON.stringify(subjects)}:${apiGroup}:${resource}:${verb}:${resourceName || ''}` +} + +function getCachedResult (cacheKey) { + const cached = authCache.get(cacheKey) + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.result + } + return null +} + +function storeCachedResult (cacheKey, result) { + if (authCache.size < MAX_CACHE_SIZE) { + authCache.set(cacheKey, { result, timestamp: Date.now() }) + } +} + +function isVersionCheckFresh () { + return lastKnownVersion !== null && + (Date.now() - lastVersionCheckAt) < VERSION_CHECK_INTERVAL_MS +} + +async function ensureVersionFresh () { + try { + const currentVersion = await RbacCacheVersionManager.getVersionWithoutTransaction() + if (lastKnownVersion !== null && currentVersion !== lastKnownVersion) { + logger.info('Cache version changed - clearing cache') + authCache.clear() + } + lastKnownVersion = currentVersion + } catch (error) { + logger.warn(`Error checking cache version: ${error.message}`) + } finally { + lastVersionCheckAt = Date.now() + } +} + /** * Resolve all rules for a subject */ @@ -92,39 +132,18 @@ function evaluateRule (rule, apiGroup, resource, verb, resourceName) { } /** - * Authorize a request - * @param {Array} subjects - Array of subjects {kind, name} - * @param {string} apiGroup - API group (empty string for core) - * @param {string} resource - Resource name (e.g., 'microservices') - * @param {string} verb - Verb (e.g., 'get', 'create', 'patch') - * @param {string} resourceName - Optional resource instance name (e.g., microservice UUID) - * @param {Object} transaction - Database transaction - * @returns {Promise<{allowed: boolean, reason?: string}>} + * Full authorization path (requires a database transaction). + * Used on cache miss after a cheap version check. */ async function authorize (subjects, apiGroup, resource, verb, resourceName, transaction) { if (!subjects || !Array.isArray(subjects) || subjects.length === 0) { return { allowed: false, reason: 'No subjects provided' } } - // Check if cache version has changed (for multi-instance cache invalidation) - try { - const currentVersion = await RbacCacheVersionManager.getVersion(transaction) - if (lastKnownVersion !== null && currentVersion !== lastKnownVersion) { - // Cache version changed - clear local cache - logger.info('Cache version changed - clearing cache') - authCache.clear() - } - lastKnownVersion = currentVersion - } catch (error) { - // Log error but continue - if version check fails, we'll just skip cache version check - logger.warn(`Error checking cache version: ${error.message}`) - } - - // Check cache - const cacheKey = `${JSON.stringify(subjects)}:${apiGroup}:${resource}:${verb}:${resourceName || ''}` - const cached = authCache.get(cacheKey) - if (cached && Date.now() - cached.timestamp < CACHE_TTL) { - return cached.result + const cacheKey = buildCacheKey(subjects, apiGroup, resource, verb, resourceName) + const cached = getCachedResult(cacheKey) + if (cached) { + return cached } // Check system roles first (Admin, SRE, Developer, Viewer) @@ -132,7 +151,7 @@ async function authorize (subjects, apiGroup, resource, verb, resourceName, tran for (const subject of subjects) { if (subject.kind === 'Group' && subject.name) { const roleName = subject.name.toLowerCase() - + // Check if it matches a system role if (RbacRoleManager.isSystemRole(roleName)) { const systemRole = RbacRoleManager.getSystemRole(roleName) @@ -143,10 +162,7 @@ async function authorize (subjects, apiGroup, resource, verb, resourceName, tran try { if (evaluateRule(rule, apiGroup, resource, verb, resourceName)) { const result = { allowed: true, reason: `${roleName} system role has permission` } - // Cache result - if (authCache.size < MAX_CACHE_SIZE) { - authCache.set(cacheKey, { result, timestamp: Date.now() }) - } + storeCachedResult(cacheKey, result) return result } } catch (ruleError) { @@ -191,10 +207,7 @@ async function authorize (subjects, apiGroup, resource, verb, resourceName, tran try { if (evaluateRule(rule, apiGroup, resource, verb, resourceName)) { const result = { allowed: true, reason: 'Rule matched' } - // Cache result - if (authCache.size < MAX_CACHE_SIZE) { - authCache.set(cacheKey, { result, timestamp: Date.now() }) - } + storeCachedResult(cacheKey, result) return result } } catch (ruleError) { @@ -213,13 +226,45 @@ async function authorize (subjects, apiGroup, resource, verb, resourceName, tran // Deny by default const result = { allowed: false, reason: 'Authorization denied: You do not have permission to perform this action. Please contact your administrator.' } - // Cache result - if (authCache.size < MAX_CACHE_SIZE) { - authCache.set(cacheKey, { result, timestamp: Date.now() }) - } + storeCachedResult(cacheKey, result) return result } +/** + * Authorize a request with cache fast path (no DB on cache hit within version window). + * @param {Array} subjects - Array of subjects {kind, name} + * @param {string} apiGroup - API group (empty string for core) + * @param {string} resource - Resource name (e.g., 'microservices') + * @param {string} verb - Verb (e.g., 'get', 'create', 'patch') + * @param {string} resourceName - Optional resource instance name (e.g., microservice UUID) + * @returns {Promise<{allowed: boolean, reason?: string}>} + */ +async function authorizeRequest (subjects, apiGroup, resource, verb, resourceName) { + if (!subjects || !Array.isArray(subjects) || subjects.length === 0) { + return { allowed: false, reason: 'No subjects provided' } + } + + const cacheKey = buildCacheKey(subjects, apiGroup, resource, verb, resourceName) + + if (isVersionCheckFresh()) { + const cached = getCachedResult(cacheKey) + if (cached) { + return cached + } + } else { + await ensureVersionFresh() + const cached = getCachedResult(cacheKey) + if (cached) { + return cached + } + } + + return transactionRunner.runInTransaction( + (transaction) => authorize(subjects, apiGroup, resource, verb, resourceName, transaction), + { label: 'rbac-authorize' } + ) +} + /** * Clear authorization cache */ @@ -239,13 +284,30 @@ function cleanCache () { } } +function _resetStateForTests () { + authCache.clear() + lastKnownVersion = null + lastVersionCheckAt = 0 +} + +function _setVersionCheckStateForTests ({ lastKnownVersion: version, lastVersionCheckAt: checkedAt } = {}) { + if (version !== undefined) { + lastKnownVersion = version + } + if (checkedAt !== undefined) { + lastVersionCheckAt = checkedAt + } +} + // Clean cache every 10 minutes setInterval(cleanCache, 10 * 60 * 1000) module.exports = { authorize, + authorizeRequest, clearCache, - cleanCache + cleanCache, + _resetStateForTests, + _setVersionCheckStateForTests, + VERSION_CHECK_INTERVAL_MS } - - diff --git a/src/lib/rbac/middleware.js b/src/lib/rbac/middleware.js index 505b52a0..75e59206 100644 --- a/src/lib/rbac/middleware.js +++ b/src/lib/rbac/middleware.js @@ -7,7 +7,6 @@ const config = require('../../config') const db = require('../../data/models') const { getOidcSettings } = require('../../config/oidc') const { PASSWORD_CHANGE_REQUIRED_CLAIM } = require('../../services/auth-token-service') -const { runInTransaction } = require('../../helpers/transaction-runner') const PASSWORD_CHANGE_ALLOWLIST = [ { method: 'GET', path: '/api/v3/user/profile' }, @@ -319,16 +318,12 @@ function requirePermission (resource, verb) { const finalVerb = verb || routeDef.verb const resourceName = routeDef.resourceName - const authResult = await runInTransaction( - (transaction) => authorizer.authorize( - subjects, - routeDef.apiGroup || '', - finalResource, - finalVerb, - resourceName, - transaction - ), - { label: 'rbac-authorize' } + const authResult = await authorizer.authorizeRequest( + subjects, + routeDef.apiGroup || '', + finalResource, + finalVerb, + resourceName ) if (!authResult.allowed) { @@ -410,16 +405,12 @@ async function authorizeWebSocket (req, token) { subjects: subjects }) - const authResult = await runInTransaction( - (transaction) => authorizer.authorize( - subjects, - routeDef.apiGroup || '', - routeDef.resource, - routeDef.verb, - routeDef.resourceName, - transaction - ), - { label: 'rbac-authorize-ws' } + const authResult = await authorizer.authorizeRequest( + subjects, + routeDef.apiGroup || '', + routeDef.resource, + routeDef.verb, + routeDef.resourceName ) logger.debug(`WebSocket authorization result:`, { @@ -571,16 +562,12 @@ function protect (_roles) { return callback() } - const authResult = await runInTransaction( - (transaction) => authorizer.authorize( - subjects, - routeDef.apiGroup || '', - routeDef.resource, - routeDef.verb, - routeDef.resourceName, - transaction - ), - { label: 'rbac-protect' } + const authResult = await authorizer.authorizeRequest( + subjects, + routeDef.apiGroup || '', + routeDef.resource, + routeDef.verb, + routeDef.resourceName ) if (!authResult.allowed) { diff --git a/test/src/lib/rbac/authorizer.test.js b/test/src/lib/rbac/authorizer.test.js new file mode 100644 index 00000000..cda8343e --- /dev/null +++ b/test/src/lib/rbac/authorizer.test.js @@ -0,0 +1,211 @@ +const { expect } = require('chai') +const sinon = require('sinon') + +const RbacCacheVersionManager = require('../../../../src/data/managers/rbac-cache-version-manager') +const RbacRoleBindingManager = require('../../../../src/data/managers/rbac-role-binding-manager') +const RbacRoleManager = require('../../../../src/data/managers/rbac-role-manager') +const transactionRunner = require('../../../../src/helpers/transaction-runner') +const authorizer = require('../../../../src/lib/rbac/authorizer') + +const VIEWER_SUBJECTS = [{ kind: 'Group', name: 'viewer' }] +const USER_SUBJECTS = [{ kind: 'User', name: 'alice' }] +const MICROSERVICES_GET = ['', 'microservices', 'get', null] + +describe('RBAC authorizer fast path', () => { + def('sandbox', () => sinon.createSandbox()) + def('fakeTransaction', () => ({ id: 'test-tx' })) + + beforeEach(() => { + authorizer._resetStateForTests() + $sandbox.stub(RbacCacheVersionManager, 'getVersionWithoutTransaction').resolves(1) + $sandbox.stub(transactionRunner, 'runInTransaction').callsFake(async (fn) => fn($fakeTransaction)) + $sandbox.stub(RbacRoleBindingManager, 'findRoleBindingsBySubject').resolves([]) + }) + + afterEach(() => { + $sandbox.restore() + authorizer._resetStateForTests() + }) + + describe('authorizeRequest()', () => { + it('denies when no subjects are provided', async () => { + const result = await authorizer.authorizeRequest([], ...MICROSERVICES_GET) + + expect(result).to.deep.equal({ allowed: false, reason: 'No subjects provided' }) + expect(transactionRunner.runInTransaction).to.not.have.been.called + expect(RbacCacheVersionManager.getVersionWithoutTransaction).to.not.have.been.called + }) + + it('runs full authorization in a transaction on cache miss', async () => { + const result = await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + + expect(result.allowed).to.equal(true) + expect(transactionRunner.runInTransaction).to.have.been.calledOnce + expect(RbacCacheVersionManager.getVersionWithoutTransaction).to.have.been.calledOnce + }) + + it('serves cache hits without DB or transaction when version check is fresh', async () => { + await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + + transactionRunner.runInTransaction.resetHistory() + RbacCacheVersionManager.getVersionWithoutTransaction.resetHistory() + + const result = await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + + expect(result.allowed).to.equal(true) + expect(transactionRunner.runInTransaction).to.not.have.been.called + expect(RbacCacheVersionManager.getVersionWithoutTransaction).to.not.have.been.called + }) + + it('re-checks version after the interval but still skips transaction on cache hit', async () => { + const clock = $sandbox.useFakeTimers({ now: Date.now(), toFake: ['Date'] }) + + await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + + clock.tick(authorizer.VERSION_CHECK_INTERVAL_MS + 1) + transactionRunner.runInTransaction.resetHistory() + RbacCacheVersionManager.getVersionWithoutTransaction.resetHistory() + + const result = await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + + expect(result.allowed).to.equal(true) + expect(RbacCacheVersionManager.getVersionWithoutTransaction).to.have.been.calledOnce + expect(transactionRunner.runInTransaction).to.not.have.been.called + + clock.restore() + }) + + it('clears cache and re-authorizes when version changes', async () => { + await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + + RbacCacheVersionManager.getVersionWithoutTransaction.resolves(2) + authorizer._setVersionCheckStateForTests({ lastVersionCheckAt: 0 }) + + transactionRunner.runInTransaction.resetHistory() + + const result = await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + + expect(result.allowed).to.equal(true) + expect(transactionRunner.runInTransaction).to.have.been.calledOnce + }) + + it('caches deny decisions and serves them from the fast path', async () => { + const result = await authorizer.authorizeRequest(USER_SUBJECTS, ...MICROSERVICES_GET) + + expect(result.allowed).to.equal(false) + expect(transactionRunner.runInTransaction).to.have.been.calledOnce + + transactionRunner.runInTransaction.resetHistory() + RbacCacheVersionManager.getVersionWithoutTransaction.resetHistory() + + const cachedDeny = await authorizer.authorizeRequest(USER_SUBJECTS, ...MICROSERVICES_GET) + + expect(cachedDeny.allowed).to.equal(false) + expect(transactionRunner.runInTransaction).to.not.have.been.called + expect(RbacCacheVersionManager.getVersionWithoutTransaction).to.not.have.been.called + }) + + it('uses separate cache entries for different subjects and resources', async () => { + await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + transactionRunner.runInTransaction.resetHistory() + + await authorizer.authorizeRequest(USER_SUBJECTS, ...MICROSERVICES_GET) + + expect(transactionRunner.runInTransaction).to.have.been.calledOnce + + transactionRunner.runInTransaction.resetHistory() + await authorizer.authorizeRequest(VIEWER_SUBJECTS, '', 'applications', 'get', null) + expect(transactionRunner.runInTransaction).to.have.been.calledOnce + }) + }) + + describe('authorize() full path', () => { + it('allows viewer system role for microservices get', async () => { + const result = await authorizer.authorize( + VIEWER_SUBJECTS, + ...MICROSERVICES_GET, + $fakeTransaction + ) + + expect(result.allowed).to.equal(true) + expect(result.reason).to.include('viewer system role') + expect(RbacRoleBindingManager.findRoleBindingsBySubject).to.not.have.been.called + }) + + it('resolves custom role bindings when system role check does not match', async () => { + RbacRoleBindingManager.findRoleBindingsBySubject.resolves([{ + roleRef: { kind: 'Role', name: 'custom-reader' } + }]) + $sandbox.stub(RbacRoleManager, 'getRoleWithRules').resolves({ + rules: [{ + apiGroups: [''], + resources: ['microservices'], + verbs: ['get'] + }] + }) + + const result = await authorizer.authorize( + USER_SUBJECTS, + ...MICROSERVICES_GET, + $fakeTransaction + ) + + expect(result.allowed).to.equal(true) + expect(result.reason).to.equal('Rule matched') + expect(RbacRoleBindingManager.findRoleBindingsBySubject).to.have.been.calledOnce + }) + + it('denies when no matching system role or role binding exists', async () => { + const result = await authorizer.authorize( + USER_SUBJECTS, + ...MICROSERVICES_GET, + $fakeTransaction + ) + + expect(result.allowed).to.equal(false) + expect(RbacRoleBindingManager.findRoleBindingsBySubject).to.have.been.called + }) + + it('returns cached result inside transaction path without re-querying bindings', async () => { + await authorizer.authorize(VIEWER_SUBJECTS, ...MICROSERVICES_GET, $fakeTransaction) + RbacRoleBindingManager.findRoleBindingsBySubject.resetHistory() + + const result = await authorizer.authorize( + VIEWER_SUBJECTS, + ...MICROSERVICES_GET, + $fakeTransaction + ) + + expect(result.allowed).to.equal(true) + expect(RbacRoleBindingManager.findRoleBindingsBySubject).to.not.have.been.called + }) + }) + + describe('cache maintenance', () => { + it('clearCache removes all cached entries forcing a new transaction', async () => { + await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + authorizer.clearCache() + authorizer._setVersionCheckStateForTests({ lastVersionCheckAt: Date.now() }) + + transactionRunner.runInTransaction.resetHistory() + + await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + + expect(transactionRunner.runInTransaction).to.have.been.calledOnce + }) + + it('continues to serve cache hits when version check fails', async () => { + await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + + RbacCacheVersionManager.getVersionWithoutTransaction.rejects(new Error('db unavailable')) + authorizer._setVersionCheckStateForTests({ lastVersionCheckAt: 0 }) + + transactionRunner.runInTransaction.resetHistory() + + const result = await authorizer.authorizeRequest(VIEWER_SUBJECTS, ...MICROSERVICES_GET) + + expect(result.allowed).to.equal(true) + expect(transactionRunner.runInTransaction).to.not.have.been.called + }) + }) +}) diff --git a/test/src/lib/rbac/middleware-oidc.test.js b/test/src/lib/rbac/middleware-oidc.test.js index 770ac591..b71e7487 100644 --- a/test/src/lib/rbac/middleware-oidc.test.js +++ b/test/src/lib/rbac/middleware-oidc.test.js @@ -148,7 +148,7 @@ describe('RBAC middleware OIDC integration', () => { } }) - $sandbox.stub(authorizer, 'authorize').resolves({ allowed: true }) + $sandbox.stub(authorizer, 'authorizeRequest').resolves({ allowed: true }) const req = { method: 'GET', @@ -158,13 +158,13 @@ describe('RBAC middleware OIDC integration', () => { await rbacMiddleware.protect()(req, $res, $callback) - expect(authorizer.authorize).to.have.been.calledOnce + expect(authorizer.authorizeRequest).to.have.been.calledOnce expect($callback).to.have.been.calledOnce expect($res.statusCode).to.equal(null) }) it('returns 403 when authorizer denies access', async () => { - $sandbox.stub(authorizer, 'authorize').resolves({ + $sandbox.stub(authorizer, 'authorizeRequest').resolves({ allowed: false, reason: 'insufficient permissions' }) diff --git a/test/src/services/auth-forced-password.test.js b/test/src/services/auth-forced-password.test.js index c6dbf88e..be72363a 100644 --- a/test/src/services/auth-forced-password.test.js +++ b/test/src/services/auth-forced-password.test.js @@ -98,7 +98,7 @@ describe('Forced password change (CLI + RBAC)', () => { }, false) const authorizer = require('../../../src/lib/rbac/authorizer') - $sandbox.stub(authorizer, 'authorize').resolves({ allowed: true }) + $sandbox.stub(authorizer, 'authorizeRequest').resolves({ allowed: true }) const rbacMiddleware = require('../../../src/lib/rbac/middleware') const profileReq = { diff --git a/test/src/services/auth-login.test.js b/test/src/services/auth-login.test.js index 64a61f31..0a88f9d0 100644 --- a/test/src/services/auth-login.test.js +++ b/test/src/services/auth-login.test.js @@ -261,7 +261,7 @@ describe('RBAC middleware without bearer token', () => { it('returns 401 when no authentication information is present', async () => { const authorizer = require('../../../src/lib/rbac/authorizer') - $sandbox.stub(authorizer, 'authorize').resolves(true) + $sandbox.stub(authorizer, 'authorizeRequest').resolves({ allowed: true }) const rbacMiddleware = require('../../../src/lib/rbac/middleware') const req = { diff --git a/test/src/websocket/ws-security.test.js b/test/src/websocket/ws-security.test.js index 42995c27..c4d6282e 100644 --- a/test/src/websocket/ws-security.test.js +++ b/test/src/websocket/ws-security.test.js @@ -29,7 +29,7 @@ describe('WebSocket session security', () => { const req = createMockRequest(`/api/v3/microservices/exec/${$ids.microserviceUuid}`) req.headers.authorization = 'Bearer denied-token' - $sandbox.stub(authorizer, 'authorize').resolves({ + $sandbox.stub(authorizer, 'authorizeRequest').resolves({ allowed: false, reason: 'Access denied: insufficient permissions' }) @@ -49,7 +49,7 @@ describe('WebSocket session security', () => { const token = buildFakeJwt() req.headers.authorization = token - $sandbox.stub(authorizer, 'authorize').resolves({ allowed: true }) + $sandbox.stub(authorizer, 'authorizeRequest').resolves({ allowed: true }) const handlerCalled = sinon.stub().resolves() const protectedHandler = rbacMiddleware.protectWebSocket(handlerCalled)