Skip to content

Commit

Permalink
OPA: authorization based on request body
Browse files Browse the repository at this point in the history
Signed-off-by: Magnus Jungsbluth <[email protected]>
  • Loading branch information
mjungsbluth committed Jan 19, 2024
1 parent 4a0606a commit b6041d4
Show file tree
Hide file tree
Showing 15 changed files with 703 additions and 55 deletions.
26 changes: 16 additions & 10 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,13 @@ type Config struct {
LuaModules *listFlag `yaml:"lua-modules"`
LuaSources *listFlag `yaml:"lua-sources"`

EnableOpenPolicyAgent bool `yaml:"enable-open-policy-agent"`
OpenPolicyAgentConfigTemplate string `yaml:"open-policy-agent-config-template"`
OpenPolicyAgentEnvoyMetadata string `yaml:"open-policy-agent-envoy-metadata"`
OpenPolicyAgentCleanerInterval time.Duration `yaml:"open-policy-agent-cleaner-interval"`
OpenPolicyAgentStartupTimeout time.Duration `yaml:"open-policy-agent-startup-timeout"`
EnableOpenPolicyAgent bool `yaml:"enable-open-policy-agent"`
OpenPolicyAgentConfigTemplate string `yaml:"open-policy-agent-config-template"`
OpenPolicyAgentEnvoyMetadata string `yaml:"open-policy-agent-envoy-metadata"`
OpenPolicyAgentCleanerInterval time.Duration `yaml:"open-policy-agent-cleaner-interval"`
OpenPolicyAgentStartupTimeout time.Duration `yaml:"open-policy-agent-startup-timeout"`
OpenPolicyAgentMaxRequestBodySize int64 `yaml:"open-policy-agent-max-request-body-size"`
OpenPolicyAgentMaxMemoryBodyParsing int64 `yaml:"open-policy-agent-max-memory-body-parsing"`
}

