70 %
Chris Biscardi

How to implement Role Based Access Control (RBAC) in DynamoDB

You can use IAM to restrict access to DynamoDB items but for those unfamiliar with the intricacies of IAM roles or wishing for more dynamic permissions on the application level we can use a second table instead.

If we have a set of entities we want to control access to, we can use the pattern actor controlType entity. For example:

user 2 is-owner document 1
user 5 is-editor document 1
user 6 is-writer document 1
user 9 is-audience document 1

So for example let's say the following steps happen.

  1. user 2 creates document 1
  2. user 2 adds user 6 as a writer so they can start writing.
  3. user 2 adds user 5 as an editor, so that they can make changes and suggestions to the document
  4. user 5 sends the document to user 9, who can now read it but not make any changes

We would end up with the following DynamoDB table items

access-control-table
JS
[
{
pk: 'access#user#2',
sk: 'is-owner#doc#1'
},
{
pk: 'access#user#6',
sk: 'is-writer#doc#1'
},
{
pk: 'access#user#5',
sk: 'is-editor#doc#1'
},
{
pk: 'access#user#9',
sk: 'is-audience#doc#1'
}
]

With an index of pk+sk on the table, this allows us to query for any roles a user has.

query.js
JS
const params = {
TableName: "access-control",
KeyConditionExpression: "pk = :userid and begins_with(sk, :control)",
ExpressionAttributeValues: {
":userid": `access#user#${userid}`,
":control": "is-"
}
};

and then we can use the Items returned from our query to determine if a user has a sufficient role for a given entity.

In Practice

Here's an implementation that uses dynamodb-toolbox to create the AccessControl entity.

JS
const AccessControl = new Entity({
name: "access-control",
timestamps: true,
attributes: {
pk: { hidden: true, partitionKey: true },
sk: { hidden: true, sortKey: true },
access: ["pk", 0, { default: "access", save: false }],
// user
actorType: ["pk", 1, { required: true, save: false }],
actorId: ["pk", 2, { required: true, save: false }],
// can-access, is-admin
control: ["sk", 0, { required: true, save: false }],
entityType: ["sk", 1, { required: true, save: false }],
entityId: ["sk", 2, { required: true, save: false }],
},
table: AccessControlTable,
});

hidden means the serialized object won't have that key, and save means that field won't be saved in the DynamoDB table as an attribute. So what we end up with is two fields, built up from the other 6.

JS
await AccessControl.put({
actorType: "user",
actorId: userId,
control: "is-admin",
entityType: "mdx",
entityId: mdxId,
});