diff --git a/packages/entity-example/src/entities/AllowIfUserOwnerPrivacyRule.ts b/packages/entity-example/src/entities/AllowIfUserOwnerPrivacyRule.ts index ec12d27e7..7d44221ac 100644 --- a/packages/entity-example/src/entities/AllowIfUserOwnerPrivacyRule.ts +++ b/packages/entity-example/src/entities/AllowIfUserOwnerPrivacyRule.ts @@ -35,7 +35,13 @@ export default class AllowIfUserOwnerPrivacyRule< async evaluateAsync( viewerContext: ExampleViewerContext, _queryContext: EntityQueryContext, - _evaluationContext: EntityPrivacyPolicyEvaluationContext, + _evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + ExampleViewerContext, + TEntity, + TSelectedFields + >, entity: TEntity ): Promise { if (viewerContext.isUserViewerContext()) { diff --git a/packages/entity/src/EntityAssociationLoader.ts b/packages/entity/src/EntityAssociationLoader.ts index ee42215e1..cc626a989 100644 --- a/packages/entity/src/EntityAssociationLoader.ts +++ b/packages/entity/src/EntityAssociationLoader.ts @@ -74,7 +74,7 @@ export default class EntityAssociationLoader< .getViewerContext() .getViewerScopedEntityCompanionForClass(associatedEntityClass) .getLoaderFactory() - .forLoad(queryContext, { cascadingDeleteCause: null }); + .forLoad(queryContext, { previousValue: null, cascadingDeleteCause: null }); return (await loader.loadByIDAsync(associatedEntityID as unknown as TAssociatedID)) as Result< null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity @@ -128,7 +128,7 @@ export default class EntityAssociationLoader< .getViewerContext() .getViewerScopedEntityCompanionForClass(associatedEntityClass) .getLoaderFactory() - .forLoad(queryContext, { cascadingDeleteCause: null }); + .forLoad(queryContext, { previousValue: null, cascadingDeleteCause: null }); return await loader.loadManyByFieldEqualingAsync( associatedEntityFieldContainingThisID, thisID as any @@ -185,7 +185,7 @@ export default class EntityAssociationLoader< .getViewerContext() .getViewerScopedEntityCompanionForClass(associatedEntityClass) .getLoaderFactory() - .forLoad(queryContext, { cascadingDeleteCause: null }); + .forLoad(queryContext, { previousValue: null, cascadingDeleteCause: null }); return await loader.loadByFieldEqualingAsync( associatedEntityLookupByField, associatedFieldValue as any @@ -243,7 +243,7 @@ export default class EntityAssociationLoader< .getViewerContext() .getViewerScopedEntityCompanionForClass(associatedEntityClass) .getLoaderFactory() - .forLoad(queryContext, { cascadingDeleteCause: null }); + .forLoad(queryContext, { previousValue: null, cascadingDeleteCause: null }); return await loader.loadManyByFieldEqualingAsync( associatedEntityLookupByField, associatedFieldValue as any diff --git a/packages/entity/src/EntityLoader.ts b/packages/entity/src/EntityLoader.ts index 005fad8e1..57ac0fbe9 100644 --- a/packages/entity/src/EntityLoader.ts +++ b/packages/entity/src/EntityLoader.ts @@ -43,7 +43,13 @@ export default class EntityLoader< constructor( private readonly viewerContext: TViewerContext, private readonly queryContext: EntityQueryContext, - private readonly privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext, + private readonly privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, private readonly entityConfiguration: EntityConfiguration, private readonly entityClass: IEntityClass< TFields, diff --git a/packages/entity/src/EntityLoaderFactory.ts b/packages/entity/src/EntityLoaderFactory.ts index 40dd43083..523d123a0 100644 --- a/packages/entity/src/EntityLoaderFactory.ts +++ b/packages/entity/src/EntityLoaderFactory.ts @@ -45,7 +45,13 @@ export default class EntityLoaderFactory< forLoad( viewerContext: TViewerContext, queryContext: EntityQueryContext, - privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext + privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + > ): EntityLoader { return new EntityLoader( viewerContext, diff --git a/packages/entity/src/EntityMutator.ts b/packages/entity/src/EntityMutator.ts index 8fa0a7c4e..7f6cac117 100644 --- a/packages/entity/src/EntityMutator.ts +++ b/packages/entity/src/EntityMutator.ts @@ -212,6 +212,7 @@ export class CreateMutator< this.validateFields(this.fieldsForEntity); const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext, { + previousValue: null, cascadingDeleteCause: null, }); @@ -224,7 +225,7 @@ export class CreateMutator< this.privacyPolicy.authorizeCreateAsync( this.viewerContext, queryContext, - { cascadingDeleteCause: null }, + { previousValue: null, cascadingDeleteCause: null }, temporaryEntityForPrivacyCheck, this.metricsAdapter ) @@ -423,6 +424,7 @@ export class UpdateMutator< this.validateFields(this.updatedFields); const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext, { + previousValue: this.originalEntity, cascadingDeleteCause, }); @@ -431,7 +433,7 @@ export class UpdateMutator< this.privacyPolicy.authorizeUpdateAsync( this.viewerContext, queryContext, - { cascadingDeleteCause }, + { previousValue: this.originalEntity, cascadingDeleteCause }, entityAboutToBeUpdated, this.metricsAdapter ) @@ -633,7 +635,7 @@ export class DeleteMutator< this.privacyPolicy.authorizeDeleteAsync( this.viewerContext, queryContext, - { cascadingDeleteCause }, + { previousValue: null, cascadingDeleteCause }, this.entity, this.metricsAdapter ) @@ -671,6 +673,7 @@ export class DeleteMutator< } const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext, { + previousValue: null, cascadingDeleteCause, }); queryContext.appendPostCommitInvalidationCallback( @@ -774,7 +777,10 @@ export class DeleteMutator< } const inboundReferenceEntities = await loaderFactory - .forLoad(queryContext, { cascadingDeleteCause: newCascadingDeleteCause }) + .forLoad(queryContext, { + previousValue: null, + cascadingDeleteCause: newCascadingDeleteCause, + }) .enforcing() .loadManyByFieldEqualingAsync( fieldName, diff --git a/packages/entity/src/EntityPrivacyPolicy.ts b/packages/entity/src/EntityPrivacyPolicy.ts index e153faccb..2aa396335 100644 --- a/packages/entity/src/EntityPrivacyPolicy.ts +++ b/packages/entity/src/EntityPrivacyPolicy.ts @@ -11,7 +11,19 @@ import PrivacyPolicyRule, { RuleEvaluationResult } from './rules/PrivacyPolicyRu /** * Information about the reason this privacy policy is being evaluated. */ -export type EntityPrivacyPolicyEvaluationContext = { +export type EntityPrivacyPolicyEvaluationContext< + TFields extends object, + TID extends NonNullable, + TViewerContext extends ViewerContext, + TEntity extends ReadonlyEntity, + TSelectedFields extends keyof TFields = keyof TFields +> = { + /** + * When this privacy policy is being evaluated as a result of an update, this will be populated with the value + * of the entity before the update. Note that this doesn't only apply to UPDATE authorization actions though: + * when an entity is updated it is re-LOADed after the update completes. + */ + previousValue: TEntity | null; /** * When this privacy policy is being evaluated as a result of a cascading deletion, this will be populated * with information on the cascading delete. @@ -154,7 +166,13 @@ export default abstract class EntityPrivacyPolicy< async authorizeCreateAsync( viewerContext: TViewerContext, queryContext: EntityQueryContext, - evaluationContext: EntityPrivacyPolicyEvaluationContext, + evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, entity: TEntity, metricsAdapter: IEntityMetricsAdapter ): Promise { @@ -180,7 +198,13 @@ export default abstract class EntityPrivacyPolicy< async authorizeReadAsync( viewerContext: TViewerContext, queryContext: EntityQueryContext, - evaluationContext: EntityPrivacyPolicyEvaluationContext, + evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, entity: TEntity, metricsAdapter: IEntityMetricsAdapter ): Promise { @@ -206,7 +230,13 @@ export default abstract class EntityPrivacyPolicy< async authorizeUpdateAsync( viewerContext: TViewerContext, queryContext: EntityQueryContext, - evaluationContext: EntityPrivacyPolicyEvaluationContext, + evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, entity: TEntity, metricsAdapter: IEntityMetricsAdapter ): Promise { @@ -232,7 +262,13 @@ export default abstract class EntityPrivacyPolicy< async authorizeDeleteAsync( viewerContext: TViewerContext, queryContext: EntityQueryContext, - evaluationContext: EntityPrivacyPolicyEvaluationContext, + evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, entity: TEntity, metricsAdapter: IEntityMetricsAdapter ): Promise { @@ -251,7 +287,13 @@ export default abstract class EntityPrivacyPolicy< ruleset: readonly PrivacyPolicyRule[], viewerContext: TViewerContext, queryContext: EntityQueryContext, - evaluationContext: EntityPrivacyPolicyEvaluationContext, + evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, entity: TEntity, action: EntityAuthorizationAction, metricsAdapter: IEntityMetricsAdapter @@ -354,7 +396,13 @@ export default abstract class EntityPrivacyPolicy< ruleset: readonly PrivacyPolicyRule[], viewerContext: TViewerContext, queryContext: EntityQueryContext, - evaluationContext: EntityPrivacyPolicyEvaluationContext, + evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, entity: TEntity, action: EntityAuthorizationAction ): Promise { diff --git a/packages/entity/src/ReadonlyEntity.ts b/packages/entity/src/ReadonlyEntity.ts index dec07d735..6cdbaf3d5 100644 --- a/packages/entity/src/ReadonlyEntity.ts +++ b/packages/entity/src/ReadonlyEntity.ts @@ -156,6 +156,6 @@ export default abstract class ReadonlyEntity< return viewerContext .getViewerScopedEntityCompanionForClass(this) .getLoaderFactory() - .forLoad(queryContext, { cascadingDeleteCause: null }); + .forLoad(queryContext, { previousValue: null, cascadingDeleteCause: null }); } } diff --git a/packages/entity/src/ViewerScopedEntityLoaderFactory.ts b/packages/entity/src/ViewerScopedEntityLoaderFactory.ts index 6c55bea8d..e0ee5ce27 100644 --- a/packages/entity/src/ViewerScopedEntityLoaderFactory.ts +++ b/packages/entity/src/ViewerScopedEntityLoaderFactory.ts @@ -36,7 +36,13 @@ export default class ViewerScopedEntityLoaderFactory< forLoad( queryContext: EntityQueryContext, - privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext + privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + > ): EntityLoader { return this.entityLoaderFactory.forLoad( this.viewerContext, diff --git a/packages/entity/src/__tests__/EntityCommonUseCases-test.ts b/packages/entity/src/__tests__/EntityCommonUseCases-test.ts index 2f82ca415..2a3926228 100644 --- a/packages/entity/src/__tests__/EntityCommonUseCases-test.ts +++ b/packages/entity/src/__tests__/EntityCommonUseCases-test.ts @@ -69,7 +69,12 @@ class DenyIfNotOwnerPrivacyPolicyRule extends PrivacyPolicyRule< async evaluateAsync( viewerContext: TestUserViewerContext, _queryContext: EntityQueryContext, - _evaluationContext: EntityPrivacyPolicyEvaluationContext, + _evaluationContext: EntityPrivacyPolicyEvaluationContext< + BlahFields, + string, + TestUserViewerContext, + BlahEntity + >, entity: BlahEntity ): Promise { if (viewerContext.getUserID() === entity.getField('ownerID')) { diff --git a/packages/entity/src/__tests__/EntityEdges-test.ts b/packages/entity/src/__tests__/EntityEdges-test.ts index 20c0cd040..5419a0e71 100644 --- a/packages/entity/src/__tests__/EntityEdges-test.ts +++ b/packages/entity/src/__tests__/EntityEdges-test.ts @@ -85,7 +85,13 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) => async evaluateAsync( _viewerContext: TestViewerContext, _queryContext: EntityQueryContext, - evaluationContext: EntityPrivacyPolicyEvaluationContext, + evaluationContext: EntityPrivacyPolicyEvaluationContext< + any, + string, + TestViewerContext, + any, + any + >, entity: any ): Promise { if (privacyPolicyEvaluationRecords.shouldRecord) { diff --git a/packages/entity/src/__tests__/EntityLoader-constructor-test.ts b/packages/entity/src/__tests__/EntityLoader-constructor-test.ts index ca100c31d..ce502809f 100644 --- a/packages/entity/src/__tests__/EntityLoader-constructor-test.ts +++ b/packages/entity/src/__tests__/EntityLoader-constructor-test.ts @@ -121,7 +121,17 @@ export default class TestEntity extends Entity< describe(EntityLoader, () => { it('handles thrown errors and literals from constructor', async () => { const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock< + EntityPrivacyPolicyEvaluationContext< + TestFields, + string, + ViewerContext, + TestEntity, + TestFieldSelection + > + >() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); diff --git a/packages/entity/src/__tests__/EntityLoader-test.ts b/packages/entity/src/__tests__/EntityLoader-test.ts index c8ae44c7f..97b74777f 100644 --- a/packages/entity/src/__tests__/EntityLoader-test.ts +++ b/packages/entity/src/__tests__/EntityLoader-test.ts @@ -24,7 +24,9 @@ describe(EntityLoader, () => { it('loads entities', async () => { const dateToInsert = new Date(); const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); @@ -128,7 +130,9 @@ describe(EntityLoader, () => { const privacyPolicy = new TestEntityPrivacyPolicy(); const spiedPrivacyPolicy = spy(privacyPolicy); const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); @@ -227,7 +231,9 @@ describe(EntityLoader, () => { const privacyPolicy = new TestEntityPrivacyPolicy(); const spiedPrivacyPolicy = spy(privacyPolicy); const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); @@ -323,7 +329,9 @@ describe(EntityLoader, () => { const privacyPolicy = new TestEntityPrivacyPolicy(); const spiedPrivacyPolicy = spy(privacyPolicy); const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); @@ -380,7 +388,9 @@ describe(EntityLoader, () => { const spiedPrivacyPolicy = spy(privacyPolicy); const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); @@ -441,7 +451,9 @@ describe(EntityLoader, () => { it('invalidates upon invalidate one', async () => { const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); const privacyPolicy = instance(mock(TestEntityPrivacyPolicy)); @@ -469,7 +481,9 @@ describe(EntityLoader, () => { it('invalidates upon invalidate by field', async () => { const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); const privacyPolicy = instance(mock(TestEntityPrivacyPolicy)); @@ -496,7 +510,9 @@ describe(EntityLoader, () => { it('invalidates upon invalidate by entity', async () => { const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); const privacyPolicy = instance(mock(TestEntityPrivacyPolicy)); @@ -527,7 +543,9 @@ describe(EntityLoader, () => { it('returns error result when not allowed', async () => { const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); const privacyPolicyMock = mock(TestEntityPrivacyPolicy); @@ -573,7 +591,9 @@ describe(EntityLoader, () => { it('throws upon database adapter error', async () => { const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapter = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); const privacyPolicy = instance(mock(TestEntityPrivacyPolicy)); diff --git a/packages/entity/src/__tests__/EntityMutator-test.ts b/packages/entity/src/__tests__/EntityMutator-test.ts index a6b563968..ac2a97769 100644 --- a/packages/entity/src/__tests__/EntityMutator-test.ts +++ b/packages/entity/src/__tests__/EntityMutator-test.ts @@ -295,7 +295,6 @@ const createEntityMutatorFactory = ( afterAll: [new TestMutationTrigger()], afterCommit: [new TestNonTransactionalMutationTrigger()], }; - const privacyPolicy = new TestEntityPrivacyPolicy(); const databaseAdapter = new StubDatabaseAdapter( testEntityConfiguration, StubDatabaseAdapter.convertFieldObjectsToDataStore( @@ -352,7 +351,7 @@ const createEntityMutatorFactory = ( companionProvider, testEntityConfiguration, TestEntity, - privacyPolicy, + companionProvider.getCompanionForEntity(TestEntity).privacyPolicy, mutationValidators, mutationTriggers, entityLoaderFactory, @@ -360,7 +359,7 @@ const createEntityMutatorFactory = ( metricsAdapter ); return { - privacyPolicy, + privacyPolicy: companionProvider.getCompanionForEntity(TestEntity).privacyPolicy, entityLoaderFactory, entityMutatorFactory, metricsAdapter, @@ -438,7 +437,7 @@ describe(EntityMutatorFactory, () => { spiedPrivacyPolicy.authorizeCreateAsync( viewerContext, anyOfClass(EntityTransactionalQueryContext), - deepEqual({ cascadingDeleteCause: null }), + deepEqual({ previousValue: null, cascadingDeleteCause: null }), anyOfClass(TestEntity), anything() ) @@ -531,7 +530,17 @@ describe(EntityMutatorFactory, () => { describe('forUpdate', () => { it('updates entities', async () => { const viewerContext = mock(); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock< + EntityPrivacyPolicyEvaluationContext< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + > + >() + ); const queryContext = StubQueryContextProvider.getQueryContext(); const id1 = uuidv4(); @@ -580,7 +589,6 @@ describe(EntityMutatorFactory, () => { it('checks privacy', async () => { const viewerContext = mock(); - const privacyPolicyEvaluationContext = instance(mock()); const queryContext = StubQueryContextProvider.getQueryContext(); const id1 = uuidv4(); @@ -609,7 +617,7 @@ describe(EntityMutatorFactory, () => { const existingEntity = await enforceAsyncResult( entityLoaderFactory - .forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext) + .forLoad(viewerContext, queryContext, { previousValue: null, cascadingDeleteCause: null }) .loadByIDAsync(id2) ); @@ -622,7 +630,17 @@ describe(EntityMutatorFactory, () => { spiedPrivacyPolicy.authorizeUpdateAsync( viewerContext, anyOfClass(EntityTransactionalQueryContext), - deepEqual({ cascadingDeleteCause: null }), + deepEqual({ previousValue: existingEntity, cascadingDeleteCause: null }), + anyOfClass(TestEntity), + anything() + ) + ).once(); + + verify( + spiedPrivacyPolicy.authorizeReadAsync( + viewerContext, + anyOfClass(EntityTransactionalQueryContext), + deepEqual({ previousValue: existingEntity, cascadingDeleteCause: null }), anyOfClass(TestEntity), anything() ) @@ -631,7 +649,17 @@ describe(EntityMutatorFactory, () => { it('executes triggers', async () => { const viewerContext = mock(); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock< + EntityPrivacyPolicyEvaluationContext< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + > + >() + ); const queryContext = StubQueryContextProvider.getQueryContext(); const id1 = uuidv4(); @@ -689,7 +717,17 @@ describe(EntityMutatorFactory, () => { }); it('executes validators', async () => { const viewerContext = mock(); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock< + EntityPrivacyPolicyEvaluationContext< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + > + >() + ); const queryContext = StubQueryContextProvider.getQueryContext(); const id1 = uuidv4(); @@ -738,7 +776,17 @@ describe(EntityMutatorFactory, () => { describe('forDelete', () => { it('deletes entities', async () => { const viewerContext = mock(); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock< + EntityPrivacyPolicyEvaluationContext< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + > + >() + ); const queryContext = StubQueryContextProvider.getQueryContext(); const id1 = uuidv4(); @@ -773,7 +821,17 @@ describe(EntityMutatorFactory, () => { it('checks privacy', async () => { const viewerContext = mock(); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock< + EntityPrivacyPolicyEvaluationContext< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + > + >() + ); const queryContext = StubQueryContextProvider.getQueryContext(); const id1 = uuidv4(); @@ -812,7 +870,17 @@ describe(EntityMutatorFactory, () => { it('executes triggers', async () => { const viewerContext = mock(); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock< + EntityPrivacyPolicyEvaluationContext< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + > + >() + ); const queryContext = StubQueryContextProvider.getQueryContext(); const id1 = uuidv4(); @@ -855,7 +923,17 @@ describe(EntityMutatorFactory, () => { it('does not execute validators', async () => { const viewerContext = mock(); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock< + EntityPrivacyPolicyEvaluationContext< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + > + >() + ); const queryContext = StubQueryContextProvider.getQueryContext(); const id1 = uuidv4(); @@ -889,7 +967,17 @@ describe(EntityMutatorFactory, () => { it('invalidates cache for fields upon create', async () => { const viewerContext = mock(); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock< + EntityPrivacyPolicyEvaluationContext< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + > + >() + ); const queryContext = StubQueryContextProvider.getQueryContext(); const id1 = uuidv4(); diff --git a/packages/entity/src/__tests__/EntityPrivacyPolicy-test.ts b/packages/entity/src/__tests__/EntityPrivacyPolicy-test.ts index e67dc3766..2fb5e3d9c 100644 --- a/packages/entity/src/__tests__/EntityPrivacyPolicy-test.ts +++ b/packages/entity/src/__tests__/EntityPrivacyPolicy-test.ts @@ -205,7 +205,12 @@ class AlwaysThrowPrivacyPolicyRule extends PrivacyPolicyRule< evaluateAsync( _viewerContext: ViewerContext, _queryContext: EntityQueryContext, - _evaluationContext: EntityPrivacyPolicyEvaluationContext, + _evaluationContext: EntityPrivacyPolicyEvaluationContext< + BlahFields, + string, + ViewerContext, + BlahEntity + >, _entity: BlahEntity ): Promise { throw new Error('WooHoo!'); @@ -269,7 +274,9 @@ describe(EntityPrivacyPolicy, () => { it('throws EntityNotAuthorizedError when deny', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -303,7 +310,9 @@ describe(EntityPrivacyPolicy, () => { it('returns entity when allowed', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -336,7 +345,9 @@ describe(EntityPrivacyPolicy, () => { it('throws EntityNotAuthorizedError when all skipped', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -370,7 +381,9 @@ describe(EntityPrivacyPolicy, () => { it('throws when an invalid result is returned', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -394,7 +407,9 @@ describe(EntityPrivacyPolicy, () => { it('throws EntityNotAuthorizedError when empty policy', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -428,7 +443,9 @@ describe(EntityPrivacyPolicy, () => { it('throws when rule throws', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -455,7 +472,9 @@ describe(EntityPrivacyPolicy, () => { it('returns entity when denied but calls denialHandler', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -494,7 +513,9 @@ describe(EntityPrivacyPolicy, () => { it('does not log when not denied', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -533,7 +554,9 @@ describe(EntityPrivacyPolicy, () => { it('passes through other errors', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -566,7 +589,9 @@ describe(EntityPrivacyPolicy, () => { it('denies when denied but calls denialHandler', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -606,7 +631,9 @@ describe(EntityPrivacyPolicy, () => { it('does not log when not denied', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ @@ -645,7 +672,9 @@ describe(EntityPrivacyPolicy, () => { it('passes through other errors', async () => { const viewerContext = instance(mock(ViewerContext)); const queryContext = instance(mock(EntityQueryContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const metricsAdapterMock = mock(); const metricsAdapter = instance(metricsAdapterMock); const entity = new BlahEntity({ diff --git a/packages/entity/src/__tests__/ViewerScopedEntityLoaderFactory-test.ts b/packages/entity/src/__tests__/ViewerScopedEntityLoaderFactory-test.ts index 597981937..c35814076 100644 --- a/packages/entity/src/__tests__/ViewerScopedEntityLoaderFactory-test.ts +++ b/packages/entity/src/__tests__/ViewerScopedEntityLoaderFactory-test.ts @@ -9,7 +9,9 @@ import ViewerScopedEntityLoaderFactory from '../ViewerScopedEntityLoaderFactory' describe(ViewerScopedEntityLoaderFactory, () => { it('correctly scopes viewer to entity loads', async () => { const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = instance(mock()); + const privacyPolicyEvaluationContext = instance( + mock>() + ); const queryContext = instance(mock(EntityQueryContext)); const baseLoader = mock>(EntityLoaderFactory); const baseLoaderInstance = instance(baseLoader); diff --git a/packages/entity/src/rules/AlwaysAllowPrivacyPolicyRule.ts b/packages/entity/src/rules/AlwaysAllowPrivacyPolicyRule.ts index 702573030..f5616a88a 100644 --- a/packages/entity/src/rules/AlwaysAllowPrivacyPolicyRule.ts +++ b/packages/entity/src/rules/AlwaysAllowPrivacyPolicyRule.ts @@ -17,7 +17,13 @@ export default class AlwaysAllowPrivacyPolicyRule< async evaluateAsync( _viewerContext: TViewerContext, _queryContext: EntityQueryContext, - _evaluationContext: EntityPrivacyPolicyEvaluationContext, + _evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, _entity: TEntity ): Promise { return RuleEvaluationResult.ALLOW; diff --git a/packages/entity/src/rules/AlwaysDenyPrivacyPolicyRule.ts b/packages/entity/src/rules/AlwaysDenyPrivacyPolicyRule.ts index 2b6b50113..92a91396d 100644 --- a/packages/entity/src/rules/AlwaysDenyPrivacyPolicyRule.ts +++ b/packages/entity/src/rules/AlwaysDenyPrivacyPolicyRule.ts @@ -17,7 +17,13 @@ export default class AlwaysDenyPrivacyPolicyRule< async evaluateAsync( _viewerContext: TViewerContext, _queryContext: EntityQueryContext, - _evaluationContext: EntityPrivacyPolicyEvaluationContext, + _evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, _entity: TEntity ): Promise { return RuleEvaluationResult.DENY; diff --git a/packages/entity/src/rules/AlwaysSkipPrivacyPolicyRule.ts b/packages/entity/src/rules/AlwaysSkipPrivacyPolicyRule.ts index 7d716e1ed..ef7034484 100644 --- a/packages/entity/src/rules/AlwaysSkipPrivacyPolicyRule.ts +++ b/packages/entity/src/rules/AlwaysSkipPrivacyPolicyRule.ts @@ -17,7 +17,13 @@ export default class AlwaysSkipPrivacyPolicyRule< async evaluateAsync( _viewerContext: TViewerContext, _queryContext: EntityQueryContext, - _evaluationContext: EntityPrivacyPolicyEvaluationContext, + _evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, _entity: TEntity ): Promise { return RuleEvaluationResult.SKIP; diff --git a/packages/entity/src/rules/PrivacyPolicyRule.ts b/packages/entity/src/rules/PrivacyPolicyRule.ts index 71a6f4683..73fd4cac3 100644 --- a/packages/entity/src/rules/PrivacyPolicyRule.ts +++ b/packages/entity/src/rules/PrivacyPolicyRule.ts @@ -46,7 +46,13 @@ export default abstract class PrivacyPolicyRule< abstract evaluateAsync( viewerContext: TViewerContext, queryContext: EntityQueryContext, - evaluationContext: EntityPrivacyPolicyEvaluationContext, + evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, entity: TEntity ): Promise; } diff --git a/packages/entity/src/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.ts b/packages/entity/src/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.ts index e37d32717..ef7b2bcf9 100644 --- a/packages/entity/src/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.ts +++ b/packages/entity/src/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.ts @@ -11,7 +11,9 @@ describePrivacyPolicyRule(new AlwaysAllowPrivacyPolicyRule(), { { viewerContext: instance(mock(ViewerContext)), queryContext: instance(mock(EntityQueryContext)), - evaluationContext: instance(mock()), + evaluationContext: instance( + mock>() + ), entity: anything(), }, ], diff --git a/packages/entity/src/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.ts b/packages/entity/src/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.ts index e9c7cc4cc..0255d7531 100644 --- a/packages/entity/src/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.ts +++ b/packages/entity/src/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.ts @@ -11,7 +11,9 @@ describePrivacyPolicyRule(new AlwaysDenyPrivacyPolicyRule(), { { viewerContext: instance(mock(ViewerContext)), queryContext: instance(mock(EntityQueryContext)), - evaluationContext: instance(mock()), + evaluationContext: instance( + mock>() + ), entity: anything(), }, ], diff --git a/packages/entity/src/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.ts b/packages/entity/src/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.ts index a54aaa629..13ceadd25 100644 --- a/packages/entity/src/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.ts +++ b/packages/entity/src/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.ts @@ -11,7 +11,9 @@ describePrivacyPolicyRule(new AlwaysSkipPrivacyPolicyRule(), { { viewerContext: instance(mock(ViewerContext)), queryContext: instance(mock(EntityQueryContext)), - evaluationContext: instance(mock()), + evaluationContext: instance( + mock>() + ), entity: anything(), }, ], diff --git a/packages/entity/src/utils/EntityPrivacyUtils.ts b/packages/entity/src/utils/EntityPrivacyUtils.ts index 6d021cc53..c0fbf1809 100644 --- a/packages/entity/src/utils/EntityPrivacyUtils.ts +++ b/packages/entity/src/utils/EntityPrivacyUtils.ts @@ -97,7 +97,7 @@ async function canViewerUpdateInternalAsync< privacyPolicy.authorizeUpdateAsync( sourceEntity.getViewerContext(), queryContext, - { cascadingDeleteCause }, + { previousValue: null, cascadingDeleteCause }, sourceEntity, companion.getMetricsAdapter() ) @@ -191,12 +191,13 @@ async function canViewerDeleteInternalAsync< const viewerScopedCompanion = sourceEntity .getViewerContext() .getViewerScopedEntityCompanionForClass(entityClass); + const privacyPolicy = viewerScopedCompanion.entityCompanion.privacyPolicy; const evaluationResult = await asyncResult( privacyPolicy.authorizeDeleteAsync( sourceEntity.getViewerContext(), queryContext, - { cascadingDeleteCause }, + { previousValue: null, cascadingDeleteCause }, sourceEntity, viewerScopedCompanion.getMetricsAdapter() ) @@ -235,7 +236,10 @@ async function canViewerDeleteInternalAsync< const loader = viewerContext .getViewerScopedEntityCompanionForClass(inboundEdge) .getLoaderFactory() - .forLoad(queryContext, { cascadingDeleteCause: newCascadingDeleteCause }); + .forLoad(queryContext, { + previousValue: null, + cascadingDeleteCause: newCascadingDeleteCause, + }); for (const [fieldName, fieldDefinition] of configurationForInboundEdge.schema) { const association = fieldDefinition.association; diff --git a/packages/entity/src/utils/__tests__/EntityPrivacyUtils-test.ts b/packages/entity/src/utils/__tests__/EntityPrivacyUtils-test.ts index 5e0317d10..dc829cc3b 100644 --- a/packages/entity/src/utils/__tests__/EntityPrivacyUtils-test.ts +++ b/packages/entity/src/utils/__tests__/EntityPrivacyUtils-test.ts @@ -239,7 +239,13 @@ class ThrowOtherErrorEntityPrivacyPolicy< async evaluateAsync( _viewerContext: TViewerContext, _queryContext: EntityQueryContext, - evaluationContext: EntityPrivacyPolicyEvaluationContext, + evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, _entity: TEntity ): Promise { if (evaluationContext.cascadingDeleteCause) { @@ -281,7 +287,13 @@ class DenyReadEntityPrivacyPolicy< async evaluateAsync( _viewerContext: TViewerContext, queryContext: EntityQueryContext, - evaluationContext: EntityPrivacyPolicyEvaluationContext, + evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, _entity: TEntity ): Promise { if (queryContext.isInTransaction()) { diff --git a/packages/entity/src/utils/testing/PrivacyPolicyRuleTestUtils.ts b/packages/entity/src/utils/testing/PrivacyPolicyRuleTestUtils.ts index eb36cbe1a..206e9ce98 100644 --- a/packages/entity/src/utils/testing/PrivacyPolicyRuleTestUtils.ts +++ b/packages/entity/src/utils/testing/PrivacyPolicyRuleTestUtils.ts @@ -13,7 +13,13 @@ export interface Case< > { viewerContext: TViewerContext; queryContext: EntityQueryContext; - evaluationContext: EntityPrivacyPolicyEvaluationContext; + evaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >; entity: TEntity; } diff --git a/packages/entity/src/utils/testing/__tests__/PrivacyPolicyRuleTestUtils-test.ts b/packages/entity/src/utils/testing/__tests__/PrivacyPolicyRuleTestUtils-test.ts index 86e558d61..d2358f06e 100644 --- a/packages/entity/src/utils/testing/__tests__/PrivacyPolicyRuleTestUtils-test.ts +++ b/packages/entity/src/utils/testing/__tests__/PrivacyPolicyRuleTestUtils-test.ts @@ -16,7 +16,9 @@ describe(describePrivacyPolicyRuleWithAsyncTestCase, () => { async () => ({ viewerContext: instance(mock(ViewerContext)), queryContext: instance(mock(EntityQueryContext)), - evaluationContext: instance(mock()), + evaluationContext: instance( + mock>() + ), entity: anything(), }), ], @@ -30,7 +32,9 @@ describe(describePrivacyPolicyRuleWithAsyncTestCase, () => { async () => ({ viewerContext: instance(mock(ViewerContext)), queryContext: instance(mock(EntityQueryContext)), - evaluationContext: instance(mock()), + evaluationContext: instance( + mock>() + ), entity: anything(), }), ],