Comparison to Other Systems

Often the easiest way to understand a new language is by comparing it to languages you already know. Here we show how policies from several existing policy systems can be implemented with the Open Policy Agent.

Role-based access control (RBAC)

Role-based access control (RBAC) is pervasive today for authorization. To use RBAC for authorization, you write down two different kinds of information.

  • Which users have which roles
  • Which roles have which permissions

Once you provide RBAC with both those assignments, RBAC tells you how to make an authorization decision. A user is authorized for all those permissions assigned to any of the roles she is assigned to.

For example, we might have the following user/role assignments:

UserRole
aliceengineering
alicewebdev
bobhr

And the following role/permission assignments:

RolePermissionResource
engineeringreadserver123
webdevwriteserver123
webdevreadserver123
hrwritedatabase456

In this example, RBAC makes the following authorization decisions:

UserOperationResourceDecision
alicereadserver123allow because alice is in engineering
alicewriteserver123allow because alice is in webdev
bobreaddatabase456allow because bob is in hr
bobreadserver123deny because bob is not in engineering or webdev

With OPA, you can write the following snippets to implement the example RBAC policy shown above.

  1. package rbac.authz
  2. # user-role assignments
  3. user_roles := {
  4. "alice": ["engineering", "webdev"],
  5. "bob": ["hr"]
  6. }
  7. # role-permissions assignments
  8. role_permissions := {
  9. "engineering": [{"action": "read", "object": "server123"}],
  10. "webdev": [{"action": "read", "object": "server123"},
  11. {"action": "write", "object": "server123"}],
  12. "hr": [{"action": "read", "object": "database456"}]
  13. }
  14. # logic that implements RBAC.
  15. default allow = false
  16. allow {
  17. # lookup the list of roles for the user
  18. roles := user_roles[input.user]
  19. # for each role in that list
  20. r := roles[_]
  21. # lookup the permissions list for role r
  22. permissions := role_permissions[r]
  23. # for each permission
  24. p := permissions[_]
  25. # check if the permission granted to r matches the user's request
  26. p == {"action": input.action, "object": input.object}
  27. }

Comparison to Other Systems - 图1

  1. allow

As you can see, querying the allow rule with the following input

Comparison to Other Systems - 图2

  1. {
  2. "user": "bob",
  3. "action": "read",
  4. "object": "server123"
  5. }

Results in the response you’d expect.

Comparison to Other Systems - 图3

  1. false

RBAC Separation of duty (SOD)

Separation of duty (SOD) refers to the idea that there are certain combinations of permissions that no one should have at the same time. For example, no one should be able to both create payments and approve payments.

In RBAC, that means there are some pairs of roles that no one should be assigned simultaneously. For example, any user assigned both of the roles in each pair below would violate SOD.

  • create-payment and approve-payment
  • create-vendor and pay-vendor

OPA’s API does not yet let you enforce SOD by rejecting improper role-assignments, but it does let you express SOD constraints and ask for all SOD violations, as shown below. (Here we assume the statements below are added to the RBAC statements above.)

  1. # Pairs of roles that no user can be assigned to simultaneously
  2. sod_roles = [
  3. ["create-payment", "approve-payment"],
  4. ["create-vendor", "pay-vendor"],
  5. ]
  6. # Find all users violating SOD
  7. sod_violation[user] {
  8. some user
  9. # grab one role for a user
  10. role1 := user_roles[user][_]
  11. # grab another role for that same user
  12. role2 := user_roles[user][_]
  13. # check if those roles are forbidden by SOD
  14. sod_roles[_] == [role1, role2]
  15. }

(For those familiar with SOD, this is the static version since SOD violations happen whenever a user is assigned two conflicting roles. The dynamic version of SOD allows a single user to be assigned two conflicting roles but requires that the same user not utilize those roles on the same transaction, which is out of scope for this document.)

Attribute-based access control (ABAC)

With attribute-based access control, you make policy decisions using the attributes of the users, objects, and actions involved in the request. It has three main components:

  • Attributes for users
  • Attributes for objects
  • Logic dictating which attribute combinations are authorized

For example, we might know the following attributes for our users

  • alice
    • joined the company 15 years ago
    • is a trader
  • bob
    • joined the company 5 years ago
    • is an analyst

We would also have attributes for the objects, in this case stock ticker symbols.

  • MSFT
    • is sold on NASDAQ
    • sells at $59.20 per share
  • AMZN
    • is sold on NASDAQ
    • sells at $813.64 per share

An example ABAC policy in english might be:

  • Traders may purchase NASDAQ stocks for under $2M
  • Traders with 10+ years experience may purchase NASDAQ stocks for under $5M

