Skip to content
Snippets Groups Projects
Commit 29da1309 authored by Yordan Kinkov's avatar Yordan Kinkov
Browse files

Merge branch '6-add-HTTP-request-headers-to-evaluation-runtime' into 'main'

Make HTTP request headers accessible during policy execution

Closes #6

See merge request gaia-x/data-infrastructure-federation-services/tsa/policy!3
parents caf0a840 1f8ba970
No related branches found
No related tags found
No related merge requests found
Pipeline #54393 failed
......@@ -169,6 +169,11 @@ can be mapped and used for evaluating all kinds of different policies. Without a
package naming rule, there's no way the service can automatically generate HTTP
endpoints for working with arbitrary dynamically uploaded policies.
### Access HTTP Headers inside a policy
HTTP Request Headers are passed to the evaluation runtime on each request. One could access any header by name within
the Rego source code using `input.header.name` or `input.header["name"]`.
### Policy Extensions Functions
A brief documentation for the available Rego extensions functions
......
......@@ -29,6 +29,7 @@ import (
goapolicy "gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/gen/policy"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/clients/cache"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/config"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/header"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/regocache"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/regofunc"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/service"
......@@ -156,6 +157,9 @@ func main() {
openapiServer = goaopenapisrv.New(openapiEndpoints, mux, dec, enc, nil, errFormatter, nil, nil)
}
// Apply middlewares on the servers
policyServer.Evaluate = header.Middleware()(policyServer.Evaluate)
// Configure the mux.
goapolicysrv.Mount(mux, policyServer)
goahealthsrv.Mount(mux, healthServer)
......
package header
import (
"context"
"net/http"
)
type key string
const headerKey key = "header"
// Middleware is an HTTP server middleware that gets all HTTP headers
// and adds them to a request context value.
func Middleware() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := ToContext(r.Context(), r)
req := r.WithContext(ctx)
h.ServeHTTP(w, req)
})
}
}
func ToContext(ctx context.Context, r *http.Request) context.Context {
return context.WithValue(ctx, headerKey, r.Header)
}
func FromContext(ctx context.Context) (http.Header, bool) {
header, ok := ctx.Value(headerKey).(http.Header)
return header, ok
}
package header_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/header"
)
func TestMiddleware(t *testing.T) {
expected := http.Header{"Authorization": []string{"my-token"}}
req := httptest.NewRequest("POST", "/example", nil)
req.Header = expected
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
value, ok := header.FromContext(r.Context())
assert.True(t, ok)
assert.Equal(t, expected, value)
})
middleware := header.Middleware()
handlerToTest := middleware(nextHandler)
handlerToTest.ServeHTTP(httptest.NewRecorder(), req)
}
......@@ -12,6 +12,7 @@ import (
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/golib/errors"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/gen/policy"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/header"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/regofunc"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/storage"
)
......@@ -20,6 +21,8 @@ import (
//go:generate counterfeiter . Storage
//go:generate counterfeiter . RegoCache
const HeaderKey = "header"
type Cache interface {
Set(ctx context.Context, key, namespace, scope string, value []byte, ttl int) error
Get(ctx context.Context, key, namespace, scope string) ([]byte, error)
......@@ -80,7 +83,14 @@ func (s *Service) Evaluate(ctx context.Context, req *policy.EvaluateRequest) (*p
return nil, errors.New("error evaluating policy", err)
}
resultSet, err := query.Eval(ctx, rego.EvalInput(req.Input))
// add headers to the request input
input, err := s.addHeadersToEvaluateInput(ctx, req.Input)
if err != nil {
logger.Error("error adding headers to evaluate input", zap.Error(err))
return nil, errors.New("error adding headers to evaluate input", err)
}
resultSet, err := query.Eval(ctx, rego.EvalInput(input))
if err != nil {
logger.Error("error evaluating rego query", zap.Error(err))
return nil, errors.New("error evaluating rego query", err)
......@@ -267,3 +277,29 @@ func (s *Service) buildRegoArgs(filename, regoPolicy, regoQuery, regoData string
func (s *Service) queryCacheKey(group, policyName, version string) string {
return fmt.Sprintf("%s,%s,%s", group, policyName, version)
}
func (s *Service) addHeadersToEvaluateInput(ctx context.Context, in interface{}) (map[string]interface{}, error) {
// goa framework decodes the body of the request into a pointer to interface
// for this reason we cast it first to interface pointer and then to map, which is the expected value
i, ok := in.(*interface{})
if !ok {
return nil, errors.New("unexpected request body: unsuccessful casting to interface")
}
i2 := *i
if i2 == nil { // no request body
i2 = map[string]interface{}{}
}
input, ok := i2.(map[string]interface{})
if !ok {
return nil, errors.New("unexpected request body: unsuccessful casting to map")
}
header, ok := header.FromContext(ctx)
if !ok {
return nil, errors.New("error getting headers from context")
}
input[HeaderKey] = header
return input, nil
}
......@@ -2,6 +2,8 @@ package policy_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
......@@ -12,6 +14,7 @@ import (
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/golib/errors"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/golib/ptr"
goapolicy "gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/gen/policy"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/header"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/service/policy"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/service/policy/policyfakes"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/storage"
......@@ -33,6 +36,9 @@ func TestService_Evaluate(t *testing.T) {
// prepare test policy using static json data during evaluation
testPolicyWithStaticData := `package testgroup.example default allow = false allow { data.msg == "hello world" }`
// prepare test policy accessing headers during evaluation
testPolicyAccessingHeaders := `package testgroup.example token := input.header["Authorization"]`
// prepare test query that can be retrieved from rego queryCache
testQuery, err := rego.New(
rego.Module("example.rego", testPolicy),
......@@ -43,17 +49,48 @@ func TestService_Evaluate(t *testing.T) {
// prepare test request to be used in tests
testReq := func() *goapolicy.EvaluateRequest {
input := map[string]interface{}{"msg": "yes"}
var body interface{} = input
return &goapolicy.EvaluateRequest{
Group: "testgroup",
PolicyName: "example",
Version: "1.0",
Input: &body,
TTL: ptr.Int(30),
}
}
// prepare test request with empty body
testEmptyReq := func() *goapolicy.EvaluateRequest {
var body interface{} = nil
return &goapolicy.EvaluateRequest{
Group: "testgroup",
PolicyName: "example",
Version: "1.0",
Input: map[string]interface{}{"msg": "yes"},
Input: &body,
TTL: ptr.Int(30),
}
}
// prepare http.Request for tests
httpReq := func() *http.Request {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "my-token")
return req
}
// prepare context containing headers
ctxWithHeaders := func() context.Context {
ctx := header.ToContext(context.Background(), httpReq())
return ctx
}
tests := []struct {
// test input
name string
ctx context.Context
req *goapolicy.EvaluateRequest
storage policy.Storage
regocache policy.RegoCache
......@@ -65,6 +102,7 @@ func TestService_Evaluate(t *testing.T) {
}{
{
name: "prepared query is found in queryCache",
ctx: ctxWithHeaders(),
req: testReq(),
regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
......@@ -134,6 +172,7 @@ func TestService_Evaluate(t *testing.T) {
},
{
name: "policy is found in storage and isn't locked",
ctx: ctxWithHeaders(),
req: testReq(),
regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
......@@ -163,6 +202,7 @@ func TestService_Evaluate(t *testing.T) {
},
{
name: "policy is executed successfully, but storing the result in cache fails",
ctx: ctxWithHeaders(),
req: testReq(),
regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
......@@ -191,6 +231,7 @@ func TestService_Evaluate(t *testing.T) {
},
{
name: "policy with blank variable assignment is evaluated successfully",
ctx: ctxWithHeaders(),
req: testReq(),
regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
......@@ -220,13 +261,8 @@ func TestService_Evaluate(t *testing.T) {
},
{
name: "policy is evaluated successfully with TTL sent in the request headers",
req: &goapolicy.EvaluateRequest{
Group: "testgroup",
PolicyName: "example",
Version: "1.0",
Input: map[string]interface{}{"msg": "yes"},
TTL: ptr.Int(30),
},
ctx: ctxWithHeaders(),
req: testReq(),
regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
return nil, false
......@@ -255,6 +291,7 @@ func TestService_Evaluate(t *testing.T) {
},
{
name: "policy using static json data is evaluated successfully",
ctx: ctxWithHeaders(),
req: testReq(),
regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
......@@ -283,12 +320,76 @@ func TestService_Evaluate(t *testing.T) {
Result: map[string]interface{}{"allow": true},
},
},
{
name: "policy accessing headers is evaluated successfully",
ctx: ctxWithHeaders(),
req: testReq(),
regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
return nil, false
},
},
storage: &policyfakes.FakeStorage{
PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
return &storage.Policy{
Name: "example",
Group: "testgroup",
Version: "1.0",
Rego: testPolicyAccessingHeaders,
Locked: false,
LastUpdate: time.Now(),
}, nil
},
},
cache: &policyfakes.FakeCache{
SetStub: func(ctx context.Context, s string, s2 string, s3 string, bytes []byte, i int) error {
return nil
},
},
res: &goapolicy.EvaluateResult{
Result: map[string]interface{}{"token": []interface{}{"my-token"}},
},
},
{
name: "policy with empty input is evaluated successfully",
ctx: ctxWithHeaders(),
req: testEmptyReq(),
regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
return nil, false
},
},
storage: &policyfakes.FakeStorage{
PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
return &storage.Policy{
Name: "example",
Group: "testgroup",
Version: "1.0",
Rego: testPolicy,
Locked: false,
LastUpdate: time.Now(),
}, nil
},
},
cache: &policyfakes.FakeCache{
SetStub: func(ctx context.Context, s string, s2 string, s3 string, bytes []byte, i int) error {
return nil
},
},
res: &goapolicy.EvaluateResult{
Result: map[string]interface{}{"allow": false},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
svc := policy.New(test.storage, test.regocache, test.cache, zap.NewNop())
res, err := svc.Evaluate(context.Background(), test.req)
ctx := context.Background()
if test.ctx != nil {
ctx = test.ctx
}
res, err := svc.Evaluate(ctx, test.req)
if err == nil {
assert.Empty(t, test.errtext)
assert.NotNil(t, res)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment