libs/acl/src/lib/acl-engine/acl-engine.ts
Properties |
Methods |
Accessors |
constructor(store?: ExpressionStoreAdapter)
|
||||||
Defined in libs/acl/src/lib/acl-engine/acl-engine.ts:26
|
||||||
Parameters :
|
unscoped |
Type : AclEvaluationInterface
|
Default value : this
|
Defined in libs/acl/src/lib/acl-engine/acl-engine.ts:15
|
_setGlobalRules | ||||||
_setGlobalRules(rules: AclRule[])
|
||||||
Defined in libs/acl/src/lib/acl-engine/acl-engine.ts:61
|
||||||
This replaces the rules with a new set of rules. Only use this if you know what you are doing! Internal usage only! This might be removed at any time without deprecation.
Parameters :
Returns :
void
|
canEdit | ||||||
canEdit(aclPath: string)
|
||||||
Parameters :
Returns :
boolean
|
canEdit$ | ||||||
canEdit$(aclPath: string)
|
||||||
Parameters :
Returns :
Observable<boolean>
|
canShow | ||||||
Use `!aclEngine.isHidden(aclPath)` instead | ||||||
canShow(aclPath: string)
|
||||||
Parameters :
Returns :
boolean
|
canShow$ | ||||||
Use `aclEngine.isHidden$(aclPath).pipe(map((value) => !value))` instead | ||||||
canShow$(aclPath: string)
|
||||||
Defined in libs/acl/src/lib/acl-engine/acl-engine.ts:97
|
||||||
Parameters :
Returns :
Observable<boolean>
|
isDisabled | ||||||
isDisabled(aclPath: string)
|
||||||
Parameters :
Returns :
boolean
|
isDisabled$ | ||||||
isDisabled$(aclPath: string)
|
||||||
Parameters :
Returns :
Observable<boolean>
|
isHidden | ||||||
isHidden(aclPath: string)
|
||||||
Parameters :
Returns :
boolean
|
isHidden$ | ||||||
isHidden$(aclPath: string)
|
||||||
Parameters :
Returns :
Observable<boolean>
|
isReadonly | ||||||
isReadonly(aclPath: string)
|
||||||
Parameters :
Returns :
boolean
|
isReadonly$ | ||||||
isReadonly$(aclPath: string)
|
||||||
Parameters :
Returns :
Observable<boolean>
|
listen | ||||||
listen(callback: (decision: AclDecision) => void)
|
||||||
Defined in libs/acl/src/lib/acl-engine/acl-engine.ts:47
|
||||||
Parameters :
Returns :
void
|
removeDynamicFormRules | ||||||
removeDynamicFormRules(formId: string)
|
||||||
Defined in libs/acl/src/lib/acl-engine/acl-engine.ts:82
|
||||||
Parameters :
Returns :
void
|
setDynamicFormRules |
setDynamicFormRules(formId: string, aclRules: AclRule[])
|
Defined in libs/acl/src/lib/acl-engine/acl-engine.ts:69
|
Returns :
void
|
setEnvironmentValue |
setEnvironmentValue(key: string, value: string)
|
Defined in libs/acl/src/lib/acl-engine/acl-engine.ts:51
|
Returns :
void
|
setGlobalRulesFromPolicyTxtFile | ||||||
setGlobalRulesFromPolicyTxtFile(fileContent: string)
|
||||||
Defined in libs/acl/src/lib/acl-engine/acl-engine.ts:32
|
||||||
Parameters :
Returns :
void
|
rules |
getrules()
|
Defined in libs/acl/src/lib/acl-engine/acl-engine.ts:43
|
import { combineLatest, EMPTY, merge, Observable, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, share, startWith } from 'rxjs/operators';
import { isConditionMet } from './condition';
import { parsePolicyTxtContent } from './policy';
import { isRuleRelevantForPath } from './rule';
import {
AclDecision,
AclEvaluationInterface,
AclResourceState,
AclRule,
ExpressionStoreAdapter
} from './types';
export class AclEngine implements AclEvaluationInterface {
unscoped: AclEvaluationInterface = this;
private _rules: AclRule[] = [];
private _globalRules: AclRule[] = [];
private _dynamicFormRulesMap = new Map<string, AclRule[]>();
private environment: Record<string, string> = {};
private reportListener?: (rule: AclDecision) => void;
private cacheInvalidation$ = new Subject<void>();
private storeDataChanged$;
private relevantRulesCache = new Map<string, AclRule[]>();
constructor(private store?: ExpressionStoreAdapter) {
this.storeDataChanged$ = this.store?.dataChanged$.pipe(debounceTime(10), share());
}
setGlobalRulesFromPolicyTxtFile(fileContent: string): void {
this._globalRules = parsePolicyTxtContent(fileContent);
this.updateRules();
if (!this.store && this._globalRules.some((rule) => rule.condition.includes('s('))) {
throw new Error(
'You have used the store query function in your policy, but no store was provided!'
);
}
}
get rules(): AclRule[] {
return structuredClone(this._rules);
}
listen(callback: (decision: AclDecision) => void) {
this.reportListener = callback;
}
setEnvironmentValue(key: string, value: string): void {
this.environment[`env.${key}`] = value;
this.cacheInvalidation$.next();
}
/**
* This replaces the rules with a new set of rules.
* Only use this if you know what you are doing! Internal usage only!
* This might be removed at any time without deprecation.
*/
_setGlobalRules(rules: AclRule[]): void {
const globalRules = rules.filter((rule) => !rule.dynamicFormRule);
this._globalRules = globalRules;
this.updateRules();
this.relevantRulesCache.clear();
this.cacheInvalidation$.next();
}
setDynamicFormRules(formId: string, aclRules: AclRule[]) {
this._dynamicFormRulesMap.set(formId, aclRules);
this.updateRules();
this.relevantRulesCache.clear();
this.cacheInvalidation$.next();
if (!this.store && aclRules.some((rule) => rule.condition.includes('s('))) {
throw new Error(
'You have used the store query function in your policy, but no store was provided!'
);
}
}
removeDynamicFormRules(formId: string) {
this._dynamicFormRulesMap.delete(formId);
this.updateRules();
}
private updateRules() {
const dynamicFormRules = Array.from(this._dynamicFormRulesMap.values()).flatMap(
(aclRules) => aclRules
);
this._rules = [...this._globalRules, ...dynamicFormRules];
}
/**
* @deprecated Use `aclEngine.isHidden$(aclPath).pipe(map((value) => !value))` instead
*/
canShow$(aclPath: string): Observable<boolean> {
return this.isHidden$(aclPath).pipe(map((value) => !value));
}
canEdit$(aclPath: string): Observable<boolean> {
return combineLatest([this.isDisabled$(aclPath), this.isReadonly$(aclPath)]).pipe(
map(([isDisabled, isReadonly]) => !isDisabled && !isReadonly)
);
}
/**
* @deprecated Use `!aclEngine.isHidden(aclPath)` instead
*/
canShow(aclPath: string): boolean {
return !this.isHidden(aclPath);
}
canEdit(aclPath: string): boolean {
return !this.isDisabled(aclPath) && !this.isReadonly(aclPath);
}
isReadonly(aclPath: string): boolean {
return this.is(aclPath, 'readonly');
}
isReadonly$(aclPath: string): Observable<boolean> {
return this.mapStoreChanges$(() => this.is(aclPath, 'readonly'));
}
isDisabled(aclPath: string): boolean {
return this.is(aclPath, 'disabled');
}
isDisabled$(aclPath: string): Observable<boolean> {
return this.mapStoreChanges$(() => this.is(aclPath, 'disabled'));
}
isHidden(aclPath: string): boolean {
return this.is(aclPath, 'hidden');
}
isHidden$(aclPath: string): Observable<boolean> {
return this.mapStoreChanges$(() => this.is(aclPath, 'hidden'));
}
private is(aclPath: string, state: AclResourceState): boolean {
const cacheKey = `${aclPath}~~${state}`;
let relevantRules = this.relevantRulesCache.get(cacheKey);
if (!relevantRules) {
relevantRules = this._rules.filter((rule) => isRuleRelevantForPath(rule, aclPath, state));
this.relevantRulesCache.set(cacheKey, relevantRules);
}
const applicableRule = relevantRules.find((rule) =>
isConditionMet(
rule.condition,
this.environment,
this.store?.queryStoreValue ??
(() => {
throw new Error('No store available!');
})
)
);
// At this point, we know that either
// - there is no applicable rule or
// - the applicable rule that we found is the one that determines the state of the resource.
// The found applicable rule will always be "relevant" for the requested state.
// If, for example, "hidden" was requested, the applicable rule will always have either "hidden"
// or "visible" as its state.
// If, for example, "readonly" was requested, the applicable rule will always have either
// "readonly" or "editable" as its state.
// If there is no applicable rule, the below equation evaluates to false and we end up with
// our default decision, which is "false" (not hidden, not readonly, not disabled).
// This means that we can simply compare the two states and use that as our decision.
const decision = state === applicableRule?.state;
if (this.reportListener) {
this.reportListener({ path: aclPath, state, decision, timestamp: Date.now() });
}
return decision;
}
private mapStoreChanges$(mapFn: () => boolean): Observable<boolean> {
return merge(this.storeDataChanged$ ?? EMPTY, this.cacheInvalidation$).pipe(
startWith(null),
map(mapFn),
distinctUntilChanged()
);
}
}