diff --git a/common/authorizer/authorizer.go b/common/authorizer/authorizer.go index f48562d..20d1de0 100644 --- a/common/authorizer/authorizer.go +++ b/common/authorizer/authorizer.go @@ -11,6 +11,14 @@ type ClaimsVerifier func([]string, []string) (string, []error) // (map of acct_id to map of service to array of features) type AcctEntitlementsType map[string]map[string][]string +// FilterCompartmentPermissionsType is a convenience data type, returned by FilterCompartmentPermissions() +// (map of application to array of permissions) +type FilterCompartmentPermissionsType []string + +// FilterCompartmentFeaturesType is a convenience data type, returned by FilterCompartmentFeatures() +// (map of application to array of feature) +type FilterCompartmentFeaturesType map[string][]string + // Authorizer interface is implemented for making arbitrary requests to Opa. type Authorizer interface { // Evaluate evaluates the authorization policy for the given request. @@ -31,4 +39,8 @@ type Authorizer interface { GetAcctEntitlements(ctx context.Context, accountIDs, serviceNames []string) (*AcctEntitlementsType, error) GetCurrentUserCompartments(ctx context.Context) ([]string, error) + + FilterCompartmentPermissions(ctx context.Context, permissions FilterCompartmentPermissionsType) (FilterCompartmentPermissionsType, error) + + FilterCompartmentFeatures(ctx context.Context, features FilterCompartmentFeaturesType) (FilterCompartmentFeaturesType, error) } diff --git a/common/authorizer/literal.go b/common/authorizer/literal.go index 0ba4bea..c0700b3 100644 --- a/common/authorizer/literal.go +++ b/common/authorizer/literal.go @@ -14,6 +14,12 @@ const ( // DefaultCurrentUserCompartmentsPath is default OPA path to fetch current user's compartments DefaultCurrentUserCompartmentsPath = "v1/data/authz/rbac/current_user_compartments" + // DefaultFilterCompartmentPermissionsApiPath is default OPA path to filter compartment permissions + DefaultFilterCompartmentPermissionsApiPath = "v1/data/authz/rbac/filter_compartment_permissions_api" + + // DefaultFilterCompartmentFeaturesApiPath is default OPA path to filter compartment features + DefaultFilterCompartmentFeaturesApiPath = "v1/data/authz/rbac/filter_compartment_features_api" + REDACTED = "redacted" TypeKey = ABACKey("ABACType") VerbKey = ABACKey("ABACVerb") diff --git a/common/authorizer/mock_Authorizer.go b/common/authorizer/mock_Authorizer.go index 8c78c8d..25bd116 100644 --- a/common/authorizer/mock_Authorizer.go +++ b/common/authorizer/mock_Authorizer.go @@ -108,3 +108,33 @@ func (mr *MockAuthorizerMockRecorder) GetCurrentUserCompartments(ctx context.Con mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentUserCompartments", reflect.TypeOf((*MockAuthorizer)(nil).GetCurrentUserCompartments), ctx) } + +// FilterCompartmentPermissions mocks base method. +func (m *MockAuthorizer) FilterCompartmentPermissions(ctx context.Context, permissions FilterCompartmentPermissionsType) (FilterCompartmentPermissionsType, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FilterCompartmentPermissions", ctx) + ret0, _ := ret[0].(FilterCompartmentPermissionsType) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FilterCompartmentPermissions indicates an expected call of FilterCompartmentPermissions. +func (mr *MockAuthorizerMockRecorder) FilterCompartmentPermissions(ctx context.Context, permissions FilterCompartmentPermissionsType) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterCompartmentPermissions", reflect.TypeOf((*MockAuthorizer)(nil).FilterCompartmentPermissions), ctx, permissions) +} + +// FilterCompartmentFeatures mocks base method. +func (m *MockAuthorizer) FilterCompartmentFeatures(ctx context.Context, features FilterCompartmentFeaturesType) (FilterCompartmentFeaturesType, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FilterCompartmentFeatures", ctx) + ret0, _ := ret[0].(FilterCompartmentFeaturesType) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FilterCompartmentFeatures indicates an expected call of FilterCompartmentFeatures. +func (mr *MockAuthorizerMockRecorder) FilterCompartmentFeatures(ctx context.Context, features FilterCompartmentFeaturesType) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterCompartmentFeatures", reflect.TypeOf((*MockAuthorizer)(nil).FilterCompartmentFeatures), ctx, features) +} diff --git a/common/opautil/util.go b/common/opautil/util.go index 9e12115..386654d 100644 --- a/common/opautil/util.go +++ b/common/opautil/util.go @@ -46,6 +46,7 @@ func (o OPAResponse) Obligations() (*ObligationsNode, error) { func RedactJWT(jwt string) string { parts := strings.Split(jwt, ".") + // Redact signature if len(parts) > 0 { parts[len(parts)-1] = az.REDACTED } @@ -54,7 +55,7 @@ func RedactJWT(jwt string) string { func RedactJWTForDebug(jwt string) string { parts := strings.Split(jwt, ".") - // Redact signature, header and body since we do not want to display any for debug logging + // Redact header/payload/signature, since we do not want to display any for debug logging for i := range parts { parts[i] = parts[i][:Min(len(parts[i]), 16)] + "/" + az.REDACTED } diff --git a/go.mod b/go.mod index 1ea11bb..27a5396 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 - github.com/infobloxopen/atlas-claims v1.0.0 + github.com/infobloxopen/atlas-claims v1.1.2 github.com/infobloxopen/seal v0.2.3 github.com/open-policy-agent/opa v0.37.2 github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index 3a96c44..be3021c 100644 --- a/go.sum +++ b/go.sum @@ -302,8 +302,8 @@ github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/infobloxopen/atlas-claims v1.0.0 h1:uGwFxbEGDZSql3ePeH/z/TA14IInGBNOkzOOGNrdrBI= -github.com/infobloxopen/atlas-claims v1.0.0/go.mod h1:6aN87f8OZRqQZ6abcI6tbHiXnE5QiTyNVd31Cb67hy8= +github.com/infobloxopen/atlas-claims v1.1.2 h1:IzKTrRYuXuaBL3gIIffaLT1rOMM7k0ApOPlI6MyP2pE= +github.com/infobloxopen/atlas-claims v1.1.2/go.mod h1:6aN87f8OZRqQZ6abcI6tbHiXnE5QiTyNVd31Cb67hy8= github.com/infobloxopen/seal v0.2.3 h1:TVIw52FxVVwehat/m23+hjoFXbIvyKBA9XCVI21p68A= github.com/infobloxopen/seal v0.2.3/go.mod h1:IHbkKw7rx7oJKNtyjHL+1XaGKo5NU8CjFE3ZpA5mrB8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/http_opa/authorizer.go b/http_opa/authorizer.go index 1e235de..044cd37 100644 --- a/http_opa/authorizer.go +++ b/http_opa/authorizer.go @@ -23,15 +23,17 @@ var SERVICENAME = "opa" // httpAuthorizer is an implementation of the az.Authorizer interface for HTTP-based authorization using OPA. type httpAuthorizer struct { - application string - clienter opa_client.Clienter - opaEvaluator az.OpaEvaluator - decisionInputHandler az.DecisionInputHandler - claimsVerifier az.ClaimsVerifier - entitledServices []string - acctEntitlementsApi string - currUserCompartmentsApi string - endpointModifier *EndpointModifier + application string + clienter opa_client.Clienter + opaEvaluator az.OpaEvaluator + decisionInputHandler az.DecisionInputHandler + claimsVerifier az.ClaimsVerifier + entitledServices []string + acctEntitlementsApi string + currUserCompartmentsApi string + filterCompartmentPermsApi string + filterCompartmentFeatsApi string + endpointModifier *EndpointModifier } var defDecisionInputer = new(az.DefaultDecisionInputer) @@ -40,11 +42,13 @@ var defDecisionInputer = new(az.DefaultDecisionInputer) func NewHttpAuthorizer(application string, opts ...Option) az.Authorizer { // Configuration options for the authorizer cfg := &Config{ - address: opa_client.DefaultAddress, - decisionInputHandler: defDecisionInputer, - claimsVerifier: commonClaim.UnverifiedClaimFromBearers, - acctEntitlementsApi: az.DefaultAcctEntitlementsApiPath, - currUserCompartmentsApi: az.DefaultCurrentUserCompartmentsPath, + address: opa_client.DefaultAddress, + decisionInputHandler: defDecisionInputer, + claimsVerifier: commonClaim.UnverifiedClaimFromBearers, + acctEntitlementsApi: az.DefaultAcctEntitlementsApiPath, + currUserCompartmentsApi: az.DefaultCurrentUserCompartmentsPath, + filterCompartmentPermsApi: az.DefaultFilterCompartmentPermissionsApiPath, + filterCompartmentFeatsApi: az.DefaultFilterCompartmentFeaturesApiPath, } for _, opt := range opts { opt(cfg) @@ -56,15 +60,17 @@ func NewHttpAuthorizer(application string, opts ...Option) az.Authorizer { } a := httpAuthorizer{ - clienter: clienter, - opaEvaluator: cfg.opaEvaluator, - application: application, - decisionInputHandler: cfg.decisionInputHandler, - claimsVerifier: cfg.claimsVerifier, - entitledServices: cfg.entitledServices, - acctEntitlementsApi: cfg.acctEntitlementsApi, - currUserCompartmentsApi: cfg.currUserCompartmentsApi, - endpointModifier: cfg.endpointModifier, + clienter: clienter, + opaEvaluator: cfg.opaEvaluator, + application: application, + decisionInputHandler: cfg.decisionInputHandler, + claimsVerifier: cfg.claimsVerifier, + entitledServices: cfg.entitledServices, + acctEntitlementsApi: cfg.acctEntitlementsApi, + currUserCompartmentsApi: cfg.currUserCompartmentsApi, + filterCompartmentPermsApi: cfg.filterCompartmentPermsApi, + filterCompartmentFeatsApi: cfg.filterCompartmentFeatsApi, + endpointModifier: cfg.endpointModifier, } return &a } diff --git a/http_opa/config.go b/http_opa/config.go index d8ef6cf..69acbea 100644 --- a/http_opa/config.go +++ b/http_opa/config.go @@ -44,15 +44,17 @@ type Config struct { // address to opa address string - clienter opa_client.Clienter - opaEvaluator az.OpaEvaluator - authorizer []az.Authorizer - decisionInputHandler az.DecisionInputHandler - claimsVerifier az.ClaimsVerifier - entitledServices []string - acctEntitlementsApi string - currUserCompartmentsApi string - endpointModifier *EndpointModifier + clienter opa_client.Clienter + opaEvaluator az.OpaEvaluator + authorizer []az.Authorizer + decisionInputHandler az.DecisionInputHandler + claimsVerifier az.ClaimsVerifier + entitledServices []string + acctEntitlementsApi string + currUserCompartmentsApi string + filterCompartmentPermsApi string + filterCompartmentFeatsApi string + endpointModifier *EndpointModifier } func (c Config) GetAuthorizer() []az.Authorizer { @@ -162,6 +164,20 @@ func WithCurrentUserCompartmentsPath(currUserCompartmentsApi string) Option { } } +// WithFilterComparmentPermissionsApiPath overrides default CurrentUserCompartmentsApiPath +func WithFilterComparmentPermissionsApiPath(filterCompartmentPermsApi string) Option { + return func(c *Config) { + c.filterCompartmentPermsApi = filterCompartmentPermsApi + } +} + +// WithFilterComparmentFeaturesApiPath overrides default CurrentUserCompartmentsApiPath +func WithFilterComparmentFeaturesApiPath(filterCompartmentFeatsApi string) Option { + return func(c *Config) { + c.filterCompartmentFeatsApi = filterCompartmentFeatsApi + } +} + // WithAcctSegmentsNeeded overrides default 0 func WithEndpointModifier(modifier *EndpointModifier) Option { return func(c *Config) { diff --git a/http_opa/filter_compartment_perms_feats.go b/http_opa/filter_compartment_perms_feats.go new file mode 100644 index 0000000..7e8eb6b --- /dev/null +++ b/http_opa/filter_compartment_perms_feats.go @@ -0,0 +1,117 @@ +package httpopa + +import ( + "context" + "fmt" + + "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" + az "github.com/infobloxopen/atlas-authz-middleware/v2/common/authorizer" + commonClaim "github.com/infobloxopen/atlas-authz-middleware/v2/common/claim" + "github.com/infobloxopen/atlas-authz-middleware/v2/common/opautil" + atlas_claims "github.com/infobloxopen/atlas-claims" + logrus "github.com/sirupsen/logrus" +) + +// FilterCompartmentPermissionsInput is the input payload for filter_compartment_permissions_api +type FilterCompartmentPermissionsInput struct { + JWT string `json:"jwt"` + Permissions az.FilterCompartmentPermissionsType `json:"permissions"` +} + +// FilterCompartmentPermissionsResult is the data type json.Unmarshaled from OPA RESTAPI query +// to filter_compartment_permissions_api rego rule +type FilterCompartmentPermissionsResult struct { + Result az.FilterCompartmentPermissionsType `json:"result"` +} + +// FilterCompartmentPermissions filters list of permissions based on the JWT in the context +func (a *httpAuthorizer) FilterCompartmentPermissions(ctx context.Context, permissions az.FilterCompartmentPermissionsType) (az.FilterCompartmentPermissionsType, error) { + lgNtry := ctxlogrus.Extract(ctx) + permsResult := FilterCompartmentPermissionsResult{} + + // This fetches auth data from auth headers in metadata from context: + // bearer = data from "authorization bearer" metadata header + // newBearer = data from "set-authorization bearer" metadata header + bearer, newBearer := atlas_claims.AuthBearersFromCtx(ctx) + + claimsVerifier := a.claimsVerifier + if claimsVerifier == nil { + claimsVerifier = commonClaim.UnverifiedClaimFromBearers + } + + rawJWT, errs := claimsVerifier([]string{bearer}, []string{newBearer}) + if len(errs) > 0 { + return nil, fmt.Errorf("%q", errs) + } + + opaReq := opautil.OPARequest{ + Input: &FilterCompartmentPermissionsInput{ + JWT: opautil.RedactJWT(rawJWT), + Permissions: permissions, + }, + } + + err := a.clienter.CustomQuery(ctx, a.filterCompartmentPermsApi, opaReq, &permsResult) + if err != nil { + lgNtry.WithError(err).Error("filter_compartment_permissions_fail") + return nil, err + } + + lgNtry.WithFields(logrus.Fields{ + "permsResult": fmt.Sprintf("%#v", permsResult), + }).Trace("filter_compartment_permissions_okay") + + return permsResult.Result, nil +} + +// FilterCompartmentFeaturesInput is the input payload for filter_compartment_features_api +type FilterCompartmentFeaturesInput struct { + JWT string `json:"jwt"` + ApplicationFeatures az.FilterCompartmentFeaturesType `json:"application_features"` +} + +// FilterCompartmentFeaturesResult is the data type json.Unmarshaled from OPA RESTAPI query +// to filter_compartment_features_api rego rule +type FilterCompartmentFeaturesResult struct { + Result az.FilterCompartmentFeaturesType `json:"result"` +} + +// FilterCompartmentFeatures filters list of features based on the JWT in the context +func (a *httpAuthorizer) FilterCompartmentFeatures(ctx context.Context, features az.FilterCompartmentFeaturesType) (az.FilterCompartmentFeaturesType, error) { + lgNtry := ctxlogrus.Extract(ctx) + featsResult := FilterCompartmentFeaturesResult{} + + // This fetches auth data from auth headers in metadata from context: + // bearer = data from "authorization bearer" metadata header + // newBearer = data from "set-authorization bearer" metadata header + bearer, newBearer := atlas_claims.AuthBearersFromCtx(ctx) + + claimsVerifier := a.claimsVerifier + if claimsVerifier == nil { + claimsVerifier = commonClaim.UnverifiedClaimFromBearers + } + + rawJWT, errs := claimsVerifier([]string{bearer}, []string{newBearer}) + if len(errs) > 0 { + return nil, fmt.Errorf("%q", errs) + } + + opaReq := opautil.OPARequest{ + Input: &FilterCompartmentFeaturesInput{ + JWT: opautil.RedactJWT(rawJWT), + ApplicationFeatures: features, + }, + } + + err := a.clienter.CustomQuery(ctx, a.filterCompartmentFeatsApi, opaReq, &featsResult) + if err != nil { + lgNtry.WithError(err).Error("filter_compartment_features_fail") + return nil, err + } + + lgNtry.WithFields(logrus.Fields{ + "featsResult": fmt.Sprintf("%#v", featsResult), + }).Trace("filter_compartment_features_okay") + + return featsResult.Result, nil +} diff --git a/http_opa/filter_compartment_perms_feats_test.go b/http_opa/filter_compartment_perms_feats_test.go new file mode 100644 index 0000000..ebc77bb --- /dev/null +++ b/http_opa/filter_compartment_perms_feats_test.go @@ -0,0 +1,394 @@ +package httpopa + +import ( + "context" + "io/ioutil" + "reflect" + "sort" + "testing" + "time" + + "github.com/infobloxopen/atlas-authz-middleware/v2/pkg/opa_client" + "github.com/infobloxopen/atlas-authz-middleware/v2/utils_test" + atlas_claims "github.com/infobloxopen/atlas-claims" + + "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" + logrus "github.com/sirupsen/logrus" +) + +func TestFilterCompartmentPermissionsOpa(t *testing.T) { + stdLoggr := logrus.StandardLogger() + ctx, cancel := context.WithCancel(context.Background()) + ctx = context.WithValue(ctx, utils_test.TestingTContextKey, t) + ctx = ctxlogrus.ToContext(ctx, logrus.NewEntry(stdLoggr)) + + done := make(chan struct{}) + clienter := utils_test.StartOpa(ctx, t, done) + cli, ok := clienter.(*opa_client.Client) + if !ok { + t.Fatal("Unable to convert interface to (*Client)") + return + } + + // Errors above here will leak containers + defer func() { + cancel() + // Wait for container to be shutdown + <-done + }() + + policyRego, err := ioutil.ReadFile("testdata/mock_authz_policy.rego") + if err != nil { + t.Fatalf("ReadFile fatal err: %#v", err) + return + } + + var resp interface{} + err = cli.UploadRegoPolicy(ctx, "mock_authz_policyid", policyRego, resp) + if err != nil { + t.Fatalf("OpaUploadPolicy fatal err: %#v", err) + return + } + + auther := NewHttpAuthorizer("bogus_unused_application_value", + WithOpaClienter(cli), + ) + + testCases := []struct { + name string + cptId string + inpVal []string + expVal []string + }{ + { + name: "empty-compartment", + cptId: "", + inpVal: []string{"user-view", "tag-read"}, + expVal: []string{"user-view", "tag-read"}, + }, + { + name: "root-compartment", + cptId: ".", + inpVal: []string{"user-view", "tag-read"}, + expVal: []string{"user-view", "tag-read"}, + }, + { + name: "non-root-compartment", + cptId: "green.", + inpVal: []string{"user-view", "tag-read"}, + expVal: []string{"filtered-perm-a", "filtered-perm-b"}, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + claims := &atlas_claims.Claims{ + CompartmentID: tt.cptId, + } + + jwt, err := atlas_claims.BuildJwt(claims, "some-hmac-key-we-dont-care", time.Hour*9) + if err != nil { + t.Fatalf("FAIL: BuildJwt() unexpected err=%v", err) + } + + ttCtx := utils_test.ContextWithJWT(ctx, jwt) + + gotVal, err := auther.FilterCompartmentPermissions(ttCtx, tt.inpVal) + if err != nil { + t.Errorf("FAIL: FilterCompartmentPermissions() unexpected err=%v", err) + } + + sort.Strings(gotVal) + //t.Logf("gotVal=%#v", gotVal) + + sort.Strings(tt.expVal) + if !reflect.DeepEqual([]string(gotVal), tt.expVal) { + t.Errorf("FAIL:\ngotVal: %#v\nexpVal: %#v", + gotVal, tt.expVal) + } + }) + } +} + +func TestFilterCompartmentPermissionsMockOpaClient(t *testing.T) { + testCases := []struct { + name string + respJson string + expErr bool + expVal []string + }{ + { + name: `valid result`, + respJson: `{ "result": [ "red.", "green.", "blue." ] }`, + expErr: false, + expVal: []string{"red.", "green.", "blue."}, + }, + { + name: `null result ok`, + respJson: `{ "result": null }`, + expErr: false, + expVal: nil, + }, + { + name: `empty result ok`, + respJson: `{ "result": [] }`, + expErr: false, + expVal: []string{}, + }, + { + name: `incorrect result type`, + respJson: `[ null ]`, + expErr: true, + expVal: nil, + }, + { + name: `no result key`, + respJson: `{ "rresult": null }`, + expErr: false, + expVal: nil, + }, + { + name: `invalid result object`, + respJson: `{ "result": { "one": 1, "two": 2 } }`, + expErr: true, + expVal: nil, + }, + } + + stdLoggr := logrus.StandardLogger() + ctx := context.WithValue(context.Background(), utils_test.TestingTContextKey, t) + ctx = ctxlogrus.ToContext(ctx, logrus.NewEntry(stdLoggr)) + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + mockOpaClienter := utils_test.MockOpaClienter{ + Loggr: stdLoggr, + RegoRespJSON: tt.respJson, + } + auther := NewHttpAuthorizer("bogus_unused_application_value", + WithOpaClienter(&mockOpaClienter), + ) + + claims := &atlas_claims.Claims{} + jwt, err := atlas_claims.BuildJwt(claims, "some-hmac-key-we-dont-care", time.Hour*9) + if err != nil { + t.Fatalf("FAIL: BuildJwt() unexpected err=%v", err) + } + ttCtx := utils_test.ContextWithJWT(ctx, jwt) + + gotVal, gotErr := auther.FilterCompartmentPermissions(ttCtx, nil) + //t.Logf("gotErr=%#v, gotVal=%#v", gotVal, gotErr) + + if tt.expErr && gotErr == nil { + t.Errorf("FAIL: expected err, but got no err") + } else if !tt.expErr && gotErr != nil { + t.Errorf("FAIL: got unexpected err=%s", gotErr) + } + + if gotErr != nil && gotVal != nil { + t.Errorf("FAIL: returned val should be nil if err returned") + } + + if !reflect.DeepEqual([]string(gotVal), tt.expVal) { + t.Errorf("FAIL: expVal=%#v gotVal=%#v", + tt.expVal, gotVal) + } + }) + } +} + +func TestFilterCompartmentFeaturesOpa(t *testing.T) { + stdLoggr := logrus.StandardLogger() + ctx, cancel := context.WithCancel(context.Background()) + ctx = context.WithValue(ctx, utils_test.TestingTContextKey, t) + ctx = ctxlogrus.ToContext(ctx, logrus.NewEntry(stdLoggr)) + + done := make(chan struct{}) + clienter := utils_test.StartOpa(ctx, t, done) + cli, ok := clienter.(*opa_client.Client) + if !ok { + t.Fatal("Unable to convert interface to (*Client)") + return + } + + // Errors above here will leak containers + defer func() { + cancel() + // Wait for container to be shutdown + <-done + }() + + policyRego, err := ioutil.ReadFile("testdata/mock_authz_policy.rego") + if err != nil { + t.Fatalf("ReadFile fatal err: %#v", err) + return + } + + var resp interface{} + err = cli.UploadRegoPolicy(ctx, "mock_authz_policyid", policyRego, resp) + if err != nil { + t.Fatalf("OpaUploadPolicy fatal err: %#v", err) + return + } + + auther := NewHttpAuthorizer("bogus_unused_application_value", + WithOpaClienter(cli), + ) + + testCases := []struct { + name string + cptId string + inpVal map[string][]string + expVal map[string][]string + }{ + { + name: "empty-compartment", + cptId: "", + inpVal: map[string][]string{"ddi": {"dhcp", "ipam"}, "ui": {"anycast"}}, + expVal: map[string][]string{"ddi": {"dhcp", "ipam"}, "ui": {"anycast"}}, + }, + { + name: "root-compartment", + cptId: ".", + inpVal: map[string][]string{"ddi": {"dhcp", "ipam"}, "ui": {"anycast"}}, + expVal: map[string][]string{"ddi": {"dhcp", "ipam"}, "ui": {"anycast"}}, + }, + { + name: "non-root-compartment", + cptId: "green.", + inpVal: map[string][]string{"ddi": {"dhcp", "ipam"}, "ui": {"anycast"}}, + expVal: map[string][]string{ + "filtered-app-a": {"filtered-app-a-feat-a", "filtered-app-a-feat-b"}, + "filtered-app-b": {"filtered-app-b-feat-a"}, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + claims := &atlas_claims.Claims{ + CompartmentID: tt.cptId, + } + + jwt, err := atlas_claims.BuildJwt(claims, "some-hmac-key-we-dont-care", time.Hour*9) + if err != nil { + t.Fatalf("FAIL: BuildJwt() unexpected err=%v", err) + } + + ttCtx := utils_test.ContextWithJWT(ctx, jwt) + + gotVal, err := auther.FilterCompartmentFeatures(ttCtx, tt.inpVal) + if err != nil { + t.Errorf("FAIL: FilterCompartmentFeatures() unexpected err=%v", err) + } + + //sort.Strings(gotVal) + //t.Logf("gotVal=%#v", gotVal) + + //sort.Strings(tt.expVal) + if !reflect.DeepEqual(map[string][]string(gotVal), tt.expVal) { + t.Errorf("FAIL:\ngotVal: %#v\nexpVal: %#v", + gotVal, tt.expVal) + } + }) + } +} + +func TestFilterCompartmentFeaturesMockOpaClient(t *testing.T) { + testCases := []struct { + name string + respJson string + expErr bool + expVal map[string][]string + }{ + { + name: `valid result`, + respJson: `{ "result": { "app": [ "feat-a", "feat-b" ] } }`, + expErr: false, + expVal: map[string][]string{"app": {"feat-a", "feat-b"}}, + }, + { + name: `null result ok`, + respJson: `{ "result": null }`, + expErr: false, + expVal: nil, + }, + { + name: `empty result ok`, + respJson: `{ "result": {} }`, + expErr: false, + expVal: map[string][]string{}, + }, + { + name: `null features ok`, + respJson: `{ "result": { "app": null } }`, + expErr: false, + expVal: map[string][]string{"app": nil}, + }, + { + name: `empty features ok`, + respJson: `{ "result": { "app": [] } }`, + expErr: false, + expVal: map[string][]string{"app": {}}, + }, + { + name: `incorrect result type`, + respJson: `[ null ]`, + expErr: true, + expVal: nil, + }, + { + name: `no result key`, + respJson: `{ "rresult": null }`, + expErr: false, + expVal: nil, + }, + { + name: `invalid result object`, + respJson: `{ "result": [ "one", "two" ] }`, + expErr: true, + expVal: nil, + }, + } + + stdLoggr := logrus.StandardLogger() + ctx := context.WithValue(context.Background(), utils_test.TestingTContextKey, t) + ctx = ctxlogrus.ToContext(ctx, logrus.NewEntry(stdLoggr)) + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + mockOpaClienter := utils_test.MockOpaClienter{ + Loggr: stdLoggr, + RegoRespJSON: tt.respJson, + } + auther := NewHttpAuthorizer("bogus_unused_application_value", + WithOpaClienter(&mockOpaClienter), + ) + + claims := &atlas_claims.Claims{} + jwt, err := atlas_claims.BuildJwt(claims, "some-hmac-key-we-dont-care", time.Hour*9) + if err != nil { + t.Fatalf("FAIL: BuildJwt() unexpected err=%v", err) + } + ttCtx := utils_test.ContextWithJWT(ctx, jwt) + + gotVal, gotErr := auther.FilterCompartmentFeatures(ttCtx, nil) + //t.Logf("gotErr=%#v, gotVal=%#v", gotVal, gotErr) + + if tt.expErr && gotErr == nil { + t.Errorf("FAIL: expected err, but got no err") + } else if !tt.expErr && gotErr != nil { + t.Errorf("FAIL: got unexpected err=%s", gotErr) + } + + if gotErr != nil && gotVal != nil { + t.Errorf("FAIL: returned val should be nil if err returned") + } + + if !reflect.DeepEqual(map[string][]string(gotVal), tt.expVal) { + t.Errorf("FAIL: expVal=%#v gotVal=%#v", + tt.expVal, gotVal) + } + }) + } +} diff --git a/http_opa/testdata/mock_authz_policy.rego b/http_opa/testdata/mock_authz_policy.rego index 53ad113..8a7cfeb 100644 --- a/http_opa/testdata/mock_authz_policy.rego +++ b/http_opa/testdata/mock_authz_policy.rego @@ -136,7 +136,7 @@ group_compartment_roles := { "40": { "all-resources": { "custom-admin-group": { - "root-compartment": [ + ".": [ "custom-admin-role", "administrator-role" ], @@ -146,7 +146,7 @@ group_compartment_roles := { ] }, "user-group-40": { - "root-compartment": [ + ".": [ "custom-admin-role", "administrator-role" ], @@ -165,7 +165,7 @@ group_compartment_roles := { "16": { "all-resources": { "custom-admin-group-16": { - "root-compartment": [ + ".": [ "custom-admin-role", "administrator-role" ], @@ -175,7 +175,7 @@ group_compartment_roles := { ] }, "user-group-16": { - "root-compartment": [ + ".": [ "custom-admin-role", "administrator-role" ], @@ -194,7 +194,7 @@ group_compartment_roles := { "3101": { "all-resources": { "devops-group": { - "root-compartment": [ + ".": [ "widget-role-read" ], "red.": [ @@ -209,7 +209,7 @@ group_compartment_roles := { ] }, "secops-group": { - "root-compartment": [ + ".": [ "gadget-role-read", "gadget-role-list" ], @@ -238,7 +238,7 @@ group_compartment_roles := { } # Well-known hardcoded root-compartment id used throughout AuthZ/Identity code -ROOT_COMPARTMENT_ID := "root-compartment" +ROOT_COMPARTMENT_ID := "." current_user_compartments[compartment] { compartment != ROOT_COMPARTMENT_ID @@ -262,6 +262,72 @@ test_current_user_compartments { {"compartment-40-red.", "compartment-40-green."}) } +filter_compartment_permissions_api = filtered_perm_arr { + count(trim_space(merged_input.compartment_id)) > 0 + merged_input.compartment_id != ROOT_COMPARTMENT_ID + filtered_perm_arr := ["filtered-perm-a", "filtered-perm-b"] +} else = filtered_perm_arr { + filtered_perm_arr := input.permissions +} + +filter_compartment_features_api = filtered_feat_map { + count(trim_space(merged_input.compartment_id)) > 0 + merged_input.compartment_id != ROOT_COMPARTMENT_ID + filtered_feat_map := { + "filtered-app-a": ["filtered-app-a-feat-a", "filtered-app-a-feat-b"], + "filtered-app-b": ["filtered-app-b-feat-a"], + } +} else = filtered_feat_map { + filtered_feat_map := input.application_features +} + +filter_compartment_permissions_test_fn(cpt_id, perm_arr, exp_set) { + got_set := filter_compartment_permissions_api with input as { + "compartment_id": cpt_id, + "permissions": perm_arr, + } + trace(sprintf("got_set: %v", [got_set])) + trace(sprintf("exp_set: %v", [exp_set])) + got_set == exp_set +} + +filter_compartment_features_test_fn(cpt_id, app_feat_map, exp_map) { + got_map := filter_compartment_features_api with input as { + "compartment_id": cpt_id, + "application_features": app_feat_map, + } + trace(sprintf("got_map: %v", [got_map])) + trace(sprintf("exp_map: %v", [exp_map])) + got_map == exp_map +} + +test_filter_compartment_api { + filter_compartment_permissions_test_fn("", + ["user-view", "tag-read"], + ["user-view", "tag-read"]) + + filter_compartment_permissions_test_fn(".", + ["user-view", "tag-read"], + ["user-view", "tag-read"]) + + filter_compartment_permissions_test_fn("green.", + ["user-view", "tag-read"], + ["filtered-perm-a", "filtered-perm-b"]) + + filter_compartment_features_test_fn("", + {"ddi": ["dhcp", "ipam"], "ui": ["anycast"]}, + {"ddi": ["dhcp", "ipam"], "ui": ["anycast"]}) + + filter_compartment_features_test_fn(".", + {"ddi": ["dhcp", "ipam"], "ui": ["anycast"]}, + {"ddi": ["dhcp", "ipam"], "ui": ["anycast"]}) + + filter_compartment_features_test_fn("green.", + {"ddi": ["dhcp", "ipam"], "ui": ["anycast"]}, + {"filtered-app-a": ["filtered-app-a-feat-a", "filtered-app-a-feat-b"], + "filtered-app-b": ["filtered-app-b-feat-a"]}) +} + # opa test -v mock_authz_policy.rego # opa run --server mock_authz_policy.rego # curl -X GET -H 'Content-Type: application/json' http://localhost:8181/v1/data/authz/rbac/acct_entitlements_api | jq .