Files
releases/server/authorizer/authorizer_test.go
T
Torin Sandall 2d425494aa Refactor discovery implementation
These changes refactor the discovery implementation a bit to improve
test coverage and remove duplication of common logic shared with the
bundle plugin.

Specifically, the downloading logic has been moved into a separate
package that is shared by bundle and discovery. Second, test coverage in
the discovery implementation is increased from ~15% to ~85%.

These changes also include a few functional improvements:

- The default decision paths can be updated dynamically
- The decision logger can be enabled dynamically
- Discovery downloading errors are reported in status updates
- Discovery bundle is evaluated with all runtime params
- Custom plugins can be created dynamically
- Status updates include both discovery and bundle status

Signed-off-by: Torin Sandall <torinsandall@gmail.com>
2018-12-08 00:45:36 +01:00

268 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(func() ast.Ref {
return 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)
}
}