const (
Expand Down Expand Up @@ -501,6 +503,8 @@ func NewConfig() *Config {
flag.StringVar(&cfg.OpenPolicyAgentEnvoyMetadata, "open-policy-agent-envoy-metadata", "", "JSON file containing meta-data passed as input for compatibility with Envoy policies in the format")
flag.DurationVar(&cfg.OpenPolicyAgentCleanerInterval, "open-policy-agent-cleaner-interval", openpolicyagent.DefaultCleanerInterval, "Duration in seconds to wait before cleaning up unused opa instances")
flag.DurationVar(&cfg.OpenPolicyAgentStartupTimeout, "open-policy-agent-startup-timeout", openpolicyagent.DefaultOpaStartupTimeout, "Maximum duration in seconds to wait for the open policy agent to start up")
flag.Int64Var(&cfg.OpenPolicyAgentMaxRequestBodySize, "open-policy-agent-max-request-body-size", openpolicyagent.DefaultMaxRequestBodySize, "Maximum number of bytes from a http request body that are passed as input to the policy")
flag.Int64Var(&cfg.OpenPolicyAgentMaxMemoryBodyParsing, "open-policy-agent-max-memory-body-parsing", openpolicyagent.DefaultMaxMemoryBodyParsing, "Total number of bytes used to parse http request bodies across all requests. Once the limit is met, requests will be rejected.")

// TLS client certs
flag.StringVar(&cfg.ClientKeyFile, "client-tls-key", "", "TLS Key file for backend connections, multiple keys may be given comma separated - the order must match the certs")
Expand Down Expand Up @@ -901,11 +905,13 @@ func (c *Config) ToOptions() skipper.Options {
LuaModules: c.LuaModules.values,
LuaSources: c.LuaSources.values,

EnableOpenPolicyAgent: c.EnableOpenPolicyAgent,
OpenPolicyAgentConfigTemplate: c.OpenPolicyAgentConfigTemplate,
OpenPolicyAgentEnvoyMetadata: c.OpenPolicyAgentEnvoyMetadata,
OpenPolicyAgentCleanerInterval: c.OpenPolicyAgentCleanerInterval,
OpenPolicyAgentStartupTimeout: c.OpenPolicyAgentStartupTimeout,
EnableOpenPolicyAgent: c.EnableOpenPolicyAgent,
OpenPolicyAgentConfigTemplate: c.OpenPolicyAgentConfigTemplate,
OpenPolicyAgentEnvoyMetadata: c.OpenPolicyAgentEnvoyMetadata,
OpenPolicyAgentCleanerInterval: c.OpenPolicyAgentCleanerInterval,
OpenPolicyAgentStartupTimeout: c.OpenPolicyAgentStartupTimeout,
OpenPolicyAgentMaxRequestBodySize: c.OpenPolicyAgentMaxRequestBodySize,
OpenPolicyAgentMaxMemoryBodyParsing: c.OpenPolicyAgentMaxMemoryBodyParsing,
}
for _, rcci := range c.CloneRoute {
eskipClone := eskip.NewClone(rcci.Reg, rcci.Repl)
Expand Down
5 changes: 4 additions & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

log "github.com/sirupsen/logrus"
"github.com/zalando/skipper/filters/openpolicyagent"
"github.com/zalando/skipper/net"
"github.com/zalando/skipper/proxy"
"gopkg.in/yaml.v2"
Expand Down Expand Up @@ -160,8 +161,10 @@ func defaultConfig() *Config {
ValidateQueryLog: true,
LuaModules: commaListFlag(),
LuaSources: commaListFlag(),
OpenPolicyAgentCleanerInterval: 10 * time.Second,
OpenPolicyAgentCleanerInterval: openpolicyagent.DefaultCleanIdlePeriod,
OpenPolicyAgentStartupTimeout: 30 * time.Second,
OpenPolicyAgentMaxRequestBodySize: openpolicyagent.DefaultMaxRequestBodySize,
OpenPolicyAgentMaxMemoryBodyParsing: openpolicyagent.DefaultMaxMemoryBodyParsing,
}
}

Expand Down
28 changes: 28 additions & 0 deletions docs/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -1844,6 +1844,20 @@ Headers both to the upstream and the downstream service can be manipulated the s

This allows both to add and remove unwanted headers in allow/deny cases.

#### opaAuthorizeRequestWithBody

Requests can also be authorized based on the request body the same way that is supported with the [Open Policy Agent Envoy plugin](https://www.openpolicyagent.org/docs/latest/envoy-primer/#example-input), look for the input attribute `parsed_body` in the upstream documentation.

This filter has the same parameters that the `opaAuthorizeRequest` filter has.

A request's body is parsed up to a maximum size with a default of 1MB that can be configured via the `-open-policy-agent-max-request-body-size` command line argument. To avoid OOM errors due to too many concurrent authorized body requests, another flag `-open-policy-agent-max-memory-body-parsing` controls how much memory can be used across all requests with a default of 100MB. If in-flight requests that use body authorization exceed that limit, incoming requests that use the body will be rejected with an internal server error. The number of concurrent requests is

$$ n_{max-memory-body-parsing} \over min(avg(n_{request-content-length}), n_{max-request-body-size}) $$

so if requests on average have 100KB and the maximum memory is set to 100MB, on average 1024 authorized requests can be processed concurrently.

The filter also honors the `skip-request-body-parse` of the corresponding [configuration](https://www.openpolicyagent.org/docs/latest/envoy-introduction/#configuration) that the OPA plugin uses.

#### opaServeResponse

Always serves the response even if the policy allows the request and can customize the response completely. Can be used to re-implement legacy authorization services by already using data in Open Policy Agent but implementing an old REST API. This can also be useful to support Single Page Applications to return the calling users' permissions.
Expand Down Expand Up @@ -1887,6 +1901,20 @@ For this filter, the data flow looks like this independent of an allow/deny deci
```

#### opaServeResponseWithReqBody

If you want to serve requests directly from an Open Policy Agent policy that uses the request body, this can be done by using the `input.parsed_body` attribute the same way that is supported with the [Open Policy Agent Envoy plugin](https://www.openpolicyagent.org/docs/latest/envoy-primer/#example-input).

This filter has the same parameters that the `opaServeResponse` filter has.

A request's body is parsed up to a maximum size with a default of 1MB that can be configured via the `-open-policy-agent-max-request-body-size` command line argument. To avoid OOM errors due to too many concurrent authorized body requests, another flag `-open-policy-agent-max-memory-body-parsing` controls how much memory can be used across all requests with a default of 100MB. If in-flight requests that use body authorization exceed that limit, incoming requests that use the body will be rejected with an internal server error. The number of concurrent requests is

$$ n_{max-memory-body-parsing} \over min(avg(n_{request-content-length}), n_{max-request-body-size}) $$

so if requests on average have 100KB and the maximum memory is set to 100MB, on average 1024 authorized requests can be processed concurrently.

The filter also honors the `skip-request-body-parse` of the corresponding [configuration](https://www.openpolicyagent.org/docs/latest/envoy-introduction/#configuration) that the OPA plugin uses.

## Cookie Handling
### dropRequestCookie

Expand Down
2 changes: 2 additions & 0 deletions filters/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,9 @@ const (
ConsistentHashKeyName = "consistentHashKey"
ConsistentHashBalanceFactorName = "consistentHashBalanceFactor"
OpaAuthorizeRequestName = "opaAuthorizeRequest"
OpaAuthorizeRequestWithBodyName = "opaAuthorizeRequestWithBody"
OpaServeResponseName = "opaServeResponse"
OpaServeResponseWithReqBodyName = "opaServeResponseWithReqBody"

// Undocumented filters
HealthCheckName = "healthcheck"
Expand Down
2 changes: 1 addition & 1 deletion filters/openpolicyagent/evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (opa *OpenPolicyAgentInstance) Eval(ctx context.Context, req *ext_authz_v3.
}

logger := opa.manager.Logger().WithFields(map[string]interface{}{"decision-id": result.DecisionID})
input, err = envoyauth.RequestToInput(req, logger, nil, true)
input, err = envoyauth.RequestToInput(req, logger, nil, opa.EnvoyPluginConfig().SkipRequestBodyParse)
if err != nil {
return nil, fmt.Errorf("failed to convert request to input: %w", err)
}
Expand Down
6 changes: 4 additions & 2 deletions filters/openpolicyagent/internal/envoy/envoyplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ func (p *Plugin) Reconfigure(ctx context.Context, config interface{}) {

// PluginConfig represents the plugin configuration.
type PluginConfig struct {
Path string `json:"path"`
DryRun bool `json:"dry-run"`
Path string `json:"path"`
DryRun bool `json:"dry-run"`
SkipRequestBodyParse bool `json:"skip-request-body-parse"`

ParsedQuery ast.Body
}

Expand Down
3 changes: 2 additions & 1 deletion filters/openpolicyagent/internal/envoy/skipperadapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
)

func AdaptToExtAuthRequest(req *http.Request, metadata *ext_authz_v3_core.Metadata, contextExtensions map[string]string) *ext_authz_v3.CheckRequest {
func AdaptToExtAuthRequest(req *http.Request, metadata *ext_authz_v3_core.Metadata, contextExtensions map[string]string, rawBody []byte) *ext_authz_v3.CheckRequest {

headers := make(map[string]string, len(req.Header))
for h, vv := range req.Header {
Expand All @@ -25,6 +25,7 @@ func AdaptToExtAuthRequest(req *http.Request, metadata *ext_authz_v3_core.Metada
Method: req.Method,
Path: req.URL.Path,
Headers: headers,
RawBody: rawBody,
},
},
ContextExtensions: contextExtensions,
Expand Down
48 changes: 43 additions & 5 deletions filters/openpolicyagent/opaauthorizerequest/opaauthorizerequest.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package opaauthorizerequest

import (
ext_authz_v3_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
"encoding/json"
"errors"
"io"
"net/http"
"time"

ext_authz_v3_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"

"github.com/zalando/skipper/filters"
"gopkg.in/yaml.v2"

Expand All @@ -15,19 +19,31 @@ import (
const responseHeadersKey = "open-policy-agent:decision-response-headers"

type spec struct {
registry *openpolicyagent.OpenPolicyAgentRegistry
opts []func(*openpolicyagent.OpenPolicyAgentInstanceConfig) error
registry *openpolicyagent.OpenPolicyAgentRegistry
opts []func(*openpolicyagent.OpenPolicyAgentInstanceConfig) error
name string
bodyParsing bool
}

func NewOpaAuthorizeRequestSpec(registry *openpolicyagent.OpenPolicyAgentRegistry, opts ...func(*openpolicyagent.OpenPolicyAgentInstanceConfig) error) filters.Spec {
return &spec{
registry: registry,
opts: opts,
name: filters.OpaAuthorizeRequestName,
}
}

func NewOpaAuthorizeRequestWithBodySpec(registry *openpolicyagent.OpenPolicyAgentRegistry, opts ...func(*openpolicyagent.OpenPolicyAgentInstanceConfig) error) filters.Spec {
return &spec{
registry: registry,
opts: opts,
name: filters.OpaAuthorizeRequestWithBodyName,
bodyParsing: true,
}
}

func (s *spec) Name() string {
return filters.OpaAuthorizeRequestName
return s.name
}

func (s *spec) CreateFilter(args []interface{}) (filters.Filter, error) {
Expand Down Expand Up @@ -75,26 +91,48 @@ func (s *spec) CreateFilter(args []interface{}) (filters.Filter, error) {
opa: opa,
registry: s.registry,
envoyContextExtensions: envoyContextExtensions,
bodyParsing: s.bodyParsing,
}, nil
}

type opaAuthorizeRequestFilter struct {
opa *openpolicyagent.OpenPolicyAgentInstance
registry *openpolicyagent.OpenPolicyAgentRegistry
envoyContextExtensions map[string]string
bodyParsing bool
}

func (f *opaAuthorizeRequestFilter) Request(fc filters.FilterContext) {
req := fc.Request()
span, ctx := f.opa.StartSpanFromFilterContext(fc)
defer span.Finish()

authzreq := envoy.AdaptToExtAuthRequest(req, f.opa.InstanceConfig().GetEnvoyMetadata(), f.envoyContextExtensions)
var rawBodyBytes []byte
if f.bodyParsing {
var body io.ReadCloser
var err error
var finalizer func()
body, rawBodyBytes, finalizer, err = f.opa.ExtractHttpBodyOptionally(req)
defer finalizer()
if err != nil {
f.opa.HandleInvalidDecisionError(fc, span, nil, err, !f.opa.EnvoyPluginConfig().DryRun)
return
}
req.Body = body
}

authzreq := envoy.AdaptToExtAuthRequest(req, f.opa.InstanceConfig().GetEnvoyMetadata(), f.envoyContextExtensions, rawBodyBytes)

start := time.Now()
result, err := f.opa.Eval(ctx, authzreq)
fc.Metrics().MeasureSince(f.opa.MetricsKey("eval_time"), start)

var jsonErr *json.SyntaxError
if errors.As(err, &jsonErr) {
f.opa.HandleEvaluationError(fc, span, result, err, !f.opa.EnvoyPluginConfig().DryRun, http.StatusBadRequest)
return
}

if err != nil {
f.opa.HandleInvalidDecisionError(fc, span, result, err, !f.opa.EnvoyPluginConfig().DryRun)
return
Expand Down
Loading

0 comments on commit b6041d4

Please sign in to comment.