How Cedar authorization works
Each time a user of your application wants to perform an action on a protected resource, the application needs to invoke the Cedar authorization engine (or authorizer, for short) to check if this request is allowed. The authorizer considers the request against the application’s store of policies in order to make a decision, Allow or Deny. This topic discusses how the Cedar authorizer decides the answer to a particular request.
Request creation
A Cedar authorization request asks the question “Can this principal take this action on this resource in this context?”. More formally, an authorization request has four parts, abbreviated PARC:
- P is the principal,
- A is the action,
- R is the resource, and
- C is the request context.
P, A, and R are entity references, while C is a record.
Conceptually, you should imagine that the authorizer is able to consider all of your application’s policies and entity data while evaluating a request. As a practical matter, making all policies and entity data available might be too difficult or too expensive. In that case, your application needs to determine which policies and entity data are relevant to properly handling the request. One way to determine what entity data you need is policy level validation and level based slicing.
Request authorization
Given an authorization request, Cedar’s authorizer returns Allow if the request is granted or Deny if it is rejected, along with some diagnostics. How does it make this decision?
Algorithm
First, the authorizer evaluates each of the policies to determine if the policy satisfies the request. More details about evaluation follow, but in summary, know that the evaluator can return:
true, when the policy satisfies the request;false, when the policy does not satisfy the request; orerror, when there is an error when evaluating the policy on the request data.
After evaluating each policy, the authorizer combines the results to make an authorization decision. It makes its decision by applying the following rules:
-
If any
forbidpolicy evaluates totrue, then the final result isDeny. -
Else, if any
permitpolicy evaluates totrue, then the final result isAllow. -
Otherwise (i.e., no policy is satisfied), the final result is
Deny.
The authorizer returns an authorization response, which includes its decision along with some diagnostics. These diagnostics include the determining policies and any error conditions. If the decision is Allow, the determining policies are the permit policies that satisfy the request (rule 2). Otherwise, the determining policies are the forbid policies, if any, that satisfy the request (rule 1). If the decision is Deny because no policies were satisfied (rule 3), then the list of determining policies is empty. Whatever the final result, if the evaluation of any policies resulted in error, then the IDs of the erroneous policies are included in the diagnostics, too, along with the particulars of the errors.
Discussion
Cedar’s authorization algorithm has three useful properties:
- default deny: no request is authorized (decision
Allow) unless there is a specificpermitpolicy that grants it; by default, the decision isDeny. - forbid overrides permit: even if a
permitpolicy is satisfied, any satisfiedforbidpolicy overrides it, producing aDenydecision. - skip on error: if a policy’s evaluation returns
error, the policy does not factor into the authorization response; it is skipped.
Why was Cedar’s authorization algorithm designed to satisfy these properties? The first two properties make Cedar policies easier to understand. Since permit policies are the only way access is granted, readers just have to understand what each policy says, not what it doesn’t. Because forbid policies always deny access, readers can understand them independently of any permit policies created now or in the future; forbid policies effectively define permission “guardrails” that permit policies cannot cross.
The reasoning for the skip-on-error property is more involved. An alternative authorization algorithm we considered would be to Deny a request when any policy evaluation exhibits an error. While this might sound good at first, deny-on-error raises concerns of safety. An application that was working fine with 100 policies might suddenly start denying all requests if the 101st policy has an error. Skip-on-error avoids this dramatic failure mode, and is more flexible: applications can always choose to look at the authorization response’s diagnostics and take a different decision if an evaluated policy produces errors. For more information, see this blog post written by one of the Cedar designers.
Policy evaluation
As just discussed, to reach its decision the Cedar authorizer’s algorithm evaluates a request PARC against each policy it is given. Evaluation returns whether or not the policy is satisfied by the request (true/false), or whether an error occurred during evaluation (error). How does evaluation work?
Expression evaluation
The key component of policy evaluation is expression evaluation. Each constraint in the policy scope is an expression. Each when clause also contains an expression, as does each unless clause. Evaluating a policy requires evaluating its constituent expressions. Example expressions include resource.tags.contains("private"), action == Action::"viewPhoto", principal in Team::"admin", and resource in principal.account.
As with a typical programming language, evaluating an expression simplifies, or “executes”, the expression until no further simplification is possible. The final result is either a Cedar value – like true, 1, User::"Alice", or "blue" – or it is an error. Evaluating an expression with no variables is straightforward. The expression 2+2 evaluates to 4. Expression Action::"viewPhoto" == Action::"viewPhoto" evaluates to true. Expression if false then "blue" else "green" evaluates to "green". See here for complete descriptions of the various operators you can use in Cedar expressions.
What about expressions that have variables principal, action, resource, and context in them? To evaluate such expressions the Cedar authorizer first binds any variables that appear in the expressions to values of the appropriate type. Then the authorizer evaluates the expressions with those values in place of the variables.
For example, consider the expression action == Action::"viewPhoto". If the authorizer binds the action variable to the entity Action::"viewPhoto", then the result is true. That’s because replacing action with Action::"viewPhoto" gives expression Action::"viewPhoto" == Action::"viewPhoto" which is obviously true.
As another example, consider the expression resource.tags.contains("Private"). If the authorizer binds the resource variable to the entity Photo::"vacation94.jpg" we get Photo::"vacation94.jpg".tags.contains("Private"). Evaluating further, the authorizer must look up Photo::"vacation94.jpg" in the provided entities data, and then extract its tags attribute. If that attribute contains a set with the string "Private" in it, the result is true; if it’s a set without "Private" the result is false. Otherwise tags is either not a valid attribute or contains a non-set, and Cedar generates an error.
Policy satisfaction
Determining whether a policy satisfies a request is a straightforward use of expression evaluation. To explain it, let’s introduce some notation. For a policy y:
- Principal(y) is the constraint involving the
principalin y’s policy scope. If there is no constraint onprincipal, then Principal(y) istrue. - Action(y) is the constraint involving
actionin y’s policy scope. If there is no constraint onaction, then Action(y) istrue. - Resource(y) is the constraint involving
resourcein y’s policy scope. If there is no constraint onresource, then Resource(y) istrue. - Conds(y) is the list of
whenandunlessexpressions in y.
Here’s how the Cedar authorizer evaluates a policy y with respect to a PARC request. First, the authorizer tests whether y matches the request, as follows:
- Bind
principalto P in expression Principal(y) and evaluate it - Bind
actionto A in expression Action(y) and evaluate it - Bind
resourceto R in expression Resource(y) and evaluate it
If all three steps evaluate to true, then y matches the request. Otherwise it does not. (Cedar’s design ensures that none of these three steps can possibly evaluate to error.)
If y matches the request, the authorizer evaluates the request’s conditions Conds(y) in order. The authorizer binds the principal, action, resource, and context variables to the PARC values when we do so. If all of the when conditions evaluate to true, and all of the unless conditions evaluate to false, then policy y satisfies the request, and the final evaluation result is true. If evaluating any condition expression yields error then policy evaluation halts at that point (any remaining conditions are skipped), and error is returned as the final result. Otherwise, false is returned.
Detailed Example
To illustrate policy evaluation, consider whether a set of four policies authorizes the following request: “Can the user jane perform the action viewPhoto on the photo vacation.jpg?” Precisely, the request is:
- P =
User::"jane" - A =
Action::"viewPhoto" - R =
Photo::"vacation.jpg" - C =
{}(the empty record)
Assume that the entities data includes the following details:
- Entity
User::"jane"is a member ofGroup::"kevinsFriends" - Entity
Photo::"vacation.jpg"has the following attributes:.ownerisUser::"kevin".tagsis["Private","Work"](i.e., a set containing the strings"Private"and"Work")
The Cedar authorizer evaluates each of the four policies against this request.
-
P1 – Jane can perform any action on photo
vacation.jpg.permit ( principal == User::"jane", action, resource == Photo::"vacation.jpg" );This policy is satisfied.
- Principal(P1) is
principal == User::"jane", so after bindingprincipaltoUser::"jane"(the P in the request), the expression evaluates totrue. - Action(P1) is simply
truesince there is no action constraint. - Resource(P1) is
resource == Photo::"vacation.jpg", so after bindingresourcetoPhoto::"vacation.jpg"(the R in the request), the expression evaluates totrue. - Cond(P1) is empty, so evaluates trivially to
true.
- Principal(P1) is
-
P2 – A member of group
kevinFriendscan view any of Kevin’s photos when they are taggedHolidaypermit ( principal in UserGroup::"kevinFriends", action == Action::"viewPhoto", resource ) when { resource.tags.contains("Holiday") };This policy is not satisfied. While it matches the request, its condition evaluates to
false.- Principal(P2) is
principal in UserGroup::"kevinFriends", so after bindingprincipaltoUser::"jane"(the P in the request), the expression evaluates totruebecauseUser::"jane"is a member ofGroup::"kevinsFriends" - Action(P2) is
action == Action::"viewPhoto", so after bindingactiontoAction::"viewPhoto"the expression evaluates totrue - Resource(P2) is simply
truesince there is no resource constraint - Cond(P2) is the list containing
whenexpressionresource.tags.contains("Holiday"). After bindingresourcetoPhoto::"vacation.jpg"(the R in the request), the expression evaluates tofalsebecause the.tagsattribute ofPhoto::"vacation.jpg"is["Private","Work"], i.e., it does not contain"Holiday".
- Principal(P2) is
-
P3 – Users are forbidden from viewing any photos tagged
Privateunless they are the owner of the photo.forbid ( principal, action == Action::"viewPhoto", resource ) when { resource.tags.contains("Private") } unless { principal == resource.owner };This policy is satisfied.
- The policy matches the request:
principalandresourceare unconstrained, and Action(c) evaluates totruebecause A isAction::"viewPhoto"; - The policy’s
whencondition istruebecause the.tagsattribute ofPhoto::"vacation.jpg"contains"Private"; and - The policy’s
unlesscondition isfalsebecause the.ownerattribute ofPhoto::"vacation.jpg"(which isUser::"kevin") is not equal to P (which isUser::"jane").
- The policy matches the request:
-
P4 – Users can perform
updateTagson a resource, like aPhotoorAlbum, when they are the owner of the resourcepermit ( principal, action == Action::"updateTags", resource ) when { principal == resource.owner };This policy is not satisfied.
- The policy fails to match the request because while
principalandresourceare unconstrained, Action(P4) evaluates tofalsebecause bindingactionto A yields expressionAction::"viewPhoto" == Action::"updateTags".
- The policy fails to match the request because while
In sum:
permitpolicy P1 evaluates totruepermitpolicy P2 evaluates tofalseforbidpolicy P3 evaluates totruepermitpolicy P4 evaluates tofalse
Combining these policy evaluation results, the Cedar authorizer returns a decision of Deny, where the determining policy is P3. This result follows from rule 1 of our authorization logic: “If any forbid policy evaluates to true, then the final result is Deny” (and the determining policies are the satisfied forbid policies).