mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add a cache to the AgentGroupAccessChecker
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import type { Term } from 'n3';
|
||||
import type { Store, Term } from 'n3';
|
||||
|
||||
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
||||
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
||||
import type { ExpiringStorage } from '../../storage/keyvalue/ExpiringStorage';
|
||||
import { fetchDataset } from '../../util/FetchUtil';
|
||||
import { promiseSome } from '../../util/PromiseUtil';
|
||||
import { readableToQuads } from '../../util/StreamUtil';
|
||||
@@ -11,13 +13,23 @@ import { AccessChecker } from './AccessChecker';
|
||||
/**
|
||||
* Checks if the given WebID belongs to a group that has access.
|
||||
* Implements the behaviour of groups from the WAC specification.
|
||||
*
|
||||
* Fetched results will be stored in an ExpiringStorage.
|
||||
*
|
||||
* Requires a storage that can store JS objects.
|
||||
* `expiration` parameter is how long entries in the cache should be stored in seconds, defaults to 3600.
|
||||
*/
|
||||
export class AgentGroupAccessChecker extends AccessChecker {
|
||||
private readonly converter: RepresentationConverter;
|
||||
private readonly cache: ExpiringStorage<string, Promise<Store>>;
|
||||
private readonly expiration: number;
|
||||
|
||||
public constructor(converter: RepresentationConverter) {
|
||||
public constructor(converter: RepresentationConverter, cache: ExpiringStorage<string, Promise<Store>>,
|
||||
expiration = 3600) {
|
||||
super();
|
||||
this.converter = converter;
|
||||
this.cache = cache;
|
||||
this.expiration = expiration * 1000;
|
||||
}
|
||||
|
||||
public async handle({ acl, rule, credentials }: AccessCheckerArgs): Promise<boolean> {
|
||||
@@ -42,9 +54,24 @@ export class AgentGroupAccessChecker extends AccessChecker {
|
||||
const groupDocument: ResourceIdentifier = { path: /^[^#]*/u.exec(group.value)![0] };
|
||||
|
||||
// Fetch the required vCard group file
|
||||
const dataset = await fetchDataset(groupDocument.path, this.converter);
|
||||
|
||||
const quads = await readableToQuads(dataset.data);
|
||||
const quads = await this.fetchCachedQuads(groupDocument.path);
|
||||
return quads.countQuads(group, VCARD.terms.hasMember, webId, null) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches quads from the given URL.
|
||||
* Will cache the values for later re-use.
|
||||
*/
|
||||
private async fetchCachedQuads(url: string): Promise<Store> {
|
||||
let result = await this.cache.get(url);
|
||||
if (!result) {
|
||||
const prom = (async(): Promise<Store> => {
|
||||
const representation = await fetchDataset(url, this.converter);
|
||||
return readableToQuads(representation.data);
|
||||
})();
|
||||
await this.cache.set(url, prom, this.expiration);
|
||||
result = await prom;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class TokenOwnershipValidator extends OwnershipValidator {
|
||||
// No reason to fetch the WebId if we don't have a token yet
|
||||
if (!token) {
|
||||
token = this.generateToken();
|
||||
await this.storage.set(key, token, new Date(Date.now() + this.expiration));
|
||||
await this.storage.set(key, token, this.expiration);
|
||||
this.throwError(webId, token);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,13 +43,13 @@ export class ExpiringAdapter implements Adapter {
|
||||
|
||||
public async upsert(id: string, payload: AdapterPayload, expiresIn?: number): Promise<void> {
|
||||
// Despite what the typings say, `expiresIn` can be undefined
|
||||
const expires = expiresIn ? new Date(Date.now() + (expiresIn * 1000)) : undefined;
|
||||
const expiration = expiresIn ? expiresIn * 1000 : undefined;
|
||||
const key = this.keyFor(id);
|
||||
|
||||
this.logger.debug(`Storing payload data for ${id}`);
|
||||
|
||||
const storagePromises: Promise<unknown>[] = [
|
||||
this.storage.set(key, payload, expires),
|
||||
this.storage.set(key, payload, expiration),
|
||||
];
|
||||
if (payload.grantId) {
|
||||
storagePromises.push(
|
||||
@@ -57,15 +57,15 @@ export class ExpiringAdapter implements Adapter {
|
||||
const grantKey = this.grantKeyFor(payload.grantId!);
|
||||
const grants = (await this.storage.get(grantKey) || []) as string[];
|
||||
grants.push(key);
|
||||
await this.storage.set(grantKey, grants, expires);
|
||||
await this.storage.set(grantKey, grants, expiration);
|
||||
})(),
|
||||
);
|
||||
}
|
||||
if (payload.userCode) {
|
||||
storagePromises.push(this.storage.set(this.userCodeKeyFor(payload.userCode), id, expires));
|
||||
storagePromises.push(this.storage.set(this.userCodeKeyFor(payload.userCode), id, expiration));
|
||||
}
|
||||
if (payload.uid) {
|
||||
storagePromises.push(this.storage.set(this.uidKeyFor(payload.uid), id, expires));
|
||||
storagePromises.push(this.storage.set(this.uidKeyFor(payload.uid), id, expiration));
|
||||
}
|
||||
await Promise.all(storagePromises);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import type { KeyValueStorage } from './KeyValueStorage';
|
||||
|
||||
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||
/**
|
||||
* A KeyValueStorage in which the values can expire.
|
||||
* Entries with no expiration date never expire.
|
||||
*/
|
||||
export interface ExpiringStorage<TKey, TValue> extends KeyValueStorage<TKey, TValue> {
|
||||
/**
|
||||
* Sets the value for the given key.
|
||||
* Should error if the data is already expired.
|
||||
*
|
||||
* @param key - Key to set/update.
|
||||
* @param value - Value to store.
|
||||
* @param expiration - How long this data should stay valid in milliseconds.
|
||||
*
|
||||
* @returns The storage.
|
||||
*/
|
||||
set(key: TKey, value: TValue, expiration?: number): Promise<this>;
|
||||
|
||||
/**
|
||||
* Sets the value for the given key.
|
||||
* Should error if the data is already expired.
|
||||
@@ -15,5 +28,5 @@ export interface ExpiringStorage<TKey, TValue> extends KeyValueStorage<TKey, TVa
|
||||
*
|
||||
* @returns The storage.
|
||||
*/
|
||||
set: (key: TKey, value: TValue, expires?: Date) => Promise<this>;
|
||||
set(key: TKey, value: TValue, expires?: Date): Promise<this>;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ export class WrappedExpiringStorage<TKey, TValue> implements ExpiringStorage<TKe
|
||||
return Boolean(await this.getUnexpired(key));
|
||||
}
|
||||
|
||||
public async set(key: TKey, value: TValue, expires?: Date): Promise<this> {
|
||||
public async set(key: TKey, value: TValue, expiration?: number): Promise<this>;
|
||||
public async set(key: TKey, value: TValue, expires?: Date): Promise<this>;
|
||||
public async set(key: TKey, value: TValue, expireValue?: number | Date): Promise<this> {
|
||||
const expires = typeof expireValue === 'number' ? new Date(Date.now() + expireValue) : expireValue;
|
||||
if (this.isExpired(expires)) {
|
||||
throw new InternalServerError('Value is already expired');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user