Adds API filter registry, style theme registry, SW bitmask cache clear, KV namespacing, session expiry checks, accessibility improvements, and expanded test coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
636 lines
21 KiB
JavaScript
636 lines
21 KiB
JavaScript
import { getProvider } from '../../platform/storage.js';
|
|
import { api } from '../../platform/api.js';
|
|
import { SECURITY_REQUEST_FILTER } from '../runtime/api-auth.js';
|
|
import { SecurityPolicy } from './SecurityPolicy.js';
|
|
import {
|
|
AccountProfile,
|
|
Permit,
|
|
Realm,
|
|
Resource,
|
|
Role,
|
|
Securable,
|
|
Session,
|
|
User,
|
|
hasRequiredRights,
|
|
normalizeRightsInput
|
|
} from '../model/index.js';
|
|
|
|
const SESSION_KEY = 'security.api.session';
|
|
|
|
function clone(value) {
|
|
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function pathMatches(resourcePath, targetPath) {
|
|
if (!resourcePath || resourcePath === '*') {
|
|
return true;
|
|
}
|
|
|
|
if (resourcePath.endsWith('*')) {
|
|
return targetPath.startsWith(resourcePath.slice(0, -1));
|
|
}
|
|
|
|
return targetPath === resourcePath || targetPath.startsWith(`${resourcePath}/`);
|
|
}
|
|
|
|
export class ApiSecurityPolicy extends SecurityPolicy {
|
|
constructor(config = {}) {
|
|
super(config);
|
|
this.storage = getProvider('kv', 'security.api');
|
|
this.baseURL = config.base_url || '/api/security';
|
|
this.client = api.scope(this.baseURL);
|
|
this.cache = {
|
|
session: null,
|
|
user: null,
|
|
profile: null,
|
|
realm: null,
|
|
resources: [],
|
|
permits: [],
|
|
subjects: [],
|
|
admin: null,
|
|
adminPromise: null
|
|
};
|
|
}
|
|
|
|
async init() {
|
|
await this._hydrateCurrentSession();
|
|
}
|
|
|
|
async _request(path, options = {}, extra = {}) {
|
|
const { authToken = null, trackActivity = true } = extra;
|
|
const headers = new Headers(options.headers || {});
|
|
if (authToken) {
|
|
headers.set('Authorization', `Bearer ${authToken}`);
|
|
}
|
|
|
|
return this.client.requestJSON(path, {
|
|
...options,
|
|
trackActivity,
|
|
skipRequestFilters: [SECURITY_REQUEST_FILTER],
|
|
headers
|
|
});
|
|
}
|
|
|
|
async getRequestAuthorization(_requestContext, { session } = {}) {
|
|
const token = session?.jwt_token || this.cache.session?.jwt_token || null;
|
|
if (!token) {
|
|
return null;
|
|
}
|
|
return { scheme: 'Bearer', token };
|
|
}
|
|
|
|
_applyBundle(bundle = {}) {
|
|
this.cache.session = bundle.session ? new Session(bundle.session) : null;
|
|
this.cache.user = bundle.user ? new User(bundle.user) : null;
|
|
this.cache.profile = bundle.profile ? new AccountProfile(bundle.profile) : null;
|
|
this.cache.realm = bundle.realm ? new Realm(bundle.realm) : null;
|
|
this.cache.resources = Array.isArray(bundle.resources) ? bundle.resources.map((item) => new Resource(item)) : [];
|
|
this.cache.permits = Array.isArray(bundle.permits) ? bundle.permits.map((item) => new Permit(item)) : [];
|
|
}
|
|
|
|
async _hydrateCurrentSession() {
|
|
const stored = await this.storage.get(SESSION_KEY, null);
|
|
if (!stored?.jwt_token) {
|
|
await this.clearSession();
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const bundle = await this._request('/session', { method: 'GET' }, { authToken: stored.jwt_token, trackActivity: false });
|
|
this._applyBundle(bundle);
|
|
await this.saveSession(this.cache.session);
|
|
return this.cache.session;
|
|
} catch (error) {
|
|
await this.clearSession();
|
|
if (error?.status === 401) {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async authenticate(credentials = {}) {
|
|
const username = credentials.username || credentials.email || '';
|
|
const password = credentials.password || '';
|
|
if (typeof btoa !== 'function') {
|
|
throw new Error('Basic authentication requires btoa in the current runtime');
|
|
}
|
|
const basicToken = btoa(`${username}:${password}`);
|
|
const bundle = await this._request('/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Basic ${basicToken}`
|
|
},
|
|
body: JSON.stringify({
|
|
deterministic: Boolean(credentials.deterministic),
|
|
products: Array.isArray(credentials.products) ? credentials.products : [],
|
|
licence_key: credentials.licence_key || null,
|
|
dimensions: Array.isArray(credentials.dimensions) ? credentials.dimensions : []
|
|
})
|
|
});
|
|
this._applyBundle(bundle);
|
|
await this.saveSession(this.cache.session);
|
|
this.cache.admin = null;
|
|
this.cache.adminPromise = null;
|
|
return {
|
|
session: clone(this.cache.session),
|
|
user: clone(this.cache.user),
|
|
profile: clone(this.cache.profile)
|
|
};
|
|
}
|
|
|
|
async logout(session) {
|
|
const token = session?.jwt_token || this.cache.session?.jwt_token || null;
|
|
if (!token) {
|
|
return;
|
|
}
|
|
try {
|
|
await this._request('/logout', { method: 'POST' }, { authToken: token });
|
|
} catch (error) {
|
|
if (error?.status !== 401) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
async getCurrentSession() {
|
|
if (this.cache.session?.jwt_token) {
|
|
return clone(this.cache.session);
|
|
}
|
|
const session = await this._hydrateCurrentSession();
|
|
return clone(session);
|
|
}
|
|
|
|
async saveSession(session) {
|
|
if (!session) {
|
|
await this.clearSession();
|
|
return;
|
|
}
|
|
await this.storage.set(SESSION_KEY, clone(session));
|
|
}
|
|
|
|
async clearSession() {
|
|
this.cache = {
|
|
session: null,
|
|
user: null,
|
|
profile: null,
|
|
realm: null,
|
|
resources: [],
|
|
permits: [],
|
|
subjects: [],
|
|
admin: null,
|
|
adminPromise: null
|
|
};
|
|
await this.storage.remove(SESSION_KEY);
|
|
}
|
|
|
|
async _ensureSessionLoaded() {
|
|
if (this.cache.session?.jwt_token && this.cache.user?.id) {
|
|
return this.cache.session;
|
|
}
|
|
return this._hydrateCurrentSession();
|
|
}
|
|
|
|
async _loadAdminDataset() {
|
|
await this._ensureSessionLoaded();
|
|
if (!this.cache.session?.jwt_token) {
|
|
return null;
|
|
}
|
|
if (this.cache.admin) {
|
|
return this.cache.admin;
|
|
}
|
|
if (this.cache.adminPromise) {
|
|
return this.cache.adminPromise;
|
|
}
|
|
const token = this.cache.session.jwt_token;
|
|
this.cache.adminPromise = (async () => {
|
|
const [users, roles, realms, resources, subjects, permits] = await Promise.all([
|
|
this._request('/admin/users', { method: 'GET' }, { authToken: token }),
|
|
this._request('/admin/roles', { method: 'GET' }, { authToken: token }),
|
|
this._request('/admin/realms', { method: 'GET' }, { authToken: token }),
|
|
this._request('/admin/resources', { method: 'GET' }, { authToken: token }),
|
|
this._request('/admin/subjects', { method: 'GET' }, { authToken: token }),
|
|
this._request('/admin/permits', { method: 'GET' }, { authToken: token })
|
|
]);
|
|
this.cache.admin = {
|
|
users: Array.isArray(users) ? users.map((item) => new User(item)) : [],
|
|
roles: Array.isArray(roles) ? roles.map((item) => new Role(item)) : [],
|
|
realms: Array.isArray(realms) ? realms.map((item) => new Realm(item)) : [],
|
|
resources: Array.isArray(resources) ? resources.map((item) => new Resource(item)) : [],
|
|
subjects: Array.isArray(subjects) ? subjects.map((item) => new Securable(item)) : [],
|
|
permits: Array.isArray(permits) ? permits.map((item) => new Permit(item)) : []
|
|
};
|
|
return this.cache.admin;
|
|
})();
|
|
try {
|
|
return await this.cache.adminPromise;
|
|
} finally {
|
|
this.cache.adminPromise = null;
|
|
}
|
|
}
|
|
|
|
async listUsers() {
|
|
const admin = await this._loadAdminDataset();
|
|
return clone(admin?.users || []);
|
|
}
|
|
|
|
async getUser(userId) {
|
|
await this._ensureSessionLoaded();
|
|
if (this.cache.user?.id === userId) {
|
|
return clone(this.cache.user);
|
|
}
|
|
const admin = await this._loadAdminDataset();
|
|
return clone(admin?.users?.find((item) => item.id === userId) || null);
|
|
}
|
|
|
|
async listRoles() {
|
|
const admin = await this._loadAdminDataset();
|
|
return clone(admin?.roles || []);
|
|
}
|
|
|
|
async listSubjects() {
|
|
const admin = await this._loadAdminDataset();
|
|
return clone(admin?.subjects || []);
|
|
}
|
|
|
|
async listRealms() {
|
|
const admin = await this._loadAdminDataset();
|
|
return clone(admin?.realms || []);
|
|
}
|
|
|
|
async getRealm(realmId) {
|
|
await this._ensureSessionLoaded();
|
|
if (this.cache.realm?.id === realmId) {
|
|
return clone(this.cache.realm);
|
|
}
|
|
const admin = await this._loadAdminDataset();
|
|
return clone(admin?.realms?.find((item) => item.id === realmId) || null);
|
|
}
|
|
|
|
_invalidateAdminCache() {
|
|
this.cache.admin = null;
|
|
this.cache.adminPromise = null;
|
|
}
|
|
|
|
async createUser(userData) {
|
|
await this._ensureSessionLoaded();
|
|
const created = await this._request('/admin/users', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(userData)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
return created ? new User(created) : null;
|
|
}
|
|
|
|
async updateUser(userId, patch) {
|
|
await this._ensureSessionLoaded();
|
|
const updated = await this._request(`/admin/users/${encodeURIComponent(userId)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
return updated ? new User(updated) : null;
|
|
}
|
|
|
|
async deleteUser(userId) {
|
|
await this._ensureSessionLoaded();
|
|
await this._request(`/admin/users/${encodeURIComponent(userId)}`, {
|
|
method: 'DELETE'
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
}
|
|
|
|
async getRole(roleId) {
|
|
const admin = await this._loadAdminDataset();
|
|
return clone(admin?.roles?.find((item) => item.id === roleId) || null);
|
|
}
|
|
|
|
async createRole(roleData) {
|
|
await this._ensureSessionLoaded();
|
|
const created = await this._request('/admin/roles', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(roleData)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
return created ? new Role(created) : null;
|
|
}
|
|
|
|
async updateRole(roleId, patch) {
|
|
await this._ensureSessionLoaded();
|
|
const updated = await this._request(`/admin/roles/${encodeURIComponent(roleId)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
return updated ? new Role(updated) : null;
|
|
}
|
|
|
|
async deleteRole(roleId) {
|
|
await this._ensureSessionLoaded();
|
|
await this._request(`/admin/roles/${encodeURIComponent(roleId)}`, {
|
|
method: 'DELETE'
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
}
|
|
|
|
async createRealm(realmData) {
|
|
await this._ensureSessionLoaded();
|
|
const created = await this._request('/admin/realms', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(realmData)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
return created ? new Realm(created) : null;
|
|
}
|
|
|
|
async updateRealm(realmId, patch) {
|
|
await this._ensureSessionLoaded();
|
|
const updated = await this._request(`/admin/realms/${encodeURIComponent(realmId)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
return updated ? new Realm(updated) : null;
|
|
}
|
|
|
|
async deleteRealm(realmId) {
|
|
await this._ensureSessionLoaded();
|
|
await this._request(`/admin/realms/${encodeURIComponent(realmId)}`, {
|
|
method: 'DELETE'
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
}
|
|
|
|
async registerResource(resource) {
|
|
await this._ensureSessionLoaded();
|
|
const created = await this._request('/admin/resources', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(resource)
|
|
}, { authToken: this.cache.session?.jwt_token, trackActivity: false });
|
|
this._invalidateAdminCache();
|
|
return created ? new Resource(created) : null;
|
|
}
|
|
|
|
async updateResource(path, patch) {
|
|
await this._ensureSessionLoaded();
|
|
const normalized = String(path || '').replace(/^\/+/, '');
|
|
const updated = await this._request(`/admin/resources/${normalized}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
return updated ? new Resource(updated) : null;
|
|
}
|
|
|
|
async deleteResource(path) {
|
|
await this._ensureSessionLoaded();
|
|
const normalized = String(path || '').replace(/^\/+/, '');
|
|
await this._request(`/admin/resources/${normalized}`, {
|
|
method: 'DELETE'
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
}
|
|
|
|
async listResources() {
|
|
const admin = await this._loadAdminDataset();
|
|
if (admin?.resources?.length) {
|
|
return clone(admin.resources);
|
|
}
|
|
await this._ensureSessionLoaded();
|
|
return clone(this.cache.resources);
|
|
}
|
|
|
|
async listPermits() {
|
|
const admin = await this._loadAdminDataset();
|
|
if (admin?.permits?.length) {
|
|
return clone(admin.permits);
|
|
}
|
|
await this._ensureSessionLoaded();
|
|
return clone(this.cache.permits);
|
|
}
|
|
|
|
async getAccountProfile(userId) {
|
|
await this._ensureSessionLoaded();
|
|
if (this.cache.profile && this.cache.profile.user_id === userId) {
|
|
return clone(this.cache.profile);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async updateAccountProfile(userId, patch) {
|
|
await this._ensureSessionLoaded();
|
|
if (!this.cache.user || this.cache.user.id !== userId) {
|
|
throw new Error('Profile update is only available for the authenticated user');
|
|
}
|
|
const profile = await this._request('/account/profile', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this.cache.profile = profile ? new AccountProfile(profile) : null;
|
|
if (this.cache.user) {
|
|
if (patch.display_name !== undefined) this.cache.user.display_name = patch.display_name;
|
|
if (patch.email !== undefined) this.cache.user.email = patch.email;
|
|
if (patch.image_url !== undefined) this.cache.user.image_url = patch.image_url;
|
|
}
|
|
return clone(this.cache.profile);
|
|
}
|
|
|
|
async uploadAccountAvatar(userId, file) {
|
|
await this._ensureSessionLoaded();
|
|
if (!this.cache.user || this.cache.user.id !== userId) {
|
|
throw new Error('Avatar upload is only available for the authenticated user');
|
|
}
|
|
const form = new FormData();
|
|
form.append('file', file);
|
|
const profile = await this._request('/account/avatar', {
|
|
method: 'POST',
|
|
body: form
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this.cache.profile = profile ? new AccountProfile(profile) : null;
|
|
if (this.cache.user && profile?.image_url !== undefined) {
|
|
this.cache.user.image_url = profile.image_url;
|
|
}
|
|
return clone(this.cache.profile);
|
|
}
|
|
|
|
async changePassword(userId, passwordInput = {}) {
|
|
await this._ensureSessionLoaded();
|
|
if (!this.cache.user || this.cache.user.id !== userId) {
|
|
throw new Error('Password change is only available for the authenticated user');
|
|
}
|
|
await this._request('/account/change-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(passwordInput)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
}
|
|
|
|
async listAccountSettings(userId) {
|
|
await this._ensureSessionLoaded();
|
|
if (!this.cache.user || this.cache.user.id !== userId) {
|
|
throw new Error('Account settings are only available for the authenticated user');
|
|
}
|
|
const rows = await this._request('/account/settings', {
|
|
method: 'GET'
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
return Array.isArray(rows) ? clone(rows) : [];
|
|
}
|
|
|
|
async getAccountSetting(userId, key) {
|
|
await this._ensureSessionLoaded();
|
|
if (!this.cache.user || this.cache.user.id !== userId) {
|
|
throw new Error('Account settings are only available for the authenticated user');
|
|
}
|
|
return clone(await this._request(`/account/settings/${encodeURIComponent(key)}`, {
|
|
method: 'GET'
|
|
}, { authToken: this.cache.session?.jwt_token }));
|
|
}
|
|
|
|
async updateAccountSetting(userId, key, patch = {}) {
|
|
await this._ensureSessionLoaded();
|
|
if (!this.cache.user || this.cache.user.id !== userId) {
|
|
throw new Error('Account settings are only available for the authenticated user');
|
|
}
|
|
return clone(await this._request(`/account/settings/${encodeURIComponent(key)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch)
|
|
}, { authToken: this.cache.session?.jwt_token }));
|
|
}
|
|
|
|
async deleteAccountSetting(userId, key) {
|
|
await this._ensureSessionLoaded();
|
|
if (!this.cache.user || this.cache.user.id !== userId) {
|
|
throw new Error('Account settings are only available for the authenticated user');
|
|
}
|
|
return this._request(`/account/settings/${encodeURIComponent(key)}`, {
|
|
method: 'DELETE'
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
}
|
|
|
|
async grantPermit(permitData) {
|
|
await this._ensureSessionLoaded();
|
|
const created = await this._request('/admin/permits', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(permitData)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
return created ? new Permit(created) : null;
|
|
}
|
|
|
|
async updatePermit(permitId, patch) {
|
|
await this._ensureSessionLoaded();
|
|
const updated = await this._request(`/admin/permits/${encodeURIComponent(permitId)}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch)
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
return updated ? new Permit(updated) : null;
|
|
}
|
|
|
|
async revokePermit(permitId) {
|
|
await this._ensureSessionLoaded();
|
|
await this._request(`/admin/permits/${encodeURIComponent(permitId)}`, {
|
|
method: 'DELETE'
|
|
}, { authToken: this.cache.session?.jwt_token });
|
|
this._invalidateAdminCache();
|
|
}
|
|
|
|
_buildPrincipals(user) {
|
|
if (!user?.id) {
|
|
return [];
|
|
}
|
|
|
|
const principals = [{ principal_type: 'user', principal_id: user.id }];
|
|
if (Array.isArray(user.role_ids)) {
|
|
user.role_ids.forEach((roleId) => {
|
|
principals.push({ principal_type: 'role', principal_id: roleId });
|
|
});
|
|
}
|
|
return principals;
|
|
}
|
|
|
|
_evaluateCached(rights, resourcePath) {
|
|
if (!this.cache.user?.id || !this.cache.session?.jwt_token) {
|
|
return {
|
|
allowed: false,
|
|
requires_login: true,
|
|
reason: 'Authentication required',
|
|
matched_permits: []
|
|
};
|
|
}
|
|
|
|
const requestedRights = normalizeRightsInput(rights);
|
|
const targetPath = resourcePath || '/';
|
|
const principals = this._buildPrincipals(this.cache.user);
|
|
const matchingPermits = this.cache.permits.filter((permit) => {
|
|
const principalMatch = principals.some((principal) => (
|
|
principal.principal_type === permit.principal_type
|
|
&& principal.principal_id === permit.principal_id
|
|
));
|
|
return principalMatch && pathMatches(permit.resource_path, targetPath);
|
|
});
|
|
const denyMatch = matchingPermits.find((permit) => permit.effect === 'deny' && hasRequiredRights(permit.rights, requestedRights));
|
|
if (denyMatch) {
|
|
return {
|
|
allowed: false,
|
|
requires_login: false,
|
|
reason: 'Denied by explicit policy',
|
|
matched_permits: clone([denyMatch])
|
|
};
|
|
}
|
|
const allowMatches = matchingPermits.filter((permit) => permit.effect === 'allow' && hasRequiredRights(permit.rights, requestedRights));
|
|
if (allowMatches.length > 0 || requestedRights === 0) {
|
|
return {
|
|
allowed: true,
|
|
requires_login: false,
|
|
reason: allowMatches.length > 0 ? 'Permit granted' : 'No specific rights requested',
|
|
matched_permits: clone(allowMatches)
|
|
};
|
|
}
|
|
return {
|
|
allowed: false,
|
|
requires_login: false,
|
|
reason: `Missing required rights on ${targetPath}`,
|
|
matched_permits: clone(matchingPermits)
|
|
};
|
|
}
|
|
|
|
async evaluate(userId, rights, resourcePath, context = {}) {
|
|
await this._ensureSessionLoaded();
|
|
if (!this.cache.user?.id || this.cache.user.id !== userId) {
|
|
return {
|
|
allowed: false,
|
|
requires_login: true,
|
|
reason: 'Authentication required',
|
|
matched_permits: []
|
|
};
|
|
}
|
|
return this._evaluateCached(rights, resourcePath || context.resource_path || '/');
|
|
}
|
|
|
|
evaluateSync(userId, rights, resourcePath, context = {}) {
|
|
if (!this.cache.user?.id || this.cache.user.id !== userId) {
|
|
return {
|
|
allowed: false,
|
|
requires_login: true,
|
|
reason: 'Authentication required',
|
|
matched_permits: []
|
|
};
|
|
}
|
|
return this._evaluateCached(rights, resourcePath || context.resource_path || '/');
|
|
}
|
|
}
|