Skip to content

Commit

Permalink
Provide OPA SDK integration option
Browse files Browse the repository at this point in the history
This PR introduces an alternative way to provide policy to the
OPA middleware for Dapr, where rather than providing a single
Rego file inlined in the manifest for the middleware, one may
choose to instead provide an OPA configuration file, and use
that to start an instance of the full OPA service, as provided
by the OPA [Go SDK](https://www.openpolicyagent.org/docs/latest/integration/#integrating-with-the-go-sdk).

This allows decoupling policy management from enforcement, as
the user is then allowed to point the OPA middleware at the
same remote bundle endpoints where they'd normally fetch their
policy and data. Fetching
[policy from bundles](https://www.openpolicyagent.org/docs/latest/management-bundles/)
also allows the user to have any number of policy files included
in the decision rather than a single one.

The drawback is obviously that the service needs to run as
a continuous process. Allowing the user to choose which model
works best for them seems like a good option to me.

This is not a complete PR, but before I start fleshing out
on things like documentation, I wanted to bounce this off of
the Dapr maintainers for some feedback.

Would it be preferable to have the OPA configuration inlined
in the config for the middleware? The current approach is to
simply point to a file, but that obviously requires something
like a configmap to have been mounted into the container. The
drawback of inlining the config would be that it'll be "config
in config" i.e. one YAML or JSON document embedded into another.

Some feedback much appreciated before I either proceed here, or
drop this. Thanks!

Signed-off-by: Anders Eknert <[email protected]>
  • Loading branch information
anderseknert committed Oct 20, 2023
1 parent 84cc54b commit 482eb80
Show file tree
Hide file tree
Showing 2 changed files with 338 additions and 11 deletions.
79 changes: 68 additions & 11 deletions middleware/http/opa/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import (
"math"
"net/http"
"net/textproto"
"os"
"reflect"
"strconv"
"strings"
"time"

"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/sdk"
"k8s.io/utils/strings/slices"

"github.com/dapr/components-contrib/internal/httputils"
Expand All @@ -41,11 +43,12 @@ import (
type Status int

type middlewareMetadata struct {
Rego string `json:"rego" mapstructure:"rego"`
DefaultStatus Status `json:"defaultStatus,omitempty" mapstructure:"defaultStatus"`
IncludedHeaders string `json:"includedHeaders,omitempty" mapstructure:"includedHeaders"`
ReadBody string `json:"readBody,omitempty" mapstructure:"readBody"`
internalIncludedHeadersParsed []string `json:"-" mapstructure:"-"`
OPAConfigFile string `json:"opaConfigFile,omitempty" mapstructure:"opaConfigFile" json:"opa_config_file,omitempty"`
Rego string `json:"rego" mapstructure:"rego" json:"rego,omitempty"`
DefaultStatus Status `json:"defaultStatus,omitempty" mapstructure:"defaultStatus" json:"default_status,omitempty"`
IncludedHeaders string `json:"includedHeaders,omitempty" mapstructure:"includedHeaders" json:"included_headers,omitempty"`
ReadBody string `json:"readBody,omitempty" mapstructure:"readBody" json:"read_body,omitempty"`
internalIncludedHeadersParsed []string `json:"-" mapstructure:"-" json:"internal_included_headers_parsed,omitempty"`
}

// NewMiddleware returns a new Open Policy Agent middleware.
Expand All @@ -56,6 +59,7 @@ func NewMiddleware(logger logger.Logger) middleware.Middleware {
// Middleware is an OPA middleware.
type Middleware struct {
logger logger.Logger
opa *sdk.OPA
}

// RegoResult is the expected result from rego policy.
Expand Down Expand Up @@ -102,18 +106,68 @@ func (s *Status) UnmarshalJSON(b []byte) error {
return nil
}

// Check status is in the correct range for RFC 2616 status codes [100-599].
// Valid checks that status is in the correct range for RFC 2616 status codes [100-599].
func (s *Status) Valid() bool {
return s != nil && *s >= 100 && *s < 600
}

func (m *Middleware) startOPA(ctx context.Context, configFile string) error {
opaConf, err := os.ReadFile(configFile)
if err != nil {
return err
}

m.opa, err = sdk.New(ctx, sdk.Options{
ID: "opa-dapr",
Config: bytes.NewReader(opaConf),
})
if err != nil {
return err
}

return nil
}

func (m *Middleware) stopOPA(ctx context.Context) {
if m.opa != nil {
m.opa.Stop(ctx)
}
}

// GetHandler returns the HTTP handler provided by the middleware.
func (m *Middleware) GetHandler(parentCtx context.Context, metadata middleware.Metadata) (func(next http.Handler) http.Handler, error) {
meta, err := m.getNativeMetadata(metadata)
if err != nil {
return nil, err
}

if meta.OPAConfigFile != "" {
err = m.startOPA(parentCtx, meta.OPAConfigFile)
if err != nil {
return nil, err
}

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
input := buildInputMap(r, meta)
result, err := m.opa.Decision(
parentCtx,
sdk.DecisionOptions{Path: "/http/allow", Input: input})

if err != nil {
m.opaError(w, meta, err)
return
}

if !m.handleRegoResult(w, r, meta, result.Result) {
return
}

next.ServeHTTP(w, r)
})
}, nil
}

ctx, cancel := context.WithTimeout(parentCtx, time.Minute)
query, err := rego.New(
rego.Query("result = data.http.allow"),
Expand All @@ -134,7 +188,7 @@ func (m *Middleware) GetHandler(parentCtx context.Context, metadata middleware.M
}, nil
}

func (m *Middleware) evalRequest(w http.ResponseWriter, r *http.Request, meta *middlewareMetadata, query *rego.PreparedEvalQuery) bool {
func buildInputMap(r *http.Request, meta *middlewareMetadata) map[string]any {
headers := map[string]string{}

for key, value := range r.Header {
Expand All @@ -153,8 +207,9 @@ func (m *Middleware) evalRequest(w http.ResponseWriter, r *http.Request, meta *m
}

pathParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
input := map[string]interface{}{
"request": map[string]interface{}{

return map[string]any{
"request": map[string]any{
"method": r.Method,
"path": r.URL.Path,
"path_parts": pathParts,
Expand All @@ -165,8 +220,10 @@ func (m *Middleware) evalRequest(w http.ResponseWriter, r *http.Request, meta *m
"body": body,
},
}
}

results, err := query.Eval(r.Context(), rego.EvalInput(input))
func (m *Middleware) evalRequest(w http.ResponseWriter, r *http.Request, meta *middlewareMetadata, query *rego.PreparedEvalQuery) bool {
results, err := query.Eval(r.Context(), rego.EvalInput(buildInputMap(r, meta)))
if err != nil {
m.opaError(w, meta, err)
return false
Expand Down Expand Up @@ -234,7 +291,7 @@ func (m *Middleware) handleRegoResult(w http.ResponseWriter, r *http.Request, me
func (m *Middleware) opaError(w http.ResponseWriter, meta *middlewareMetadata, err error) {
w.Header().Set(opaErrorHeaderKey, "true")
httputils.RespondWithError(w, int(meta.DefaultStatus))
m.logger.Warnf("Error procesing rego policy: %v", err)
m.logger.Warnf("Error processing rego policy: %v", err)
}

func (m *Middleware) getNativeMetadata(metadata middleware.Metadata) (*middlewareMetadata, error) {
Expand Down
Loading

0 comments on commit 482eb80

Please sign in to comment.