Skip to main content

Privacy Policy

The PrivacyPolicy is used to apply permission checks on an object. It's used in Ents, Actions and Queries.

interface PrivacyPolicyRule {
apply(v: Viewer, ent?: Ent): Promise<PrivacyResult>;
}

interface PrivacyPolicy {
rules: PrivacyPolicyRule[];
}

Each PrivacyPolicy defines a list of ordered rules in terms of priority.

PrivacyPolicyRule

Each rule takes a Viewer and an Ent (depending on where it's being called) and returns one of three results:

  • Allow()
  • Deny()
  • Skip()

Allow

When a rule returns Allow() , it's saying that it has enough information to pass this check.

Deny

When a rule returns Deny() , it's saying that it has enough information to deny this check

Skip

When a rule returns Skip() , it's saying that it doesn't have enough information to make a decision, and you should go to the next rule.

Guidelines:

  • The last rule in a policy should always allow or always deny. It shouldn't be ambiguous what the result is.
  • If not an always allow or always deny rule, a rule should return
    • Allow() or Skip() or
    • Deny() or Skip()
  • For readability and reusability purposes, rules should be broken down into the simplest reusable components and not try to do too much.

AlwaysAllowPrivacyPolicy

This is a simple policy that comes with the framework that has only one rule: AlwaysAllowRule . This always passes the privacy check

AlwaysDenyPrivacyPolicy

This is a simple policy that comes with the framework that has only one rule: AlwaysDenyRule . This always denies the privacy check

AllowIfViewerPrivacyPolicy

This is a simple policy that comes with the framework that says the ent is visible if the id of the viewer is equal to the id of the ent.

Examples

user private social network

Consider a private social network where a user can only see another user if they're friends (ignoring the issue of how they become friends), here's what a simple privacy policy looks like:

src/ent/user.ts
export class User extends UserBase {
getPrivacyPolicy() {
return {
rules: [
AllowIfViewerRule,
new AllowIfViewerInboundEdgeExistsRule(EdgeType.UserToFriends),
AlwaysDenyRule,
],
};
}
}

This has 3 rules:

  • AllowIfViewerRule: this checks that the id of the Viewer is equal to the id of the ent. If so, ent is visible.
  • AllowIfViewerInboundEdgeExistsRule: this checks that an edge exists from viewer's id to the id of the ent. If so, ent is visible.
  • AlwaysDenyRule: otherwise, ent isn't visible.

contact management system

Or a User having a list of contacts and you want to ensure that only the owner of the contacts can see them. A simple privacy policy looks like:

src/ent/contact.ts
export class Contact extends ContactBase {
getPrivacyPolicy() {
return {
rules: [new AllowIfViewerIsEntPropertyRule("userID"), AlwaysDenyRule],
};
}
}

This has 2 rules:

  • AllowIfViewerIsEntPropertyRule: this checks that the id of the Viewer is equal to the field userID of the ent. If so, ent is visible.
  • AlwaysDenyRule: otherwise, ent isn't visible.

events based system

Event based system with guests.

src/ent/guest.ts
export class Guest extends GuestBase {
getPrivacyPolicy() {
return {
rules: [
// guest can view self
AllowIfViewerRule,
// can view guest group if creator of event
new AllowIfEventCreatorRule(this.eventID),
new AllowIfGuestInSameGuestGroupRule(),
AlwaysDenyRule,
],
};
}
}

This has 4 rules:

  • AllowIfViewerRule: this checks that the id of the Viewer is equal to the id of the ent. If so, ent is visible.
  • AllowIfEventCreatorRule: custom rule. Guest is visible if viewer is event creator
  • AllowIfGuestInSameGuestGroupRule: custom rule. Guest is visible if viewer and guest are part of a group invited together e.g. +1 for a wedding
  • AlwaysDenyRule: otherwise, ent isn't visible.

social media with blocking

src/ent/user.ts
export class User extends UserBase {
getPrivacyPolicy() {
return {
rules: [
AllowIfViewerRule,
new DenyIfViewerOutboundEdgeExistsRule(EdgeType.UserToBlocks),
AlwaysAllowRule,
],
};
}
}

This has 3 rules:

  • AllowIfViewerRule: this checks that the id of the Viewer is equal to the id of the ent. If so, ent is visible.
  • DenyIfViewerOutboundEdgeExistsRule: denies if there's an edge from ent to viewer e.g. has the user (the ent we're trying to load) blocked the viewer, if so, ent is not visible
  • AlwaysAllowRule: otherwise, ent is visible.

In this scenario, we're denying early if possible and then ending open

rbac

Or in a contrived simplified RBAC system, something like:

src/privacy/roles.ts
enum Role {
///....
}
export class AllowIfViewerHasRoleRule {
constructor(private role: Role) {}

async apply(v: Viewer, ent?: Ent): Promise<PrivacyResult> {
if (!v.viewerID) {
return Skip();
}
const userRole = await fetchRole(v.viewerID);
if (userRole === this.role) {
return Allow();
}
return Skip();
}
}

export class AllowIfViewerHasRolePrivacyPolicy {
constructor(private role: Role) {}

rules: PrivacyPolicyRule[] = [
new AllowIfViewerHasRoleRule(this.role),
AlwaysDenyRule,
];
}

src/ent/post.ts
export class Post extends PostBase {
getPrivacyPolicy() {
return new AllowIfViewerHasRolePrivacyPolicy(Role.Post);
}
}
src/ent/blog.ts
export class Blog extends BlogBase {
getPrivacyPolicy() {
return new AllowIfViewerHasRolePrivacyPolicy(Role.PublishBlog);
}
}