Skip to content

Commit

Permalink
server: Add message body to authorization policy input
Browse files Browse the repository at this point in the history
This commit updates the server's basic authorizer to include the
deserialized message body in the input to the authorization policy so
that the latter can make decisions based on policy query input
documents. The authorizer caches the parsed message body on the
request context and the server retrieves the value to avoid parsing twice.

Signed-off-by: Torin Sandall <torinsandall@gmail.com>
  • Loading branch information
tsandall committed Dec 8, 2020
1 parent be8234f commit d40ff6f
Show file tree
Hide file tree
Showing 5 changed files with 334 additions and 6 deletions.
26 changes: 25 additions & 1 deletion docs/content/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,31 @@ policy:
# characters following a hyphen are uppercase. The rest are lowercase.
# If the header key contains space or invalid header field bytes,
# no conversion is performed.
"headers": {"...": [...]}
"headers": {"...": [...]},

# Request message body if present for applicable APIs.
#
# Example Request:
#
# POST v1/data HTTP/1.1
# Content-Type: application/json
#
# {"input": {"action": "trade", "stock": "ACME"}}
#
# Example input.body Value:
#
# {"input": {"action": "trade", "stock": "ACME"}}
#
# Example body check:
#
# input.body.input.stock == "ACME"
#
# The 'body' field is provided for the following APIs:
#
# * POST v1/data
# * POST v0/data
# * POST /
"body": ...,
}
```

Expand Down
103 changes: 99 additions & 4 deletions server/authorizer/authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
package authorizer

import (
"context"
"io/ioutil"
"net/http"
"net/url"
"strings"
Expand All @@ -16,6 +18,7 @@ import (
"github.com/open-policy-agent/opa/server/types"
"github.com/open-policy-agent/opa/server/writer"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/util"
)

// Basic provides policy-based authorization over incoming requests.
Expand Down Expand Up @@ -59,7 +62,9 @@ func NewBasic(inner http.Handler, compiler func() *ast.Compiler, store storage.S

func (h *Basic) ServeHTTP(w http.ResponseWriter, r *http.Request) {

input, err := makeInput(r)
// TODO(tsandall): Pass AST value as input instead of Go value to avoid unnecessary
// conversions.
r, input, err := makeInput(r)
if err != nil {
writer.ErrorString(w, http.StatusBadRequest, types.CodeInvalidParameter, err)
return
Expand Down Expand Up @@ -97,28 +102,92 @@ func (h *Basic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
writer.Error(w, http.StatusUnauthorized, types.NewErrorV1(types.CodeUnauthorized, types.MsgUnauthorizedError))
}

func makeInput(r *http.Request) (interface{}, error) {
func makeInput(r *http.Request) (*http.Request, interface{}, error) {

path, err := parsePath(r.URL.Path)
if err != nil {
return nil, err
return r, nil, err
}

method := strings.ToUpper(r.Method)
query := r.URL.Query()

var rawBody []byte

if expectBody(r.Method, path) {
rawBody, err = readBody(r)
if err != nil {
return r, nil, err
}
}

input := map[string]interface{}{
"path": path,
"method": method,
"params": query,
"headers": r.Header,
}

if len(rawBody) > 0 {
var body interface{}
if expectYAML(r) {
if err := util.Unmarshal(rawBody, &body); err != nil {
return r, nil, err
}
} else if err := util.UnmarshalJSON(rawBody, &body); err != nil {
return r, nil, err
}

// We cache the parsed body on the context so the server does not have
// to parse the input document twice.
input["body"] = body
ctx := SetBodyOnContext(r.Context(), body)
r = r.WithContext(ctx)
}

identity, ok := identifier.Identity(r)
if ok {
input["identity"] = identity
}

return input, nil
return r, input, nil
}

var dataAPIVersions = map[string]bool{
"v0": true,
"v1": true,
}

func expectBody(method string, path []interface{}) bool {
if method == http.MethodPost {
if len(path) == 1 {
s := path[0].(string)
return s == ""
} else if len(path) >= 2 {
s1 := path[0].(string)
s2 := path[1].(string)
return dataAPIVersions[s1] && s2 == "data"
}
}
return false
}

func expectYAML(r *http.Request) bool {
// NOTE(tsandall): This check comes from the server's HTTP handler code. The docs
// are a bit more strict, but the authorizer should be consistent w/ the original
// server handler implementation.
return strings.Contains(r.Header.Get("Content-Type"), "yaml")
}

func readBody(r *http.Request) ([]byte, error) {

bs, err := ioutil.ReadAll(r.Body)

if err != nil {
return nil, err
}

return bs, nil
}

func parsePath(path string) ([]interface{}, error) {
Expand All @@ -139,3 +208,29 @@ func parsePath(path string) ([]interface{}, error) {
}
return sl, nil
}

type authorizerCachedBody struct {
parsed interface{}
}

type authorizerCachedBodyKey string

const ctxkey authorizerCachedBodyKey = "authorizerCachedBodyKey"

// SetBodyOnContext adds the parsed input value to the context. This function is only
// exposed for test purposes.
func SetBodyOnContext(ctx context.Context, x interface{}) context.Context {
return context.WithValue(ctx, ctxkey, authorizerCachedBody{
parsed: x,
})
}

// GetBodyOnContext returns the parsed input from the request context if it exists.
// The authorizer saves the parsed input on the context when it runs.
func GetBodyOnContext(ctx context.Context) (interface{}, bool) {
input, ok := ctx.Value(ctxkey).(authorizerCachedBody)
if !ok {
return nil, false
}
return input.parsed, true
}
123 changes: 122 additions & 1 deletion server/authorizer/authorizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package authorizer

import (
"bytes"
"encoding/json"
"net/http"
"reflect"
Expand Down Expand Up @@ -251,7 +252,7 @@ func TestMakeInput(t *testing.T) {

req = identifier.SetIdentity(req, "bob")

result, err := makeInput(req)
req, result, err := makeInput(req)
if err != nil {
panic(err)
}
Expand All @@ -275,3 +276,123 @@ func TestMakeInput(t *testing.T) {
}

}

func TestMakeInputWithBody(t *testing.T) {

reqs := []struct {
method string
path string
headers map[string]string
body string
useYAML bool
assertBodyExists bool
assertBodyDoesNotExist bool
}{
{
method: "POST",
path: "/",
body: `{"foo": "bar"}`,
assertBodyExists: true,
},
{
method: "POST",
path: "/",
body: `foo: bar`,
useYAML: true,
assertBodyExists: true,
},
{
method: "POST",
path: "/v0/data",
body: `{"foo": "bar"}`,
assertBodyExists: true,
},
{
method: "POST",
path: "/v1/data",
body: `{"foo": "bar"}`,
assertBodyExists: true,
},
{
method: "PUT",
path: "/v1/data",
body: `{"foo": "bar"}`,
assertBodyDoesNotExist: true,
},
{
method: "PATCH",
path: "/v1/data",
body: `{"foo": "bar"}`,
assertBodyDoesNotExist: true,
},
{
method: "GET",
path: "/v1/data",
assertBodyDoesNotExist: true,
},
{
method: "PUT",
path: "/v1/policies/test",
body: "package test\np = 7",
assertBodyDoesNotExist: true,
},
}

for _, tc := range reqs {

t.Run(tc.method+"_"+tc.path, func(t *testing.T) {

req, err := http.NewRequest(tc.method, "http://localhost:8181"+tc.path, bytes.NewBufferString(tc.body))
if err != nil {
t.Fatal(err)
}

if tc.useYAML {
req.Header.Set("Content-Type", "application/x-yaml")
}

req, input, err := makeInput(req)
if err != nil {
t.Fatal(err)
}

if tc.assertBodyExists {

var want interface{}

if tc.useYAML {
if err := util.Unmarshal([]byte(tc.body), &want); err != nil {
t.Fatal(err)
}
} else {
want = util.MustUnmarshalJSON([]byte(tc.body))
}

body := input.(map[string]interface{})["body"]

if !reflect.DeepEqual(body, want) {
t.Fatalf("expected parsed bodies to be equal but got %v and want %v", body, want)
}

body, ok := GetBodyOnContext(req.Context())
if !ok || !reflect.DeepEqual(body, want) {
t.Fatalf("expected parsed body to be cached on context but got %v and want %v", body, want)
}
}

if tc.assertBodyDoesNotExist {
_, ok := input.(map[string]interface{})["body"]
if ok {
t.Fatal("expected no parsed body in input")
}
_, ok = GetBodyOnContext(req.Context())
if ok {
t.Fatal("expected no parsed body to be cached on context")
}
}

})

}

}
18 changes: 18 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2313,14 +2313,22 @@ func getExplain(p []string, zero types.ExplainModeV1) types.ExplainModeV1 {
}

func readInputV0(r *http.Request) (ast.Value, error) {

parsed, ok := authorizer.GetBodyOnContext(r.Context())
if ok {
return ast.InterfaceToValue(parsed)
}

bs, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}

bs = bytes.TrimSpace(bs)
if len(bs) == 0 {
return nil, nil
}

var x interface{}

if strings.Contains(r.Header.Get("Content-Type"), "yaml") {
Expand All @@ -2344,6 +2352,16 @@ func readInputGetV1(str string) (ast.Value, error) {

func readInputPostV1(r *http.Request) (ast.Value, error) {

parsed, ok := authorizer.GetBodyOnContext(r.Context())
if ok {
if obj, ok := parsed.(map[string]interface{}); ok {
if input, ok := obj["input"]; ok {
return ast.InterfaceToValue(input)
}
}
return nil, nil
}

bs, err := ioutil.ReadAll(r.Body)

if err != nil {
Expand Down

0 comments on commit d40ff6f

Please sign in to comment.