Files
releases/server/authorizer/authorizer_test.go
T
Torin Sandall b95227688b Add default decision configuration
Previously, OPA would serve POST requests with an empty URL path by
querying data.system.main and returning the generated value. In some
cases, it's useful to be able to reconfigure OPA to use a different
document to serve these kinds of requests. The same goes for the OPA
authorization policy.

These changes make the default decision and default authorization
decision paths configurable.

Signed-off-by: Torin Sandall <torinsandall@gmail.com>
2018-10-17 15:07:26 -07:00

266 lines
7.5 KiB
Go

// Copyright 2017 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package authorizer
import (
"encoding/json"
"net/http"
"reflect"
"strings"
"testing"
"net/http/httptest"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/server/identifier"
"github.com/open-policy-agent/opa/server/types"
"github.com/open-policy-agent/opa/storage/inmem"
"github.com/open-policy-agent/opa/util"
"github.com/open-policy-agent/opa/util/test"
)
type mockHandler struct {
}
func (h *mockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}
func TestBasic(t *testing.T) {
// Policy for testing access to policies.
compiler := func() *ast.Compiler {
module := `
package system.authz
import data.system.tokens
allow = allow_inner {
not input.path[0] = "undefined" # testing undefined
not divide_by_zero # testing eval errors
}
divide_by_zero {
input.path[0] = "divide_by_zero"
x = 1 / 0
}
default allow_inner = false
allow_inner {
valid_method
valid_path
}
valid_method {
rights[_].access[_] = access_map[input.method]
}
valid_path {
rights[_].path = "*"
}
valid_path {
rights[_].path = input.path
}
rights[right] {
role = tokens[input.identity].roles[_]
right = all_rights[role][_]
}
all_rights = {
"admin": [{
"path": "*",
"access": ["read", "write"],
}],
"service_read_only_path": [
{
"path": ["data", "some", "specific", "document"],
"access": ["read"],
},
],
"service_read_write_path": [
{
"path": ["data", "some", "other", "document"],
"access": ["read", "write"],
},
],
}
access_map = {
"GET": "read",
"HEAD": "read",
"PATCH": "write",
"POST": "write",
"PUT": "write",
"DELETE": "write",
}
`
c := ast.NewCompiler()
c.Compile(map[string]*ast.Module{
"test.rego": ast.MustParseModule(module),
})
if c.Failed() {
t.Fatalf("Unexpected error compiling test module: %v", c.Errors)
}
return c
}
// Data used for testing authorizer access to storage.
data := util.MustUnmarshalJSON([]byte(`
{
"system": {
"tokens": {
"token0": {
"roles": ["admin"]
},
"token1": {
"roles": ["service_read_only_path"]
},
"token2": {
"roles": ["service_read_write_path"]
}
}
}
}
`))
store := inmem.NewFromObject(data.(map[string]interface{}))
tests := []struct {
note string
identity string
method string
path string
expectedStatus int
expectedCode string
expectedMsg string
}{
{"root (ok)", "token0", http.MethodGet, "", http.StatusOK, "", ""},
{"index.html (ok)", "token0", http.MethodGet, "/index.html", http.StatusOK, "", ""},
{"undefined", "token0", http.MethodGet, "/undefined", http.StatusInternalServerError, types.CodeInternal, types.MsgUnauthorizedUndefinedError},
{"evaluation error", "token0", http.MethodGet, "/divide_by_zero", http.StatusInternalServerError, types.CodeInternal, types.MsgEvaluationError},
{"ok", "token1", http.MethodGet, "/data/some/specific/document", http.StatusOK, "", ""},
{"ok (w/ query params)", "token1", http.MethodGet, "/data/some/specific/document?pretty=true", http.StatusOK, "", ""},
{"unauthorized method", "token1", http.MethodPut, "/data/some/specific/document", http.StatusUnauthorized, types.CodeUnauthorized, types.MsgUnauthorizedError},
{"unauthorized path", "token2", http.MethodGet, "/data/some/doc/not/allowed", http.StatusUnauthorized, types.CodeUnauthorized, types.MsgUnauthorizedError},
{"unauthorized path (w/ query params)", "token2", http.MethodGet, "/data/some/doc/not/allowed?pretty=true", http.StatusUnauthorized, types.CodeUnauthorized, types.MsgUnauthorizedError},
}
for _, tc := range tests {
test.Subtest(t, tc.note, func(t *testing.T) {
recorder := httptest.NewRecorder()
req, err := http.NewRequest(tc.method, "http://localhost:8181"+tc.path, nil)
if err != nil {
t.Fatalf("Unexpected error creating request for %v: %v", tc, err)
}
if len(tc.identity) > 0 {
req = identifier.SetIdentity(req, tc.identity)
}
NewBasic(&mockHandler{}, compiler, store, Decision(ast.MustParseRef("data.system.authz.allow"))).ServeHTTP(recorder, req)
if recorder.Code != tc.expectedStatus {
t.Fatalf("Expected status code %v but got: %v", tc.expectedStatus, recorder)
}
// Check code/message if response should be error.
if tc.expectedStatus != http.StatusOK {
var x interface{}
if err := util.NewJSONDecoder(recorder.Body).Decode(&x); err != nil {
t.Fatalf("Expected JSON response but got: %v", recorder)
}
response := ast.MustInterfaceToValue(x)
code, err := response.Find(ast.RefTerm(ast.StringTerm("code")).Value.(ast.Ref))
if err != nil {
t.Fatalf("Missing code in response: %v", recorder)
} else if code.Compare(ast.String(tc.expectedCode)) != 0 {
t.Fatalf("Expected code %v but got: %v", tc.expectedCode, recorder)
}
msg, err := response.Find(ast.RefTerm(ast.StringTerm("message")).Value.(ast.Ref))
if err != nil {
t.Fatalf("Missing message in response: %v", recorder)
} else if !strings.Contains(msg.String(), tc.expectedMsg) {
t.Fatalf("Expected msg to contain %v but got: %v", tc.expectedMsg, response)
}
}
})
}
}
func TestBasicEscapeError(t *testing.T) {
recorder := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodGet, "http://localhost:8181", nil)
if err != nil {
panic(err)
}
req.URL.Path = `/invalid/path/foo%LALALA`
compiler := func() *ast.Compiler {
return ast.NewCompiler()
}
store := inmem.New()
NewBasic(&mockHandler{}, compiler, store).ServeHTTP(recorder, req)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("Expected bad request but got: %v", recorder)
}
var response types.ErrorV1
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("Expected error response but got: %v", recorder)
}
if response.Code != types.CodeInvalidParameter ||
!strings.Contains(response.Message, "invalid URL") {
t.Fatalf("Expected invalid parameter and URL parse error but got: %v", recorder)
}
}
func TestMakeInput(t *testing.T) {
path := "/foo/bar?pretty=true&explain=\"full\""
req, err := http.NewRequest(http.MethodGet, "http://localhost:8181"+path, nil)
if err != nil {
panic(err)
}
query := req.URL.Query()
// set query parameters
query.Set("pretty", "true")
query.Set("explain", "full")
req.URL.RawQuery = query.Encode()
req = identifier.SetIdentity(req, "bob")
result, err := makeInput(req)
if err != nil {
panic(err)
}
expectedResult := util.MustUnmarshalJSON([]byte(`
{
"path": ["foo","bar"],
"method": "GET",
"identity": "bob",
"params": {"explain": ["full"], "pretty": ["true"]}
}
`))
if !reflect.DeepEqual(util.MustMarshalJSON(expectedResult), util.MustMarshalJSON(result)) {
t.Fatalf("Expected %+v but got %+v", expectedResult, result)
}
}