OPA supports ABAC policies as shown below.

  1. package abac
  2. # User attributes
  3. user_attributes = {
  4. "alice": {"tenure": 15, "title": "trader"},
  5. "bob": {"tenure": 5, "title": "analyst"}
  6. }
  7. # Stock attributes
  8. ticker_attributes = {
  9. "MSFT": {"exchange": "NASDAQ", "price": 59.20},
  10. "AMZN": {"exchange": "NASDAQ", "price": 813.64}
  11. }
  12. default allow = false
  13. # all traders may buy NASDAQ under $2M
  14. allow {
  15. # lookup the user's attributes
  16. user := user_attributes[input.user]
  17. # check that the user is a trader
  18. user.title == "trader"
  19. # check that the stock being purchased is sold on the NASDAQ
  20. ticker_attributes[input.ticker].exchange == "NASDAQ"
  21. # check that the purchase amount is under $2M
  22. input.amount <= 2000000
  23. }
  24. # traders with 10+ years experience may buy NASDAQ under $5M
  25. allow {
  26. # lookup the user's attributes
  27. user := user_attributes[input.user]
  28. # check that the user is a trader
  29. user.title == "trader"
  30. # check that the stock being purchased is sold on the NASDAQ
  31. ticker_attributes[input.ticker].exchange == "NASDAQ"
  32. # check that the user has at least 10 years of experience
  33. user.tenure > 10
  34. # check that the purchase amount is under $5M
  35. input.amount <= 5000000
  36. }

Comparison to Other Systems - 图4

  1. allow

Comparison to Other Systems - 图5

  1. {
  2. "user": "alice",
  3. "ticker": "MSFT",
  4. "action": "buy",
  5. "amount": 1000000
  6. }

Querying the allow rule with the input above returns the following answer:

Comparison to Other Systems - 图6

  1. true

In OPA, there’s nothing special about users and objects. You can attach attributes to anything. And the attributes can themselves be structured JSON objects and have attributes on attributes on attributes, etc. Because OPA was designed to work with arbitrarily nested JSON data, it supports incredibly rich ABAC policies.

Amazon Web Services IAM

Amazon Web Services (AWS) lets you create policies that can be attached to users, roles, groups, and selected resources. You write allow and deny statements to enforce which users/roles can/can’t execute which API calls on which resources under certain conditions. By default all API access requests are implicitly denied (i.e., not allowed). Policy statements can explicitly allow or deny API requests. If a request is both allowed and denied, it is always denied. Let’s assume that the following customer managed policy is defined in AWS:

  1. {
  2. "Version": "2012-10-17",
  3. "Statement": [
  4. {
  5. "Sid": "FirstStatement",
  6. "Effect": "Allow",
  7. "Action": ["iam:ChangePassword"],
  8. "Resource": "*"
  9. },
  10. {
  11. "Sid": "SecondStatement",
  12. "Effect": "Allow",
  13. "Action": "s3:ListAllMyBuckets",
  14. "Resource": "*"
  15. },
  16. {
  17. "Sid": "ThirdStatement",
  18. "Effect": "Allow",
  19. "Action": [
  20. "s3:List*",
  21. "s3:Get*"
  22. ],
  23. "Resource": [
  24. "arn:aws:s3:::confidential-data",
  25. "arn:aws:s3:::confidential-data/*"
  26. ]
  27. }
  28. ]
  29. }

And the above policy is attached to principal alice in AWS using attach-user-policy API. In OPA, you write each of the AWS allow statements as a separate statement, and you expect the input to have principal, action, and resource fields.

  1. package aws
  2. default allow = false
  3. # FirstStatement
  4. allow {
  5. principals_match
  6. input.action == "iam:ChangePassword"
  7. }
  8. # SecondStatement
  9. allow {
  10. principals_match
  11. input.action == "s3:ListAllMyBuckets"
  12. }
  13. # ThirdStatement
  14. # Use helpers to handle implicit OR in the AWS policy.
  15. # Below all of the 'principals_match', 'actions_match' and 'resources_match' must be true.
  16. allow {
  17. principals_match
  18. actions_match
  19. resources_match
  20. }
  21. # principals_match is true if input.principal matches
  22. principals_match {
  23. input.principal == "alice"
  24. }
  25. # actions_match is true if input.action matches one in the list
  26. actions_match {
  27. # iterate over the actions in the list
  28. actions := ["s3:List.*","s3:Get.*"]
  29. action := actions[_]
  30. # check if input.action matches an action
  31. regex.globs_match(input.action, action)
  32. }
  33. # resources_match is true if input.resource matches one in the list
  34. resources_match {
  35. # iterate over the resources in the list
  36. resources := ["arn:aws:s3:::confidential-data","arn:aws:s3:::confidential-data/.*"]
  37. resource := resources[_]
  38. # check if input.resource matches a resource
  39. regex.globs_match(input.resource, resource)
  40. }

Comparison to Other Systems - 图7

  1. {
  2. "principal": "alice",
  3. "action": "ec2:StartInstance",
  4. "resource": "arn:aws:ec2:::instance/i78999879"
  5. }

Querying allow with the input above returns the following answer:

Comparison to Other Systems - 图8

  1. allow

Comparison to Other Systems - 图9

  1. false

XACML

