Impersonation

Users can impersonate other users, which means they inherit some subset of the permissions that the other user has.

This pattern is useful in any system where some users need to troubleshoot or oversee other users. For example: customer support, HR, and payroll systems.

Implement the logic

In our example, customer support reps can get read-only access to all organizations a user belongs to.

The two main components are:

  1. A fact that indicates whether a user is impersonating another user.
  2. A policy that grants permissions to users who are impersonating another user.

actor User {
permissions = ["impersonate"];
"impersonate" if global "support";
}
global {
roles = ["support"];
}
resource Organization {
roles = ["admin", "member"];
permissions = ["read", "write"];
"member" if "admin";
"read" if "member";
"write" if "admin";
}
# a user can do anything some other user can do
# if they are allowed to impersonate that user and
# are currently impersonating them
allow(user: User, action: String, resource: Resource) if
other_user matches User and
has_permission(user, "impersonate", other_user) and
is_impersonating(user, other_user) and
has_permission(other_user, action, resource);
# we need to specify the default allow rule here
# because we added our own custom one above
allow(user: User, action: String, resource: Resource) if
has_permission(user, action, resource);

Test it out

In our test case, we'll put all the pieces together:

  1. Alice is a customer support rep and is impersonating Bob.
  2. Bob is an admin of the acme organization.
  3. And so Alice can see anything Bob can.

test "global support users can read user organizations via impersonation" {
setup {
has_role(User{"alice"}, "support");
has_role(User{"bob"}, "admin", Organization{"acme"});
has_role(User{"charlie"}, "member", Organization{"bar"});
is_impersonating(User{"alice"}, User{"bob"});
}
# bob can read as a member
assert allow(User{"bob"}, "read", Organization{"acme"});
# alice can impersonate bob
assert allow(User{"alice"}, "impersonate", User{"bob"});
# alice can read via Bob by impersonating bob
assert allow(User{"alice"}, "read", Organization{"acme"});
# charlie can read as a member
assert allow(User{"charlie"}, "read", Organization{"bar"});
# alice cannot read because alice is not impersonating charlie
assert_not allow(User{"alice"}, "read", Organization{"bar"});
}

Extension

There may be several ways to define who has permission to impersonate another user. Common ones include:

From a relationship with the user:


actor User {
permissions = ["impersonate"];
relations = { manager: User };
"impersonate" if "manager";
}

Or from a role on the organization:


actor User {
permissions = ["impersonate"];
}
resource Organization {
roles = ["member", "admin"];
}
# Organization admins can impersonate members
has_permission(user: User, "impersonate", other_user: User) if
org matches Organization and
has_role(user, "admin", org) and
has_role(other_user, "member", org);

The is_impersonating fact can be included as an ephemeral context fact or more durably synced to Oso Cloud. In the former case, when a user "ends" an impersonation session, the application stops sending the impersonation context fact. In the latter case, you need to delete the persisted fact.

Prefer context facts when you want impersonation restricted to a single application or service and persisted facts when you want the impersonation session to be shared across services.