diff --git a/grpc_opa/authorizer.go b/grpc_opa/authorizer.go index 82e81bb..4bbfc68 100644 --- a/grpc_opa/authorizer.go +++ b/grpc_opa/authorizer.go @@ -144,10 +144,13 @@ func NewDefaultAuthorizer(application string, opts ...Option) *DefaultAuthorizer claimsVerifier: cfg.claimsVerifier, entitledServices: cfg.entitledServices, acctEntitlementsApi: cfg.acctEntitlementsApi, + extraInputFields: cfg.extraInputFields, } return &a } +type ExtraInputFields map[string]interface{} + type DefaultAuthorizer struct { application string clienter opa_client.Clienter @@ -156,6 +159,7 @@ type DefaultAuthorizer struct { claimsVerifier ClaimsVerifier entitledServices []string acctEntitlementsApi string + extraInputFields ExtraInputFields } type Config struct { @@ -170,6 +174,7 @@ type Config struct { claimsVerifier ClaimsVerifier entitledServices []string acctEntitlementsApi string + extraInputFields ExtraInputFields } type ClaimsVerifier func([]string, []string) (string, []error) @@ -221,6 +226,7 @@ func (a *DefaultAuthorizer) Evaluate(ctx context.Context, fullMethod string, grp JWT: redactJWT(rawJWT), RequestID: reqID, EntitledServices: a.entitledServices, + ExtraInputFields: a.extraInputFields, } decisionInput, err := a.decisionInputHandler.GetDecisionInput(ctx, fullMethod, grpcReq) @@ -407,6 +413,7 @@ type Payload struct { RequestID string `json:"request_id"` EntitledServices []string `json:"entitled_services"` DecisionInput + ExtraInputFields `json:"extra,omitempty"` } // OPARequest is used to query OPA diff --git a/grpc_opa/authorizer_test.go b/grpc_opa/authorizer_test.go index 50f4ed2..53bac5c 100644 --- a/grpc_opa/authorizer_test.go +++ b/grpc_opa/authorizer_test.go @@ -422,9 +422,102 @@ func TestDebugLogging(t *testing.T) { } } +func TestInputPayload(t *testing.T) { + stdLoggr := logrus.StandardLogger() + ctx := context.WithValue(context.Background(), utils_test.TestingTContextKey, t) + ctx = ctxlogrus.ToContext(ctx, logrus.NewEntry(stdLoggr)) + + newMockOpaClienterFn := func(expectInputJSON string) *MockOpaClienter { + return &MockOpaClienter{ + Loggr: stdLoggr, + RegoRespJSON: `{"allow": true}`, + VerifyInput: true, + ExpectInputJSON: expectInputJSON, + } + } + + testMap := []struct { + name string + authzr *DefaultAuthorizer + }{ + { + name: `no-options`, + authzr: NewDefaultAuthorizer("fakeapp", + WithClaimsVerifier(NullClaimsVerifier), + WithOpaClienter(newMockOpaClienterFn(`{ + "endpoint": "FakeMethod", + "application": "fakeapp", + "full_method": "FakeMethod", + "jwt": "redacted", + "request_id": "no-request-uuid", + "entitled_services": null, + "type": "", + "verb": "", + "ctx": null + }`)), + ), + }, + { + name: `with-one-extra-input-field`, + authzr: NewDefaultAuthorizer("fakeapp", + WithClaimsVerifier(NullClaimsVerifier), + WithExtraInputField("my extra field 1", "my extra value 1"), + WithOpaClienter(newMockOpaClienterFn(`{ + "endpoint": "FakeMethod", + "application": "fakeapp", + "full_method": "FakeMethod", + "jwt": "redacted", + "request_id": "no-request-uuid", + "entitled_services": null, + "type": "", + "verb": "", + "ctx": null, + "extra": { + "my extra field 1": "my extra value 1" + } + }`)), + ), + }, + { + name: `with-mult-extra-input-field`, + authzr: NewDefaultAuthorizer("fakeapp", + WithClaimsVerifier(NullClaimsVerifier), + WithExtraInputField("my extra field 1", "my extra value 1"), + WithExtraInputField("my extra field 2", true), + WithExtraInputField("my extra field 3", 123), + WithOpaClienter(newMockOpaClienterFn(`{ + "endpoint": "FakeMethod", + "application": "fakeapp", + "full_method": "FakeMethod", + "jwt": "redacted", + "request_id": "no-request-uuid", + "entitled_services": null, + "type": "", + "verb": "", + "ctx": null, + "extra": { + "my extra field 1": "my extra value 1", + "my extra field 2": true, + "my extra field 3": 123 + } + }`)), + ), + }, + } + + for nth, tm := range testMap { + tcCtx := context.WithValue(ctx, utils_test.TestCaseIndexContextKey, nth) + tcCtx = context.WithValue(tcCtx, utils_test.TestCaseNameContextKey, tm.name) + tm.authzr.AffirmAuthorization(tcCtx, "FakeMethod", nil) + } +} + type MockOpaClienter struct { Loggr *logrus.Logger RegoRespJSON string + + VerifyInput bool + ExpectInputJSON string } func (m MockOpaClienter) String() string { @@ -452,6 +545,33 @@ func (m MockOpaClienter) CustomQueryBytes(ctx context.Context, document string, } func (m MockOpaClienter) CustomQuery(ctx context.Context, document string, reqData, resp interface{}) error { + if m.VerifyInput { + t, _ := ctx.Value(utils_test.TestingTContextKey).(*testing.T) + tcIdx, _ := ctx.Value(utils_test.TestCaseIndexContextKey).(int) + tcName, _ := ctx.Value(utils_test.TestCaseNameContextKey).(string) + payload, _ := reqData.(Payload) + payloadJSON, _ := json.MarshalIndent(payload, "", " ") + t.Logf("%d: %s: payload=%#v", tcIdx, tcName, payload) + actualInput := map[string]interface{}{} + expectInput := map[string]interface{}{} + err := json.Unmarshal(payloadJSON, &actualInput) + if err != nil { + t.Errorf("%d: %s: FAIL: json.Unmarshal err=%s\npayloadJSON=%s", + tcIdx, tcName, err, string(payloadJSON)) + } + err = json.Unmarshal([]byte(m.ExpectInputJSON), &expectInput) + if err != nil { + t.Errorf("%d: %s: FAIL: json.Unmarshal err=%s\nExpectInputJSON=%s", + tcIdx, tcName, err, m.ExpectInputJSON) + } + if !reflect.DeepEqual(actualInput, expectInput) { + t.Errorf("%d: %s: FAIL:\npayloadJSON=%s\nExpectInputJSON=%s", + tcIdx, tcName, string(payloadJSON), m.ExpectInputJSON) + t.Errorf("%d: %s: FAIL:\nactualInput=%#v\nexpectInput=%#v", + tcIdx, tcName, actualInput, expectInput) + } + } + err := json.Unmarshal([]byte(m.RegoRespJSON), resp) m.Loggr.Debugf("CustomQuery: resp=%#v", resp) return err diff --git a/grpc_opa/options.go b/grpc_opa/options.go index 562e023..b70bd2f 100644 --- a/grpc_opa/options.go +++ b/grpc_opa/options.go @@ -79,3 +79,23 @@ func WithAcctEntitlementsApiPath(acctEntitlementsApi string) Option { c.acctEntitlementsApi = acctEntitlementsApi } } + +// WithExtraInputFields merges extra input fields, can be called multiple times +func WithExtraInputFields(extra ExtraInputFields) Option { + return func(c *Config) { + if extra == nil || len(extra) <= 0 { + return + } + if c.extraInputFields == nil { + c.extraInputFields = ExtraInputFields{} + } + for key, val := range extra { + c.extraInputFields[key] = val + } + } +} + +// WithExtraInputField merges extra input field, can be called multiple times +func WithExtraInputField(name string, value interface{}) Option { + return WithExtraInputFields(ExtraInputFields{name: value}) +} diff --git a/grpc_opa/options_test.go b/grpc_opa/options_test.go index 59a59a1..bc2186e 100644 --- a/grpc_opa/options_test.go +++ b/grpc_opa/options_test.go @@ -15,37 +15,37 @@ func Test_WithEntitledServices_payload(t *testing.T) { var uninitializedStrSlice []string withEntitledServicesTests := []struct { name string - inputEntitledServices interface{} + configEntitledServices interface{} expectEntitledServices []string }{ { name: `dont-call-WithEntitledServices`, - inputEntitledServices: `dont-call-WithEntitledServices`, + configEntitledServices: `dont-call-WithEntitledServices`, expectEntitledServices: nil, }, { name: `WithEntitledServices(nil)`, - inputEntitledServices: nil, + configEntitledServices: nil, expectEntitledServices: nil, }, { name: `WithEntitledServices(uninitializedStrSlice)`, - inputEntitledServices: uninitializedStrSlice, + configEntitledServices: uninitializedStrSlice, expectEntitledServices: nil, }, { name: `WithEntitledServices([])`, - inputEntitledServices: []string{}, + configEntitledServices: []string{}, expectEntitledServices: []string{}, }, { name: `WithEntitledServices(["lic"])`, - inputEntitledServices: []string{"lic"}, + configEntitledServices: []string{"lic"}, expectEntitledServices: []string{"lic"}, }, { name: `WithEntitledServices(["lic","rpz"])`, - inputEntitledServices: []string{"lic", "rpz"}, + configEntitledServices: []string{"lic", "rpz"}, expectEntitledServices: []string{"lic", "rpz"}, }, } @@ -66,14 +66,79 @@ func Test_WithEntitledServices_payload(t *testing.T) { WithClaimsVerifier(NullClaimsVerifier), ) - inputEntitledServices, ok := tstc.inputEntitledServices.([]string) - if tstc.inputEntitledServices == nil || ok { + configEntitledServices, ok := tstc.configEntitledServices.([]string) + if tstc.configEntitledServices == nil || ok { t.Logf("tst#%d: name=%s; calling option WithEntitledServices(%#v)", - idx, tstc.name, inputEntitledServices) + idx, tstc.name, configEntitledServices) auther = NewDefaultAuthorizer("app", WithOpaClienter(&mockOpaClienter), WithClaimsVerifier(NullClaimsVerifier), - WithEntitledServices(inputEntitledServices...), + WithEntitledServices(configEntitledServices...), + ) + } + + auther.AffirmAuthorization(tcCtx, "FakeMethod", nil) + } +} + +func Test_WithExtraInputFields_payload(t *testing.T) { + var uninitializedExtraInputFields ExtraInputFields + withExtraInputFieldsTests := []struct { + name string + configExtraInputFields interface{} + expectExtraInputFields ExtraInputFields + }{ + { + name: `dont-call-WithExtraInputFields`, + configExtraInputFields: `dont-call-WithExtraInputFields`, + expectExtraInputFields: nil, + }, + { + name: `WithExtraInputFields(nil)`, + configExtraInputFields: nil, + expectExtraInputFields: nil, + }, + { + name: `WithExtraInputFields(uninitializedExtraInputFields)`, + configExtraInputFields: uninitializedExtraInputFields, + expectExtraInputFields: nil, + }, + { + name: `WithExtraInputFields(ExtraInputFields{})`, + configExtraInputFields: ExtraInputFields{}, + expectExtraInputFields: nil, + }, + { + name: `WithExtraInputFields(ExtraInputFields{name:val})`, + configExtraInputFields: ExtraInputFields{"k1": "v1", "k2": true, "k3": 123}, + expectExtraInputFields: ExtraInputFields{"k1": "v1", "k2": true, "k3": 123}, + }, + } + + testingTCtx := context.WithValue(context.Background(), utils_test.TestingTContextKey, t) + + for idx, tstc := range withExtraInputFieldsTests { + tcCtx := context.WithValue(testingTCtx, utils_test.TestCaseIndexContextKey, idx) + tcCtx = context.WithValue(tcCtx, utils_test.TestCaseNameContextKey, tstc.name) + + mockOpaClienter := optionsMockOpaClienter{ + VerifyExtraInputFields: true, + ExpectExtraInputFields: tstc.expectExtraInputFields, + } + + auther := NewDefaultAuthorizer("app", + WithOpaClienter(&mockOpaClienter), + WithClaimsVerifier(NullClaimsVerifier), + ) + + configExtraInputFields, ok := tstc.configExtraInputFields.(ExtraInputFields) + if tstc.configExtraInputFields == nil || ok { + t.Logf("tst#%d: name=%s; calling option WithExtraInputFields(%#v)", + idx, tstc.name, configExtraInputFields) + auther = NewDefaultAuthorizer("app", + WithOpaClienter(&mockOpaClienter), + WithClaimsVerifier(NullClaimsVerifier), + WithExtraInputFields(configExtraInputFields), ) } @@ -84,6 +149,9 @@ func Test_WithEntitledServices_payload(t *testing.T) { type optionsMockOpaClienter struct { VerifyEntitledServices bool ExpectEntitledServices []string + + VerifyExtraInputFields bool + ExpectExtraInputFields ExtraInputFields } func (m optionsMockOpaClienter) String() string { @@ -120,5 +188,9 @@ func (m optionsMockOpaClienter) CustomQuery(ctx context.Context, document string t.Errorf("tst#%d: FAIL: name=%s; not equal: payload.EntitledServices=%#v; m.ExpectEntitledServices=%#v", tcIdx, tcName, payload.EntitledServices, m.ExpectEntitledServices) } + if m.VerifyExtraInputFields && !reflect.DeepEqual(payload.ExtraInputFields, m.ExpectExtraInputFields) { + t.Errorf("tst#%d: FAIL: name=%s; not equal: payload.ExtraInputFields=%#v; m.ExpectExtraInputFields=%#v", + tcIdx, tcName, payload.ExtraInputFields, m.ExpectExtraInputFields) + } return json.Unmarshal([]byte(`{"allow": true}`), resp) }