diff --git a/src/backend/src/services/auth/AuthService.js b/src/backend/src/services/auth/AuthService.js index e7b1241f2..bfc5c8d2d 100644 --- a/src/backend/src/services/auth/AuthService.js +++ b/src/backend/src/services/auth/AuthService.js @@ -475,6 +475,8 @@ class AuthService extends BaseService { [token_uid], ); /* eslint-enable */ + const svc_permission = this.services.get('permission'); + svc_permission.invalidate_permission_scan_cache_for_access_token(token_uid); } /** diff --git a/src/backend/src/services/auth/PermissionService.js b/src/backend/src/services/auth/PermissionService.js index cc7bfdc19..ea5820889 100644 --- a/src/backend/src/services/auth/PermissionService.js +++ b/src/backend/src/services/auth/PermissionService.js @@ -237,6 +237,24 @@ class PermissionService extends BaseService { return reading; } + /** + * Removes permission-scan cache entries for an access token. + * Used when revoking an access token so stale scan results are not served. + * Only keys for this token are removed (see PermissionUtil.permission_scan_cache_pattern_for_access_token). + * + * @param {string} token_uid - The access token UUID. + */ + invalidate_permission_scan_cache_for_access_token (token_uid) { + const kv = this.modules.memKVMap; + if ( ! kv?.keys ) return; + const pattern = PermissionUtil.permission_scan_cache_pattern_for_access_token(token_uid); + const keys = kv.keys(pattern); + if ( ! Array.isArray(keys) ) return; + for ( const key of keys ) { + kv.del(key); + } + } + async validateUserPerms ({ actor, permissions }) { const flatPermsReading = await this.#flat_validateUserPerms({ actor, permissions }); diff --git a/src/backend/src/services/auth/permissionUtils.mjs b/src/backend/src/services/auth/permissionUtils.mjs index 3006a9b5f..3ec47a8f3 100644 --- a/src/backend/src/services/auth/permissionUtils.mjs +++ b/src/backend/src/services/auth/permissionUtils.mjs @@ -79,6 +79,19 @@ export const PermissionUtil = { ; }, + /** + * Glob pattern for permission-scan cache keys belonging to a given access token. + * Cache keys are built as join('permission-scan', actor.uid, 'options-list', ...); + * for access tokens, actor.uid ends with ':' + token_uid (token_uid is not escaped). + * Use with kv.keys() to list only entries for that token when invalidating. + * + * @param {string} token_uid - The access token UUID. + * @returns {string} A glob pattern matching only that token's permission-scan cache keys. + */ + permission_scan_cache_pattern_for_access_token (token_uid) { + return `permission-scan:*${token_uid}:options-list:*`; + }, + /** * Converts a permission reading structure into an array of option objects. * Recursively traverses the reading tree to collect all options with their associated path and data.