Imagine the following data models for an internal API targeted at a company's own developers and support agents.
- User: id, name, email, is_gov
- UserActivityRecord: id, user_id, details, is_hidden
- SupportTickets: id, assigned_agent_id
And these are the potential authorization considerations:
- Two personas: Developers and Support Agents
- Developers can generally view all user's information
- Support agents can only view data for users whose tickets they have been assigned
- Some user information is protected
- PII fields (name, email) unless the API consumer has explicit access
- Government user data is restricted unless the API consumer has explicit access
The authentication system would be simple. Four roles:
- developer
- support_agent
- has_access_to_pii
- has_access_to_gov
A user's roles are the result of all RolePredicate
s that return true
.
kind: RolePredicate
version: v1
definition:
name: has_access_to_pii
condition:
equals:
left: sessionVariable: x-hasura-has-pii-access
right: literal: true
kind: RolePredicate
version: v1
definition:
name: has_access_to_gov
condition:
equals:
left: sessionVariable: x-hasura-has-gov-access
right: literal: true
kind: RolePredicate
version: v1
definition:
name: developer
condition:
equals:
left: sessionVariable: x-hasura-role
right: literal: developer
kind: RolePredicate
version: v1
definition:
name: support_agent
condition:
equals:
left: sessionVariable: x-hasura-role
right: literal: support_agent
kind: TypePermissions
version: v2
definition:
typeName: User
permissions:
# Rule 1: everybody can access `id` fields
- roles: [developer, support_agent]
output:
allowedFields: [id]
# Rule 2: people with the a special role allows access to everything
- roles: [has_access_to_pii]
output:
# we cumulatively build up access to fields, so these users can also see the `id`
# field
allowedFields: [name, dob]
kind: ModelPermissions
version: v2
definition:
modelName: User
permissions:
# Rule 1: Developers are allowed to access all users by default
- roles: [developer]
select: null
# Rule 2: Support agents are allowed to access users they have support
# tickets for.
# we express this as a partially preset `where` clause on the model
- roles: [support_agent]
select:
# row permissions, expressed as a boolean expression
filter:
fieldComparison:
field: agent_id
operator: _eq
value: sessionVariable: x-hasura-agent-id
# Rule 3: If a developer / support_agent does not have gov access,
# don't allow them to access gov users.
# This rule matches on the absence of a role, and we accumulate predicates
# to add to the `where` clause
# we may need to think about what to do where two comparison fields
# conflict. `withoutRole` feels like it should be more important, like
# `deny`, but need to think about this more. Or base it on ordering, and put
# this entry first.
- withoutRole: has_access_to_gov
select:
# row permissions, expressed as a boolean expression
filter:
fieldComparison:
field: is_gov
operator: _eq
value: literal: false
# the suggestion of "if I can access X, then I can access Y" makes sense
# and it's straightforward to take any predicate presets and use them on the
# nested object / object relationship.
kind: ModelPermissions
version: v2
definition:
modelName: UserActivity
# we look at the permissions for `user`, and use them to decide if we can
# look at `UserActivity`
inheritedPermissions:
- relationship:
name: user
relatedObjectAllowed: true
permissions:
# nobody can see the hidden fields
- roles: '*'
select:
filter:
fieldComparison:
fieldName: is_hidden
operator: _eq
value: literal: true