Entitlements

In applications with differentiated subscription tiers or paid features users' permissions are determined by the features they have paid for.

Implement the logic

The core part of the entitlements logic is to conditionally grant a user permission based on a combination of their role and attributes of the organization (like the subscription tier they are on).

For example, we can grant users permission to create repositories for an organization if they have the "member" role for that organization and the organization has quota remaining for creating repositories.


actor User { }
resource Organization {
roles = ["admin", "member"];
permissions = ["repository.create"];
"member" if "admin";
}
resource Plan {
roles = ["subscriber"];
relations = { subscribed_organization: Organization };
"subscriber" if role on "subscribed_organization";
}
resource Feature {
relations = { plan: Plan };
}
has_permission(user: User, "repository.create", org: Organization) if
has_role(user, "member", org) and
has_quota_remaining(org, Feature{"repository"});
has_quota_remaining(org: Organization, feature: Feature) if
quota matches Integer and
has_quota(org, feature, quota) and
used matches Integer and
quota_used(org, feature, used) and
used < quota;
has_quota(org: Organization, feature: Feature, quota: Integer) if
plan matches Plan and
has_relation(plan, "subscribed", org) and
plan_quota(plan, feature, quota);

Test the logic

To test this we have:

  • A few organizations with different plans and quotas used.
  • Users belonging to each of those organizations.
  • Tests to check which users can still create repositories.

declare plan_quota(Plan, Feature, Integer);
declare quota_used(Organization, Feature, Integer);
plan_quota(Plan{"pro"}, Feature{"repository"}, 10);
plan_quota(Plan{"basic"}, Feature{"repository"}, 0);
test "members can create repositories if they have quota" {
setup {
quota_used(Organization{"apple"}, Feature{"repository"}, 5);
quota_used(Organization{"netflix"}, Feature{"repository"}, 10);
quota_used(Organization{"amazon"}, Feature{"repository"}, 0);
has_relation(Plan{"pro"}, "subscribed", Organization{"apple"});
has_relation(Plan{"pro"}, "subscribed", Organization{"netflix"});
has_relation(Plan{"basic"}, "subscribed", Organization{"amazon"});
has_role(User{"alice"}, "member", Organization{"apple"});
has_role(User{"bob"}, "member", Organization{"netflix"});
has_role(User{"charlie"}, "member", Organization{"amazon"});
}
assert has_quota_remaining(Organization{"apple"}, Feature{"repository"});
# Apple has quota remaining, so all good
assert allow(User{"alice"}, "repository.create", Organization{"apple"});
# Netflix has used all quota
assert_not allow(User{"bob"}, "repository.create", Organization{"netflix"});
# Amazon doesn't have any quota left
assert_not allow(User{"charlie"}, "repository.create", Organization{"amazon"});
}