mirror of
https://github.com/expo/entity.git
synced 2026-11-13 20:28:06 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58797a50b6 | ||
|
|
d09a735264 | ||
|
|
dc3aafc6ab |
No files matched your search
@@ -1,9 +1,10 @@
|
||||
import {
|
||||
PrivacyPolicyRule,
|
||||
ReadonlyEntity,
|
||||
EntityQueryContext,
|
||||
RuleEvaluationResult,
|
||||
EntityPrivacyPolicyEvaluationContext,
|
||||
RuleComplexity,
|
||||
AllowOrSkipPrivacyPolicyRule,
|
||||
} from '@expo/entity';
|
||||
|
||||
import { ExampleViewerContext } from '../viewerContexts';
|
||||
@@ -27,17 +28,19 @@ export default class AllowIfUserOwnerPrivacyRule<
|
||||
TID extends NonNullable<TFields[TSelectedFields]>,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, ExampleViewerContext>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends PrivacyPolicyRule<TFields, TID, ExampleViewerContext, TEntity> {
|
||||
> extends AllowOrSkipPrivacyPolicyRule<TFields, TID, ExampleViewerContext, TEntity> {
|
||||
constructor(private readonly entityOwnerField: keyof TFields) {
|
||||
super();
|
||||
}
|
||||
|
||||
override complexity = RuleComplexity.LOW;
|
||||
|
||||
async evaluateAsync(
|
||||
viewerContext: ExampleViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
entity: TEntity
|
||||
): Promise<RuleEvaluationResult> {
|
||||
): Promise<RuleEvaluationResult.ALLOW | RuleEvaluationResult.SKIP> {
|
||||
if (viewerContext.isUserViewerContext()) {
|
||||
if (String(entity.getField(this.entityOwnerField)) === viewerContext.userID) {
|
||||
return RuleEvaluationResult.ALLOW;
|
||||
|
||||
@@ -6,7 +6,9 @@ import EntityNotAuthorizedError from './errors/EntityNotAuthorizedError';
|
||||
import IEntityMetricsAdapter, {
|
||||
EntityMetricsAuthorizationResult,
|
||||
} from './metrics/IEntityMetricsAdapter';
|
||||
import PrivacyPolicyRule, { RuleEvaluationResult } from './rules/PrivacyPolicyRule';
|
||||
import { RuleEvaluationResult } from './rules/PrivacyPolicyRuleEnums';
|
||||
import { PrivacyPolicyRule } from './rules/PrivacyPolicyRuleTypes';
|
||||
import { reorderRulesByRuleComplexityGroups } from './rules/internal/PrivacyPolicyRuleOrdering';
|
||||
|
||||
/**
|
||||
* Information about the reason this privacy policy is being evaluated.
|
||||
@@ -143,6 +145,15 @@ export default abstract class EntityPrivacyPolicy<
|
||||
};
|
||||
}
|
||||
|
||||
protected async shouldAutoReorderRulesAccordingToComplexityAsync(
|
||||
_viewerContext: TViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
_action: EntityAuthorizationAction
|
||||
): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize an entity against creation policy.
|
||||
* @param viewerContext - viewer context of user creating the entity
|
||||
@@ -248,7 +259,13 @@ export default abstract class EntityPrivacyPolicy<
|
||||
}
|
||||
|
||||
private async authorizeForRulesetAsync(
|
||||
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
|
||||
rulesetOriginalOrder: readonly PrivacyPolicyRule<
|
||||
TFields,
|
||||
TID,
|
||||
TViewerContext,
|
||||
TEntity,
|
||||
TSelectedFields
|
||||
>[],
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
@@ -257,6 +274,15 @@ export default abstract class EntityPrivacyPolicy<
|
||||
metricsAdapter: IEntityMetricsAdapter
|
||||
): Promise<TEntity> {
|
||||
const privacyPolicyEvaluator = this.getPrivacyPolicyEvaluator(viewerContext);
|
||||
const shouldAutoReorder = await this.shouldAutoReorderRulesAccordingToComplexityAsync(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
evaluationContext,
|
||||
action
|
||||
);
|
||||
const ruleset = shouldAutoReorder
|
||||
? reorderRulesByRuleComplexityGroups(rulesetOriginalOrder)
|
||||
: rulesetOriginalOrder;
|
||||
switch (privacyPolicyEvaluator.mode) {
|
||||
case EntityPrivacyPolicyEvaluationMode.ENFORCE:
|
||||
try {
|
||||
|
||||
@@ -12,7 +12,8 @@ import { enforceResultsAsync } from '../entityUtils';
|
||||
import EntityNotAuthorizedError from '../errors/EntityNotAuthorizedError';
|
||||
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
|
||||
import AlwaysDenyPrivacyPolicyRule from '../rules/AlwaysDenyPrivacyPolicyRule';
|
||||
import PrivacyPolicyRule, { RuleEvaluationResult } from '../rules/PrivacyPolicyRule';
|
||||
import { RuleComplexity, RuleEvaluationResult } from '../rules/PrivacyPolicyRuleEnums';
|
||||
import { DenyOrSkipPrivacyPolicyRule } from '../rules/PrivacyPolicyRuleTypes';
|
||||
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
|
||||
|
||||
class TestUserViewerContext extends ViewerContext {
|
||||
@@ -60,18 +61,20 @@ class BlahEntity extends Entity<BlahFields, string, TestUserViewerContext> {
|
||||
}
|
||||
}
|
||||
|
||||
class DenyIfNotOwnerPrivacyPolicyRule extends PrivacyPolicyRule<
|
||||
class DenyIfNotOwnerPrivacyPolicyRule extends DenyOrSkipPrivacyPolicyRule<
|
||||
BlahFields,
|
||||
string,
|
||||
TestUserViewerContext,
|
||||
BlahEntity
|
||||
> {
|
||||
override complexity = RuleComplexity.LOW;
|
||||
|
||||
async evaluateAsync(
|
||||
viewerContext: TestUserViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
entity: BlahEntity
|
||||
): Promise<RuleEvaluationResult> {
|
||||
): Promise<RuleEvaluationResult.DENY | RuleEvaluationResult.SKIP> {
|
||||
if (viewerContext.getUserID() === entity.getField('ownerID')) {
|
||||
return RuleEvaluationResult.SKIP;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import EntityPrivacyPolicy, {
|
||||
} from '../EntityPrivacyPolicy';
|
||||
import { EntityTransactionalQueryContext, EntityQueryContext } from '../EntityQueryContext';
|
||||
import { CacheStatus } from '../internal/ReadThroughEntityCache';
|
||||
import PrivacyPolicyRule, { RuleEvaluationResult } from '../rules/PrivacyPolicyRule';
|
||||
import { RuleComplexity, RuleEvaluationResult } from '../rules/PrivacyPolicyRuleEnums';
|
||||
import { AllowOrSkipPrivacyPolicyRule } from '../rules/PrivacyPolicyRuleTypes';
|
||||
import TestViewerContext from '../testfixtures/TestViewerContext';
|
||||
import { InMemoryFullCacheStubCacheAdapter } from '../utils/testing/StubCacheAdapter';
|
||||
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
|
||||
@@ -66,13 +67,15 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
||||
},
|
||||
};
|
||||
|
||||
class AlwaysAllowPrivacyPolicyRuleThatRecords extends PrivacyPolicyRule<
|
||||
class AlwaysAllowPrivacyPolicyRuleThatRecords extends AllowOrSkipPrivacyPolicyRule<
|
||||
any,
|
||||
string,
|
||||
TestViewerContext,
|
||||
any,
|
||||
any
|
||||
> {
|
||||
override complexity = RuleComplexity.LOW;
|
||||
|
||||
constructor(private readonly action: EntityAuthorizationAction) {
|
||||
super();
|
||||
}
|
||||
@@ -82,7 +85,7 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
||||
_queryContext: EntityQueryContext,
|
||||
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
entity: any
|
||||
): Promise<RuleEvaluationResult> {
|
||||
): Promise<RuleEvaluationResult.ALLOW> {
|
||||
if (privacyPolicyEvaluationRecords.shouldRecord) {
|
||||
(privacyPolicyEvaluationRecords as any)[entity.constructor.name][this.action].push(
|
||||
evaluationContext
|
||||
|
||||
@@ -19,7 +19,8 @@ import IEntityMetricsAdapter, {
|
||||
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
|
||||
import AlwaysDenyPrivacyPolicyRule from '../rules/AlwaysDenyPrivacyPolicyRule';
|
||||
import AlwaysSkipPrivacyPolicyRule from '../rules/AlwaysSkipPrivacyPolicyRule';
|
||||
import PrivacyPolicyRule, { RuleEvaluationResult } from '../rules/PrivacyPolicyRule';
|
||||
import { RuleComplexity, RuleEvaluationResult } from '../rules/PrivacyPolicyRuleEnums';
|
||||
import { AllowOrSkipPrivacyPolicyRule } from '../rules/PrivacyPolicyRuleTypes';
|
||||
|
||||
type BlahFields = {
|
||||
id: string;
|
||||
@@ -172,18 +173,20 @@ class SkipAllPolicy extends EntityPrivacyPolicy<BlahFields, string, ViewerContex
|
||||
];
|
||||
}
|
||||
|
||||
class AlwaysThrowPrivacyPolicyRule extends PrivacyPolicyRule<
|
||||
class AlwaysThrowPrivacyPolicyRule extends AllowOrSkipPrivacyPolicyRule<
|
||||
BlahFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
BlahEntity
|
||||
> {
|
||||
override complexity = RuleComplexity.LOW;
|
||||
|
||||
evaluateAsync(
|
||||
_viewerContext: ViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
_entity: BlahEntity
|
||||
): Promise<RuleEvaluationResult> {
|
||||
): Promise<RuleEvaluationResult.ALLOW> {
|
||||
throw new Error('WooHoo!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import { mock, instance } from 'ts-mockito';
|
||||
|
||||
import Entity from '../Entity';
|
||||
import { EntityCompanionDefinition } from '../EntityCompanionProvider';
|
||||
import EntityConfiguration from '../EntityConfiguration';
|
||||
import { UUIDField } from '../EntityFields';
|
||||
import EntityPrivacyPolicy, {
|
||||
EntityAuthorizationAction,
|
||||
EntityPrivacyPolicyEvaluationContext,
|
||||
} from '../EntityPrivacyPolicy';
|
||||
import { EntityQueryContext } from '../EntityQueryContext';
|
||||
import ViewerContext from '../ViewerContext';
|
||||
import IEntityMetricsAdapter from '../metrics/IEntityMetricsAdapter';
|
||||
import { RuleComplexity, RuleEvaluationResult } from '../rules/PrivacyPolicyRuleEnums';
|
||||
import {
|
||||
AllowOrSkipPrivacyPolicyRule,
|
||||
DenyOrSkipPrivacyPolicyRule,
|
||||
SkipPrivacyPolicyRule,
|
||||
} from '../rules/PrivacyPolicyRuleTypes';
|
||||
|
||||
type BlahFields = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
class BlahEntity extends Entity<BlahFields, string, ViewerContext> {
|
||||
static defineCompanionDefinition(): EntityCompanionDefinition<
|
||||
BlahFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
BlahEntity,
|
||||
any
|
||||
> {
|
||||
return {
|
||||
entityClass: BlahEntity,
|
||||
entityConfiguration: new EntityConfiguration<BlahFields>({
|
||||
idField: 'id',
|
||||
tableName: 'blah_table',
|
||||
schema: {
|
||||
id: new UUIDField({
|
||||
columnName: 'id',
|
||||
}),
|
||||
},
|
||||
databaseAdapterFlavor: 'postgres',
|
||||
cacheAdapterFlavor: 'redis',
|
||||
}),
|
||||
privacyPolicyClass: ReorderablePolicy,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class LargeAsyncComplexityAllowRule extends AllowOrSkipPrivacyPolicyRule<
|
||||
BlahFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
BlahEntity
|
||||
> {
|
||||
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP | RuleEvaluationResult.ALLOW> {
|
||||
return RuleEvaluationResult.SKIP;
|
||||
}
|
||||
override complexity = RuleComplexity.HIGH;
|
||||
}
|
||||
|
||||
class ConstantTimeComplexityAllowRule extends AllowOrSkipPrivacyPolicyRule<
|
||||
BlahFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
BlahEntity
|
||||
> {
|
||||
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP | RuleEvaluationResult.ALLOW> {
|
||||
return RuleEvaluationResult.SKIP;
|
||||
}
|
||||
override complexity = RuleComplexity.LOW;
|
||||
}
|
||||
|
||||
class LargeAsyncComplexitySkipRule extends SkipPrivacyPolicyRule<
|
||||
BlahFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
BlahEntity
|
||||
> {
|
||||
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP> {
|
||||
return RuleEvaluationResult.SKIP;
|
||||
}
|
||||
override complexity = RuleComplexity.HIGH;
|
||||
}
|
||||
|
||||
class ConstantTimeComplexitySkipRule extends SkipPrivacyPolicyRule<
|
||||
BlahFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
BlahEntity
|
||||
> {
|
||||
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP> {
|
||||
return RuleEvaluationResult.SKIP;
|
||||
}
|
||||
override complexity = RuleComplexity.LOW;
|
||||
}
|
||||
|
||||
class LargeAsyncComplexityDenyRule extends DenyOrSkipPrivacyPolicyRule<
|
||||
BlahFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
BlahEntity
|
||||
> {
|
||||
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP | RuleEvaluationResult.DENY> {
|
||||
return RuleEvaluationResult.SKIP;
|
||||
}
|
||||
override complexity = RuleComplexity.HIGH;
|
||||
}
|
||||
|
||||
class ConstantTimeComplexityDenyRule extends DenyOrSkipPrivacyPolicyRule<
|
||||
BlahFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
BlahEntity
|
||||
> {
|
||||
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP | RuleEvaluationResult.DENY> {
|
||||
return RuleEvaluationResult.SKIP;
|
||||
}
|
||||
override complexity = RuleComplexity.LOW;
|
||||
}
|
||||
|
||||
class ReorderablePolicy extends EntityPrivacyPolicy<BlahFields, string, ViewerContext, BlahEntity> {
|
||||
protected override async shouldAutoReorderRulesAccordingToComplexityAsync(
|
||||
_viewerContext: ViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
_action: EntityAuthorizationAction
|
||||
): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override readonly createRules = [
|
||||
new LargeAsyncComplexityDenyRule(),
|
||||
new ConstantTimeComplexityDenyRule(),
|
||||
|
||||
new LargeAsyncComplexityAllowRule(),
|
||||
new ConstantTimeComplexitySkipRule(),
|
||||
new ConstantTimeComplexityAllowRule(),
|
||||
|
||||
new LargeAsyncComplexityDenyRule(),
|
||||
new LargeAsyncComplexitySkipRule(),
|
||||
];
|
||||
protected override readonly readRules = [];
|
||||
protected override readonly updateRules = [];
|
||||
protected override readonly deleteRules = [];
|
||||
}
|
||||
|
||||
class NonReorderablePolicy extends EntityPrivacyPolicy<
|
||||
BlahFields,
|
||||
string,
|
||||
ViewerContext,
|
||||
BlahEntity
|
||||
> {
|
||||
protected override readonly createRules = [
|
||||
new LargeAsyncComplexityDenyRule(),
|
||||
new ConstantTimeComplexityDenyRule(),
|
||||
|
||||
new LargeAsyncComplexityAllowRule(),
|
||||
new ConstantTimeComplexitySkipRule(),
|
||||
new ConstantTimeComplexityAllowRule(),
|
||||
|
||||
new LargeAsyncComplexityDenyRule(),
|
||||
new LargeAsyncComplexitySkipRule(),
|
||||
];
|
||||
protected override readonly readRules = [];
|
||||
protected override readonly updateRules = [];
|
||||
protected override readonly deleteRules = [];
|
||||
}
|
||||
|
||||
describe(EntityPrivacyPolicy, () => {
|
||||
test('reorderable policy', async () => {
|
||||
const viewerContext = instance(mock(ViewerContext));
|
||||
const queryContext = instance(mock(EntityQueryContext));
|
||||
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
|
||||
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
||||
const metricsAdapter = instance(metricsAdapterMock);
|
||||
const entity = new BlahEntity({
|
||||
viewerContext,
|
||||
id: '1',
|
||||
databaseFields: { id: '1' },
|
||||
selectedFields: { id: '1' },
|
||||
});
|
||||
const policy = new ReorderablePolicy();
|
||||
|
||||
const spy0 = jest.spyOn(policy['createRules'][0]!, 'evaluateAsync');
|
||||
const spy1 = jest.spyOn(policy['createRules'][1]!, 'evaluateAsync');
|
||||
const spy2 = jest.spyOn(policy['createRules'][2]!, 'evaluateAsync');
|
||||
const spy3 = jest.spyOn(policy['createRules'][3]!, 'evaluateAsync');
|
||||
const spy4 = jest.spyOn(policy['createRules'][4]!, 'evaluateAsync');
|
||||
const spy5 = jest.spyOn(policy['createRules'][5]!, 'evaluateAsync');
|
||||
const spy6 = jest.spyOn(policy['createRules'][6]!, 'evaluateAsync');
|
||||
|
||||
// this will deny since all rules skip, but we only care about rule evaluation order
|
||||
try {
|
||||
await policy.authorizeCreateAsync(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
privacyPolicyEvaluationContext,
|
||||
entity,
|
||||
metricsAdapter
|
||||
);
|
||||
} catch {}
|
||||
|
||||
const spy0CallOrder = spy0.mock.invocationCallOrder[0]!;
|
||||
const spy1CallOrder = spy1.mock.invocationCallOrder[0]!;
|
||||
const spy2CallOrder = spy2.mock.invocationCallOrder[0]!;
|
||||
const spy3CallOrder = spy3.mock.invocationCallOrder[0]!;
|
||||
const spy4CallOrder = spy4.mock.invocationCallOrder[0]!;
|
||||
const spy5CallOrder = spy5.mock.invocationCallOrder[0]!;
|
||||
const spy6CallOrder = spy6.mock.invocationCallOrder[0]!;
|
||||
|
||||
// expected reordering: 1, 0, 3, 4, 2, 5, 6
|
||||
expect(spy1CallOrder).toBeLessThan(spy0CallOrder);
|
||||
expect(spy0CallOrder).toBeLessThan(spy3CallOrder);
|
||||
expect(spy3CallOrder).toBeLessThan(spy4CallOrder);
|
||||
expect(spy4CallOrder).toBeLessThan(spy2CallOrder);
|
||||
expect(spy2CallOrder).toBeLessThan(spy5CallOrder);
|
||||
expect(spy5CallOrder).toBeLessThan(spy6CallOrder);
|
||||
});
|
||||
|
||||
test('non-reorderable policy', async () => {
|
||||
const viewerContext = instance(mock(ViewerContext));
|
||||
const queryContext = instance(mock(EntityQueryContext));
|
||||
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
|
||||
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
|
||||
const metricsAdapter = instance(metricsAdapterMock);
|
||||
const entity = new BlahEntity({
|
||||
viewerContext,
|
||||
id: '1',
|
||||
databaseFields: { id: '1' },
|
||||
selectedFields: { id: '1' },
|
||||
});
|
||||
const policy = new NonReorderablePolicy();
|
||||
|
||||
const spy0 = jest.spyOn(policy['createRules'][0]!, 'evaluateAsync');
|
||||
const spy1 = jest.spyOn(policy['createRules'][1]!, 'evaluateAsync');
|
||||
const spy2 = jest.spyOn(policy['createRules'][2]!, 'evaluateAsync');
|
||||
const spy3 = jest.spyOn(policy['createRules'][3]!, 'evaluateAsync');
|
||||
const spy4 = jest.spyOn(policy['createRules'][4]!, 'evaluateAsync');
|
||||
const spy5 = jest.spyOn(policy['createRules'][5]!, 'evaluateAsync');
|
||||
const spy6 = jest.spyOn(policy['createRules'][6]!, 'evaluateAsync');
|
||||
|
||||
// this will deny since all rules skip, but we only care about rule evaluation order
|
||||
try {
|
||||
await policy.authorizeCreateAsync(
|
||||
viewerContext,
|
||||
queryContext,
|
||||
privacyPolicyEvaluationContext,
|
||||
entity,
|
||||
metricsAdapter
|
||||
);
|
||||
} catch {}
|
||||
|
||||
const spy0CallOrder = spy0.mock.invocationCallOrder[0]!;
|
||||
const spy1CallOrder = spy1.mock.invocationCallOrder[0]!;
|
||||
const spy2CallOrder = spy2.mock.invocationCallOrder[0]!;
|
||||
const spy3CallOrder = spy3.mock.invocationCallOrder[0]!;
|
||||
const spy4CallOrder = spy4.mock.invocationCallOrder[0]!;
|
||||
const spy5CallOrder = spy5.mock.invocationCallOrder[0]!;
|
||||
const spy6CallOrder = spy6.mock.invocationCallOrder[0]!;
|
||||
|
||||
// expected reordering: 0, 1, 2, 3, 4, 5, 6
|
||||
expect(spy0CallOrder).toBeLessThan(spy1CallOrder);
|
||||
expect(spy1CallOrder).toBeLessThan(spy2CallOrder);
|
||||
expect(spy2CallOrder).toBeLessThan(spy3CallOrder);
|
||||
expect(spy3CallOrder).toBeLessThan(spy4CallOrder);
|
||||
expect(spy4CallOrder).toBeLessThan(spy5CallOrder);
|
||||
expect(spy5CallOrder).toBeLessThan(spy6CallOrder);
|
||||
});
|
||||
});
|
||||
@@ -66,8 +66,8 @@ export { default as NoOpEntityMetricsAdapter } from './metrics/NoOpEntityMetrics
|
||||
export { default as AlwaysAllowPrivacyPolicyRule } from './rules/AlwaysAllowPrivacyPolicyRule';
|
||||
export { default as AlwaysDenyPrivacyPolicyRule } from './rules/AlwaysDenyPrivacyPolicyRule';
|
||||
export { default as AlwaysSkipPrivacyPolicyRule } from './rules/AlwaysSkipPrivacyPolicyRule';
|
||||
export { default as PrivacyPolicyRule } from './rules/PrivacyPolicyRule';
|
||||
export * from './rules/PrivacyPolicyRule';
|
||||
export * from './rules/PrivacyPolicyRuleEnums';
|
||||
export * from './rules/PrivacyPolicyRuleTypes';
|
||||
export * from './utils/testing/PrivacyPolicyRuleTestUtils';
|
||||
export * from './utils/testing/StubCacheAdapter';
|
||||
export { default as describeFieldTestCase } from './utils/testing/describeFieldTestCase';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import PrivacyPolicyRule, { RuleEvaluationResult } from './PrivacyPolicyRule';
|
||||
import { RuleComplexity, RuleEvaluationResult } from './PrivacyPolicyRuleEnums';
|
||||
import { AllowOrSkipPrivacyPolicyRule } from './PrivacyPolicyRuleTypes';
|
||||
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
|
||||
import { EntityQueryContext } from '../EntityQueryContext';
|
||||
import ReadonlyEntity from '../ReadonlyEntity';
|
||||
@@ -13,13 +14,15 @@ export default class AlwaysAllowPrivacyPolicyRule<
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
> extends AllowOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
override complexity = RuleComplexity.LOW;
|
||||
|
||||
async evaluateAsync(
|
||||
_viewerContext: TViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
_entity: TEntity
|
||||
): Promise<RuleEvaluationResult> {
|
||||
): Promise<RuleEvaluationResult.ALLOW | RuleEvaluationResult.SKIP> {
|
||||
return RuleEvaluationResult.ALLOW;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import PrivacyPolicyRule, { RuleEvaluationResult } from './PrivacyPolicyRule';
|
||||
import { RuleComplexity, RuleEvaluationResult } from './PrivacyPolicyRuleEnums';
|
||||
import { DenyOrSkipPrivacyPolicyRule } from './PrivacyPolicyRuleTypes';
|
||||
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
|
||||
import { EntityQueryContext } from '../EntityQueryContext';
|
||||
import ReadonlyEntity from '../ReadonlyEntity';
|
||||
@@ -13,13 +14,15 @@ export default class AlwaysDenyPrivacyPolicyRule<
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
> extends DenyOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
override complexity = RuleComplexity.LOW;
|
||||
|
||||
async evaluateAsync(
|
||||
_viewerContext: TViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
_entity: TEntity
|
||||
): Promise<RuleEvaluationResult> {
|
||||
): Promise<RuleEvaluationResult.DENY | RuleEvaluationResult.SKIP> {
|
||||
return RuleEvaluationResult.DENY;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import PrivacyPolicyRule, { RuleEvaluationResult } from './PrivacyPolicyRule';
|
||||
import { RuleComplexity, RuleEvaluationResult } from './PrivacyPolicyRuleEnums';
|
||||
import { SkipPrivacyPolicyRule } from './PrivacyPolicyRuleTypes';
|
||||
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
|
||||
import { EntityQueryContext } from '../EntityQueryContext';
|
||||
import ReadonlyEntity from '../ReadonlyEntity';
|
||||
@@ -13,13 +14,15 @@ export default class AlwaysSkipPrivacyPolicyRule<
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
> extends SkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
override complexity = RuleComplexity.LOW;
|
||||
|
||||
async evaluateAsync(
|
||||
_viewerContext: TViewerContext,
|
||||
_queryContext: EntityQueryContext,
|
||||
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
_entity: TEntity
|
||||
): Promise<RuleEvaluationResult> {
|
||||
): Promise<RuleEvaluationResult.SKIP> {
|
||||
return RuleEvaluationResult.SKIP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Enum return type of privacy policy rule evaluation.
|
||||
*/
|
||||
export enum RuleEvaluationResult {
|
||||
/**
|
||||
* Deny viewer access to the entity.
|
||||
*/
|
||||
DENY = -1,
|
||||
|
||||
/**
|
||||
* Defer entity viewer access to subsequent rule in the privacy policy.
|
||||
*/
|
||||
SKIP = 0,
|
||||
|
||||
/**
|
||||
* Allow viewer access to the entity.
|
||||
*/
|
||||
ALLOW = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimated complexity of a privacy policy rule. Definition of what rules fall into what complexity category
|
||||
* is application-specific, but the AlwaysAllow/AlwaysDeny/AlwaysSkip rules provided are all defined as LOW
|
||||
* complexity.
|
||||
*/
|
||||
export enum RuleComplexity {
|
||||
/**
|
||||
* Rule complexity is low. Definition dependent on application.
|
||||
*/
|
||||
LOW = 0,
|
||||
|
||||
/**
|
||||
* Rule complexity is medium. Definition dependent on application.
|
||||
*/
|
||||
MEDIUM = 1,
|
||||
|
||||
/**
|
||||
* Rule complexity is high. Definition dependent on application.
|
||||
*/
|
||||
HIGH = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for disambiguation between classes of privacy policy rules at runtime.
|
||||
* Multiple rules of the same type in a row can be reordered safely to improve policy evaluation time
|
||||
* based on complexities.
|
||||
*/
|
||||
export enum RuleEvaluationResultType {
|
||||
/**
|
||||
* Rule is a DenyOrSkipPrivacyPolicyRule. Can be safely reordered with other sequential DenyOrSkipPrivacyPolicyRule or SkipPrivacyPolicyRule in a policy.
|
||||
*/
|
||||
DENY_OR_SKIP,
|
||||
/**
|
||||
* Rule is a SkipPrivacyPolicyRule. Can be safely reordered with any other sequential privacy policy rules in a policy.
|
||||
*/
|
||||
SKIP,
|
||||
/**
|
||||
* Rule is an AllowOrSkipPrivacyPolicyRule. Can be safely reordered with other sequential AllowOrSkipPrivacyPolicyRule or SkipPrivacyPolicyRule in a policy.
|
||||
*/
|
||||
ALLOW_OR_SKIP,
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { RuleEvaluationResult, RuleEvaluationResultType } from './PrivacyPolicyRuleEnums';
|
||||
import { PrivacyPolicyRuleInternal } from './internal/PrivacyPolicyRule';
|
||||
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
|
||||
import { EntityQueryContext } from '../EntityQueryContext';
|
||||
import ReadonlyEntity from '../ReadonlyEntity';
|
||||
import ViewerContext from '../ViewerContext';
|
||||
|
||||
export type PrivacyPolicyRule<
|
||||
TFields extends object,
|
||||
TID extends NonNullable<TFields[TSelectedFields]>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> =
|
||||
| AllowOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>
|
||||
| DenyOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>
|
||||
| SkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>;
|
||||
|
||||
export abstract class AllowOrSkipPrivacyPolicyRule<
|
||||
TFields extends object,
|
||||
TID extends NonNullable<TFields[TSelectedFields]>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends PrivacyPolicyRuleInternal<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
abstract override evaluateAsync(
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
entity: TEntity
|
||||
): Promise<RuleEvaluationResult.ALLOW | RuleEvaluationResult.SKIP>;
|
||||
|
||||
override resultType = RuleEvaluationResultType.ALLOW_OR_SKIP;
|
||||
}
|
||||
|
||||
export abstract class DenyOrSkipPrivacyPolicyRule<
|
||||
TFields extends object,
|
||||
TID extends NonNullable<TFields[TSelectedFields]>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends PrivacyPolicyRuleInternal<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
abstract override evaluateAsync(
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
entity: TEntity
|
||||
): Promise<RuleEvaluationResult.DENY | RuleEvaluationResult.SKIP>;
|
||||
|
||||
override resultType = RuleEvaluationResultType.DENY_OR_SKIP;
|
||||
}
|
||||
|
||||
export abstract class SkipPrivacyPolicyRule<
|
||||
TFields extends object,
|
||||
TID extends NonNullable<TFields[TSelectedFields]>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> extends PrivacyPolicyRuleInternal<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
|
||||
abstract override evaluateAsync(
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
entity: TEntity
|
||||
): Promise<RuleEvaluationResult.SKIP>;
|
||||
|
||||
override resultType = RuleEvaluationResultType.SKIP;
|
||||
}
|
||||
+18
-22
@@ -1,24 +1,12 @@
|
||||
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
|
||||
import { EntityQueryContext } from '../EntityQueryContext';
|
||||
import ReadonlyEntity from '../ReadonlyEntity';
|
||||
import ViewerContext from '../ViewerContext';
|
||||
|
||||
export enum RuleEvaluationResult {
|
||||
/**
|
||||
* Deny viewer access to the entity.
|
||||
*/
|
||||
DENY = -1,
|
||||
|
||||
/**
|
||||
* Defer entity viewer access to subsequent rule in the privacy policy.
|
||||
*/
|
||||
SKIP = 0,
|
||||
|
||||
/**
|
||||
* Allow viewer access to the entity.
|
||||
*/
|
||||
ALLOW = 1,
|
||||
}
|
||||
import { EntityPrivacyPolicyEvaluationContext } from '../../EntityPrivacyPolicy';
|
||||
import { EntityQueryContext } from '../../EntityQueryContext';
|
||||
import ReadonlyEntity from '../../ReadonlyEntity';
|
||||
import ViewerContext from '../../ViewerContext';
|
||||
import {
|
||||
RuleComplexity,
|
||||
RuleEvaluationResult,
|
||||
RuleEvaluationResultType,
|
||||
} from '../PrivacyPolicyRuleEnums';
|
||||
|
||||
/**
|
||||
* A single unit of which declarative privacy policies are composed, allowing for simple
|
||||
@@ -36,17 +24,25 @@ export enum RuleEvaluationResult {
|
||||
* - Blocking. For example, a user blocks another user from seeing their posts, and the rule
|
||||
* would be named something like `DenyIfViewerHasBeenBlockedPrivacyPolicyRule`.
|
||||
*/
|
||||
export default abstract class PrivacyPolicyRule<
|
||||
export abstract class PrivacyPolicyRuleInternal<
|
||||
TFields extends object,
|
||||
TID extends NonNullable<TFields[TSelectedFields]>,
|
||||
TViewerContext extends ViewerContext,
|
||||
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
|
||||
TSelectedFields extends keyof TFields = keyof TFields
|
||||
> {
|
||||
/**
|
||||
* Estimated complexity of a privacy policy rule. Used for automatic (safe) rule reordering.
|
||||
* Typed as a number so that applications can define their own complexity scale.
|
||||
*/
|
||||
abstract readonly complexity: RuleComplexity;
|
||||
|
||||
abstract evaluateAsync(
|
||||
viewerContext: TViewerContext,
|
||||
queryContext: EntityQueryContext,
|
||||
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
||||
entity: TEntity
|
||||
): Promise<RuleEvaluationResult>;
|
||||
|
||||
abstract readonly resultType: RuleEvaluationResultType;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import invariant from 'invariant';
|
||||
|
||||
import { RuleEvaluationResultType } from '../PrivacyPolicyRuleEnums';
|
||||
|
||||
/**
|
||||
* Privacy policy list of PrivacyPolicyRule can be reordered safely according to
|
||||
* the following constraints on RuleEvaluationResultType:
|
||||
* 1. Two adjacent ALLOW_OR_SKIP rules can be swapped.
|
||||
* 2. Two adjacent DENY_OR_SKIP rules can be swapped.
|
||||
* 3. SKIP rules can be swapped with any other adjacent rule.
|
||||
*
|
||||
* @param rulesetOriginalOrder - ruleset with original ordering defined in code
|
||||
* @returns reordered ruleset depending on constraints above
|
||||
*/
|
||||
export function reorderRulesByRuleComplexityGroups<
|
||||
TRule extends { resultType: RuleEvaluationResultType; complexity: number }
|
||||
>(rulesetOriginalOrder: readonly TRule[]): readonly TRule[] {
|
||||
// first, group the rules according to constraints
|
||||
const groups = groupRulesByResultType(rulesetOriginalOrder);
|
||||
|
||||
// sort the groups by declared complexity
|
||||
const internallySortedGroups = groups.map((group) => sortGroupByComplexity(group));
|
||||
|
||||
// flatten the groups
|
||||
return internallySortedGroups.flat();
|
||||
}
|
||||
|
||||
export function sortGroupByComplexity<TRule extends { complexity: number }>(
|
||||
ruleGroup: readonly TRule[]
|
||||
): readonly TRule[] {
|
||||
return [...ruleGroup].sort((a, b) => a.complexity - b.complexity);
|
||||
}
|
||||
|
||||
export function groupRulesByResultType<TRule extends { resultType: RuleEvaluationResultType }>(
|
||||
ruleset: readonly TRule[]
|
||||
): readonly (readonly TRule[])[] {
|
||||
const groups: TRule[][] = [[]];
|
||||
let currentGroupIdx = 0;
|
||||
let currentGroupType:
|
||||
| RuleEvaluationResultType.ALLOW_OR_SKIP
|
||||
| RuleEvaluationResultType.DENY_OR_SKIP
|
||||
| null = null;
|
||||
|
||||
for (const rule of ruleset) {
|
||||
// if group type of this rule is not compatible with the current group type, start a new group
|
||||
if (
|
||||
currentGroupType !== null &&
|
||||
rule.resultType !== currentGroupType &&
|
||||
rule.resultType !== RuleEvaluationResultType.SKIP
|
||||
) {
|
||||
currentGroupIdx += 1;
|
||||
groups.push([]);
|
||||
currentGroupType = null;
|
||||
}
|
||||
|
||||
// add the current rule to the current group
|
||||
const currentGroup = groups[currentGroupIdx];
|
||||
invariant(currentGroup, `invalid groups index ${currentGroupIdx}`);
|
||||
currentGroup.push(rule);
|
||||
|
||||
// if there isn't a known group type yet for the current group, set it to the group type of the rule.
|
||||
// note that if there isn't a known group type yet and the current rule is SKIP, we'll
|
||||
// continue to the next rule to determine group type
|
||||
if (currentGroupType === null && rule.resultType !== RuleEvaluationResultType.SKIP) {
|
||||
currentGroupType = rule.resultType;
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { RuleComplexity, RuleEvaluationResultType } from '../../PrivacyPolicyRuleEnums';
|
||||
import {
|
||||
groupRulesByResultType,
|
||||
reorderRulesByRuleComplexityGroups,
|
||||
sortGroupByComplexity,
|
||||
} from '../PrivacyPolicyRuleOrdering';
|
||||
|
||||
type TestRuleJustResultType = { id: number; resultType: RuleEvaluationResultType };
|
||||
|
||||
describe(groupRulesByResultType, () => {
|
||||
const rule1 = { id: 1, resultType: RuleEvaluationResultType.ALLOW_OR_SKIP };
|
||||
const rule2 = { id: 2, resultType: RuleEvaluationResultType.ALLOW_OR_SKIP };
|
||||
const rule3 = { id: 3, resultType: RuleEvaluationResultType.SKIP };
|
||||
const rule4 = { id: 4, resultType: RuleEvaluationResultType.SKIP };
|
||||
const rule5 = { id: 5, resultType: RuleEvaluationResultType.DENY_OR_SKIP };
|
||||
const rule6 = { id: 6, resultType: RuleEvaluationResultType.DENY_OR_SKIP };
|
||||
|
||||
test.each([
|
||||
// two adjacent of the same type should be grouped
|
||||
{ ruleset: [rule1, rule2], expectedResultGroups: [[rule1, rule2]] },
|
||||
{ ruleset: [rule3, rule4], expectedResultGroups: [[rule3, rule4]] },
|
||||
{ ruleset: [rule5, rule6], expectedResultGroups: [[rule5, rule6]] },
|
||||
|
||||
// two adjacent of ALLOW_OR_SKIP or DENY_OR_SKIP should not be grouped
|
||||
{ ruleset: [rule1, rule5], expectedResultGroups: [[rule1], [rule5]] },
|
||||
{ ruleset: [rule5, rule1], expectedResultGroups: [[rule5], [rule1]] },
|
||||
|
||||
// adjacent (ALLOW_OR_SKIP and SKIP) or (DENY_OR_SKIP and SKIP) should be grouped
|
||||
{ ruleset: [rule1, rule2, rule3], expectedResultGroups: [[rule1, rule2, rule3]] },
|
||||
{ ruleset: [rule1, rule2, rule3, rule4], expectedResultGroups: [[rule1, rule2, rule3, rule4]] },
|
||||
{ ruleset: [rule5, rule6, rule3], expectedResultGroups: [[rule5, rule6, rule3]] },
|
||||
{ ruleset: [rule5, rule6, rule3, rule4], expectedResultGroups: [[rule5, rule6, rule3, rule4]] },
|
||||
|
||||
// skip cases group with first group
|
||||
{
|
||||
ruleset: [rule1, rule2, rule3, rule4, rule5, rule6],
|
||||
expectedResultGroups: [
|
||||
[rule1, rule2, rule3, rule4],
|
||||
[rule5, rule6],
|
||||
],
|
||||
},
|
||||
|
||||
// skip in first N slots should take the first group type
|
||||
{
|
||||
ruleset: [rule3, rule4, rule1, rule2, rule5, rule6],
|
||||
expectedResultGroups: [
|
||||
[rule3, rule4, rule1, rule2],
|
||||
[rule5, rule6],
|
||||
],
|
||||
},
|
||||
|
||||
// lots of groups
|
||||
{
|
||||
ruleset: [rule1, rule5, rule3, rule2, rule6, rule4],
|
||||
expectedResultGroups: [[rule1], [rule5, rule3], [rule2], [rule6, rule4]],
|
||||
},
|
||||
|
||||
// skip in first N slots
|
||||
{
|
||||
ruleset: [rule3, rule4, rule5, rule1],
|
||||
expectedResultGroups: [[rule3, rule4, rule5], [rule1]],
|
||||
},
|
||||
])(
|
||||
'case %#',
|
||||
({
|
||||
ruleset,
|
||||
expectedResultGroups,
|
||||
}: {
|
||||
ruleset: TestRuleJustResultType[];
|
||||
expectedResultGroups: TestRuleJustResultType[][];
|
||||
}) => {
|
||||
const resultGroups = groupRulesByResultType(ruleset);
|
||||
expect(resultGroups).toStrictEqual(expectedResultGroups);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
type TestRuleJustComplexity = { id: number; complexity: number };
|
||||
|
||||
describe(sortGroupByComplexity, () => {
|
||||
const rule1 = { id: 1, complexity: RuleComplexity.LOW };
|
||||
const rule2 = { id: 2, complexity: RuleComplexity.LOW };
|
||||
const rule3 = { id: 3, complexity: RuleComplexity.MEDIUM };
|
||||
const rule5 = { id: 3, complexity: RuleComplexity.HIGH };
|
||||
|
||||
test.each([
|
||||
{ ruleGroup: [rule1, rule2], expectedRuleGroup: [rule1, rule2] },
|
||||
{ ruleGroup: [rule1, rule3], expectedRuleGroup: [rule1, rule3] },
|
||||
{ ruleGroup: [rule1, rule3, rule5], expectedRuleGroup: [rule1, rule3, rule5] },
|
||||
{ ruleGroup: [rule5, rule3, rule1], expectedRuleGroup: [rule1, rule3, rule5] },
|
||||
])(
|
||||
'case %#',
|
||||
({
|
||||
ruleGroup,
|
||||
expectedRuleGroup,
|
||||
}: {
|
||||
ruleGroup: TestRuleJustComplexity[];
|
||||
expectedRuleGroup: TestRuleJustComplexity[];
|
||||
}) => {
|
||||
const resultGroup = sortGroupByComplexity(ruleGroup);
|
||||
expect(resultGroup).toStrictEqual(expectedRuleGroup);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
type TestRule = { id: number; resultType: RuleEvaluationResultType; complexity: number };
|
||||
|
||||
describe(reorderRulesByRuleComplexityGroups, () => {
|
||||
const rule1 = {
|
||||
id: 1,
|
||||
resultType: RuleEvaluationResultType.ALLOW_OR_SKIP,
|
||||
complexity: RuleComplexity.HIGH,
|
||||
};
|
||||
const rule2 = {
|
||||
id: 2,
|
||||
resultType: RuleEvaluationResultType.ALLOW_OR_SKIP,
|
||||
complexity: RuleComplexity.LOW,
|
||||
};
|
||||
const rule3 = {
|
||||
id: 3,
|
||||
resultType: RuleEvaluationResultType.SKIP,
|
||||
complexity: RuleComplexity.LOW,
|
||||
};
|
||||
const rule4 = {
|
||||
id: 4,
|
||||
resultType: RuleEvaluationResultType.SKIP,
|
||||
complexity: RuleComplexity.HIGH,
|
||||
};
|
||||
const rule5 = {
|
||||
id: 5,
|
||||
resultType: RuleEvaluationResultType.DENY_OR_SKIP,
|
||||
complexity: RuleComplexity.MEDIUM,
|
||||
};
|
||||
const rule6 = {
|
||||
id: 6,
|
||||
resultType: RuleEvaluationResultType.DENY_OR_SKIP,
|
||||
complexity: RuleComplexity.LOW,
|
||||
};
|
||||
|
||||
test.each([
|
||||
{
|
||||
ruleset: [rule1, rule2, rule3, rule4, rule5, rule6],
|
||||
expectedRuleset: [rule2, rule3, rule1, rule4, rule6, rule5],
|
||||
},
|
||||
])(
|
||||
'case %#',
|
||||
({ ruleset, expectedRuleset }: { ruleset: TestRule[]; expectedRuleset: TestRule[] }) => {
|
||||
const resultRuleset = reorderRulesByRuleComplexityGroups(ruleset);
|
||||
expect(resultRuleset).toStrictEqual(expectedRuleset);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -2,7 +2,8 @@ import { EntityPrivacyPolicyEvaluationContext } from '../../EntityPrivacyPolicy'
|
||||
import { EntityQueryContext } from '../../EntityQueryContext';
|
||||
import ReadonlyEntity from '../../ReadonlyEntity';
|
||||
import ViewerContext from '../../ViewerContext';
|
||||
import PrivacyPolicyRule, { RuleEvaluationResult } from '../../rules/PrivacyPolicyRule';
|
||||
import { RuleEvaluationResult } from '../../rules/PrivacyPolicyRuleEnums';
|
||||
import { PrivacyPolicyRule } from '../../rules/PrivacyPolicyRuleTypes';
|
||||
|
||||
export interface Case<
|
||||
TFields extends object,
|
||||
|
||||
Reference in new issue
Block a user