Read this section if you want to extend OPA with custom built-in functions.

OPA supports built-in functions for simple operations like string manipulation and arithmetic as well as more complex operations like JWT verification and executing HTTP requests. If you need to to extend OPA with custom built-in functions for use cases or integrations that are not supported out-of-the-box you can supply the function definitions when you prepare queries.

Using custom built-in functions involves providing a declaration and implementation. The declaration tells OPA the function’s type signature and the implementation provides the callback that OPA can execute during query evaluation.

To get started you need to import three packages:

The ast and types packages contain the types for declarations and runtime objects passed to your implementation. Here is a trivial example that shows the process:

  1. r := rego.New(
  2. rego.Query(`x = hello("bob")`),
  3. rego.Function1(
  4. &rego.Function{
  5. Name: "hello",
  6. Decl: types.NewFunction(types.Args(types.S), types.S),
  7. },
  8. func(_ rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
  9. if str, ok := a.Value.(ast.String); ok {
  10. return ast.StringTerm("hello, " + string(str)), nil
  11. }
  12. return nil, nil
  13. }),
  14. ))
  15. query, err := r.PrepareForEval(ctx)
  16. if err != nil {
  17. // handle error.
  18. }

At this point you can execute the query:

  1. rs, err := query.Eval(ctx)
  2. if err != nil {
  3. // handle error.
  4. }
  5. // Do something with result.
  6. fmt.Println(rs[0].Bindings["x"])

If you executed this code you the output would be:

  1. "hello, bob"

The example above highlights a few important points.

  • The rego package includes variants of rego.Function1 for accepting different numbers of operands (e.g., rego.Function2, rego.Function3, etc.)
  • The rego.Function#Name struct field specifies the operator that queries can refer to.
  • The rego.Function#Decl struct field specifies the function’s type signature. In the example above the function accepts a string and returns a string.
  • The function indicates it’s undefined by returning nil for the first return argument.

Let’s look at another example. Imagine you want to expose GitHub repository metadata to your policies. One option is to implement a custom built-in function to fetch the data for specific repositories on-the-fly.

  1. r := rego.New(
  2. rego.Query(`github.repo("open-policy-agent", "opa")`),
  3. rego.Function2(
  4. &rego.Function{
  5. Name: "github.repo",
  6. Decl: types.NewFunction(types.Args(types.S, types.S), types.A),
  7. Memoize: true,
  8. },
  9. func(bctx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error) {
  10. // see implementation below.
  11. },
  12. ),
  13. )

The declaration also sets rego.Function#Memoize to true to enable memoization across multiple calls in the same query. If your built-in function performs I/O, you should enable memoization as it ensures function evaluation is deterministic.

The implementation wraps the Go standard library to perform HTTP requests to GitHub’s API:

The implementation is careful to use the context passed to the built-in function when executing the HTTP request. See the appendix at the end of this page for the complete example.

Read this section if you want to customize or extend the OPA runtime/executable with custom behaviour.

OPA defines a plugin interface that allows you to customize certain behaviour like decision logging or add new behaviour like different query APIs. To implement a custom plugin you must implement two interfaces:

You can register your factory with OPA by calling github.com/open-policy-agent/opa/runtime#RegisterPlugin inside your main function.

The plugin may (optionally) report its current status to the plugin Manager via the plugins.Manager#UpdatePluginStatus API.

Typically the plugin should report StatusNotReady at creation time and update to StatusOK (or StatusErr) when appropriate.

  1. import (
  2. "encoding/json"
  3. "github.com/open-policy-agent/opa/plugins/logs"
  4. )
  5. const PluginName = "println_decision_logger"
  6. type Config struct {
  7. Stderr bool `json:"stderr"` // false => stdout, true => stderr
  8. }
  9. type PrintlnLogger struct {
  10. mtx sync.Mutex
  11. config Config
  12. }
  13. p.manager.UpdatePluginStatus(PluginName, &plugins.Status{State: plugins.StateOK})
  14. return nil
  15. }
  16. func (p *PrintlnLogger) Stop(ctx context.Context) {
  17. p.manager.UpdatePluginStatus(PluginName, &plugins.Status{State: plugins.StateNotReady})
  18. }
  19. func (p *PrintlnLogger) Reconfigure(ctx context.Context, config interface{}) {
  20. p.mtx.Lock()
  21. defer p.mtx.Unlock()
  22. p.config = config.(Config)
  23. }
  24. // Log is called by the decision logger when a record (event) should be emitted. The logs.EventV1 fields
  25. // map 1:1 to those described in https://www.openpolicyagent.org/docs/latest/management-decision-logs
  26. func (p *PrintlnLogger) Log(ctx context.Context, event logs.EventV1) error {
  27. p.mtx.Lock()
  28. defer p.mtx.Unlock()
  29. w := os.Stdout
  30. if p.config.Stderr {
  31. w = os.Stderr
  32. }
  33. bs, err := json.Marshal(event)
  34. if err != nil {
  35. p.manager.UpdatePluginStatus(PluginName, &plugins.Status{State: plugins.StateErr})
  36. return nil
  37. }
  38. _, err = fmt.Fprintln(w, string(bs))
  39. if err != nil {
  40. p.manager.UpdatePluginStatus(PluginName, &plugins.Status{State: plugins.StateErr})
  41. }
  42. return nil
  43. }