eXtensible Access Control Markup Language (XACML) was designed to express security policies: allow/deny decisions using attributes of users, resources, actions, and the environment. The following policy says that users from the organization Curtiss or Packard who are US or GreatBritain nationals and who work on DetailedDesign or Simulation are permitted access to documents about NavigationSystems.

  1. <Policy PolicyId="urn:curtiss:ba:taa:taa-1.1" RuleCombiningAlgId="urn:oasis:names:tc:xacml:1.0:rule-combining-algorithm:deny-overrides">
  2. <Description>Policy for Business Authorization category TAA-1.1</Description>
  3. <Target>
  4. <AnyOf>
  5. <AllOf>
  6. <Match
  7. MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
  8. <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">NavigationSystem</AttributeValue>
  9. <AttributeDesignator
  10. MustBePresent="true"
  11. Category="urn:oasis:names:tc:xacml:3.0:attribute-category:resource"
  12. AttributeId="urn:curtiss:names:tc:xacml:1.0:resource:Topics"
  13. DataType="http://www.w3.org/2001/XMLSchema#string"/>
  14. </Match>
  15. </AllOf>
  16. </AnyOf>
  17. </Target>
  18. <Rule Effect="Permit">
  19. <Description />
  20. <Target>
  21. <Actions>
  22. <Action>
  23. <ActionMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal">
  24. <ActionAttributeDesignator
  25. AttributeId="urn:oasis:names:tc:xacml:1.0:action:action-id"
  26. DataType="http://www.w3.org/2001/XMLSchema#string" />
  27. <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">Any</AttributeValue>
  28. </ActionMatch>
  29. </Action>
  30. </Actions>
  31. </Target>
  32. <Condition FunctionId="urn:oasis:names:tc:xacml:1.0:function:and">
  33. <Apply xsi:type="AtLeastMemberOf" functionId="urn:oasis:names:tc:xacml:1.0:function:string-at-least-one-member-of">
  34. <Apply functionId="urn:oasis:names:tc:xacml:1.0:function:string-bag">
  35. <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">Curtiss</AttributeValue>
  36. <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">Packard</AttributeValue>
  37. </Apply>
  38. <AttributeDesignator AttributeId="http://schemas.tscp.org/2012-03/claims/OrganizationID" DataType="http://www.w3.org/2001/XMLSchema#string" />
  39. </Apply>
  40. <Apply xsi:type="AtLeastMemberOf" functionId="urn:oasis:names:tc:xacml:1.0:function:string-at-least-one-member-of">
  41. <Apply functionId="urn:oasis:names:tc:xacml:1.0:function:string-bag">
  42. <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">US</AttributeValue>
  43. <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">GB</AttributeValue>
  44. </Apply>
  45. <AttributeDesignator AttributeId="http://schemas.tscp.org/2012-03/claims/Nationality" DataType="http://www.w3.org/2001/XMLSchema#string" />
  46. </Apply>
  47. <Apply xsi:type="AtLeastMemberOf" functionId="urn:oasis:names:tc:xacml:1.0:function:string-at-least-one-member-of">
  48. <Apply functionId="urn:oasis:names:tc:xacml:1.0:function:string-bag">
  49. <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">DetailedDesign</AttributeValue>
  50. <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">Simulation</AttributeValue>
  51. </Apply>
  52. <AttributeDesignator AttributeId="http://schemas.tscp.org/2012-03/claims/Work-Effort" DataType="http://www.w3.org/2001/XMLSchema#string" />
  53. </Apply>
  54. <Apply xsi:type="AndFunction" functionId="urn:oasis:names:tc:xacml:1.0:function:and" />
  55. </Condition>
  56. </Rule>
  57. </Policy>

The same statement is shown below in OPA. Here the inputs are assumed to be roughly the same as for XACML: attributes of users, actions, and resources.

  1. package xacml
  2. permit {
  3. # Check that resource has a "NavigationSystem" entry
  4. input.resource["NavigationSystem"]
  5. # Check that organization is one of the options (underscore implements "any")
  6. org_options := ["Packard", "Curtiss"]
  7. input.user.organization == org_options[_]
  8. # Check that nationality is one of the options (underscore implements "any")
  9. nationality_options := ["GB", "US"]
  10. input.user.nationality == nationality_options[_]
  11. # Check that work_effort is one of the options (underscore implements "any")
  12. work_options := ["DetailedDesign", "Simulation"]
  13. input.user.work_effort == work_options[_]
  14. }

Comparison to Other Systems - 图10

  1. {
  2. "user": {
  3. "name": "alice",
  4. "organization": "Packard",
  5. "nationality": "GB",
  6. "work_effort": "DetailedDesign"
  7. },
  8. "resource": {
  9. "NavigationSystem": true
  10. },
  11. "action": {
  12. "name": "read"
  13. }
  14. }

Querying permit with the input above returns the following answer:

Comparison to Other Systems - 图11

  1. permit

Comparison to Other Systems - 图12

  1. true