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()
orSkip()
orDeny()
orSkip()
- 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:
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 theViewer
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:
export class Contact extends ContactBase {
getPrivacyPolicy() {
return {
rules: [new AllowIfViewerIsEntPropertyRule("userID"), AlwaysDenyRule],
};
}
}
This has 2 rules:
AllowIfViewerIsEntPropertyRule
: this checks that the id of theViewer
is equal to the fielduserID
of the ent. If so, ent is visible.AlwaysDenyRule
: otherwise, ent isn't visible.
events based system
Event based system with guests.
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 theViewer
is equal to the id of the ent. If so, ent is visible.AllowIfEventCreatorRule
: custom rule. Guest is visible if viewer is event creatorAllowIfGuestInSameGuestGroupRule
: custom rule. Guest is visible if viewer and guest are part of a group invited together e.g. +1 for a weddingAlwaysDenyRule
: otherwise, ent isn't visible.
social media with blocking
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 theViewer
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 visibleAlwaysAllowRule
: 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:
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,
];
}
export class Post extends PostBase {
getPrivacyPolicy() {
return new AllowIfViewerHasRolePrivacyPolicy(Role.Post);
}
}
export class Blog extends BlogBase {
getPrivacyPolicy() {
return new AllowIfViewerHasRolePrivacyPolicy(Role.PublishBlog);
}
}