Next, implement a factory function that instantiates your plugin:

  1. import (
  2. "github.com/open-policy-agent/opa/plugins"
  3. "github.com/open-policy-agent/opa/util"
  4. )
  5. type Factory struct{}
  6. func (Factory) New(m *plugins.Manager, config interface{}) plugins.Plugin {
  7. m.UpdatePluginStatus(PluginName, &plugins.Status{State: plugins.StateNotReady})
  8. return &PrintlnLogger{
  9. manager: m,
  10. config: config.(Config),
  11. }
  12. }
  13. func (Factory) Validate(_ *plugins.Manager, config []byte) (interface{}, error) {
  14. parsedConfig := Config{}
  15. return parsedConfig, util.Unmarshal(config, &parsedConfig)
  16. }

Finally, register your factory with OPA and call cmd.RootCommand.Execute. The latter starts OPA and does not return.

  1. import (
  2. "github.com/open-policy-agent/opa/cmd"
  3. "github.com/open-policy-agent/opa/runtime"
  4. )
  5. func main() {
  6. runtime.RegisterPlugin(PluginName, Factory{})
  7. if err := cmd.RootCommand.Execute(); err != nil {
  8. fmt.Println(err)
  9. os.Exit(1)
  10. }
  11. }

At this point you can build an OPA executable including your plugin.

  1. go build -o opa++

Define an OPA configuration file that will use your plugin:

config.yaml:

Start OPA with the configuration file:

  1. ./opa++ run --server --config-file config.yaml

Exercise the plugin via the OPA API:

    If everything worked you will see the Go struct representation of the decision log event written to stdout.

    The source code for this example can be found .

    These values can be set on the command-line when building OPA from source:

    1. go build -o opa++ -ldflags "-X github.com/open-policy-agent/opa/version.Version=MY_VERSION" main.go
    1. package main
    2. import (
    3. "context"
    4. "encoding/json"
    5. "fmt"
    6. "log"
    7. "net/http"
    8. "github.com/open-policy-agent/opa/ast"
    9. "github.com/open-policy-agent/opa/rego"
    10. "github.com/open-policy-agent/opa/types"
    11. )
    12. func main() {
    13. r := rego.New(
    14. rego.Query(`github.repo("open-policy-agent", "opa")`),
    15. rego.Function2(
    16. &rego.Function{
    17. Name: "github.repo",
    18. Decl: types.NewFunction(types.Args(types.S, types.S), types.A),
    19. Memoize: true,
    20. },
    21. func(bctx rego.BuiltinContext, a, b *ast.Term) (*ast.Term, error) {
    22. var org, repo string
    23. if err := ast.As(a.Value, &org); err != nil {
    24. return nil, err
    25. } else if ast.As(b.Value, &repo); err != nil {
    26. return nil, err
    27. }
    28. req, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%v/%v", org, repo), nil)
    29. if err != nil {
    30. return nil, err
    31. }
    32. resp, err := http.DefaultClient.Do(req.WithContext(bctx.Context))
    33. if err != nil {
    34. return nil, err
    35. }
    36. defer resp.Body.Close()
    37. if resp.StatusCode != http.StatusOK {
    38. return nil, fmt.Errorf(resp.Status)
    39. }
    40. v, err := ast.ValueFromReader(resp.Body)
    41. if err != nil {
    42. return nil, err
    43. }
    44. return ast.NewTerm(v), nil
    45. },
    46. ),
    47. )
    48. rs, err := r.Eval(context.Background())
    49. if err != nil {
    50. log.Fatal(err)
    51. } else if len(rs) == 0 {
    52. fmt.Println("undefined")
    53. } else {
    54. bs, _ := json.MarshalIndent(rs[0].Expressions[0].Value, "", " ")
    55. fmt.Println(string(bs))
    56. }
    57. }