OPA is a full-featured policy engine that offloads policy decisions from your service. You can think of it as a concierge for your service who can answer detailed questions on behalf of your users to meet their specific needs.

OPA’s RESTful APIs use JSON over HTTP so you and your users can integrate OPA with any programming language. At a high level, integrating OPA into your service involves:

  • Deploying OPA alongside your service
  • Pushing relevant data about your service’s state into OPA’s document store
  • Offloading some or all decision-making to OPA by querying it

When your service is integrated with OPA, your users will be able author and deploy custom policies that control the behavior of your service’s policy-enabled features. Furthermore, users can publish data to OPA that is not available to your service about their own deployment context.

OPA’s query and decision model

In the future, both your service and its users will be able to register for, and react to, notifications triggered when OPA detects a policy-relevant change.

Unless you embed OPA as a Go library, you will deploy it alongside your service – either directly as an operating system daemon or inside a container. In this way, transactions will have low latency and availability will be determined through shared fate with your service.

When OPA starts for the first time, it will not contain any policies or data. Policies and data can be added, removed, and modified at any time. For example: by deployment automation software or your service as it is deployed, by your service during an upgrade, or by administrators as needed.

The primary unit of data in OPA is a document, which is similar to a JSON value. Documents typically correspond to single, self-contained objects and are capable of representing both primitive types (strings, numbers, booleans, and null) as well as structured types (objects, and arrays). Documents are created, read, updated, and deleted via OPA’s .

How Does OPA Work? - 图2

OPA data model dependencies

Base documents are published and updated using OPA’s Data API. For example, the following request publishes a list of servers to OPA:

  1. [
  2. {
  3. "op": "add",
  4. "path": "-",
  5. "value": {
  6. "id": "s1",
  7. "name": "app",
  8. "protocols": [
  9. "http",
  10. "https",
  11. "ssh"
  12. ],
  13. "ports": [
  14. "p1",
  15. "p2",
  16. "p3"
  17. ]
  18. }
  19. },
  20. {
  21. "op": "add",
  22. "path": "-",
  23. "value": {
  24. "id": "s2",
  25. "name": "db",
  26. "protocols": [
  27. "mysql"
  28. ],
  29. "ports": [
  30. "p3"
  31. ]
  32. }
  33. },
  34. {
  35. "op": "add",
  36. "path": "-",
  37. "value": {
  38. "id": "s3",
  39. "name": "cache",
  40. "protocols": [
  41. "memcache"
  42. ],
  43. "ports": [
  44. "p3"
  45. ]
  46. }
  47. },
  48. {
  49. "op": "add",
  50. "path": "-",
  51. "value": {
  52. "id": "s4",
  53. "name": "dev",
  54. "protocols": [
  55. "http",
  56. "https",
  57. "ssh"
  58. ],
  59. "ports": [
  60. "p1",
  61. "p2"
  62. ]
  63. }
  64. }
  65. ]

Policies are written using OPA’s purpose-built, declarative language Rego. Rego includes rich support for traversing nested documents and transforming data using syntax inspired by dictionary and array access in languages like Python and JSONPath. For detailed information about using Rego, see How Do I Write Policies?.

Each Rego file defines a policy module using a collection of rules that describe the expected state of your service. Both your service and its users can publish and update policy modules using OPA’s Policy API. For example, the following request creates a policy with two rules (violations and public_servers) named “exempli-gratia”:

  1. PUT https://example.com/v1/policies/exempli-gratia HTTP/1.1
  2. Content-Type: text/plain
  1. package opa.examples
  2. import data.servers
  3. import data.networks
  4. import data.ports
  5. violations[server] {
  6. server := servers[_]
  7. server.protocols[_] == "http"
  8. public_servers[server]
  9. }
  10. public_servers[server] {
  11. some i, j
  12. server := servers[_]
  13. server.ports[_] == ports[i].id
  14. ports[i].networks[_] == networks[j].id
  15. networks[j].public == true
  16. }

A policy file must contain a single package declaration, which defines the path to the policy module and its rules (for example, data.opa.examples.violations – see The data Document for more information about accessing nested documents). The policy name itself (in this case, “exempli-gratia”) is only used to identify policies for file management purposes; it is not used otherwise.

In contrast to base documents, virtual documents embody the results of evaluating the rules included in policy modules. Virtual documents are computed when users publish new policy modules, update existing modules, run queries, and when any relevant base document is published or updated. Rules allow policy authors to write questions with yes-no answers (that is, predicates) and to generate structured values from raw data found in base documents as well as from intermediate data found in other virtual documents.

All documents pushed into OPA or computed by rules are nested under a built-in root document named data.

OPA document structure

Example data document:

  1. {
  2. "servers": [...],
  3. "ports": [...],
  4. "networks": [...],
  5. "opa": {
  6. "examples": {
  7. "public_servers": [...]
  8. }
  9. }
  10. }

As a result, any document, base or virtual, can be accessed hierarchically starting from the root data node – either as an identifier:

  1. import data.servers # Base document
  2. import data.opa.examples.violations # Virtual document

or as a URI component in an HTTP request:

  1. GET https://example.com/v1/data/servers HTTP/1.1

In some cases, policies require input values. In addition to the built-in document, OPA also has a built-in input document. When you query OPA, you can set the value of the input document.

Example input document:

  1. {
  2. "method": "GET",
  3. "path": "/servers/s2",
  4. "user": "alice"
  5. }

The input document can be referenced just like the data document.

  1. # Let 'bob' perform read-only operations.
  2. allow {
  3. input.user == "bob"
  4. input.method == "GET"
  5. }
  6. # Let 'alice' perform any operation.
  7. allow {
  8. input.user == "alice"
  9. }

