diff --git a/cmd/policy/main.go b/cmd/policy/main.go index 4f5f8cd0045c0983653fe6366fae4fbfe957b3a8..ab7ecd306b4df19086d3619a4c867f8eb4118a5d 100644 --- a/cmd/policy/main.go +++ b/cmd/policy/main.go @@ -4,10 +4,12 @@ import ( "context" "errors" "log" + "net" "net/http" "time" "github.com/kelseyhightower/envconfig" + "github.com/open-policy-agent/opa/rego" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.uber.org/zap" @@ -25,6 +27,7 @@ import ( goapolicy "code.vereign.com/gaiax/tsa/policy/gen/policy" "code.vereign.com/gaiax/tsa/policy/internal/config" "code.vereign.com/gaiax/tsa/policy/internal/regocache" + "code.vereign.com/gaiax/tsa/policy/internal/regofunc" "code.vereign.com/gaiax/tsa/policy/internal/service" "code.vereign.com/gaiax/tsa/policy/internal/service/health" "code.vereign.com/gaiax/tsa/policy/internal/service/policy" @@ -68,6 +71,14 @@ func main() { // create rego query cache regocache := regocache.New() + // register rego extension functions + { + cacheFuncs := regofunc.NewCacheFuncs(cfg.Cache.Addr, httpClient(), logger) + regofunc.Register("cacheGet", rego.Function3(cacheFuncs.CacheGetFunc())) + regofunc.Register("cacheSet", rego.Function4(cacheFuncs.CacheSetFunc())) + regofunc.Register("strictBuiltinErrors", rego.StrictBuiltinErrors(true)) + } + // subscribe the cache for policy data changes storage.AddPolicyChangeSubscriber(regocache) @@ -177,3 +188,19 @@ func createLogger(logLevel string, opts ...zap.Option) (*zap.Logger, error) { func errFormatter(e error) goahttp.Statuser { return service.NewErrorResponse(e) } + +func httpClient() *http.Client { + return &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + TLSHandshakeTimeout: 10 * time.Second, + IdleConnTimeout: 60 * time.Second, + }, + Timeout: 30 * time.Second, + } +} diff --git a/design/design.go b/design/design.go index 323e5b72211f49b8d224ec9f22c72ae876da974f..109dc62bdde2af20ef3b5ee3628027ed979823e5 100644 --- a/design/design.go +++ b/design/design.go @@ -21,7 +21,7 @@ var _ = Service("policy", func() { Method("Evaluate", func() { Description("Evaluate executes a policy with the given 'data' as input.") Payload(EvaluateRequest) - Result(EvaluateResult) + Result(Any) HTTP(func() { POST("/policy/{group}/{policyName}/{version}/evaluation") Body("input") diff --git a/gen/http/cli/policy/cli.go b/gen/http/cli/policy/cli.go index 895aaa1760a5f7231224704a7050339bf635f4bd..ca3c7841a6acdfe4e3a1c21494ad084dac3d3e6f 100644 --- a/gen/http/cli/policy/cli.go +++ b/gen/http/cli/policy/cli.go @@ -260,7 +260,7 @@ Lock a policy so that it cannot be evaluated. -version STRING: Policy version. Example: - %[1]s policy lock --group "In illum est et hic." --policy-name "Deleniti non nihil dolor aut sed." --version "Incidunt unde consequatur voluptas dolorem nisi temporibus." + %[1]s policy lock --group "Deleniti non nihil dolor aut sed." --policy-name "Incidunt unde consequatur voluptas dolorem nisi temporibus." --version "Omnis quasi aut consequuntur." `, os.Args[0]) } @@ -273,6 +273,6 @@ Unlock a policy so it can be evaluated again. -version STRING: Policy version. Example: - %[1]s policy unlock --group "Aliquam atque voluptatum ut dolorem." --policy-name "Aut facere veniam repudiandae id." --version "Aut minus alias." + %[1]s policy unlock --group "Aut facere veniam repudiandae id." --policy-name "Aut minus alias." --version "At eos facilis molestias in voluptas rem." `, os.Args[0]) } diff --git a/gen/http/openapi.json b/gen/http/openapi.json index b5984d3e4ea0da00dbc8096509d6e4125d42c9e9..e45394a13bd6d5c554dec72c55245d7e1f09fcae 100644 --- a/gen/http/openapi.json +++ b/gen/http/openapi.json @@ -1 +1 @@ -{"swagger":"2.0","info":{"title":"Policy Service","description":"The policy service exposes HTTP API for executing policies.","version":""},"host":"localhost:8081","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response."}},"schemes":["http"]}},"/policy/{group}/{policyName}/{version}/evaluation":{"post":{"tags":["policy"],"summary":"Evaluate policy","description":"Evaluate executes a policy with the given 'data' as input.","operationId":"policy#Evaluate","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"type":"string"},{"name":"policyName","in":"path","description":"Policy name.","required":true,"type":"string"},{"name":"version","in":"path","description":"Policy version.","required":true,"type":"string"},{"name":"any","in":"body","description":"Input data passed to the policy execution runtime.","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/PolicyEvaluateResponseBody","required":["result"]}}},"schemes":["http"]}},"/policy/{group}/{policyName}/{version}/lock":{"post":{"tags":["policy"],"summary":"Lock policy","description":"Lock a policy so that it cannot be evaluated.","operationId":"policy#Lock","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"type":"string"},{"name":"policyName","in":"path","description":"Policy name.","required":true,"type":"string"},{"name":"version","in":"path","description":"Policy version.","required":true,"type":"string"}],"responses":{"200":{"description":"OK response."}},"schemes":["http"]},"delete":{"tags":["policy"],"summary":"Unlock policy","description":"Unlock a policy so it can be evaluated again.","operationId":"policy#Unlock","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"type":"string"},{"name":"policyName","in":"path","description":"Policy name.","required":true,"type":"string"},{"name":"version","in":"path","description":"Policy version.","required":true,"type":"string"}],"responses":{"200":{"description":"OK response."}},"schemes":["http"]}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response."}},"schemes":["http"]}}},"definitions":{"PolicyEvaluateResponseBody":{"title":"PolicyEvaluateResponseBody","type":"object","properties":{"result":{"type":"string","description":"Arbitrary JSON response.","example":"At eos facilis molestias in voluptas rem.","format":"binary"}},"example":{"result":"Ab accusantium ut ut aliquid sint animi."},"required":["result"]}}} \ No newline at end of file +{"swagger":"2.0","info":{"title":"Policy Service","description":"The policy service exposes HTTP API for executing policies.","version":""},"host":"localhost:8081","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response."}},"schemes":["http"]}},"/policy/{group}/{policyName}/{version}/evaluation":{"post":{"tags":["policy"],"summary":"Evaluate policy","description":"Evaluate executes a policy with the given 'data' as input.","operationId":"policy#Evaluate","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"type":"string"},{"name":"policyName","in":"path","description":"Policy name.","required":true,"type":"string"},{"name":"version","in":"path","description":"Policy version.","required":true,"type":"string"},{"name":"any","in":"body","description":"Input data passed to the policy execution runtime.","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"binary"}}},"schemes":["http"]}},"/policy/{group}/{policyName}/{version}/lock":{"post":{"tags":["policy"],"summary":"Lock policy","description":"Lock a policy so that it cannot be evaluated.","operationId":"policy#Lock","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"type":"string"},{"name":"policyName","in":"path","description":"Policy name.","required":true,"type":"string"},{"name":"version","in":"path","description":"Policy version.","required":true,"type":"string"}],"responses":{"200":{"description":"OK response."}},"schemes":["http"]},"delete":{"tags":["policy"],"summary":"Unlock policy","description":"Unlock a policy so it can be evaluated again.","operationId":"policy#Unlock","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"type":"string"},{"name":"policyName","in":"path","description":"Policy name.","required":true,"type":"string"},{"name":"version","in":"path","description":"Policy version.","required":true,"type":"string"}],"responses":{"200":{"description":"OK response."}},"schemes":["http"]}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response."}},"schemes":["http"]}}}} \ No newline at end of file diff --git a/gen/http/openapi.yaml b/gen/http/openapi.yaml index 60990b84a7a9ebe2751a44abe450aa6e7b9f0bde..e8bcb485a33fadf93c1fd3cc62f9d1273a2a89d5 100644 --- a/gen/http/openapi.yaml +++ b/gen/http/openapi.yaml @@ -58,9 +58,8 @@ paths: "200": description: OK response. schema: - $ref: '#/definitions/PolicyEvaluateResponseBody' - required: - - result + type: string + format: binary schemes: - http /policy/{group}/{policyName}/{version}/lock: @@ -129,17 +128,3 @@ paths: description: OK response. schemes: - http -definitions: - PolicyEvaluateResponseBody: - title: PolicyEvaluateResponseBody - type: object - properties: - result: - type: string - description: Arbitrary JSON response. - example: At eos facilis molestias in voluptas rem. - format: binary - example: - result: Ab accusantium ut ut aliquid sint animi. - required: - - result diff --git a/gen/http/openapi3.json b/gen/http/openapi3.json index e2c4e9780257067b00d350858e29fa720157039b..157c1dd6d45193244c4688d15e7c1e0f5dfcda32 100644 --- a/gen/http/openapi3.json +++ b/gen/http/openapi3.json @@ -1 +1 @@ -{"openapi":"3.0.3","info":{"title":"Policy Service","description":"The policy service exposes HTTP API for executing policies.","version":"1.0"},"servers":[{"url":"http://localhost:8081","description":"Policy Server"}],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response."}}}},"/policy/{group}/{policyName}/{version}/evaluation":{"post":{"tags":["policy"],"summary":"Evaluate policy","description":"Evaluate executes a policy with the given 'data' as input.","operationId":"policy#Evaluate","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"schema":{"type":"string","description":"Policy group.","example":"Ut commodi perspiciatis corporis."},"example":"Accusamus autem sequi."},{"name":"policyName","in":"path","description":"Policy name.","required":true,"schema":{"type":"string","description":"Policy name.","example":"Et nulla."},"example":"In quis nesciunt autem et."},{"name":"version","in":"path","description":"Policy version.","required":true,"schema":{"type":"string","description":"Policy version.","example":"Sunt in et quia cum."},"example":"Commodi nemo fugiat id praesentium accusantium expedita."}],"requestBody":{"description":"Input data passed to the policy execution runtime.","required":true,"content":{"application/json":{"schema":{"type":"string","description":"Input data passed to the policy execution runtime.","example":"Dolorem cumque laborum quis nesciunt.","format":"binary"},"example":"Non mollitia nesciunt impedit facere."}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EvaluateResult"},"example":{"result":"Explicabo beatae quisquam officiis libero voluptatibus."}}}}}}},"/policy/{group}/{policyName}/{version}/lock":{"delete":{"tags":["policy"],"summary":"Unlock policy","description":"Unlock a policy so it can be evaluated again.","operationId":"policy#Unlock","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"schema":{"type":"string","description":"Policy group.","example":"Recusandae est rerum corrupti quia."},"example":"Quam dolores architecto itaque."},{"name":"policyName","in":"path","description":"Policy name.","required":true,"schema":{"type":"string","description":"Policy name.","example":"Voluptas ad corporis adipisci inventore ipsum."},"example":"Recusandae dolorum nisi distinctio vitae ad."},{"name":"version","in":"path","description":"Policy version.","required":true,"schema":{"type":"string","description":"Policy version.","example":"Perspiciatis voluptatem."},"example":"Corporis est rem."}],"responses":{"200":{"description":"OK response."}}},"post":{"tags":["policy"],"summary":"Lock policy","description":"Lock a policy so that it cannot be evaluated.","operationId":"policy#Lock","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"schema":{"type":"string","description":"Policy group.","example":"Qui non quia."},"example":"Error maxime quasi quia non voluptatibus error."},{"name":"policyName","in":"path","description":"Policy name.","required":true,"schema":{"type":"string","description":"Policy name.","example":"Optio quia et laborum."},"example":"In libero perspiciatis voluptatum ut soluta."},{"name":"version","in":"path","description":"Policy version.","required":true,"schema":{"type":"string","description":"Policy version.","example":"Ut amet."},"example":"Accusamus enim."}],"responses":{"200":{"description":"OK response."}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response."}}}}},"components":{"schemas":{"EvaluateResult":{"type":"object","properties":{"result":{"type":"string","description":"Arbitrary JSON response.","example":"Aut voluptas.","format":"binary"}},"example":{"result":"Sint nam voluptatem ea consequatur similique et."},"required":["result"]}}},"tags":[{"name":"health","description":"Health service provides health check endpoints."},{"name":"policy","description":"Policy Service provides evaluation of policies through Open Policy Agent."}]} \ No newline at end of file +{"openapi":"3.0.3","info":{"title":"Policy Service","description":"The policy service exposes HTTP API for executing policies.","version":"1.0"},"servers":[{"url":"http://localhost:8081","description":"Policy Server"}],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response."}}}},"/policy/{group}/{policyName}/{version}/evaluation":{"post":{"tags":["policy"],"summary":"Evaluate policy","description":"Evaluate executes a policy with the given 'data' as input.","operationId":"policy#Evaluate","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"schema":{"type":"string","description":"Policy group.","example":"Sint nam voluptatem ea consequatur similique et."},"example":"Non mollitia nesciunt impedit facere."},{"name":"policyName","in":"path","description":"Policy name.","required":true,"schema":{"type":"string","description":"Policy name.","example":"Ut commodi perspiciatis corporis."},"example":"Accusamus autem sequi."},{"name":"version","in":"path","description":"Policy version.","required":true,"schema":{"type":"string","description":"Policy version.","example":"Et nulla."},"example":"In quis nesciunt autem et."}],"requestBody":{"description":"Input data passed to the policy execution runtime.","required":true,"content":{"application/json":{"schema":{"type":"string","description":"Input data passed to the policy execution runtime.","example":"Ab accusantium ut ut aliquid sint animi.","format":"binary"},"example":"Aut voluptas."}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Dolorem cumque laborum quis nesciunt.","format":"binary"},"example":"Sunt in et quia cum."}}}}}},"/policy/{group}/{policyName}/{version}/lock":{"delete":{"tags":["policy"],"summary":"Unlock policy","description":"Unlock a policy so it can be evaluated again.","operationId":"policy#Unlock","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"schema":{"type":"string","description":"Policy group.","example":"Accusamus enim."},"example":"Recusandae est rerum corrupti quia."},{"name":"policyName","in":"path","description":"Policy name.","required":true,"schema":{"type":"string","description":"Policy name.","example":"Quam dolores architecto itaque."},"example":"Voluptas ad corporis adipisci inventore ipsum."},{"name":"version","in":"path","description":"Policy version.","required":true,"schema":{"type":"string","description":"Policy version.","example":"Recusandae dolorum nisi distinctio vitae ad."},"example":"Perspiciatis voluptatem."}],"responses":{"200":{"description":"OK response."}}},"post":{"tags":["policy"],"summary":"Lock policy","description":"Lock a policy so that it cannot be evaluated.","operationId":"policy#Lock","parameters":[{"name":"group","in":"path","description":"Policy group.","required":true,"schema":{"type":"string","description":"Policy group.","example":"Commodi nemo fugiat id praesentium accusantium expedita."},"example":"Qui non quia."},{"name":"policyName","in":"path","description":"Policy name.","required":true,"schema":{"type":"string","description":"Policy name.","example":"Error maxime quasi quia non voluptatibus error."},"example":"Optio quia et laborum."},{"name":"version","in":"path","description":"Policy version.","required":true,"schema":{"type":"string","description":"Policy version.","example":"In libero perspiciatis voluptatum ut soluta."},"example":"Ut amet."}],"responses":{"200":{"description":"OK response."}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response."}}}}},"components":{},"tags":[{"name":"health","description":"Health service provides health check endpoints."},{"name":"policy","description":"Policy Service provides evaluation of policies through Open Policy Agent."}]} \ No newline at end of file diff --git a/gen/http/openapi3.yaml b/gen/http/openapi3.yaml index 66d1ca8c53bb54867873fea7086b7e895af411f3..f594026729efd68d14d29221bec6581f88a586e7 100644 --- a/gen/http/openapi3.yaml +++ b/gen/http/openapi3.yaml @@ -31,8 +31,8 @@ paths: schema: type: string description: Policy group. - example: Ut commodi perspiciatis corporis. - example: Accusamus autem sequi. + example: Sint nam voluptatem ea consequatur similique et. + example: Non mollitia nesciunt impedit facere. - name: policyName in: path description: Policy name. @@ -40,8 +40,8 @@ paths: schema: type: string description: Policy name. - example: Et nulla. - example: In quis nesciunt autem et. + example: Ut commodi perspiciatis corporis. + example: Accusamus autem sequi. - name: version in: path description: Policy version. @@ -49,8 +49,8 @@ paths: schema: type: string description: Policy version. - example: Sunt in et quia cum. - example: Commodi nemo fugiat id praesentium accusantium expedita. + example: Et nulla. + example: In quis nesciunt autem et. requestBody: description: Input data passed to the policy execution runtime. required: true @@ -59,18 +59,19 @@ paths: schema: type: string description: Input data passed to the policy execution runtime. - example: Dolorem cumque laborum quis nesciunt. + example: Ab accusantium ut ut aliquid sint animi. format: binary - example: Non mollitia nesciunt impedit facere. + example: Aut voluptas. responses: "200": description: OK response. content: application/json: schema: - $ref: '#/components/schemas/EvaluateResult' - example: - result: Explicabo beatae quisquam officiis libero voluptatibus. + type: string + example: Dolorem cumque laborum quis nesciunt. + format: binary + example: Sunt in et quia cum. /policy/{group}/{policyName}/{version}/lock: delete: tags: @@ -86,8 +87,8 @@ paths: schema: type: string description: Policy group. - example: Recusandae est rerum corrupti quia. - example: Quam dolores architecto itaque. + example: Accusamus enim. + example: Recusandae est rerum corrupti quia. - name: policyName in: path description: Policy name. @@ -95,8 +96,8 @@ paths: schema: type: string description: Policy name. - example: Voluptas ad corporis adipisci inventore ipsum. - example: Recusandae dolorum nisi distinctio vitae ad. + example: Quam dolores architecto itaque. + example: Voluptas ad corporis adipisci inventore ipsum. - name: version in: path description: Policy version. @@ -104,8 +105,8 @@ paths: schema: type: string description: Policy version. - example: Perspiciatis voluptatem. - example: Corporis est rem. + example: Recusandae dolorum nisi distinctio vitae ad. + example: Perspiciatis voluptatem. responses: "200": description: OK response. @@ -123,8 +124,8 @@ paths: schema: type: string description: Policy group. - example: Qui non quia. - example: Error maxime quasi quia non voluptatibus error. + example: Commodi nemo fugiat id praesentium accusantium expedita. + example: Qui non quia. - name: policyName in: path description: Policy name. @@ -132,8 +133,8 @@ paths: schema: type: string description: Policy name. - example: Optio quia et laborum. - example: In libero perspiciatis voluptatum ut soluta. + example: Error maxime quasi quia non voluptatibus error. + example: Optio quia et laborum. - name: version in: path description: Policy version. @@ -141,8 +142,8 @@ paths: schema: type: string description: Policy version. - example: Ut amet. - example: Accusamus enim. + example: In libero perspiciatis voluptatum ut soluta. + example: Ut amet. responses: "200": description: OK response. @@ -155,20 +156,7 @@ paths: responses: "200": description: OK response. -components: - schemas: - EvaluateResult: - type: object - properties: - result: - type: string - description: Arbitrary JSON response. - example: Aut voluptas. - format: binary - example: - result: Sint nam voluptatem ea consequatur similique et. - required: - - result +components: {} tags: - name: health description: Health service provides health check endpoints. diff --git a/gen/http/policy/client/encode_decode.go b/gen/http/policy/client/encode_decode.go index cc62c4177f5b9f56c14adaca7dff08c6771e72a3..d4a2a5f74eef5f02bf3ef2314eb8caadb28fc280 100644 --- a/gen/http/policy/client/encode_decode.go +++ b/gen/http/policy/client/encode_decode.go @@ -83,19 +83,14 @@ func DecodeEvaluateResponse(decoder func(*http.Response) goahttp.Decoder, restor switch resp.StatusCode { case http.StatusOK: var ( - body EvaluateResponseBody + body interface{} err error ) err = decoder(resp).Decode(&body) if err != nil { return nil, goahttp.ErrDecodingError("policy", "Evaluate", err) } - err = ValidateEvaluateResponseBody(&body) - if err != nil { - return nil, goahttp.ErrValidationError("policy", "Evaluate", err) - } - res := NewEvaluateResultOK(&body) - return res, nil + return body, nil default: body, _ := ioutil.ReadAll(resp.Body) return nil, goahttp.ErrInvalidResponse("policy", "Evaluate", resp.StatusCode, string(body)) diff --git a/gen/http/policy/client/types.go b/gen/http/policy/client/types.go index 4bbf4babfe1d591928aa72fd43035cf89b3aeecd..770fa2c41568d70efaee163ca48d02f4cdfe910b 100644 --- a/gen/http/policy/client/types.go +++ b/gen/http/policy/client/types.go @@ -6,34 +6,3 @@ // $ goa gen code.vereign.com/gaiax/tsa/policy/design package client - -import ( - policy "code.vereign.com/gaiax/tsa/policy/gen/policy" - goa "goa.design/goa/v3/pkg" -) - -// EvaluateResponseBody is the type of the "policy" service "Evaluate" endpoint -// HTTP response body. -type EvaluateResponseBody struct { - // Arbitrary JSON response. - Result interface{} `form:"result,omitempty" json:"result,omitempty" xml:"result,omitempty"` -} - -// NewEvaluateResultOK builds a "policy" service "Evaluate" endpoint result -// from a HTTP "OK" response. -func NewEvaluateResultOK(body *EvaluateResponseBody) *policy.EvaluateResult { - v := &policy.EvaluateResult{ - Result: body.Result, - } - - return v -} - -// ValidateEvaluateResponseBody runs the validations defined on -// EvaluateResponseBody -func ValidateEvaluateResponseBody(body *EvaluateResponseBody) (err error) { - if body.Result == nil { - err = goa.MergeErrors(err, goa.MissingFieldError("result", "body")) - } - return -} diff --git a/gen/http/policy/server/encode_decode.go b/gen/http/policy/server/encode_decode.go index 702747a37d0596b6f22cb31d6bbc69c1543e5374..6d676cb40ded28d66368dfa9bdb89fbd11d7984c 100644 --- a/gen/http/policy/server/encode_decode.go +++ b/gen/http/policy/server/encode_decode.go @@ -12,7 +12,6 @@ import ( "io" "net/http" - policy "code.vereign.com/gaiax/tsa/policy/gen/policy" goahttp "goa.design/goa/v3/http" goa "goa.design/goa/v3/pkg" ) @@ -21,9 +20,9 @@ import ( // policy Evaluate endpoint. func EncodeEvaluateResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, interface{}) error { return func(ctx context.Context, w http.ResponseWriter, v interface{}) error { - res, _ := v.(*policy.EvaluateResult) + res, _ := v.(interface{}) enc := encoder(ctx, w) - body := NewEvaluateResponseBody(res) + body := res w.WriteHeader(http.StatusOK) return enc.Encode(body) } diff --git a/gen/http/policy/server/types.go b/gen/http/policy/server/types.go index 8f123712ae895818271de3f69460ece5d5d20577..e39296d9694ab5be167b84d4e5311b5c8b1036bc 100644 --- a/gen/http/policy/server/types.go +++ b/gen/http/policy/server/types.go @@ -11,22 +11,6 @@ import ( policy "code.vereign.com/gaiax/tsa/policy/gen/policy" ) -// EvaluateResponseBody is the type of the "policy" service "Evaluate" endpoint -// HTTP response body. -type EvaluateResponseBody struct { - // Arbitrary JSON response. - Result interface{} `form:"result" json:"result" xml:"result"` -} - -// NewEvaluateResponseBody builds the HTTP response body from the result of the -// "Evaluate" endpoint of the "policy" service. -func NewEvaluateResponseBody(res *policy.EvaluateResult) *EvaluateResponseBody { - body := &EvaluateResponseBody{ - Result: res.Result, - } - return body -} - // NewEvaluateRequest builds a policy service Evaluate endpoint payload. func NewEvaluateRequest(body interface{}, group string, policyName string, version string) *policy.EvaluateRequest { v := body diff --git a/gen/policy/client.go b/gen/policy/client.go index 554aa783b90015e0836f32342226c7507fc7d3d3..4543ff51214d584abfe8a3ecaee9d5284cc7b876 100644 --- a/gen/policy/client.go +++ b/gen/policy/client.go @@ -30,13 +30,13 @@ func NewClient(evaluate, lock, unlock goa.Endpoint) *Client { } // Evaluate calls the "Evaluate" endpoint of the "policy" service. -func (c *Client) Evaluate(ctx context.Context, p *EvaluateRequest) (res *EvaluateResult, err error) { +func (c *Client) Evaluate(ctx context.Context, p *EvaluateRequest) (res interface{}, err error) { var ires interface{} ires, err = c.EvaluateEndpoint(ctx, p) if err != nil { return } - return ires.(*EvaluateResult), nil + return ires.(interface{}), nil } // Lock calls the "Lock" endpoint of the "policy" service. diff --git a/gen/policy/service.go b/gen/policy/service.go index 25f54089f5b7c33d79059c1cb7196c6a8b8f21b3..3895194865fa885d9569cde5e97c02e2b7ae33ec 100644 --- a/gen/policy/service.go +++ b/gen/policy/service.go @@ -14,7 +14,7 @@ import ( // Policy Service provides evaluation of policies through Open Policy Agent. type Service interface { // Evaluate executes a policy with the given 'data' as input. - Evaluate(context.Context, *EvaluateRequest) (res *EvaluateResult, err error) + Evaluate(context.Context, *EvaluateRequest) (res interface{}, err error) // Lock a policy so that it cannot be evaluated. Lock(context.Context, *LockRequest) (err error) // Unlock a policy so it can be evaluated again. @@ -43,12 +43,6 @@ type EvaluateRequest struct { Input interface{} } -// EvaluateResult is the result type of the policy service Evaluate method. -type EvaluateResult struct { - // Arbitrary JSON response. - Result interface{} -} - // LockRequest is the payload type of the policy service Lock method. type LockRequest struct { // Policy group. diff --git a/internal/config/config.go b/internal/config/config.go index 008f2f483bd3d2d72bff4ebf3b633ec71e5c9db5..f9119f67420ba434f36949e3ddbb1380dae39595 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,8 +4,8 @@ import "time" type Config struct { HTTP httpConfig - Redis redisConfig Mongo mongoConfig + Cache cacheConfig LogLevel string `envconfig:"LOG_LEVEL" default:"INFO"` } @@ -18,12 +18,8 @@ type httpConfig struct { WriteTimeout time.Duration `envconfig:"HTTP_WRITE_TIMEOUT" default:"10s"` } -type redisConfig struct { - Addr string `envconfig:"REDIS_ADDR" required:"true"` - User string `envconfig:"REDIS_USER" required:"true"` - Pass string `envconfig:"REDIS_PASS" required:"true"` - DB int `envconfig:"REDIS_DB" default:"1"` - TTL time.Duration `envconfig:"REDIS_EXPIRATION"` // no default expiration, keys are set to live forever +type cacheConfig struct { + Addr string `envconfig:"CACHE_ADDR" required:"true"` } type mongoConfig struct { diff --git a/internal/regofunc/cache.go b/internal/regofunc/cache.go new file mode 100644 index 0000000000000000000000000000000000000000..8f0b2821f5fe8dc4849aa1d579bcd3382b2c94a5 --- /dev/null +++ b/internal/regofunc/cache.go @@ -0,0 +1,129 @@ +package regofunc + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/rego" + "github.com/open-policy-agent/opa/types" + "go.uber.org/zap" +) + +type CacheFuncs struct { + cacheAddr string + httpClient *http.Client + logger *zap.Logger +} + +func NewCacheFuncs(cacheAddr string, httpClient *http.Client, logger *zap.Logger) *CacheFuncs { + return &CacheFuncs{ + cacheAddr: cacheAddr, + httpClient: httpClient, + logger: logger, + } +} + +func (cf *CacheFuncs) CacheGetFunc() (*rego.Function, rego.Builtin3) { + return ®o.Function{ + Name: "cache.get", + Decl: types.NewFunction(types.Args(types.S, types.S, types.S), types.A), + Memoize: true, + }, + func(bctx rego.BuiltinContext, a, b, c *ast.Term) (*ast.Term, error) { + var key, namespace, scope string + + if err := ast.As(a.Value, &key); err != nil { + return nil, fmt.Errorf("invalid key: %s", err) + } else if err = ast.As(b.Value, &namespace); err != nil { + return nil, fmt.Errorf("invalid namespace: %s", err) + } else if err = ast.As(c.Value, &scope); err != nil { + return nil, fmt.Errorf("invalid scope: %s", err) + } + + req, err := http.NewRequest("GET", cf.cacheAddr+"/v1/cache", nil) + req.Header = http.Header{ + "x-cache-key": []string{key}, + "x-cache-namespace": []string{namespace}, + "x-cache-scope": []string{scope}, + } + if err != nil { + return nil, err + } + + resp, err := cf.httpClient.Do(req.WithContext(bctx.Context)) + if err != nil { + return nil, err + } + defer resp.Body.Close() // nolint:errcheck + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { + return nil, fmt.Errorf("unexpected response: %d %s", resp.StatusCode, resp.Status) + } + + v, err := ast.ValueFromReader(resp.Body) + if err != nil { + return nil, err + } + + return ast.NewTerm(v), nil + } +} + +func (cf *CacheFuncs) CacheSetFunc() (*rego.Function, rego.Builtin4) { + return ®o.Function{ + Name: "cache.set", + Decl: types.NewFunction(types.Args(types.S, types.S, types.S, types.S), types.A), + Memoize: true, + }, + func(bctx rego.BuiltinContext, k, n, s, d *ast.Term) (*ast.Term, error) { + var key, namespace, scope string + var data map[string]interface{} + + if err := ast.As(k.Value, &key); err != nil { + return nil, fmt.Errorf("invalid key: %s", err) + } else if err = ast.As(n.Value, &namespace); err != nil { + return nil, fmt.Errorf("invalid namespace: %s", err) + } else if err = ast.As(s.Value, &scope); err != nil { + return nil, fmt.Errorf("invalid scope: %s", err) + } else if err = ast.As(d.Value, &data); err != nil { + return nil, fmt.Errorf("invalid data: %s", err) + } + + jsonData, err := json.Marshal(data) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", cf.cacheAddr+"/v1/cache", bytes.NewReader(jsonData)) + if err != nil { + return nil, err + } + + req.Header = http.Header{ + "x-cache-key": []string{key}, + "x-cache-namespace": []string{namespace}, + "x-cache-scope": []string{scope}, + } + + resp, err := cf.httpClient.Do(req.WithContext(bctx.Context)) + if err != nil { + return nil, err + } + defer resp.Body.Close() // nolint:errcheck + + if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected response code: %d", resp.StatusCode) + } + + var val ast.Value + val, err = ast.InterfaceToValue("success") + if err != nil { + return nil, err + } + + return ast.NewTerm(val), nil + } +} diff --git a/internal/regofunc/cache_test.go b/internal/regofunc/cache_test.go new file mode 100644 index 0000000000000000000000000000000000000000..150f5c4cd21136f14aa6f696556fbda9c899ed9a --- /dev/null +++ b/internal/regofunc/cache_test.go @@ -0,0 +1,95 @@ +package regofunc_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/open-policy-agent/opa/rego" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + + "code.vereign.com/gaiax/tsa/policy/internal/regofunc" +) + +func TestCacheGetFunc(t *testing.T) { + expected := `{"taskID":"deadbeef"}` + cacheSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = fmt.Fprint(w, expected) + })) + defer cacheSrv.Close() + + cacheFuncs := regofunc.NewCacheFuncs(cacheSrv.URL, http.DefaultClient, zap.NewNop()) + + r := rego.New( + rego.Query(`cache.get("open-policy-agent", "opa", "111")`), + rego.Function3(cacheFuncs.CacheGetFunc()), + ) + resultSet, err := r.Eval(context.Background()) + assert.NoError(t, err) + + resultBytes, err := json.Marshal(resultSet[0].Expressions[0].Value) + assert.NoError(t, err) + assert.Equal(t, expected, string(resultBytes)) +} + +func TestCacheSetFuncSuccess(t *testing.T) { + expected := "success" + cacheSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expectedRequestBody := `{"test":123}` + bodyBytes, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + bodyString := string(bodyBytes) + if bodyString != expectedRequestBody { + assert.Equal(t, expectedRequestBody, bodyString) + } + + w.WriteHeader(http.StatusCreated) + })) + defer cacheSrv.Close() + + cacheFuncs := regofunc.NewCacheFuncs(cacheSrv.URL, http.DefaultClient, zap.NewNop()) + + input := map[string]interface{}{"test": 123} + query, err := rego.New( + rego.Query(`cache.set("open-policy-agent", "opa", "111", input)`), + rego.Function4(cacheFuncs.CacheSetFunc()), + ).PrepareForEval(context.Background()) + assert.NoError(t, err) + + resultSet, err := query.Eval(context.Background(), rego.EvalInput(input)) + assert.NoError(t, err) + assert.NotEmpty(t, resultSet) + assert.NotEmpty(t, resultSet[0].Expressions) + assert.Equal(t, expected, resultSet[0].Expressions[0].Value) +} + +func TestCacheSetFuncError(t *testing.T) { + cacheSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expectedRequestBody := "test" + bodyBytes, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + bodyString := string(bodyBytes) + assert.Equal(t, expectedRequestBody, bodyString) + + w.WriteHeader(http.StatusNotFound) + })) + defer cacheSrv.Close() + + cacheFuncs := regofunc.NewCacheFuncs(cacheSrv.URL, http.DefaultClient, zap.NewNop()) + + r := rego.New( + rego.Query(`cache.set("open-policy-agent", "opa", "111", "test")`), + rego.Function4(cacheFuncs.CacheSetFunc()), + ) + + resultSet, err := r.Eval(context.Background()) + assert.NoError(t, err) + assert.Empty(t, resultSet) +} diff --git a/internal/regofunc/doc.go b/internal/regofunc/doc.go new file mode 100644 index 0000000000000000000000000000000000000000..47bd85d7653a9d303a5eadf58b7337a35b1322d3 --- /dev/null +++ b/internal/regofunc/doc.go @@ -0,0 +1,4 @@ +// Package regofunc provides functions that extend the Rego runtime +// with additional capabilities and built-in functions which can be +// used when writing and evaluating Rego polices. +package regofunc diff --git a/internal/regofunc/registry.go b/internal/regofunc/registry.go new file mode 100644 index 0000000000000000000000000000000000000000..7ff98dd12873a0d759a2f4be3482598a64c4a02f --- /dev/null +++ b/internal/regofunc/registry.go @@ -0,0 +1,37 @@ +package regofunc + +import ( + "fmt" + "sync" + + "github.com/open-policy-agent/opa/rego" +) + +type RegoFunc func(*rego.Rego) + +var ( + muRegistry sync.RWMutex + regoFuncRegistry = make(map[string]RegoFunc) +) + +// Register an extension function. +func Register(name string, fn RegoFunc) { + if fn == nil { + panic(fmt.Errorf("cannot register nil Rego function: %s", name)) + } + + if _, registered := regoFuncRegistry[name]; !registered { + regoFuncRegistry[name] = fn + } +} + +// List returns all registered extension functions. +func List() []RegoFunc { + list := make([]RegoFunc, 0) + muRegistry.RLock() + for _, fn := range regoFuncRegistry { + list = append(list, fn) + } + muRegistry.RUnlock() + return list +} diff --git a/internal/regofunc/registry_test.go b/internal/regofunc/registry_test.go new file mode 100644 index 0000000000000000000000000000000000000000..dad462175a3b35c376ce5ac05203c2f1129e1114 --- /dev/null +++ b/internal/regofunc/registry_test.go @@ -0,0 +1,24 @@ +package regofunc_test + +import ( + "net/http" + "testing" + + "github.com/open-policy-agent/opa/rego" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + + "code.vereign.com/gaiax/tsa/policy/internal/regofunc" +) + +func TestList(t *testing.T) { + funcs := regofunc.List() + assert.Len(t, funcs, 0) + + cacheFuncs := regofunc.NewCacheFuncs("localhost:8080", http.DefaultClient, zap.NewNop()) + regofunc.Register("cacheGet", rego.Function3(cacheFuncs.CacheGetFunc())) + regofunc.Register("cacheSet", rego.Function3(cacheFuncs.CacheGetFunc())) + + funcs = regofunc.List() + assert.Len(t, funcs, 2) +} diff --git a/internal/service/policy/service.go b/internal/service/policy/service.go index aff12a10b1ba8603a7d799e0cdc28fc317f63cae..89bec03d4b1dacb6bdb8b614a800f0a6483bbbed 100644 --- a/internal/service/policy/service.go +++ b/internal/service/policy/service.go @@ -9,6 +9,7 @@ import ( "code.vereign.com/gaiax/tsa/golib/errors" "code.vereign.com/gaiax/tsa/policy/gen/policy" + "code.vereign.com/gaiax/tsa/policy/internal/regofunc" "code.vereign.com/gaiax/tsa/policy/internal/storage" ) @@ -26,16 +27,16 @@ type RegoCache interface { } type Service struct { - storage Storage - cache RegoCache - logger *zap.Logger + storage Storage + queryCache RegoCache + logger *zap.Logger } -func New(storage Storage, cache RegoCache, logger *zap.Logger) *Service { +func New(storage Storage, queryCache RegoCache, logger *zap.Logger) *Service { return &Service{ - storage: storage, - cache: cache, - logger: logger, + storage: storage, + queryCache: queryCache, + logger: logger, } } @@ -47,7 +48,7 @@ func New(storage Storage, cache RegoCache, logger *zap.Logger) *Service { // Evaluating the URL: `.../policies/mygroup/example/1.0/evaluation` will // return results correctly, only if the package declaration inside the policy is: // `package mygroup.example` -func (s *Service) Evaluate(ctx context.Context, req *policy.EvaluateRequest) (*policy.EvaluateResult, error) { +func (s *Service) Evaluate(ctx context.Context, req *policy.EvaluateRequest) (interface{}, error) { logger := s.logger.With( zap.String("name", req.PolicyName), zap.String("group", req.Group), @@ -67,16 +68,16 @@ func (s *Service) Evaluate(ctx context.Context, req *policy.EvaluateRequest) (*p } if len(resultSet) == 0 { - logger.Error("policy evaluation results are missing") - return nil, errors.New("policy evaluation results are missing") + logger.Error("policy evaluation results are empty") + return nil, errors.New("policy evaluation results are empty") } if len(resultSet[0].Expressions) == 0 { - logger.Error("policy evaluation result expressions are missing") - return nil, errors.New("policy evaluation result expressions are missing") + logger.Error("policy evaluation result expressions are empty") + return nil, errors.New("policy evaluation result expressions are empty") } - return &policy.EvaluateResult{Result: resultSet[0].Expressions[0].Value}, nil + return resultSet[0].Expressions[0].Value, nil } // Lock a policy so that it cannot be evaluated. @@ -142,11 +143,11 @@ func (s *Service) Unlock(ctx context.Context, req *policy.UnlockRequest) error { } // prepareQuery tries to get a prepared query from the regocache. -// If the cache entry is not found, it will try to prepare a new -// query and will set it into the cache for future use. +// If the queryCache entry is not found, it will try to prepare a new +// query and will set it into the queryCache for future use. func (s *Service) prepareQuery(ctx context.Context, policyName, group, version string) (*rego.PreparedEvalQuery, error) { key := s.queryCacheKey(policyName, group, version) - query, ok := s.cache.Get(key) + query, ok := s.queryCache.Get(key) if ok { return query, nil } @@ -170,18 +171,28 @@ func (s *Service) prepareQuery(ctx context.Context, policyName, group, version s regoQuery := fmt.Sprintf("data.%s.%s", group, policyName) newQuery, err := rego.New( - rego.Module(pol.Filename, pol.Rego), - rego.Query(regoQuery), + buildRegoArgs(pol.Filename, pol.Rego, regoQuery)..., ).PrepareForEval(ctx) if err != nil { return nil, errors.New("error preparing rego query", err) } - s.cache.Set(key, &newQuery) + s.queryCache.Set(key, &newQuery) return &newQuery, nil } +func buildRegoArgs(filename, regoPolicy, regoQuery string) (availableFuncs []func(*rego.Rego)) { + availableFuncs = make([]func(*rego.Rego), 2) + availableFuncs[0] = rego.Module(filename, regoPolicy) + availableFuncs[1] = rego.Query(regoQuery) + extensions := regofunc.List() + for i := range extensions { + availableFuncs = append(availableFuncs, extensions[i]) + } + return +} + func (s *Service) queryCacheKey(policyName, group, version string) string { return fmt.Sprintf("%s,%s,%s", policyName, group, version) } diff --git a/internal/service/policy/service_test.go b/internal/service/policy/service_test.go index a5eebd2425e15d1088ff4863ebd4130dac8190c4..5b6949a50f091c42308aa88fd89e69cd755f3008 100644 --- a/internal/service/policy/service_test.go +++ b/internal/service/policy/service_test.go @@ -27,7 +27,7 @@ func TestService_Evaluate(t *testing.T) { // prepare test policy source code that will be evaluated testPolicy := `package testgroup.example allow { input.msg == "yes" }` - // prepare test query that can be retrieved from rego cache + // prepare test query that can be retrieved from rego queryCache testQuery, err := rego.New( rego.Module("example.rego", testPolicy), rego.Query("data.testgroup.example"), @@ -52,12 +52,12 @@ func TestService_Evaluate(t *testing.T) { regocache policy.RegoCache // expected result - res *goapolicy.EvaluateResult + res interface{} errkind errors.Kind errtext string }{ { - name: "prepared query is found in cache", + name: "prepared query is found in queryCache", req: testReq(), regocache: &policyfakes.FakeRegoCache{ GetStub: func(key string) (*rego.PreparedEvalQuery, bool) { @@ -65,7 +65,7 @@ func TestService_Evaluate(t *testing.T) { return &q, true }, }, - res: &goapolicy.EvaluateResult{Result: map[string]interface{}{"allow": true}}, + res: map[string]interface{}{"allow": true}, }, { name: "policy is not found", @@ -138,7 +138,7 @@ func TestService_Evaluate(t *testing.T) { }, nil }, }, - res: &goapolicy.EvaluateResult{Result: map[string]interface{}{"allow": true}}, + res: map[string]interface{}{"allow": true}, }, }