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 ...@@ -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 package naming rule, there's no way the service can automatically generate HTTP
endpoints for working with arbitrary dynamically uploaded policies. 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 ### Policy Extensions Functions
A brief documentation for the available Rego extensions functions A brief documentation for the available Rego extensions functions
......
...@@ -29,6 +29,7 @@ import ( ...@@ -29,6 +29,7 @@ import (
goapolicy "gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/gen/policy" 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/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/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/regocache"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/regofunc" "gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/regofunc"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/service" "gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/service"
...@@ -156,6 +157,9 @@ func main() { ...@@ -156,6 +157,9 @@ func main() {
openapiServer = goaopenapisrv.New(openapiEndpoints, mux, dec, enc, nil, errFormatter, nil, nil) 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. // Configure the mux.
goapolicysrv.Mount(mux, policyServer) goapolicysrv.Mount(mux, policyServer)
goahealthsrv.Mount(mux, healthServer) 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 ( ...@@ -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/golib/errors"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/gen/policy" "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/regofunc"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/storage" "gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/storage"
) )
...@@ -20,6 +21,8 @@ import ( ...@@ -20,6 +21,8 @@ import (
//go:generate counterfeiter . Storage //go:generate counterfeiter . Storage
//go:generate counterfeiter . RegoCache //go:generate counterfeiter . RegoCache
const HeaderKey = "header"
type Cache interface { type Cache interface {
Set(ctx context.Context, key, namespace, scope string, value []byte, ttl int) error Set(ctx context.Context, key, namespace, scope string, value []byte, ttl int) error
Get(ctx context.Context, key, namespace, scope string) ([]byte, 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 ...@@ -80,7 +83,14 @@ func (s *Service) Evaluate(ctx context.Context, req *policy.EvaluateRequest) (*p
return nil, errors.New("error evaluating policy", err) 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 { if err != nil {
logger.Error("error evaluating rego query", zap.Error(err)) logger.Error("error evaluating rego query", zap.Error(err))
return nil, errors.New("error evaluating rego query", err) return nil, errors.New("error evaluating rego query", err)
...@@ -267,3 +277,29 @@ func (s *Service) buildRegoArgs(filename, regoPolicy, regoQuery, regoData string ...@@ -267,3 +277,29 @@ func (s *Service) buildRegoArgs(filename, regoPolicy, regoQuery, regoData string
func (s *Service) queryCacheKey(group, policyName, version string) string { func (s *Service) queryCacheKey(group, policyName, version string) string {
return fmt.Sprintf("%s,%s,%s", group, policyName, version) 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 ...@@ -2,6 +2,8 @@ package policy_test
import ( import (
"context" "context"
"net/http"
"net/http/httptest"
"testing" "testing"
"time" "time"
...@@ -12,6 +14,7 @@ import ( ...@@ -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/errors"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/golib/ptr" "gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/golib/ptr"
goapolicy "gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/gen/policy" 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"
"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/service/policy/policyfakes"
"gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/storage" "gitlab.com/gaia-x/data-infrastructure-federation-services/tsa/policy/internal/storage"
...@@ -33,6 +36,9 @@ func TestService_Evaluate(t *testing.T) { ...@@ -33,6 +36,9 @@ func TestService_Evaluate(t *testing.T) {
// prepare test policy using static json data during evaluation // prepare test policy using static json data during evaluation
testPolicyWithStaticData := `package testgroup.example default allow = false allow { data.msg == "hello world" }` 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 // prepare test query that can be retrieved from rego queryCache
testQuery, err := rego.New( testQuery, err := rego.New(
rego.Module("example.rego", testPolicy), rego.Module("example.rego", testPolicy),
...@@ -43,17 +49,48 @@ func TestService_Evaluate(t *testing.T) { ...@@ -43,17 +49,48 @@ func TestService_Evaluate(t *testing.T) {
// prepare test request to be used in tests // prepare test request to be used in tests
testReq := func() *goapolicy.EvaluateRequest { 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{ return &goapolicy.EvaluateRequest{
Group: "testgroup", Group: "testgroup",
PolicyName: "example", PolicyName: "example",
Version: "1.0", 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 { tests := []struct {
// test input // test input
name string name string
ctx context.Context
req *goapolicy.EvaluateRequest req *goapolicy.EvaluateRequest
storage policy.Storage storage policy.Storage
regocache policy.RegoCache regocache policy.RegoCache
...@@ -65,6 +102,7 @@ func TestService_Evaluate(t *testing.T) { ...@@ -65,6 +102,7 @@ func TestService_Evaluate(t *testing.T) {
}{ }{
{ {
name: "prepared query is found in queryCache", name: "prepared query is found in queryCache",
ctx: ctxWithHeaders(),
req: testReq(), req: testReq(),
regocache: &policyfakes.FakeRegoCache{ regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) { GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
...@@ -134,6 +172,7 @@ func TestService_Evaluate(t *testing.T) { ...@@ -134,6 +172,7 @@ func TestService_Evaluate(t *testing.T) {
}, },
{ {
name: "policy is found in storage and isn't locked", name: "policy is found in storage and isn't locked",
ctx: ctxWithHeaders(),
req: testReq(), req: testReq(),
regocache: &policyfakes.FakeRegoCache{ regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) { GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
...@@ -163,6 +202,7 @@ func TestService_Evaluate(t *testing.T) { ...@@ -163,6 +202,7 @@ func TestService_Evaluate(t *testing.T) {
}, },
{ {
name: "policy is executed successfully, but storing the result in cache fails", name: "policy is executed successfully, but storing the result in cache fails",
ctx: ctxWithHeaders(),
req: testReq(), req: testReq(),
regocache: &policyfakes.FakeRegoCache{ regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) { GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
...@@ -191,6 +231,7 @@ func TestService_Evaluate(t *testing.T) { ...@@ -191,6 +231,7 @@ func TestService_Evaluate(t *testing.T) {
}, },
{ {
name: "policy with blank variable assignment is evaluated successfully", name: "policy with blank variable assignment is evaluated successfully",
ctx: ctxWithHeaders(),
req: testReq(), req: testReq(),
regocache: &policyfakes.FakeRegoCache{ regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) { GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
...@@ -220,13 +261,8 @@ func TestService_Evaluate(t *testing.T) { ...@@ -220,13 +261,8 @@ func TestService_Evaluate(t *testing.T) {
}, },
{ {
name: "policy is evaluated successfully with TTL sent in the request headers", name: "policy is evaluated successfully with TTL sent in the request headers",
req: &goapolicy.EvaluateRequest{ ctx: ctxWithHeaders(),
Group: "testgroup", req: testReq(),
PolicyName: "example",
Version: "1.0",
Input: map[string]interface{}{"msg": "yes"},
TTL: ptr.Int(30),
},
regocache: &policyfakes.FakeRegoCache{ regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) { GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
return nil, false return nil, false
...@@ -255,6 +291,7 @@ func TestService_Evaluate(t *testing.T) { ...@@ -255,6 +291,7 @@ func TestService_Evaluate(t *testing.T) {
}, },
{ {
name: "policy using static json data is evaluated successfully", name: "policy using static json data is evaluated successfully",
ctx: ctxWithHeaders(),
req: testReq(), req: testReq(),
regocache: &policyfakes.FakeRegoCache{ regocache: &policyfakes.FakeRegoCache{
GetStub: func(key string) (*rego.PreparedEvalQuery, bool) { GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
...@@ -283,12 +320,76 @@ func TestService_Evaluate(t *testing.T) { ...@@ -283,12 +320,76 @@ func TestService_Evaluate(t *testing.T) {
Result: map[string]interface{}{"allow": true}, 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 { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
svc := policy.New(test.storage, test.regocache, test.cache, zap.NewNop()) 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 { if err == nil {
assert.Empty(t, test.errtext) assert.Empty(t, test.errtext)
assert.NotNil(t, res) 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