Composing OPA solutions
Sep 29th, 2021
Vlad Iovanov
Open Policy Agent |
Engineering
Building awesome apps with OPA just got easier
At Aserto, we use OPA heavily in our authorization API as the underlying decision engine. We’ve been looking for a simple way to compose our solution over the OPA engine, and the OPA SDK provided us with a good starting point.
We had some additional requirements, though: we wanted to
- progressively unlock the engine’s capabilities
- make these capabilities discoverable
- extend the engine without sacrificing the ability to update the underlying OPA version
Bottom line, we needed a higher-level abstraction that would let us perform common tasks: build, test, run OPA policies, combined with the ability to configure them for a wide variety of use-cases in a discoverable, typed fashion.
We decided to use the options pattern as it provides a good balance between readability, discoverability of options, and the ability to grow your implementation as required.
This post will show how you can leverage the resulting “helper” package to build your OPA-based solutions, which we call runtime
.
Installing
go get -u github.com/aserto-dev/runtime
Usage
Creating a new runtime is straightforward. OPA has many configuration options that can tweak the behavior of your application, so it's best to read up on that. All these configuration options are available in the runtime.Config
struct. As you'll see later, this runtime can do many things - build policy bundles, run plugins and execute queries.
// Create a runtime
r, cleanup, err := runtime.NewRuntime(
ctx,
&logger,
&runtime.Config{},
)
if err != nil {
return errors.Wrap(err, "failed to create runtime")
}
defer cleanup()
Simple build
Let's start with the simplest example - building a policy bundle.
[Error handling is omitted for brevity]
You need to provide a context, and a logger instance (we're opinionated here - we like zerolog a lot). Next, you call NewRuntime
and get back your runtime instance, a cleanup function, and an error. Easy peasy. You can use the runtime instance to call Build
and you're done!
package main
import (
"context"
"github.com/aserto-dev/runtime"
"github.com/rs/zerolog"
"os"
)
func main() {
logger := zerolog.New(os.Stdout).
With().Timestamp().Logger().Level(zerolog.ErrorLevel)
ctx := context.Background()
// Create a runtime
r, cleanup, _ := runtime.NewRuntime(
ctx,
&logger,
&runtime.Config{},
)
defer cleanup()
// Use the runtime to build a bundle from the current directory
r.Build(runtime.BuildParams{
OutputFile: "my-bundle.tar.gz",
}, []string{"."})
}
And here's a gist: example1.go
Run with a built-in
Ok, so that was super simple - what if I want to use the OPA engine to interpret queries and return the decisions?
[Error handling is omitted for brevity]
The code looks very similar. You first create your runtime instance. In this example, we're executing a query that references a custom function (or built-in).
Once the runtime struct is created, we start the plugin manager and wait for initialization to finish, so our bundles are loaded by OPA, and the runtime is ready to execute a query.
And that's it! You've built a tiny app that can load rego bundles and answer queries.
package main
import (
"context"
"fmt"
"github.com/aserto-dev/runtime"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/types"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"os"
"time"
)
func main() {
logger := zerolog.New(os.Stdout).
With().Timestamp().Logger().Level(zerolog.ErrorLevel)
ctx := context.Background()
// Create a runtime
r, cleanup, _ := runtime.NewRuntime(
ctx,
&logger,
&runtime.Config{
LocalBundles: runtime.LocalBundlesConfig{
Paths: []string{"./my-bundle.tar.gz"},
},
},
runtime.WithBuiltin1(®o.Function{
Name: "hello",
Memoize: false,
Decl: types.NewFunction(types.Args(types.S), types.S),
}, func(ctx rego.BuiltinContext, name *ast.Term) (*ast.Term, error) {
strName := ""
err := ast.As(name.Value, &strName)
if err != nil {
return nil, errors.Wrap(err, "name parameter is not a string")
}
if strName == "there" {
return ast.StringTerm("general kenobi"), nil
}
return nil, nil
}),
)
defer cleanup()
r.PluginsManager.Start(ctx)
r.WaitForPlugins(ctx, time.Second*5)
result, _ := r.Query(ctx, `x = hello("there")`,
nil, true, false, false, "",
)
fmt.Printf("%+v\n", result.Result)
}
Here's the gist: example-2.go
Conclusion
At Aserto we use the “runtime” all the time. OPA is an amazing project, and we believe there's value in making it accessible to developers that also want to build cool things on top.
You can start small and simple, and grow your implementation as needed.
Cheers from Aserto!
Vlad Iovanov
Founding Engineer
Related Content
Introducing the Open Policy Registry (OPCR) project
A Docker-inspired workflow for OPA policies, now available at openpolicyregistry.io!
Oct 12th, 2021
Handling data in OPA policies
Passing data into the decision engine is a critical design choice for a robust authorization system. Here are four common patterns, each with their own tradeoffs.
Oct 27th, 2021
Modern authorization requires defense in depth
Zero-trust architectures encourage defense in depth. Fine-grained authorization solutions are emerging that complement coarse-grained ones.
Dec 11th, 2021