Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add previousValue to privacy policy context for updates #232

Merged
merged 1 commit into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<RuleEvaluationResult> {
if (viewerContext.isUserViewerContext()) {
Expand Down
8 changes: 4 additions & 4 deletions packages/entity/src/EntityAssociationLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion packages/entity/src/EntityLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TFields>,
private readonly entityClass: IEntityClass<
TFields,
Expand Down
8 changes: 7 additions & 1 deletion packages/entity/src/EntityLoaderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ export default class EntityLoaderFactory<
forLoad(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
return new EntityLoader(
viewerContext,
Expand Down
14 changes: 10 additions & 4 deletions packages/entity/src/EntityMutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export class CreateMutator<
this.validateFields(this.fieldsForEntity);

const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext, {
previousValue: null,
cascadingDeleteCause: null,
});

Expand All @@ -224,7 +225,7 @@ export class CreateMutator<
this.privacyPolicy.authorizeCreateAsync(
this.viewerContext,
queryContext,
{ cascadingDeleteCause: null },
{ previousValue: null, cascadingDeleteCause: null },
temporaryEntityForPrivacyCheck,
this.metricsAdapter
)
Expand Down Expand Up @@ -423,6 +424,7 @@ export class UpdateMutator<
this.validateFields(this.updatedFields);

const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext, {
previousValue: this.originalEntity,
cascadingDeleteCause,
});

Expand All @@ -431,7 +433,7 @@ export class UpdateMutator<
this.privacyPolicy.authorizeUpdateAsync(
this.viewerContext,
queryContext,
{ cascadingDeleteCause },
{ previousValue: this.originalEntity, cascadingDeleteCause },
entityAboutToBeUpdated,
this.metricsAdapter
)
Expand Down Expand Up @@ -633,7 +635,7 @@ export class DeleteMutator<
this.privacyPolicy.authorizeDeleteAsync(
this.viewerContext,
queryContext,
{ cascadingDeleteCause },
{ previousValue: null, cascadingDeleteCause },
this.entity,
this.metricsAdapter
)
Expand Down Expand Up @@ -671,6 +673,7 @@ export class DeleteMutator<
}

const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext, {
previousValue: null,
cascadingDeleteCause,
});
queryContext.appendPostCommitInvalidationCallback(
Expand Down Expand Up @@ -774,7 +777,10 @@ export class DeleteMutator<
}

const inboundReferenceEntities = await loaderFactory
.forLoad(queryContext, { cascadingDeleteCause: newCascadingDeleteCause })
.forLoad(queryContext, {
previousValue: null,
cascadingDeleteCause: newCascadingDeleteCause,
})
.enforcing()
.loadManyByFieldEqualingAsync(
fieldName,
Expand Down
62 changes: 55 additions & 7 deletions packages/entity/src/EntityPrivacyPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
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.
Expand Down Expand Up @@ -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<TEntity> {
Expand All @@ -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<TEntity> {
Expand All @@ -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<TEntity> {
Expand All @@ -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<TEntity> {
Expand All @@ -251,7 +287,13 @@ export default abstract class EntityPrivacyPolicy<
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
entity: TEntity,
action: EntityAuthorizationAction,
metricsAdapter: IEntityMetricsAdapter
Expand Down Expand Up @@ -354,7 +396,13 @@ export default abstract class EntityPrivacyPolicy<
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
entity: TEntity,
action: EntityAuthorizationAction
): Promise<TEntity> {
Expand Down
2 changes: 1 addition & 1 deletion packages/entity/src/ReadonlyEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,6 @@ export default abstract class ReadonlyEntity<
return viewerContext
.getViewerScopedEntityCompanionForClass(this)
.getLoaderFactory()
.forLoad(queryContext, { cascadingDeleteCause: null });
.forLoad(queryContext, { previousValue: null, cascadingDeleteCause: null });
}
}
8 changes: 7 additions & 1 deletion packages/entity/src/ViewerScopedEntityLoaderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ export default class ViewerScopedEntityLoaderFactory<

forLoad(
queryContext: EntityQueryContext,
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>
): EntityLoader<TFields, TID, TViewerContext, TEntity, TPrivacyPolicy, TSelectedFields> {
return this.entityLoaderFactory.forLoad(
this.viewerContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RuleEvaluationResult> {
if (viewerContext.getUserID() === entity.getField('ownerID')) {
Expand Down
8 changes: 7 additions & 1 deletion packages/entity/src/__tests__/EntityEdges-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RuleEvaluationResult> {
if (privacyPolicyEvaluationRecords.shouldRecord) {
Expand Down
12 changes: 11 additions & 1 deletion packages/entity/src/__tests__/EntityLoader-constructor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EntityPrivacyPolicyEvaluationContext>());
const privacyPolicyEvaluationContext = instance(
mock<
EntityPrivacyPolicyEvaluationContext<
TestFields,
string,
ViewerContext,
TestEntity,
TestFieldSelection
>
>()
);
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getQueryContext();

Expand Down
Loading
Loading