mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-23 05:36:12 +00:00
fix: remove redis cache scan in favour of direct key invalidation (#2589)
This commit is contained in:
@@ -811,9 +811,6 @@ class AuthService extends BaseService {
|
||||
'DELETE FROM `access_token_permissions` WHERE `token_uid` = ?',
|
||||
[token_uid],
|
||||
);
|
||||
|
||||
const svc_permission = this.services.get('permission');
|
||||
svc_permission.invalidate_permission_scan_cache_for_access_token(token_uid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { PermissionScanRedisCacheSpace } from './PermissionScanRedisCacheSpace.js';
|
||||
import { PermissionUtil } from './permissionUtils.mjs';
|
||||
|
||||
describe('PermissionScanRedisCacheSpace', () => {
|
||||
it('builds cache keys for actor and permission options', () => {
|
||||
const actorUid = 'app-under-user:user-123:app-456';
|
||||
const permissionOptions = ['fs:node-1:read'];
|
||||
const key = PermissionScanRedisCacheSpace.key({
|
||||
actorUid,
|
||||
permissionOptions,
|
||||
joinPermissionParts: PermissionUtil.join,
|
||||
});
|
||||
|
||||
expect(key).toBe(PermissionUtil.join(
|
||||
'permission-scan',
|
||||
actorUid,
|
||||
'options-list',
|
||||
...permissionOptions,
|
||||
));
|
||||
});
|
||||
|
||||
it('builds stable exact keys for app-under-user + one permission', () => {
|
||||
const actorUid = 'app-under-user:user-123:app-456';
|
||||
const permissionOptions = ['flag:app-is-authenticated'];
|
||||
const key = PermissionScanRedisCacheSpace.key({
|
||||
actorUid,
|
||||
permissionOptions,
|
||||
joinPermissionParts: PermissionUtil.join,
|
||||
});
|
||||
|
||||
expect(key).toBe(PermissionUtil.join(
|
||||
'permission-scan',
|
||||
actorUid,
|
||||
'options-list',
|
||||
...permissionOptions,
|
||||
));
|
||||
});
|
||||
});
|
||||
@@ -56,10 +56,7 @@ class PermissionService extends BaseService {
|
||||
this._permission_rewriters = [];
|
||||
this._permission_implicators = [];
|
||||
this._permission_exploders = [];
|
||||
|
||||
// Debounce constants and state
|
||||
this._INVALIDATE_DEBOUNCE_MS = 200;
|
||||
this._invalidate_app_under_user_debounce = {};
|
||||
this._PERMISSION_SCAN_CACHE_TTL_SECONDS = 20;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,7 +239,7 @@ class PermissionService extends BaseService {
|
||||
});
|
||||
|
||||
await setRedisCacheValue(cacheKey, JSON.stringify(reading), {
|
||||
ttlSeconds: 20,
|
||||
ttlSeconds: this._PERMISSION_SCAN_CACHE_TTL_SECONDS,
|
||||
eventData: reading,
|
||||
});
|
||||
|
||||
@@ -250,68 +247,22 @@ class PermissionService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes permission-scan cache entries for a single app-under-user actor.
|
||||
* Used after grant_user_app_permission so the next check sees the new permission
|
||||
* without waiting for cache TTL. Only keys for this (user, app) are removed.
|
||||
* Removes a specific permission-scan cache entry for a single app-under-user actor.
|
||||
* This targets only the exact key for (user_uuid, app_uid, permission).
|
||||
*
|
||||
* @param {string} user_uuid - The user's UUID.
|
||||
* @param {string} app_uid - The app UID.
|
||||
* @param {string} permission - The permission string used in scan.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async invalidate_permission_scan_cache_for_app_under_user (user_uuid, app_uid) {
|
||||
const prefix = PermissionUtil.permission_scan_cache_prefix_for_app_under_user(user_uuid, app_uid);
|
||||
const pattern = `${prefix}*`;
|
||||
let cursor = '0';
|
||||
const toDelete = [];
|
||||
do {
|
||||
const [next_cursor, keys] = await redisClient.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
|
||||
cursor = next_cursor;
|
||||
if ( keys?.length ) toDelete.push(...keys);
|
||||
} while ( cursor !== '0' );
|
||||
if ( toDelete.length ) await deleteRedisKeys(toDelete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates permission-scan cache for (user_uuid, app_uid).
|
||||
* Runs immediately, or skips in a debounce period for this key.
|
||||
*
|
||||
* @param {string} user_uuid - The user's UUID.
|
||||
* @param {string} app_uid - The app UID.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async _schedule_invalidate_permission_scan_cache_for_app_under_user (user_uuid, app_uid) {
|
||||
const key = `${user_uuid}:${app_uid}`;
|
||||
if ( this._invalidate_app_under_user_debounce[key] ) return;
|
||||
this._invalidate_app_under_user_debounce[key] = setTimeout(() => {
|
||||
delete this._invalidate_app_under_user_debounce[key];
|
||||
}, this._INVALIDATE_DEBOUNCE_MS);
|
||||
try {
|
||||
await this.invalidate_permission_scan_cache_for_app_under_user(user_uuid, app_uid);
|
||||
} catch ( e ) {
|
||||
this.log.error('failed to invalidate permission scan cache', {
|
||||
actor_uid: user_uuid,
|
||||
app_uid,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
async invalidate_permission_scan_cache_for_app_under_user (user_uuid, app_uid, permission) {
|
||||
const actorUid = `app-under-user:${user_uuid}:${app_uid}`;
|
||||
const cacheKey = PermissionScanRedisCacheSpace.key({
|
||||
actorUid,
|
||||
permissionOptions: [permission],
|
||||
joinPermissionParts: PermissionUtil.join,
|
||||
});
|
||||
await deleteRedisKeys(cacheKey);
|
||||
}
|
||||
|
||||
async validateUserPerms ({ actor, permissions }) {
|
||||
@@ -518,7 +469,7 @@ class PermissionService extends BaseService {
|
||||
);
|
||||
|
||||
// Invalidate permission-scan cache for this app-under-user so the next check sees the grant.
|
||||
this._schedule_invalidate_permission_scan_cache_for_app_under_user(actor.type.user.uuid, app.uid);
|
||||
this.invalidate_permission_scan_cache_for_app_under_user(actor.type.user.uuid, app_uid, permission);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -84,19 +84,6 @@ 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:*`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Exact key prefix for permission-scan cache entries belonging to a given app-under-user actor.
|
||||
* Cache keys are built as join('permission-scan', actor.uid, 'options-list', ...);
|
||||
@@ -124,9 +111,12 @@ export const PermissionUtil = {
|
||||
*/
|
||||
reading_to_options (
|
||||
// actual arguments
|
||||
reading, parameters = {},
|
||||
reading,
|
||||
parameters = {},
|
||||
// recursion state
|
||||
options = [], extras = [], path = [],
|
||||
options = [],
|
||||
extras = [],
|
||||
path = [],
|
||||
) {
|
||||
const to_path_item = finding => ({
|
||||
key: finding.key,
|
||||
|
||||
Reference in New Issue
Block a user