User Access Control
← Previous postUser Access Control
As mentioned in the API key documentation EmoDB uses API keys to authenticate an authorize client requests. Each API key can have zero or more roles associated with it, and each of those roles contains permissions which grant access to different parts of the system.
User access control is the system in EmoDB for managing API keys, permissions and roles. The following sections describe the rules and APIs for user access control.
Java Client Library
To use the user access control API you can either call the service directly or use the Java client library.
To use the Java client add the following to your Maven POM (set the <emo-version>
to the current version of EmoDB):
<dependency>
<groupId>com.bazaarvoice.emodb</groupId>
<artifactId>emodb-uac-client</artifactId>
<version>${emo-version}</version>
</dependency>
Minimal Java client without ZooKeeper or Dropwizard:
String emodbHost = "localhost:8080"; // Adjust to point to the EmoDB server.
String apiKey = "xyz"; // Use the API key provided by EmoDB
MetricRegistry metricRegistry = new MetricRegistry(); // This is usually a singleton passed
UserAccessControl userAccessControl = ServicePoolBuilder.create(UserAccessControl.class)
.withHostDiscoverySource(new UserAccessControlFixedHostDiscoverySource(emodbHost))
.withServiceFactory(UserAccessControlClientFactory.forCluster("local_default", new MetricRegistry()).usingCredentials(apiKey))
.withMetricRegistry(metricRegistry)
.buildProxy(new ExponentialBackoffRetry(5, 50, 1000, TimeUnit.MILLISECONDS));
... use "userAccessControl" to access the service ...
ServicePoolProxies.close(userAccessControl);
Robust Java client using ZooKeeper, SOA and Dropwizard:
@Override
protected void initialize(Configuration configuration, Environment environment) throws Exception {
// YAML-friendly configuration objects.
ZooKeeperConfiguration zooKeeperConfig = configuration.getZooKeeper();
JerseyClientConfiguration jerseyClientConfig = configuration.getHttpClientConfiguration();
UserAccessControlFixedHostDiscoverySource sorEndPointOverrides = configuration.getSorEndPointOverrides();
// Connect to ZooKeeper.
CuratorFramework curator = zooKeeperConfig.newManagedCurator(environment);
curator.start();
// Configure the Jersey HTTP client library.
Client jerseyClient = new JerseyClientFactory(jerseyClientConfig).build(environment);
String apiKey = "xyz"; // Use the API key provided by EmoDB
// Connect to the UserAccessControl using ZooKeeper (Ostrich) host discovery.
ServiceFactory<UserAccessControl> userAccessControlFactory =
UserAccessControlClientFactory.forClusterAndHttpClient("local_default", jerseyClient).usingCredentials(apiKey);
UserAccessControl userAccessControl = ServicePoolBuilder.create(UserAccessControl.class)
.withHostDiscoverySource(sorEndPointOverrides)
.withHostDiscovery(new ZooKeeperHostDiscovery(curator, userAccessControlFactory.getServiceName()))
.withServiceFactory(userAccessControlFactory)
.buildProxy(new ExponentialBackoffRetry(5, 50, 1000, TimeUnit.MILLISECONDS));
environment.addHealthCheck(new UserAccessControlHealthCheck(userAccessControl));
environment.manage(new ManagedServicePoolProxy(userAccessControl));
... use "userAccessControl" to access the service ...
}
Permissions
Permissions are used to control access to EmoDB’s resources. For example, if team A creates a queue for its own project’s internal use it would be harmful if team B were to poll and ack messages from that queue without team A’s knowledge or consent. Permissions can be used to restrict the capabilities of an individual role, and assigning the role to one or more API keys transitively limits the capabilities of those API keys.
A full list of possible permissions can be found in Permissions.java. The following section highlights the general format and nuances around SoR and Blob permissions.
Permission format
In general permissions follow the format of “context|action|resource”. For example databus|poll|subscription1
indicates permission to perform the “poll” action on the databus subscription “subscription1”.
For actions and resources the value can be one of the following:
- A single value (e.g:
update
) - A wildcard value. This can indicate either the entire value,
*
, or a portion, such asget*
. - A conditional value (more on this later)
Context
The currently supported contexts are sor
, blob
, queue
, databus
, facade
and system
. The context portion
of the permission must start with one of these values. (It is technically possible to use a pure wildcard, *
, although
this is discouraged. The existing “admin” role already provides this capability.)
Action
The action restricts what the user can do within the context. As such each context typically has its own set of actions
which may not have meaning in other contexts. For example, databus|poll
makes sense but blob|poll
does not and
therefore is never utilized by EmoDB.
Resource
The resource restricts what the user can do within the context and action. For example, databus|poll|*
indicates
that polling all databus subscriptions is permitted, while databus|poll|subscription1
indicates that polling is
only permitted for a single subscription, subscription1
.
Conditionals
Permitting activities using only single values and wildcard values is limiting. For example, assume there is a role that
should have permission to perform all actions in the sor
context except drop_table
. The only way to do this is to
create separate permissions for all possible actions except drop_table
(sor|update|*
, sor|create_table|*
,
and so on). If the user were further restricted by a subset of tables instead of simply *
this now requires a
complicated cartesian product of all possible combinations.
For this reason an action or resource can use a conditional to determine matching values. The
conditional is exactly the same format
as used by deltas and databus subscriptions. To create a conditional surround the condition string in an if()
statement.
Examples
Permission | Effect |
---|---|
sor|if(in("update","create_table"))|* |
Equivalent to having both sor|update|* and sor|create_table|* |
sor|if(not("drop_table"))|* |
User can perform all actions in the sor context except drop_table |
queue|*|if(and(like("team:*"),not("team:edward"))) |
User can perform all actions on all queues matching team:* except team:edward |
Table conditionals
In all contexts except sor
and blob
a conditional in the resource is evaluated using the resource name, such a the
name of a queue or databus subscription. In sor
and blob
it is evaluated as a table conditional in exactly the
same way as when creating a databus subscription.
Examples
For the follow examples assume the SoR table “ermacs_data” has been created in placement “ugc_global:ugc” and was
created with template {"team": "ermacs"}
. (The common prefix of sor|update|
has been removed from the first
column and in some cases whitespace added for readability.)
Resource in “sor” context | Matches table? | Why |
---|---|---|
ermacs_* |
Yes | Table name starts with prefix “ermacs_” |
if(intrinsic("~table":"ermacs_data")) |
Yes | Table name is an exact match |
if(intrinsic("~table":in("ermacs_data","ermacs_logs"))) |
Yes | Table name is in the “in” condition |
if(intrinsic("~placement":'ugc_global:ugc')) |
Yes | Table placement is an exact match |
if(intrinsic("~placement":like("*:ugc"))) |
Yes | Table placement is a like match |
if({..,"team":"ermacs"}) |
Yes | Table has attribute “team” set to “ermacs” |
if({..,"team":"ermacs","other":"attr")) |
No | Only one matching attribute is present on the table |
if(and(intrinsic("~table":like("ermacs_*")), intrinsic("~placement":like("*:ugc")))) |
Yes | Table name and placement both match the respective like conditions |
if(and(intrinsic("~table":like("ermacs_*")), intrinsic("~placement":like("*:cat")))) |
No | Only one of the conditions is met; placement does not end with “cat” |
Role Administration API
A role consists of the following attributes:
- Group
- ID
- Name
- Description
- Permission set
The “group” is effectively a namespace for grouping related roles, and the “ID” is a unique identifier for the role within its group. Collectively [group, ID] is a unique identifier for each role. A group must be a sequence of at most 255 characters, each of which must be either alpha-numeric or one of [-.:_]. Additionally, the group name “_” is reserved. IDs are similarly constrained except there is no restriction on creating IDs named “_”. Role names and descriptions are optional, although providing descriptive values for each is recommended.
EmoDB has several pre-defined roles that are always available. You can see these roles and what permissions they have in DefaultRoles.java
The role administration API allows you to create new roles with custom permissions. These roles can then be associated with one or more API keys to provide fine controls over what actions the API key can perform. Note that each of these role operations requires an authenticating API key which itself has specific permissions. The permissions necessary are documented with each operation below.
Create a role
Permissions required:
role|create|{group}|{id}
HTTP:
POST /uac/1/role/{group}/{id}
Content-Type: "application/x.json-create-role"
Java:
void createRole(CreateEmoRoleRequest request)
When creating a role you only need to provide those attrbutes which are being initialized. For example, if you leave out any permissions the role will be created but it will initially have no permissions associated with it.
Example:
$ curl -s -XPOST 'http://localhost:8080/uac/1/role/sample_group/sample_id' \
-H "Content-Type: application/x.json-create-role" \
-d '{"name":"Sample role","description":"A sample role","permissions":["sor|read|*","blob|read|*"]}'
{"success":true}
Java Example:
uac.createRole(
new CreateEmoRoleRequest(new EmoRoleKey("sample_group", "sample_id"))
.setName("Sample role")
.setDescription("A sample role")
.setPermissions(ImmutableSet.of("sor|read|*", "blob|read|*")))
View all roles or view all roles in one group
Permissions required:
role|read
for at least one existing or potential role
HTTP:
GET /uac/1/role
GET /uac/1/role/{group}
Java:
Iterator<EmoRole> getAllRoles()
Iterator<EmoRole> getAllRolesInGroup(String group)
Example:
$ curl -s "http://localhost:8080/uac/1/role" | jq .
[
{
"group": "sample_group",
"id": "sample_id",
"name": "Sample role",
"description": "A sample role",
"permissions": [
"blob|read|*",
"sor|read|*"
]
}
$ curl -s "http://localhost:8080/uac/1/role/sample_group" | jq .
[
{
"group": "sample_group",
"id": "sample_id",
"name": "Sample role",
"description": "A sample role",
"permissions": [
"blob|read|*",
"sor|read|*"
]
}
]
View a role
Permissions required:
role|read|{group}|{id}
HTTP:
GET /uac/1/role/{group}/{id}
Java
EmoRole getRole(EmoRoleKey roleKey)
Example:
$ curl -s "http://localhost:8080/uac/1/role/sample_group/sample_id" | jq .
{
"group": "sample_group",
"id": "sample_id",
"name": "Sample role",
"description": "A sample role",
"permissions": [
"blob|read|*",
"sor|read|*"
]
}
Java Example:
EmoRole role = uac.getRole(new EmoRoleKey("sample_group", "sample_id"));
Update a role
Permissions required:
role|update|{group}|{id}
HTTP:
PUT /uac/1/role/{group}/{id}
Content-Type: "application/x.json-update-role"
Java:
void updateRole(UpdateEmoRoleRequest request)
When updating a role you only need to provide those attributes which you want changed. Additionally, you can incrementally
add and remove individual permissions without passing back the entire permission set on each call. The following
examples revoke permission for blob|read|*
and add permission for databus|*|subscription1
.
Example:
$ curl -s -XPUT "http://localhost:8080/uac/1/role/sample_group/sample_id' \
-H "Content-Type: application/x.json-update-role" \
-d '{"name":"A new name","revokePermissions":["blob|read|*"],"grantPermissions":["databus|*|subscription1"]}'
{"success":true}
Java Example:
uac.updateRole(
new UpdateEmoRoleRequest(new EmoRoleKey("sample_group", "sample_id"))
.setName("A new name")
.revokePermissions(ImmutableSet.of("blob|read|*"))
.grantPermissions(ImmutableSet.of("databus|*|subscription1")))
Delete a role
Permissions required:
role|delete|{group}|{id}
HTTP:
DELETE /uac/1/role/{group}/{id}
Java:
void deleteRole(EmoRoleKey roleKey)
Example:
$ curl -s -XDELETE "http://localhost:8080/uac/1/role/sample_group/sample_id"
{"success":true}
Java Example:
uac.deleteRole(new EmoRoleKey("sample_group", "sample_id"))
API Key Administration API
An API key consists of the following attributes:
- key (private unique identifier used for authentication, such as in the “X-BV-API-Key” header)
- ID (public unique identifier)
- Owner
- Description
- Role set
The “owner” is the only required settable attribute. No specific format is required, although we recommend an email address or some similar identifier for contacting the API key’s owner in case there is an issue.
It is possible to assign an API key a role that does not exist. If this happens there is no error, the role simply doesn’t provide the API key with any additional permissions. If and when the role is created the API key will gain the permissions associated with that role.
As with roles there are specific permissions which are required for each API key operation. Note that the permissions to modify an API key’s roles are different than the permissions for the API key’s other attributes. For example, a user may have permission to add and remove roles in the group “sample_group” for an API key but not to perform any other API key updates.
The following documentation is not exhaustive; there are more API key operations available, and for those described some have additional parameters not presented here. This documentation includes the most common uses for API key administration; please refer to the Java client classes for a full list of operations and parameters.
Create an API key
Permissions required:
apikey|create
role|grant|{group}|{id}
for each role the request assigns to the API key
HTTP:
POST /uac/1/api-key
Content-Type: "application/x.json-create-api-key"
Java
CreateEmoApiKeyResponse createApiKey(CreateEmoApiKeyRequest request)
Note that both the private key and public ID are determined by Emo and returned in the response.
Example:
curl -s -XPOST "http://localhost:8080/uac/1/api-key" \
-H "Content-Type: application/x.json-create-api-key" \
-d '{"owner":"owner@example.com","description":"Sample API key","roles":[{"group":"sample_group","id":"sample_id"}]}' | jq .
{
"id": "MEBZF4AP3YI6PMX7F22LNQUCKI",
"key": "um3affkxtsuzehmgmrlfnh2f2wis3bwvwshsfsztdkxymkjc"
}
Java Example:
CreateEmoApiKeyResponse response = uac.createApiKey(
new CreateEmoApiKeyRequest()
.setOwner("owner@example.com")
.setDescription("Sample API key")
.setRoles(ImmutableSet.of(new EmoRoleKey("sample_group", "sample_id"))))
View an API key
Permissions required:
apikey|read
unless the API key requested matches the authenticating API key; every user has permission to view himself
HTTP:
GET /uac/1/api-key/{id}
Java:
EmoApiKey getApiKey(String id)
Example:
curl -s "http://localhost:8080/uac/1/api-key/MEBZF4AP3YI6PMX7F22LNQUCKI' | jq .
{
"description": "Sample API key",
"roles": [
{
"id": "sample_id",
"group": "sample_group"
}
],
"owner": "owner@example.com",
"issued": "2017-03-23T15:35:44.287Z",
"maskedKey": "um3a****************************************mkjc",
"id": "MEBZF4AP3YI6PMX7F22LNQUCKI"
}
Java Example:
EmoApiKey apiKey = uac.getApiKey("MEBZF4AP3YI6PMX7F22LNQUCKI")
Update an API key
Permissions required:
apikey|update
if either the owner or description is updated by the requestrole|grant|{group}|{id}
for each role the request assigns to or unassigns from the API key
HTTP:
PUT /uac/1/api-key/{id}
Content-Type: "application/x.json-update-api-key
Java:
updateApiKey(UpdateEmoApiKeyRequest request)
When updating an API key you only need to provide those attributes which you want changed. Additionally, you can incrementally add and remove individual roles without passing back the entire role set on each call. The following examples change the owner, unassign role “sample_id” and assign role “new_sample_id”, both in group “sample_group”.
As noted previously, the permissions required for updating an API depend on what attributes the request is updating.
For example, an update which only assigns a single new role only requires the role|grant
permission for that role.
Also note that this same “grant” permission is used to both assign and unassign the role. It is not possible for
a user to have permission to assign a role but not to unassign it, or vice versa.
Example:
$ curl -s -XPUT "http://localhost:8080/uac/1/api-key/MEBZF4AP3YI6PMX7F22LNQUCKI" \
-H "Content-Type: application/x.json-update-api-key" \
-d '{"owner":"new_owner@example.com","unassignRoles":[{"group":"sample_group","id":"sample_id"}],"assignRoles":[{"group":"sample_group","id":"new_sample_id"}]}'
{"success":true}
Java Example:
uac.updateApiKey(
new UpdateEmoApiKeyRequest("MEBZF4AP3YI6PMX7F22LNQUCKI")
.setOwner("new_owner@example.com")
.unassignRoles(ImmutableSet.of(new EmoRoleKey("sample_group", "sample_id")))
.assignRoles(ImmutableSet.of(new EmoRoleKey("sample_group", "new_sample_id"))))
Migrate an API key
Permissions required:
apikey|update
HTTP:
POST /uac/1/api-key/{id}/migrate
Java:
String migrateApiKey(MigrateEmoApiKeyRequest request)
Migrating an API key maintains its ID and all associated attributes and roles but invalidates the existing private key and issues a new one. This is typically done either when a caller lost his private key or when the key has definitively or potentially been leaked.
Example:
$ curl -s -XPOST "http://localhost:8080/uac/1/api-key/MEBZF4AP3YI6PMX7F22LNQUCKI/migrate" | jq .
{
"id": "MEBZF4AP3YI6PMX7F22LNQUCKI",
"key": "hyvdfvbfdmj6jvzsegp3yukmyw43e6f50tzfyyi3e3yawdct"
}
Java Example:
String newPrivateKey = uac.migrateApiKey(new MigrateEmoApiKeyRequest("MEBZF4AP3YI6PMX7F22LNQUCKI"))
Delete an API key
Permissions required:
apikey|delete
role|grant|{group}|{id}
for each role currently assigned to the API key
HTTP:
DELETE /uac/1/api-key/{id}
Java:
void deleteApiKey(String id)
Example:
$ curl -s -XDELETE "http://localhost:8080/uac/1/api-key/MEBZF4AP3YI6PMX7F22LNQUCKI"
{"successs":true}
Java Example:
deleteApiKey("MEBZF4AP3YI6PMX7F22LNQUCKI")