diff --git a/cmd/policy/main.go b/cmd/policy/main.go index 100261acdf3a11b0c346ead3548e52daecf52993..1eec5678d6967bcb8ff3652f59ed47e16b02db50 100644 --- a/cmd/policy/main.go +++ b/cmd/policy/main.go @@ -25,6 +25,7 @@ import ( goapolicysrv "code.vereign.com/gaiax/tsa/policy/gen/http/policy/server" "code.vereign.com/gaiax/tsa/policy/gen/openapi" goapolicy "code.vereign.com/gaiax/tsa/policy/gen/policy" + "code.vereign.com/gaiax/tsa/policy/internal/clients/cache" "code.vereign.com/gaiax/tsa/policy/internal/config" "code.vereign.com/gaiax/tsa/policy/internal/regocache" "code.vereign.com/gaiax/tsa/policy/internal/regofunc" @@ -65,6 +66,8 @@ func main() { } defer db.Disconnect(context.Background()) //nolint:errcheck + httpClient := httpClient() + // create storage storage, err := storage.New(db, cfg.Mongo.DB, cfg.Mongo.Collection, logger) if err != nil { @@ -76,7 +79,6 @@ func main() { // register rego extension functions { - httpClient := httpClient() cacheFuncs := regofunc.NewCacheFuncs(cfg.Cache.Addr, httpClient) didResolverFuncs := regofunc.NewDIDResolverFuncs(cfg.DIDResolver.Addr, httpClient) taskFuncs := regofunc.NewTaskFuncs(cfg.Task.Addr, httpClient) @@ -90,13 +92,16 @@ func main() { // subscribe the cache for policy data changes storage.AddPolicyChangeSubscriber(regocache) + // create cache client + cache := cache.New(cfg.Cache.Addr, cache.WithHTTPClient(httpClient)) + // create services var ( policySvc goapolicy.Service healthSvc goahealth.Service ) { - policySvc = policy.New(storage, regocache, logger) + policySvc = policy.New(storage, regocache, cache, logger) healthSvc = health.New() } diff --git a/design/types.go b/design/types.go index 3c83c2c64b5e35d1746b72ce1dbdbc9d4192ff44..26996205e55069980fe530a538f7b7f0e66f86c6 100644 --- a/design/types.go +++ b/design/types.go @@ -4,9 +4,15 @@ package design import . "goa.design/goa/v3/dsl" var EvaluateRequest = Type("EvaluateRequest", func() { - Field(1, "group", String, "Policy group.") - Field(2, "policyName", String, "Policy name.") - Field(3, "version", String, "Policy version.") + Field(1, "group", String, "Policy group.", func() { + Example("example") + }) + Field(2, "policyName", String, "Policy name.", func() { + Example("example") + }) + Field(3, "version", String, "Policy version.", func() { + Example("1.0") + }) Field(4, "input", Any, "Input data passed to the policy execution runtime.") Required("group", "policyName", "version", "input") }) diff --git a/gen/http/cli/policy/cli.go b/gen/http/cli/policy/cli.go index ca3c7841a6acdfe4e3a1c21494ad084dac3d3e6f..431940373d2895bc45f21758efb31617747abb04 100644 --- a/gen/http/cli/policy/cli.go +++ b/gen/http/cli/policy/cli.go @@ -32,7 +32,7 @@ policy (evaluate|lock|unlock) // UsageExamples produces an example of a valid invocation of the CLI tool. func UsageExamples() string { return os.Args[0] + ` health liveness` + "\n" + - os.Args[0] + ` policy evaluate --body "Similique quisquam optio." --group "Repellat velit omnis." --policy-name "Vitae qui." --version "Provident fugiat at cupiditate."` + "\n" + + os.Args[0] + ` policy evaluate --body "Ab accusamus voluptatem et est." --group "example" --policy-name "example" --version "1.0"` + "\n" + "" } @@ -247,7 +247,7 @@ Evaluate executes a policy with the given 'data' as input. -version STRING: Policy version. Example: - %[1]s policy evaluate --body "Similique quisquam optio." --group "Repellat velit omnis." --policy-name "Vitae qui." --version "Provident fugiat at cupiditate." + %[1]s policy evaluate --body "Ab accusamus voluptatem et est." --group "example" --policy-name "example" --version "1.0" `, os.Args[0]) } @@ -260,7 +260,7 @@ Lock a policy so that it cannot be evaluated. -version STRING: Policy version. Example: - %[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." + %[1]s policy lock --group "Vitae qui." --policy-name "Provident fugiat at cupiditate." --version "Commodi vitae voluptatem." `, 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 "Aut facere veniam repudiandae id." --policy-name "Aut minus alias." --version "At eos facilis molestias in voluptas rem." + %[1]s policy unlock --group "Aut ut fuga quae eius minus." --policy-name "Architecto quibusdam ab." --version "In illum est et hic." `, os.Args[0]) } diff --git a/gen/http/openapi3.json b/gen/http/openapi3.json index 157c1dd6d45193244c4688d15e7c1e0f5dfcda32..87f8be3859ea0679e8151b3d3e9cd10c22973d63 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":"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 +{"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":"example"},"example":"example"},{"name":"policyName","in":"path","description":"Policy name.","required":true,"schema":{"type":"string","description":"Policy name.","example":"example"},"example":"example"},{"name":"version","in":"path","description":"Policy version.","required":true,"schema":{"type":"string","description":"Policy version.","example":"1.0"},"example":"1.0"}],"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":"Deleniti non nihil dolor aut sed.","format":"binary"},"example":"Omnis quasi aut consequuntur."}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Incidunt unde consequatur voluptas dolorem nisi temporibus.","format":"binary"},"example":"Tempore minus."}}}}}},"/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":"Dolorem cumque laborum quis nesciunt."},"example":"Aut voluptas."},{"name":"policyName","in":"path","description":"Policy name.","required":true,"schema":{"type":"string","description":"Policy name.","example":"Sint nam voluptatem ea consequatur similique et."},"example":"Non mollitia nesciunt impedit facere."},{"name":"version","in":"path","description":"Policy version.","required":true,"schema":{"type":"string","description":"Policy version.","example":"Ut commodi perspiciatis corporis."},"example":"Accusamus autem sequi."}],"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":"Quis quos qui earum velit illum."},"example":"Aliquam atque voluptatum ut dolorem."},{"name":"policyName","in":"path","description":"Policy name.","required":true,"schema":{"type":"string","description":"Policy name.","example":"Aut facere veniam repudiandae id."},"example":"Aut minus alias."},{"name":"version","in":"path","description":"Policy version.","required":true,"schema":{"type":"string","description":"Policy version.","example":"At eos facilis molestias in voluptas rem."},"example":"Ab accusantium ut ut aliquid sint animi."}],"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 f594026729efd68d14d29221bec6581f88a586e7..f5a2f231aada97499f8271846caddaf3886f66c1 100644 --- a/gen/http/openapi3.yaml +++ b/gen/http/openapi3.yaml @@ -31,8 +31,8 @@ paths: schema: type: string description: Policy group. - example: Sint nam voluptatem ea consequatur similique et. - example: Non mollitia nesciunt impedit facere. + example: example + example: example - name: policyName in: path description: Policy name. @@ -40,8 +40,8 @@ paths: schema: type: string description: Policy name. - example: Ut commodi perspiciatis corporis. - example: Accusamus autem sequi. + example: example + example: example - name: version in: path description: Policy version. @@ -49,8 +49,8 @@ paths: schema: type: string description: Policy version. - example: Et nulla. - example: In quis nesciunt autem et. + example: "1.0" + example: "1.0" requestBody: description: Input data passed to the policy execution runtime. required: true @@ -59,9 +59,9 @@ paths: schema: type: string description: Input data passed to the policy execution runtime. - example: Ab accusantium ut ut aliquid sint animi. + example: Deleniti non nihil dolor aut sed. format: binary - example: Aut voluptas. + example: Omnis quasi aut consequuntur. responses: "200": description: OK response. @@ -69,9 +69,9 @@ paths: application/json: schema: type: string - example: Dolorem cumque laborum quis nesciunt. + example: Incidunt unde consequatur voluptas dolorem nisi temporibus. format: binary - example: Sunt in et quia cum. + example: Tempore minus. /policy/{group}/{policyName}/{version}/lock: delete: tags: @@ -87,8 +87,8 @@ paths: schema: type: string description: Policy group. - example: Accusamus enim. - example: Recusandae est rerum corrupti quia. + example: Dolorem cumque laborum quis nesciunt. + example: Aut voluptas. - name: policyName in: path description: Policy name. @@ -96,8 +96,8 @@ paths: schema: type: string description: Policy name. - example: Quam dolores architecto itaque. - example: Voluptas ad corporis adipisci inventore ipsum. + example: Sint nam voluptatem ea consequatur similique et. + example: Non mollitia nesciunt impedit facere. - name: version in: path description: Policy version. @@ -105,8 +105,8 @@ paths: schema: type: string description: Policy version. - example: Recusandae dolorum nisi distinctio vitae ad. - example: Perspiciatis voluptatem. + example: Ut commodi perspiciatis corporis. + example: Accusamus autem sequi. responses: "200": description: OK response. @@ -124,8 +124,8 @@ paths: schema: type: string description: Policy group. - example: Commodi nemo fugiat id praesentium accusantium expedita. - example: Qui non quia. + example: Quis quos qui earum velit illum. + example: Aliquam atque voluptatum ut dolorem. - name: policyName in: path description: Policy name. @@ -133,8 +133,8 @@ paths: schema: type: string description: Policy name. - example: Error maxime quasi quia non voluptatibus error. - example: Optio quia et laborum. + example: Aut facere veniam repudiandae id. + example: Aut minus alias. - name: version in: path description: Policy version. @@ -142,8 +142,8 @@ paths: schema: type: string description: Policy version. - example: In libero perspiciatis voluptatum ut soluta. - example: Ut amet. + example: At eos facilis molestias in voluptas rem. + example: Ab accusantium ut ut aliquid sint animi. responses: "200": description: OK response. diff --git a/gen/http/policy/client/cli.go b/gen/http/policy/client/cli.go index 8b74da63e55e748a960e35ff3a512a48bb008aa1..931cc1ad0c56cd7647ed6924e700826f5f6b0c10 100644 --- a/gen/http/policy/client/cli.go +++ b/gen/http/policy/client/cli.go @@ -22,7 +22,7 @@ func BuildEvaluatePayload(policyEvaluateBody string, policyEvaluateGroup string, { err = json.Unmarshal([]byte(policyEvaluateBody), &body) if err != nil { - return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "\"Similique quisquam optio.\"") + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "\"Ab accusamus voluptatem et est.\"") } } var group string diff --git a/go.mod b/go.mod index 6ebaaeedfa8b0d377ee0b3e173e7c7b7161e44c3..db4c411fdb0dcd0e9837a6aa74501a8f5f3ec80c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.17 require ( code.vereign.com/gaiax/tsa/golib v0.0.0-20220321093827-5fdf8f34aad9 + github.com/google/uuid v1.3.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/open-policy-agent/opa v0.38.1 github.com/stretchr/testify v1.7.0 @@ -22,7 +23,6 @@ require ( github.com/go-stack/stack v1.8.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect diff --git a/internal/clients/cache/client.go b/internal/clients/cache/client.go new file mode 100644 index 0000000000000000000000000000000000000000..7ad51dceaab26442febc5d8afffb29eba1812727 --- /dev/null +++ b/internal/clients/cache/client.go @@ -0,0 +1,84 @@ +package cache + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + + "code.vereign.com/gaiax/tsa/golib/errors" +) + +// Client for the Cache service. +type Client struct { + addr string + httpClient *http.Client +} + +func New(addr string, opts ...Option) *Client { + c := &Client{ + addr: addr, + httpClient: http.DefaultClient, + } + + for _, opt := range opts { + opt(c) + } + + return c +} + +func (c *Client) Set(ctx context.Context, key, namespace, scope string, value []byte) error { + req, err := http.NewRequestWithContext(ctx, "POST", c.addr+"/v1/cache", bytes.NewReader(value)) + if err != nil { + return err + } + + req.Header = http.Header{ + "x-cache-key": []string{key}, + "x-cache-namespace": []string{namespace}, + "x-cache-scope": []string{scope}, + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() // nolint:errcheck + + if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { + msg := fmt.Sprintf("unexpected response: %d %s", resp.StatusCode, resp.Status) + return errors.New(errors.GetKind(resp.StatusCode), msg) + } + + return nil +} + +func (c *Client) Get(ctx context.Context, key, namespace, scope string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, "GET", c.addr+"/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 := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() // nolint:errcheck + + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return nil, errors.New(errors.NotFound) + } + msg := fmt.Sprintf("unexpected response: %d %s", resp.StatusCode, resp.Status) + return nil, errors.New(errors.GetKind(resp.StatusCode), msg) + } + + return io.ReadAll(resp.Body) +} diff --git a/internal/clients/cache/option.go b/internal/clients/cache/option.go new file mode 100644 index 0000000000000000000000000000000000000000..10ef93337d96c9319a0d4dff4333024a581fa921 --- /dev/null +++ b/internal/clients/cache/option.go @@ -0,0 +1,13 @@ +package cache + +import ( + "net/http" +) + +type Option func(*Client) + +func WithHTTPClient(client *http.Client) Option { + return func(c *Client) { + c.httpClient = client + } +} diff --git a/internal/service/policy/policyfakes/fake_cache.go b/internal/service/policy/policyfakes/fake_cache.go new file mode 100644 index 0000000000000000000000000000000000000000..a8583cb47e02f7f86935fbe1a18298f2b48858bd --- /dev/null +++ b/internal/service/policy/policyfakes/fake_cache.go @@ -0,0 +1,210 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package policyfakes + +import ( + "context" + "sync" + + "code.vereign.com/gaiax/tsa/policy/internal/service/policy" +) + +type FakeCache struct { + GetStub func(context.Context, string, string, string) ([]byte, error) + getMutex sync.RWMutex + getArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + arg4 string + } + getReturns struct { + result1 []byte + result2 error + } + getReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + SetStub func(context.Context, string, string, string, []byte) error + setMutex sync.RWMutex + setArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + arg4 string + arg5 []byte + } + setReturns struct { + result1 error + } + setReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeCache) Get(arg1 context.Context, arg2 string, arg3 string, arg4 string) ([]byte, error) { + fake.getMutex.Lock() + ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] + fake.getArgsForCall = append(fake.getArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + arg4 string + }{arg1, arg2, arg3, arg4}) + stub := fake.GetStub + fakeReturns := fake.getReturns + fake.recordInvocation("Get", []interface{}{arg1, arg2, arg3, arg4}) + fake.getMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeCache) GetCallCount() int { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + return len(fake.getArgsForCall) +} + +func (fake *FakeCache) GetCalls(stub func(context.Context, string, string, string) ([]byte, error)) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = stub +} + +func (fake *FakeCache) GetArgsForCall(i int) (context.Context, string, string, string) { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + argsForCall := fake.getArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeCache) GetReturns(result1 []byte, result2 error) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + fake.getReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeCache) GetReturnsOnCall(i int, result1 []byte, result2 error) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + if fake.getReturnsOnCall == nil { + fake.getReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.getReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeCache) Set(arg1 context.Context, arg2 string, arg3 string, arg4 string, arg5 []byte) error { + var arg5Copy []byte + if arg5 != nil { + arg5Copy = make([]byte, len(arg5)) + copy(arg5Copy, arg5) + } + fake.setMutex.Lock() + ret, specificReturn := fake.setReturnsOnCall[len(fake.setArgsForCall)] + fake.setArgsForCall = append(fake.setArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + arg4 string + arg5 []byte + }{arg1, arg2, arg3, arg4, arg5Copy}) + stub := fake.SetStub + fakeReturns := fake.setReturns + fake.recordInvocation("Set", []interface{}{arg1, arg2, arg3, arg4, arg5Copy}) + fake.setMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeCache) SetCallCount() int { + fake.setMutex.RLock() + defer fake.setMutex.RUnlock() + return len(fake.setArgsForCall) +} + +func (fake *FakeCache) SetCalls(stub func(context.Context, string, string, string, []byte) error) { + fake.setMutex.Lock() + defer fake.setMutex.Unlock() + fake.SetStub = stub +} + +func (fake *FakeCache) SetArgsForCall(i int) (context.Context, string, string, string, []byte) { + fake.setMutex.RLock() + defer fake.setMutex.RUnlock() + argsForCall := fake.setArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 +} + +func (fake *FakeCache) SetReturns(result1 error) { + fake.setMutex.Lock() + defer fake.setMutex.Unlock() + fake.SetStub = nil + fake.setReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeCache) SetReturnsOnCall(i int, result1 error) { + fake.setMutex.Lock() + defer fake.setMutex.Unlock() + fake.SetStub = nil + if fake.setReturnsOnCall == nil { + fake.setReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeCache) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + fake.setMutex.RLock() + defer fake.setMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeCache) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ policy.Cache = new(FakeCache) diff --git a/internal/service/policy/service.go b/internal/service/policy/service.go index d453ef8e07d88c4f1a43566ed5b4ad1ede80f9a1..bf01650102a1934b4dc268b2a6c2eb666627faa7 100644 --- a/internal/service/policy/service.go +++ b/internal/service/policy/service.go @@ -2,8 +2,10 @@ package policy import ( "context" + "encoding/json" "fmt" + "github.com/google/uuid" "github.com/open-policy-agent/opa/rego" "go.uber.org/zap" @@ -13,9 +15,15 @@ import ( "code.vereign.com/gaiax/tsa/policy/internal/storage" ) +//go:generate counterfeiter . Cache //go:generate counterfeiter . Storage //go:generate counterfeiter . RegoCache +type Cache interface { + Set(ctx context.Context, key, namespace, scope string, value []byte) error + Get(ctx context.Context, key, namespace, scope string) ([]byte, error) +} + type Storage interface { Policy(ctx context.Context, group, name, version string) (*storage.Policy, error) SetPolicyLock(ctx context.Context, group, name, version string, lock bool) error @@ -29,13 +37,15 @@ type RegoCache interface { type Service struct { storage Storage queryCache RegoCache + cache Cache logger *zap.Logger } -func New(storage Storage, queryCache RegoCache, logger *zap.Logger) *Service { +func New(storage Storage, queryCache RegoCache, cache Cache, logger *zap.Logger) *Service { return &Service{ storage: storage, queryCache: queryCache, + cache: cache, logger: logger, } } @@ -47,12 +57,14 @@ func New(storage Storage, queryCache RegoCache, logger *zap.Logger) *Service { // be exactly the same as 'group.policy'. For example: // 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` +// `package mygroup.example`. func (s *Service) Evaluate(ctx context.Context, req *policy.EvaluateRequest) (interface{}, error) { + evaluationID := uuid.NewString() logger := s.logger.With( zap.String("group", req.Group), zap.String("name", req.PolicyName), zap.String("version", req.Version), + zap.String("evaluationID", evaluationID), ) query, err := s.prepareQuery(ctx, req.Group, req.PolicyName, req.Version) @@ -77,7 +89,23 @@ func (s *Service) Evaluate(ctx context.Context, req *policy.EvaluateRequest) (in return nil, errors.New("policy evaluation result expressions are empty") } - return resultSet[0].Expressions[0].Value, nil + jsonValue, err := json.Marshal(resultSet[0].Expressions[0].Value) + if err != nil { + logger.Error("error encoding result to json", zap.Error(err)) + return nil, errors.New("error encoding result to json") + } + + if err := s.cache.Set(ctx, evaluationID, "", "", jsonValue); err != nil { + logger.Error("error storing policy result in cache", zap.Error(err)) + return nil, errors.New("error storing policy result in cache") + } + + result := map[string]interface{}{ + "evaluationID": evaluationID, + "result": resultSet[0].Expressions[0].Value, + } + + return result, nil } // Lock a policy so that it cannot be evaluated. diff --git a/internal/service/policy/service_test.go b/internal/service/policy/service_test.go index 5b6949a50f091c42308aa88fd89e69cd755f3008..69c25d3c5694e39221706fcf5c08b4fed7b01ea3 100644 --- a/internal/service/policy/service_test.go +++ b/internal/service/policy/service_test.go @@ -17,9 +17,7 @@ import ( ) func TestNew(t *testing.T) { - storage := &policyfakes.FakeStorage{} - regocache := &policyfakes.FakeRegoCache{} - svc := policy.New(storage, regocache, zap.NewNop()) + svc := policy.New(nil, nil, nil, zap.NewNop()) assert.Implements(t, (*goapolicy.Service)(nil), svc) } @@ -50,7 +48,7 @@ func TestService_Evaluate(t *testing.T) { req *goapolicy.EvaluateRequest storage policy.Storage regocache policy.RegoCache - + cache policy.Cache // expected result res interface{} errkind errors.Kind @@ -65,6 +63,11 @@ func TestService_Evaluate(t *testing.T) { return &q, true }, }, + cache: &policyfakes.FakeCache{ + SetStub: func(ctx context.Context, s string, s2 string, s3 string, bytes []byte) error { + return nil + }, + }, res: map[string]interface{}{"allow": true}, }, { @@ -138,17 +141,55 @@ func TestService_Evaluate(t *testing.T) { }, nil }, }, + cache: &policyfakes.FakeCache{ + SetStub: func(ctx context.Context, s string, s2 string, s3 string, bytes []byte) error { + return nil + }, + }, res: map[string]interface{}{"allow": true}, }, + { + name: "policy is executed successfully, but storing the result in cache fails", + 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: testPolicy, + Locked: false, + LastUpdate: time.Now(), + }, nil + }, + }, + cache: &policyfakes.FakeCache{ + SetStub: func(ctx context.Context, s string, s2 string, s3 string, bytes []byte) error { + return errors.New("some error") + }, + }, + errkind: errors.Unknown, + errtext: "error storing policy result in cache", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - svc := policy.New(test.storage, test.regocache, zap.NewNop()) + svc := policy.New(test.storage, test.regocache, test.cache, zap.NewNop()) res, err := svc.Evaluate(context.Background(), test.req) if err == nil { assert.Empty(t, test.errtext) - assert.Equal(t, test.res, res) + assert.NotNil(t, res) + + result, ok := res.(map[string]interface{}) + assert.True(t, ok) + assert.Equal(t, test.res, result["result"]) + assert.NotEmpty(t, result["evaluationID"]) } else { e, ok := err.(*errors.Error) assert.True(t, ok) @@ -243,7 +284,7 @@ func TestService_Lock(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - svc := policy.New(test.storage, nil, zap.NewNop()) + svc := policy.New(test.storage, nil, nil, zap.NewNop()) err := svc.Lock(context.Background(), test.req) if err == nil { assert.Empty(t, test.errtext) @@ -340,7 +381,7 @@ func TestService_Unlock(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - svc := policy.New(test.storage, nil, zap.NewNop()) + svc := policy.New(test.storage, nil, nil, zap.NewNop()) err := svc.Unlock(context.Background(), test.req) if err == nil { assert.Empty(t, test.errtext)