To supply the input document, query OPA using the POST method.

  1. POST /v1/data/opa/examples/allow HTTP/1.1
  2. Content-Type: application/json
  1. {
  2. "input": {
  3. "method": "GET",
  4. "path": "/servers/s2",
  5. "user": "alice"
  6. }
  7. }

Let’s take a look at some documents representing the state of a hypothetical service and a policy module that uses this data. The following documents describe a set of servers, the protocols they use, the ports they open, and the networks those ports are connected to.

Example data document:

  1. {
  2. "servers": [
  3. {
  4. "id": "s1",
  5. "name": "app",
  6. "protocols": [
  7. "https",
  8. "ssh"
  9. ],
  10. "ports": [
  11. "p1",
  12. "p2",
  13. "p3"
  14. ]
  15. },
  16. {
  17. "id": "s2",
  18. "name": "db",
  19. "protocols": [
  20. "mysql"
  21. ],
  22. "ports": [
  23. "p3"
  24. ]
  25. },
  26. {
  27. "id": "s3",
  28. "name": "cache",
  29. "protocols": [
  30. "memcache",
  31. "http"
  32. ],
  33. "ports": [
  34. "p3"
  35. ]
  36. },
  37. {
  38. "id": "s4",
  39. "name": "dev",
  40. "protocols": [
  41. "http"
  42. ],
  43. "ports": [
  44. "p1",
  45. "p2"
  46. ]
  47. }
  48. ],
  49. "networks": [
  50. {
  51. "id": "n1",
  52. "public": false
  53. },
  54. {
  55. "id": "n2",
  56. "public": false
  57. },
  58. {
  59. "id": "n3",
  60. "public": true
  61. }
  62. ],
  63. "ports": [
  64. {
  65. "id": "p1",
  66. "networks": [
  67. "n1"
  68. ]
  69. },
  70. {
  71. "id": "p2",
  72. "networks": [
  73. ]
  74. },
  75. {
  76. "networks": [
  77. "n2"
  78. ]
  79. }
  80. ]
  81. }

When the data is published, we can use OPA’s API to inspect base documents like servers:

  1. GET https://example.com/v1/data/servers HTTP/1.1

The response is an object that contains the array of servers:

Now let’s write a policy that enumerates servers that are connected to public networks and that are using HTTP. These servers are violating a business rule that states that all public servers must use HTTPS.

  1. # This policy module belongs to the opa.examples package.
  2. package opa.examples
  3. # Refer to data.servers as `servers`.
  4. import data.servers
  5. # Refer to the data.networks as `networks`.
  6. import data.networks
  7. # Refer to the data.ports as `ports`.
  8. import data.ports
  9. # A server exists in the violations set if...
  10. violations[server] {
  11. # ...the server exists
  12. server := servers[_]
  13. # ...and any of the server’s protocols is HTTP
  14. server.protocols[_] == "http"
  15. # ...and the server is public.
  16. public_servers[server]
  17. }
  18. # A server exists in the public_servers set if...
  19. public_servers[server] {
  20. some i, j
  21. # ...the server exists
  22. server := servers[_]
  23. # ...and the server is connected to a port
  24. server.ports[_] == ports[i].id
  25. # ...and the port is connected to a network
  26. ports[i].networks[_] == networks[j].id
  27. # ...and the network is public.
  28. networks[j].public == true
  29. }

Note that:

  • Rules consist of assertions about data stored in OPA. In this case, the assertions test for equality with, and membership of, values in the servers, networks, and ports documents.
  • Expressions can reference elements in a collection using the [_] and [<variable>] syntax. OPA knows to evaluate such queries by iterating over each element in the corresponding collection.
  • Assertions about elements in a collection are true if any of the elements match the expression, and are only false when none of the elements match. For example, ports[i].networks[_] == networks[j].id will be true whenever any element in ports[i].networks matches the id of any element in networks.
  • Expressions can reference nested documents. For example, ports[i].networks[_] refers to each network ID listed in each port document.
  • Expressions can reference virtual documents. For example, public_servers[server] == true matches only if server is in the list produced by the public_servers rule.

After publishing this policy module, the data document will include additional documents corresponding to the module’s package declaration (opa.examples) and the virtual documents its rules generate.

  1. GET https://example.com/v1/data HTTP/1.1
  1. HTTP/1.1 200 OK
  2. Content-Type: application/json
  1. {
  2. "result": {
  3. "servers": [...],
  4. "networks": [...],
  5. "ports": [...],
  6. "opa": {
  7. "examples": {
  8. "violations": [
  9. {
  10. "id": "s4",
  11. "name": "dev",
  12. "protocols": [
  13. "http"
  14. ],
  15. "ports": [
  16. "p1",
  17. "p2"
  18. ]
  19. }
  20. ],
  21. "public_servers": [
  22. {
  23. "id": "s1",
  24. "name": "app",
  25. "protocols": [
  26. "https",
  27. "ssh"
  28. ],
  29. "ports": [
  30. "p1",
  31. "p2",
  32. "p3"
  33. ]
  34. },
  35. {
  36. "id": "s4",
  37. "name": "dev",
  38. "protocols": [
  39. "http"
  40. ],
  41. "ports": [
  42. "p1",
  43. "p2"
  44. ]
  45. }
  46. ]
  47. }
  48. }
  49. }
  50. }
  1. GET https://example.com/v1/data/opa/examples/violations HTTP/1.1

…the response is the subset of the servers base document that use HTTP and are connected to a public network: