Using Open Source Cedar to Write and Enforce Custom Authorization Policies
May 10, 2023 By Mark Otto 0Cedar is an open source language and software development kit (SDK) for writing and enforcing authorization policies for your applications. You can use Cedar to control access to resources such as photos in a photo-sharing app, compute nodes in a micro-services cluster, or components in a workflow automation system. You specify fine-grained permissions as Cedar policies, and your application authorizes access requests by calling the Cedar SDK’s authorization engine. Cedar has a simple and expressive syntax that supports common authorization paradigms, including both role-based access control (RBAC) and attribute-based access control (ABAC). Because Cedar policies are separate from application code, they can be independently authored, analyzed, and audited, and even shared among multiple applications.
In this blog post, we introduce Cedar and the SDK using an example application, TinyTodo, whose users and teams can organize, track, and share their todo lists. We present examples of TinyTodo permissions as Cedar policies and how TinyTodo uses the Cedar authorization engine to ensure that only intended users are granted access. A more detailed version of this post is included with the TinyTodo code.
TinyTodo
TinyTodo allows individuals, called Users
, and groups, called Teams
, to organize, track, and share their todo lists. Users
create Lists
which they can populate with tasks. As tasks are completed, they can be checked off the list.
TinyTodo Permissions
We don’t want to allow TinyTodo users to see or make changes to just any task list. TinyTodo uses Cedar to control who has access to what. A List
‘s creator, called its owner, can share the list with other Users
or Teams
. Owners can share lists in two different modes: reader and editor. A reader can get details of a List
and the tasks inside it. An editor can do those things as well, but may also add new tasks, as well as edit, (un)check, and remove existing tasks.
We specify and enforce these access permissions using Cedar. Here is one of TinyTodo’s Cedar policies.
// policy 1: A User can perform any action on a List they own
permit(principal, action, resource)
when {
resource has owner && resource.owner == principal
};
This policy states that any principal (a TinyTodo User
) can perform any action on any resource (a TinyTodoList
) as long as the resource has an owner
attribute that matches the requesting principal.
Here’s another TinyTodo Cedar policy.
// policy 2: A User can see a List if they are either a reader or editor
permit (
principal,
action == Action::"GetList",
resource
)
when {
principal in resource.readers || principal in resource.editors
};
This policy states that any principal can read the contents of a task list (Action::"GetList"
) so long as they are in either the list’s readers
group, or its editors
group.
Cedar’s authorizer enforces default deny: A request is authorized only if a specific permit
policy grants it.
The full set of policies can be found in the file TinyTodo file policies.cedar
(discussed below). To learn more about Cedar’s syntax and capabilities, check out the Cedar online tutorial at https://www.cedarpolicy.com/.
Building TinyTodo
To build TinyTodo you need to install Rust and Python3, and the Python3 requests
module. Download and build the TinyTodo code by doing the following:
> git clone https://github.com/cedar-policy/tinytodo
...downloading messages here
> cd tinytodo
> cargo build
...build messages here
The cargo build
command will automatically download and build the Cedar Rust packages cedar-policy-core
, cedar-policy-validator
, and others, from Rust’s standard package registry, crates.io
, and build the TinyTodo server, tiny-todo-server
. The TinyTodo CLI is a Python script, tinytodo.py
, which interacts with the server. The basic architecture is shown in Figure 1.
Running TinyTodo
Let’s run TinyTodo. To begin, we start the server, assume the identity of user andrew
, create a new todo list called Cedar blog post
, add two tasks to that list, and then complete one of the tasks.
> python -i tinytodo.py
>>> start_server()
TinyTodo server started on port 8080
>>> set_user(andrew)
User is now andrew
>>> get_lists()
No lists for andrew
>>> create_list("Cedar blog post")
Created list ID 0
>>> get_list(0)
=== Cedar blog post ===
List ID: 0
Owner: User::"andrew"
Tasks:
>>> create_task(0,"Draft the post")
Created task on list ID 0
>>> create_task(0,"Revise and polish")
Created task on list ID 0
>>> get_list(0)
=== Cedar blog post ===
List ID: 0
Owner: User::"andrew"
Tasks:
1. [ ] Draft the post
2. [ ] Revise and polish
>>> toggle_task(0,1)
Toggled task on list ID 0
>>> get_list(0)
=== Cedar blog post ===
List ID: 0
Owner: User::"andrew"
Tasks:
1. [X] Draft the post
2. [ ] Revise and polish
The get_list
, create_task
, and toggle_task
commands are all authorized by the Cedar Policy 1
we saw above: since andrew
is the owner of List
ID 0, he is allowed to carry out any action on it.
Now, continuing as user andrew
, we share the list with team interns
as a reader. TinyTodo is configured so that the relationship between users and teams is as shown in Figure 2. We switch the user identity to aaron
, list the tasks, and attempt to complete another task, but the attempt is denied because aaron
is only allowed to view the list (since he’s a member of interns
) not edit it. Finally, we switch to user kesha
and attempt to view the list, but the attempt is not allowed (interns
is a member of temp
, but not the reverse).
>>> share_list(0,interns,read_only=True)
Shared list ID 0 with interns as reader
>>> set_user(aaron)
User is now aaron
>>> get_list(0)
=== Cedar blog post ===
List ID: 0
Owner: User::"andrew"
Tasks:
1. [X] Draft the post
2. [ ] Revise and polish
>>> toggle_task(0,2)
Access denied. User aaron is not authorized to Toggle Task on [0, 2]
>>> set_user(kesha)
User is now kesha
>>> get_list(0)
Access denied. User kesha is not authorized to Get List on [0]
>>> stop_server()
TinyTodo server stopped on port 8080
Here, aaron
‘s get_list
command is authorized by the Cedar Policy 2
we saw above, since aaron
is a member of the Team interns
, which andrew
made a reader of List
0. aaron
‘s toggle_task
and kesha
‘s get_list
commands are both denied because no specific policy exists that authorizes them.
Extending TinyTodo’s Policies with Administrator Privileges
We can change the policies with no updates to the application code because they are defined and maintained independently. To see this, add the following policy to the end of the policies.cedar
file:
permit(
principal in Team::"admin",
action,
resource in Application::"TinyTodo");
This policy states that any user
who is a member of Team::"Admin
” is able to carry out any action on any List
(all of which are part of the Application::"TinyTodo"
group). Since user emina
is defined to be a member of Team::"Admin"
(see Figure 2), if we restart TinyTodo to use this new policy, we can see emina
is able to view and edit any list:
> python -i tinytodo.py
>>> start_server()
=== TinyTodo started on port 8080
>>> set_user(andrew)
User is now andrew
>>> create_list("Cedar blog post")
Created list ID 0
>>> set_user(emina)
User is now emina
>>> get_list(0)
=== Cedar blog post ===
List ID: 0
Owner: User::"andrew"
Tasks:
>>> delete_list(0)
List Deleted
>>> stop_server()
TinyTodo server stopped on port 8080
Enforcing access requests
When the TinyTodo server receives a command from the client, such as get_list
or toggle_task
, it checks to see if that command is allowed by invoking the Cedar authorization engine. To do so, it translates the command information into a Cedar request and passes it with relevant data to the Cedar authorization engine, which either allows or denies the request.
Here’s what that looks like in the server code, written in Rust. Each command has a corresponding handler, and that handler first calls the function self.is_authorized
to authorize the request before continuing with the command logic. Here’s what that function looks like:
pub fn is_authorized( &self, principal: impl AsRef<EntityUid>, action: impl AsRef<EntityUid>, resource: impl AsRef<EntityUid>,
) -> Result<()> { let es = self.entities.as_entities(); let q = Request::new( Some(principal.as_ref().clone().into()), Some(action.as_ref().clone().into()), Some(resource.as_ref().clone().into()), Context::empty(), ); info!("is_authorized request: …”); let resp = self.authorizer.is_authorized(&q, &self.policies, &es); info!("Auth response: {:?}", resp); match resp.decision() { Decision::Allow => Ok(()), Decision::Deny => Err(Error::AuthDenied(resp.diagnostics().clone())), }
}
The Cedar authorization engine is stored in the variable self.authorizer
and is invoked via the call self.authorizer.is_authorized(&q, &self.policies, &es)
. The first argument is the access request &q
— can the principal
perform action
on resource
with an empty context? An example from our sample run above is whether User::"kesha"
can perform action Action::"GetList"
on resource List::"0"
. (The notation Type::"id"
used here is of a Cedar entity UID, which has Rust type cedar_policy::EntityUid
in the code.) The second argument is the set of Cedar policies &self.policies
the engine will consult when deciding the request; these were read in by the server when it started up. The last argument &es
is the set of entities the engine will consider when consulting the policies. These are data objects that represent TinyTodo’s User
s, Team
s, and List
s, to which the policies may refer. The Cedar authorizer returns a decision: If Decision::Allow
then the TinyTodo command can proceed; if Decision::Deny
then the server returns that access is denied. The request and its outcome are logged by the calls to info!(…)
.
Learn More
We are just getting started with TinyTodo, and we have only seen some of what the Cedar SDK can do. You can find a full tutorial in TUTORIAL.md
in the tinytodo
source code directory which explains (1) the full set of TinyTodo Cedar policies; (2) information about TinyTodo’s Cedar data model, i.e., how TinyTodo stores information about users, teams, lists and tasks as Cedar entities; (3) how we specify the expected data model and structure of TinyTodo access requests as a Cedar schema, and use the Cedar SDK’s validator to ensure that policies conform to the schema; and (4) challenge problems for extending TinyTodo to be even more full featured.
Cedar and Open Source
Cedar is the authorization policy language used by customers of the Amazon Verified Permissions and AWS Verified Access managed services. With the release of the Cedar SDK on GitHub, we provide transparency into Cedar’s development, invite community contributions, and hope to build trust in Cedar’s security.
All of Cedar’s code is available at https://github.com/cedar-policy/. Check out the roadmap and issues list on the site to see where it is going and how you could contribute. We welcome submissions of issues and feature requests via GitHub issues. We built the core Cedar SDK components (for example, the authorizer) using a new process called verification-guided development in order to provide extra assurance that they are safe and secure. To contribute to these components, you can submit a “request for comments” and engage with the core team to get your change approved.
To learn more, feel free to submit questions, comments, and suggestions via the public Cedar Slack workspace, https://cedar-policy.slack.com. You can also complete the online Cedar tutorial and play with it via the language playground at https://www.cedarpolicy.com/.