diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c0c89b0..a889a40 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,7 +23,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: 1.15.x + go-version: 1.21.x - name: Unit tests if: ${{ !matrix.e2e }} run: | diff --git a/cmd/authz_mw_cli/authz_mw_cli.go b/cmd/authz_mw_cli/authz_mw_cli.go index f278efc..bd8d0b9 100644 --- a/cmd/authz_mw_cli/authz_mw_cli.go +++ b/cmd/authz_mw_cli/authz_mw_cli.go @@ -14,14 +14,15 @@ import ( "regexp" "strings" - opamw "github.com/infobloxopen/atlas-authz-middleware/grpc_opa" - opacl "github.com/infobloxopen/atlas-authz-middleware/pkg/opa_client" + "github.com/infobloxopen/atlas-authz-middleware/v2/http_opa" + opacl "github.com/infobloxopen/atlas-authz-middleware/v2/pkg/opa_client" "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" logrus "github.com/sirupsen/logrus" + az "github.com/infobloxopen/atlas-authz-middleware/v2/common/authorizer" "google.golang.org/grpc/metadata" ) @@ -107,9 +108,10 @@ func validate(ctx context.Context, opaIpPort string) { var decInputr MyDecisionInputr decInputr.DecisionInput.DecisionDocument = decisionDoc - authzr := opamw.NewDefaultAuthorizer(app, - opamw.WithAddress(opaIpPort), - opamw.WithDecisionInputHandler(&decInputr), + //TODO: add a flag to indicate whether to use http authorizer or grpc authorizer, when grpc authorizer is implemented in v2 + authzr := httpopa.NewHttpAuthorizer(app, + httpopa.WithAddress(opaIpPort), + httpopa.WithDecisionInputHandler(&decInputr), ) resultCtx, resultErr := authzr.AffirmAuthorization(ctx, fullMethod, nil) @@ -139,9 +141,9 @@ func acct_entitlements(ctx context.Context, opaIpPort string) { } type MyDecisionInputr struct { - opamw.DecisionInput + az.DecisionInput } -func (d MyDecisionInputr) GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*opamw.DecisionInput, error) { +func (d MyDecisionInputr) GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*az.DecisionInput, error) { return &d.DecisionInput, nil } diff --git a/common/authorizer/authorizer.go b/common/authorizer/authorizer.go new file mode 100644 index 0000000..cf94c78 --- /dev/null +++ b/common/authorizer/authorizer.go @@ -0,0 +1,26 @@ +package authorizer + +import "context" + +// OpaEvaluator implements calling OPA with a request and receiving the raw response +type OpaEvaluator func(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error + +type ClaimsVerifier func([]string, []string) (string, []error) + +// Authorizer interface is implemented for making arbitrary requests to Opa. +type Authorizer interface { + // Evaluate evaluates the authorization policy for the given request. + // It takes the context, full method name, request object, and an OpaEvaluator as input. + // It returns a boolean indicating whether the request is authorized, a modified context, + // and an error if any. + Evaluate(ctx context.Context, fullMethod string, req interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) + + // OpaQuery executes a query against the OPA (Open Policy Agent) with the specified decision document. + // If the decision document is an empty string, the query is executed against the default decision document + // configured in OPA. + // It takes the context, decision document name, OPA request object, and OPA response object as input. + // It returns an error if any. + OpaQuery(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error + + AffirmAuthorization(ctx context.Context, fullMethod string, eq interface{}) (context.Context, error) +} diff --git a/common/authorizer/decision_input.go b/common/authorizer/decision_input.go new file mode 100644 index 0000000..8e009b0 --- /dev/null +++ b/common/authorizer/decision_input.go @@ -0,0 +1,47 @@ +package authorizer + +import "context" + +// DecisionInput is app/service-specific data supplied by app/service ABAC requests +type DecisionInput struct { + Type string `json:"type"` // Object/resource-type to match + Verb string `json:"verb"` // Verb to match + SealCtx []interface{} `json:"ctx"` // Array of app/service-specific context data to match + DecisionDocument string `json:"-"` // OPA decision document to query, by default "", + // which is default decision document configured in OPA +} + +// fullMethod is of the form "Service.FullMethod" +type DecisionInputHandler interface { + // GetDecisionInput returns an app/service-specific DecisionInput. + // A nil DecisionInput should NOT be returned unless error. + GetDecisionInput(ctx context.Context, fullMethod string, req interface{}) (*DecisionInput, error) +} + +// DefaultDecisionInputer is an example DecisionInputHandler that is used as default +type DefaultDecisionInputer struct{} + +func (m DefaultDecisionInputer) String() string { + return "authorizer.DefaultDecisionInputer{}" +} + +// GetDecisionInput is an example DecisionInputHandler that returns some decision input +// based on some incoming Context values. App/services will most likely supply their +// own DecisionInputHandler using WithDecisionInputHandler option. +func (m *DefaultDecisionInputer) GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*DecisionInput, error) { + var abacType string + if v, ok := ctx.Value(TypeKey).(string); ok { + abacType = v + } + + var abacVerb string + if v, ok := ctx.Value(VerbKey).(string); ok { + abacVerb = v + } + + decInp := DecisionInput{ + Type: abacType, + Verb: abacVerb, + } + return &decInp, nil +} diff --git a/common/authorizer/literal.go b/common/authorizer/literal.go new file mode 100644 index 0000000..502519e --- /dev/null +++ b/common/authorizer/literal.go @@ -0,0 +1,15 @@ +package authorizer + +// ABACKey is a context.Context key type +type ABACKey string +type ObligationKey string + +const ( + // DefaultValidatePath is default OPA path to perform authz validation + DefaultValidatePath = "v1/data/authz/rbac/validate_v1" + + REDACTED = "redacted" + TypeKey = ABACKey("ABACType") + VerbKey = ABACKey("ABACVerb") + ObKey = ObligationKey("obligations") +) diff --git a/common/authorizer/mock_Authorizer.go b/common/authorizer/mock_Authorizer.go new file mode 100644 index 0000000..4a32bd0 --- /dev/null +++ b/common/authorizer/mock_Authorizer.go @@ -0,0 +1,80 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: authorizer.go + +// Package authorizer is a generated GoMock package. +package authorizer + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockAuthorizer is a mock of Authorizer interface. +type MockAuthorizer struct { + ctrl *gomock.Controller + recorder *MockAuthorizerMockRecorder +} + +// MockAuthorizerMockRecorder is the mock recorder for MockAuthorizer. +type MockAuthorizerMockRecorder struct { + mock *MockAuthorizer +} + +// NewMockAuthorizer creates a new mock instance. +func NewMockAuthorizer(ctrl *gomock.Controller) *MockAuthorizer { + mock := &MockAuthorizer{ctrl: ctrl} + mock.recorder = &MockAuthorizerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAuthorizer) EXPECT() *MockAuthorizerMockRecorder { + return m.recorder +} + +// AffirmAuthorization mocks base method. +func (m *MockAuthorizer) AffirmAuthorization(ctx context.Context, fullMethod string, eq interface{}) (context.Context, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AffirmAuthorization", ctx, fullMethod, eq) + ret0, _ := ret[0].(context.Context) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AffirmAuthorization indicates an expected call of AffirmAuthorization. +func (mr *MockAuthorizerMockRecorder) AffirmAuthorization(ctx, fullMethod, eq interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AffirmAuthorization", reflect.TypeOf((*MockAuthorizer)(nil).AffirmAuthorization), ctx, fullMethod, eq) +} + +// Evaluate mocks base method. +func (m *MockAuthorizer) Evaluate(ctx context.Context, fullMethod string, req interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Evaluate", ctx, fullMethod, req, opaEvaluator) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(context.Context) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Evaluate indicates an expected call of Evaluate. +func (mr *MockAuthorizerMockRecorder) Evaluate(ctx, fullMethod, req, opaEvaluator interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Evaluate", reflect.TypeOf((*MockAuthorizer)(nil).Evaluate), ctx, fullMethod, req, opaEvaluator) +} + +// OpaQuery mocks base method. +func (m *MockAuthorizer) OpaQuery(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OpaQuery", ctx, decisionDocument, opaReq, opaResp) + ret0, _ := ret[0].(error) + return ret0 +} + +// OpaQuery indicates an expected call of OpaQuery. +func (mr *MockAuthorizerMockRecorder) OpaQuery(ctx, decisionDocument, opaReq, opaResp interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpaQuery", reflect.TypeOf((*MockAuthorizer)(nil).OpaQuery), ctx, decisionDocument, opaReq, opaResp) +} diff --git a/grpc_opa/claims.go b/common/claim/claims.go similarity index 98% rename from grpc_opa/claims.go rename to common/claim/claims.go index 7c467a1..498be92 100644 --- a/grpc_opa/claims.go +++ b/common/claim/claims.go @@ -1,4 +1,4 @@ -package grpc_opa_middleware +package claim import ( atlas_claims "github.com/infobloxopen/atlas-claims" diff --git a/common/literal.go b/common/literal.go new file mode 100644 index 0000000..ca4d587 --- /dev/null +++ b/common/literal.go @@ -0,0 +1,6 @@ +package common + +const ( + // DefaultAcctEntitlementsApiPath is default OPA path to fetch acct entitlements + DefaultAcctEntitlementsApiPath = "v1/data/authz/rbac/acct_entitlements_api" +) diff --git a/grpc_opa/nil_interface.go b/common/nil_interface.go similarity index 93% rename from grpc_opa/nil_interface.go rename to common/nil_interface.go index 6f6d289..d648093 100644 --- a/grpc_opa/nil_interface.go +++ b/common/nil_interface.go @@ -1,4 +1,4 @@ -package grpc_opa_middleware +package common import ( "reflect" diff --git a/grpc_opa/nil_interface_test.go b/common/nil_interface_test.go similarity index 98% rename from grpc_opa/nil_interface_test.go rename to common/nil_interface_test.go index 54a4fe2..230296a 100644 --- a/grpc_opa/nil_interface_test.go +++ b/common/nil_interface_test.go @@ -1,4 +1,4 @@ -package grpc_opa_middleware +package common import ( "reflect" @@ -23,30 +23,41 @@ func Test_IsNilInterface(t *testing.T) { var uninitializedStringVar string type myStructType struct{} + var uninitializedStructVar myStructType type myPtrType *myStructType + var uninitializedPtrVar myPtrType type myMapType map[string]string + var uninitializedMapVar myMapType type myArrayType [3]string + var uninitializedArrayVar myArrayType type mySliceType []string + var uninitializedSliceVar mySliceType type myFuncType func(*testing.T) + var uninitializedFuncVar myFuncType -func myTestFunc(*testing.T){} + +func myTestFunc(*testing.T) {} + var initializedFuncVar myFuncType = myTestFunc type myChanType chan string + var uninitializedChanVar myChanType type myInterfaceType interface{ myMethod() } + func (myST *myStructType) myMethod() {} + var uninitializedStructPtrVar *myStructType var uninitializedInterfaceVar myInterfaceType var nilinitializedInterfaceVar myInterfaceType = uninitializedStructPtrVar diff --git a/common/opautil/entitled_features.go b/common/opautil/entitled_features.go new file mode 100644 index 0000000..572dcb6 --- /dev/null +++ b/common/opautil/entitled_features.go @@ -0,0 +1,22 @@ +package opautil + +import "context" + +// EntitledFeaturesKeyType is the type of the entitled_features key stored in the caller's context +type EntitledFeaturesKeyType string + +// EntitledFeaturesKey is the entitled_features key stored in the caller's context. +// It is also the entitled_features key in the OPA response. +const EntitledFeaturesKey = EntitledFeaturesKeyType("entitled_features") + +// AddRawEntitledFeatures adds raw entitled_features (if they exist) from OPAResponse to context +// The raw JSON-unmarshaled entitled_features is of the form: +// +// map[string]interface {}{"lic":[]interface {}{"dhcp", "ipam"}, "rpz":[]interface {}{"bogon", "malware"}}} +func (o OPAResponse) AddRawEntitledFeatures(ctx context.Context) context.Context { + efIfc, ok := o[string(EntitledFeaturesKey)] + if ok { + ctx = context.WithValue(ctx, EntitledFeaturesKey, efIfc) + } + return ctx +} diff --git a/grpc_opa/obligations.go b/common/opautil/obligations.go similarity index 89% rename from grpc_opa/obligations.go rename to common/opautil/obligations.go index 98d6960..d5cdd42 100644 --- a/grpc_opa/obligations.go +++ b/common/opautil/obligations.go @@ -1,17 +1,12 @@ -package grpc_opa_middleware +package opautil import ( "encoding/json" "sort" "strings" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -var ( - // ErrInvalidObligations is returned upon invalid obligations - ErrInvalidObligations = status.Errorf(codes.Internal, "Invalid obligations") + "github.com/infobloxopen/atlas-authz-middleware/v2/common" + "github.com/infobloxopen/atlas-authz-middleware/v2/http_opa/exception" ) // ObligationsEnum enumerates the different kinds of ObligationsNode @@ -135,7 +130,7 @@ func (o8n *ObligationsNode) DeepSort() { // parseOPAObligations parses the obligations returned from OPA // and returns them in standard format. -func parseOPAObligations(opaObligations interface{}) (*ObligationsNode, error) { +func ParseOPAObligations(opaObligations interface{}) (*ObligationsNode, error) { if opaObligations == nil { return nil, nil } @@ -149,13 +144,13 @@ func parseOPAObligations(opaObligations interface{}) (*ObligationsNode, error) { return parseObligationsMap(mapIfc) } - return nil, ErrInvalidObligations + return nil, exception.ErrAbstrInvalidObligations } // obligations json.Unmarshal()'d as type: // []interface {}{[]interface {}{"ctx.metric == \"dhcp\""}} func parseObligationsArray(arrIfc []interface{}) (*ObligationsNode, error) { - if IsNilInterface(arrIfc) { + if common.IsNilInterface(arrIfc) { return nil, nil } @@ -164,7 +159,7 @@ func parseObligationsArray(arrIfc []interface{}) (*ObligationsNode, error) { } for _, subIfc := range arrIfc { - if IsNilInterface(subIfc) { + if common.IsNilInterface(subIfc) { continue } @@ -174,13 +169,13 @@ func parseObligationsArray(arrIfc []interface{}) (*ObligationsNode, error) { subArrIfc, ok := subIfc.([]interface{}) if !ok { - return nil, ErrInvalidObligations + return nil, exception.ErrAbstrInvalidObligations } for _, itemIfc := range subArrIfc { s, ok := itemIfc.(string) if !ok { - return nil, ErrInvalidObligations + return nil, exception.ErrAbstrInvalidObligations } leafNode := &ObligationsNode{ @@ -210,7 +205,7 @@ func parseObligationsArray(arrIfc []interface{}) (*ObligationsNode, error) { // obligations json.Unmarshal()'d as type: // map[string]interface {}{"policy1_guid":map[string]interface {}{"stmt0":[]interface {}{"ctx.metric == \"dhcp\""}}} func parseObligationsMap(mapIfc map[string]interface{}) (*ObligationsNode, error) { - if IsNilInterface(mapIfc) { + if common.IsNilInterface(mapIfc) { return nil, nil } @@ -219,7 +214,7 @@ func parseObligationsMap(mapIfc map[string]interface{}) (*ObligationsNode, error } for policyName, subIfc := range mapIfc { - if IsNilInterface(subIfc) { + if common.IsNilInterface(subIfc) { continue } @@ -229,7 +224,7 @@ func parseObligationsMap(mapIfc map[string]interface{}) (*ObligationsNode, error stmtMapIfc, ok := subIfc.(map[string]interface{}) if !ok { - return nil, ErrInvalidObligations + return nil, exception.ErrAbstrInvalidObligations } policyNode := &ObligationsNode{ @@ -240,7 +235,7 @@ func parseObligationsMap(mapIfc map[string]interface{}) (*ObligationsNode, error for stmtName, stmtIfc := range stmtMapIfc { subArrIfc, ok := stmtIfc.([]interface{}) if !ok { - return nil, ErrInvalidObligations + return nil, exception.ErrAbstrInvalidObligations } stmtNode := &ObligationsNode{ @@ -251,7 +246,7 @@ func parseObligationsMap(mapIfc map[string]interface{}) (*ObligationsNode, error for _, itemIfc := range subArrIfc { s, ok := itemIfc.(string) if !ok { - return nil, ErrInvalidObligations + return nil, exception.ErrAbstrInvalidObligations } leafNode := &ObligationsNode{ diff --git a/grpc_opa/obligations_test.go b/common/opautil/obligations_test.go similarity index 81% rename from grpc_opa/obligations_test.go rename to common/opautil/obligations_test.go index 93ce3f7..090476b 100644 --- a/grpc_opa/obligations_test.go +++ b/common/opautil/obligations_test.go @@ -1,4 +1,4 @@ -package grpc_opa_middleware +package opautil import ( "encoding/json" @@ -6,7 +6,10 @@ import ( "strings" "testing" - "github.com/infobloxopen/seal/pkg/compiler/sql" + az "github.com/infobloxopen/atlas-authz-middleware/v2/common/authorizer" + "github.com/infobloxopen/atlas-authz-middleware/v2/http_opa/exception" + + sqlcompiler "github.com/infobloxopen/seal/pkg/compiler/sql" ) func Test_parseOPAObligations(t *testing.T) { @@ -21,14 +24,14 @@ func Test_parseOPAObligations(t *testing.T) { } t.Logf("tst#%d: resp=%#v", idx, resp) - if _, ok := resp[string(ObKey)]; !ok { + if _, ok := resp[string(az.ObKey)]; !ok { if strings.Contains(tst.regoRespJSON, `"obligations":`) { t.Errorf("tst#%d: FAIL: '%s' key not found in OPAResponse", - idx, string(ObKey)) + idx, string(az.ObKey)) } continue } - actualVal, actualErr := parseOPAObligations(resp[string(ObKey)]) + actualVal, actualErr := ParseOPAObligations(resp[string(az.ObKey)]) if actualErr != tst.expectedErr { t.Errorf("tst#%d: FAIL: expectedErr=%s actualErr=%s", @@ -77,7 +80,7 @@ var obligationsNodeTests = []struct { expectedSQL string }{ { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true }`, @@ -86,7 +89,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: ErrInvalidObligations, + expectedErr: exception.ErrAbstrInvalidObligations, regoRespJSON: `{ "allow": true, "obligations": "bad obligations value" @@ -96,7 +99,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: ErrInvalidObligations, + expectedErr: exception.ErrAbstrInvalidObligations, regoRespJSON: `{ "allow": true, "obligations": [ "bad obligations value" ] @@ -106,7 +109,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: ErrInvalidObligations, + expectedErr: exception.ErrAbstrInvalidObligations, regoRespJSON: `{ "allow": true, "obligations": [ [ 3.14 ] ] @@ -116,7 +119,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: ErrInvalidObligations, + expectedErr: exception.ErrAbstrInvalidObligations, regoRespJSON: `{ "allow": true, "obligations": { "abac.policy1_guid": "bad obligations value" } @@ -126,7 +129,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: ErrInvalidObligations, + expectedErr: exception.ErrAbstrInvalidObligations, regoRespJSON: `{ "allow": true, "obligations": { "abac.bad_obligations_value": [ 3.14 ]} @@ -136,7 +139,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: ErrInvalidObligations, + expectedErr: exception.ErrAbstrInvalidObligations, regoRespJSON: `{ "allow": true, "obligations": { "abac.policy1_guid": { "stmt0": "bad obligations value" }} @@ -146,7 +149,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": [] @@ -156,7 +159,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": [ [], null, [] ] @@ -166,19 +169,19 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": [ [], [ "type:ddi.ipam; not ctx.a =~ 1" ] ] }`, - expectedVal: &ObligationsNode{ + expectedVal: &ObligationsNode{ Kind: ObligationsOr, Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, Children: []*ObligationsNode{ &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "type:ddi.ipam; not ctx.a =~ 1", }, }, @@ -189,19 +192,19 @@ var obligationsNodeTests = []struct { expectedSQL: `(NOT (ipam.a ~ 1))`, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": [ [ "ctx.a <= 1", "ctx.b != 2" ], [ "ctx.c >= 3" ] ] }`, - expectedVal: &ObligationsNode{ + expectedVal: &ObligationsNode{ Kind: ObligationsOr, Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, Children: []*ObligationsNode{ &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.c >= 3", }, }, @@ -210,11 +213,11 @@ var obligationsNodeTests = []struct { Kind: ObligationsOr, Children: []*ObligationsNode{ &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.a <= 1", }, &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.b != 2", }, }, @@ -225,7 +228,7 @@ var obligationsNodeTests = []struct { expectedSQL: `((ctx.c >= 3) OR ((ctx.a <= 1) OR (ctx.b != 2)))`, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": {} @@ -235,7 +238,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": { @@ -249,7 +252,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": { @@ -264,7 +267,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": { @@ -281,7 +284,7 @@ var obligationsNodeTests = []struct { expectedSQL: ``, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": { @@ -291,23 +294,23 @@ var obligationsNodeTests = []struct { "abac.policy2_guid": {} } }`, - expectedVal: &ObligationsNode{ + expectedVal: &ObligationsNode{ Kind: ObligationsOr, Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "abac.policy1_guid", + Tag: "abac.policy1_guid", Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "stmt0", + Tag: "stmt0", Children: []*ObligationsNode{ &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "type:ddi.ipam; ctx.i < 1", }, &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "type:ddi.ipam; ctx.j > 2", }, }, @@ -320,7 +323,7 @@ var obligationsNodeTests = []struct { expectedSQL: `((ipam.i < 1) OR (ipam.j > 2))`, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": { @@ -332,27 +335,27 @@ var obligationsNodeTests = []struct { } } }`, - expectedVal: &ObligationsNode{ + expectedVal: &ObligationsNode{ Kind: ObligationsOr, Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "abac.policy1_guid", + Tag: "abac.policy1_guid", Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "stmt0", + Tag: "stmt0", Children: []*ObligationsNode{ &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.i == 1", }, &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.j == 2", }, &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.k == 3", }, }, @@ -365,7 +368,7 @@ var obligationsNodeTests = []struct { expectedSQL: `((ctx.i = 1) OR (ctx.j = 2) OR (ctx.k = 3))`, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": { @@ -378,19 +381,19 @@ var obligationsNodeTests = []struct { } } }`, - expectedVal: &ObligationsNode{ + expectedVal: &ObligationsNode{ Kind: ObligationsOr, Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "abac.policy1_guid", + Tag: "abac.policy1_guid", Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "stmt0", + Tag: "stmt0", Children: []*ObligationsNode{ &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.a == 1", }, }, @@ -399,28 +402,28 @@ var obligationsNodeTests = []struct { }, &ObligationsNode{ Kind: ObligationsOr, - Tag: "abac.policy2_guid", + Tag: "abac.policy2_guid", Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "stmt0", + Tag: "stmt0", Children: []*ObligationsNode{ &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.b == 2", }, &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.c == 3", }, }, }, &ObligationsNode{ Kind: ObligationsOr, - Tag: "stmt1", + Tag: "stmt1", Children: []*ObligationsNode{ &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.d == 4", }, }, @@ -433,7 +436,7 @@ var obligationsNodeTests = []struct { expectedSQL: `((ctx.a = 1) OR (((ctx.b = 2) OR (ctx.c = 3)) OR (ctx.d = 4)))`, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": { @@ -442,19 +445,19 @@ var obligationsNodeTests = []struct { } } }`, - expectedVal: &ObligationsNode{ + expectedVal: &ObligationsNode{ Kind: ObligationsOr, Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "abac.policy1_guid", + Tag: "abac.policy1_guid", Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "stmt0", + Tag: "stmt0", Children: []*ObligationsNode{ &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "type:ddi.ipam; ctx.tags[\"a\"] == 1", }, }, @@ -467,7 +470,7 @@ var obligationsNodeTests = []struct { expectedSQL: `(ipam.tags->'a' = 1)`, }, { - expectedErr: nil, + expectedErr: nil, regoRespJSON: `{ "allow": true, "obligations": { @@ -476,19 +479,19 @@ var obligationsNodeTests = []struct { } } }`, - expectedVal: &ObligationsNode{ + expectedVal: &ObligationsNode{ Kind: ObligationsOr, Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "abac.policy1_guid", + Tag: "abac.policy1_guid", Children: []*ObligationsNode{ &ObligationsNode{ Kind: ObligationsOr, - Tag: "stmt0", + Tag: "stmt0", Children: []*ObligationsNode{ &ObligationsNode{ - Kind: ObligationsCondition, + Kind: ObligationsCondition, Condition: "ctx.a in 1, 2, 3", }, }, diff --git a/grpc_opa/sql.go b/common/opautil/sql.go similarity index 85% rename from grpc_opa/sql.go rename to common/opautil/sql.go index 790ed50..0d14ab9 100644 --- a/grpc_opa/sql.go +++ b/common/opautil/sql.go @@ -1,9 +1,10 @@ -package grpc_opa_middleware +package opautil import ( "strings" - "github.com/infobloxopen/seal/pkg/compiler/sql" + "github.com/infobloxopen/atlas-authz-middleware/v2/http_opa/exception" + sqlcompiler "github.com/infobloxopen/seal/pkg/compiler/sql" ) // ToSQLPredicate recursively converts obligations node tree into SQL predicate @@ -15,11 +16,11 @@ func (o8n *ObligationsNode) ToSQLPredicate(sqlc *sqlcompiler.SQLCompiler) (strin } return AddOuterParens(singleSQL), nil } else if (o8n.Kind != ObligationsAnd) && (o8n.Kind != ObligationsOr) { - return "", ErrInvalidObligations + return "", exception.ErrAbstrInvalidObligations } if len(o8n.Children) <= 0 { - return "", ErrInvalidObligations + return "", exception.ErrAbstrInvalidObligations } childSQLArr := make([]string, 0, len(o8n.Children)) diff --git a/common/opautil/util.go b/common/opautil/util.go new file mode 100644 index 0000000..9e12115 --- /dev/null +++ b/common/opautil/util.go @@ -0,0 +1,84 @@ +package opautil + +import ( + "context" + "strings" + + az "github.com/infobloxopen/atlas-authz-middleware/v2/common/authorizer" +) + +type Payload struct { + Endpoint string `json:"endpoint"` + Application string `json:"application"` + // FullMethod is the full RPC method string, i.e., /package.service/method. + FullMethod string `json:"full_method"` + JWT string `json:"jwt"` + RequestID string `json:"request_id"` + EntitledServices []string `json:"entitled_services"` + az.DecisionInput +} + +// OPARequest is used to query OPA +type OPARequest struct { + // OPA expects field called "input" to contain input payload + Input interface{} `json:"input"` +} + +// OPAResponse unmarshals the response from OPA into a generic untyped structure +type OPAResponse map[string]interface{} + +// Allow determine if policy is allowed +func (o OPAResponse) Allow() bool { + allow, ok := o["allow"].(bool) + if !ok { + return false + } + return allow +} + +// Obligations parses the returned obligations and returns them in standard format +func (o OPAResponse) Obligations() (*ObligationsNode, error) { + if obIfc, ok := o[string(az.ObKey)]; ok { + return ParseOPAObligations(obIfc) + } + return nil, nil +} + +func RedactJWT(jwt string) string { + parts := strings.Split(jwt, ".") + if len(parts) > 0 { + parts[len(parts)-1] = az.REDACTED + } + return strings.Join(parts, ".") +} + +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 + for i := range parts { + parts[i] = parts[i][:Min(len(parts[i]), 16)] + "/" + az.REDACTED + } + return strings.Join(parts, ".") +} + +func Min(a, b int) int { + if a < b { + return a + } + return b +} + +func ShortenPayloadForDebug(full Payload) Payload { + // This is a shallow copy + shorten := Payload(full) + shorten.JWT = RedactJWTForDebug(shorten.JWT) + return shorten +} + +func AddObligations(ctx context.Context, opaResp OPAResponse) (context.Context, error) { + ob, err := opaResp.Obligations() + if ob != nil { + ctx = context.WithValue(ctx, az.ObKey, ob) + } + return ctx, err +} diff --git a/common/opautil/util_test.go b/common/opautil/util_test.go new file mode 100644 index 0000000..b67e343 --- /dev/null +++ b/common/opautil/util_test.go @@ -0,0 +1,77 @@ +package opautil + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + az "github.com/infobloxopen/atlas-authz-middleware/v2/common/authorizer" +) + +func Test_AddObligations(t *testing.T) { + for idx, tst := range obligationsNodeTests { + ctx := context.Background() + var opaResp OPAResponse + + err := json.Unmarshal([]byte(tst.regoRespJSON), &opaResp) + if err != nil { + t.Errorf("tst#%d: err=%s trying to json.Unmarshal: %s", + idx, err, tst.regoRespJSON) + continue + } + + t.Logf("tst#%d: opaResp=%#v", idx, opaResp) + newCtx, actualErr := AddObligations(ctx, opaResp) + + if actualErr != tst.expectedErr { + t.Errorf("tst#%d: expectedErr=%s actualErr=%s", + idx, tst.expectedErr, actualErr) + } + + actualVal, _ := newCtx.Value(az.ObKey).(*ObligationsNode) + if actualVal != nil { + t.Logf("tst#%d: before DeepSort: %s", idx, actualVal) + actualVal.DeepSort() + } + if !reflect.DeepEqual(actualVal, tst.expectedVal) { + // nil interface{} (untyped) does not compare equal with a nil typed value + // https://www.calhoun.io/when-nil-isnt-equal-to-nil/ + // https://stackoverflow.com/questions/13476349/check-for-nil-and-nil-interface-in-go + if actualVal != nil || tst.expectedVal != nil { + t.Errorf("tst#%d: expectedVal=%s actualVal=%s", + idx, tst.expectedVal, actualVal) + } + } + } +} + +func TestOPAResponseObligations(t *testing.T) { + for idx, tst := range obligationsNodeTests { + var opaResp OPAResponse + + err := json.Unmarshal([]byte(tst.regoRespJSON), &opaResp) + if err != nil { + t.Errorf("tst#%d: err=%s trying to json.Unmarshal: %s", + idx, err, tst.regoRespJSON) + continue + } + + t.Logf("tst#%d: opaResp=%#v", idx, opaResp) + actualVal, actualErr := opaResp.Obligations() + + if actualErr != tst.expectedErr { + t.Errorf("tst#%d: expectedErr=%s actualErr=%s", + idx, tst.expectedErr, actualErr) + } + + if actualVal != nil { + t.Logf("tst#%d: before DeepSort: %s", idx, actualVal) + actualVal.DeepSort() + } + if !reflect.DeepEqual(actualVal, tst.expectedVal) { + t.Errorf("tst#%d: expectedVal=%s actualVal=%s", + idx, tst.expectedVal, actualVal) + } + } +} diff --git a/go.mod b/go.mod index 9ed4e9a..1ea11bb 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,21 @@ -module github.com/infobloxopen/atlas-authz-middleware +module github.com/infobloxopen/atlas-authz-middleware/v2 -go 1.17 +go 1.21 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-app-toolkit v1.1.2 github.com/infobloxopen/atlas-claims v1.0.0 github.com/infobloxopen/seal v0.2.3 github.com/open-policy-agent/opa v0.37.2 github.com/sirupsen/logrus v1.8.1 - go.opencensus.io v0.23.0 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd google.golang.org/grpc v1.44.0 ) +require github.com/google/go-cmp v0.5.9 // indirect + require ( github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/bytecodealliance/wasmtime-go v0.34.0 // indirect @@ -24,11 +26,8 @@ require ( github.com/go-openapi/swag v0.21.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/golang-jwt/jwt/v4 v4.4.1 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/gorilla/mux v1.8.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4 // indirect @@ -39,9 +38,10 @@ require ( github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b // indirect go.opentelemetry.io/otel v1.4.1 // indirect go.opentelemetry.io/otel/trace v1.4.1 // indirect - golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect + golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gotest.tools/v3 v3.5.1 ) diff --git a/go.sum b/go.sum index a7f86aa..3a96c44 100644 --- a/go.sum +++ b/go.sum @@ -46,22 +46,18 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -contrib.go.opencensus.io/exporter/ocagent v0.7.0/go.mod h1:IshRmMJBhDfFj5Y67nVhMYTTIze91RUeT73ipWKs/GY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -112,12 +108,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= -github.com/dgrijalva/jwt-go v3.2.1-0.20200107013213-dc14462fd587+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= @@ -133,7 +127,6 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -173,7 +166,6 @@ github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= @@ -184,9 +176,7 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -202,6 +192,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -240,8 +231,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -263,7 +255,6 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -275,12 +266,8 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= -github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0/go.mod h1:r1hZAcvfFXuYmcKyCJI9wlyOPIZUJl6FCB8Cpca/NLE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3 h1:I8MsauTJQXZ8df8qJvEln0kYNc3bSapuaSsEsnFdEFU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.3/go.mod h1:lZdb/YAJUSj9OqrCHs2ihjtoO3+xK3G53wTYXFWRGDo= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -315,15 +302,10 @@ 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-app-toolkit v1.1.2 h1:bB7vWc2mJnqmk+RU594L1JM8fJOQMu6MHy3Uglo0SUQ= -github.com/infobloxopen/atlas-app-toolkit v1.1.2/go.mod h1:vd1L67El5az4iEwBK5wqDt+VRDNUpDtVqJCYftnJ8S0= 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/seal v0.2.3 h1:TVIw52FxVVwehat/m23+hjoFXbIvyKBA9XCVI21p68A= github.com/infobloxopen/seal v0.2.3/go.mod h1:IHbkKw7rx7oJKNtyjHL+1XaGKo5NU8CjFE3ZpA5mrB8= -github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -352,10 +334,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.1-0.20200116171513-9eb3fc897d6f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= -github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -375,7 +354,6 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4 h1:NK3O7S5FRD/wj7ORQ5C3Mx1STpyEMuFe+/F0Lakd1Nk= github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4/go.mod h1:FqD3ES5hx6zpzDainDaHgkTIqrPaI9uX4CVWqYZoQjY= @@ -452,12 +430,10 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/speps/go-hashids/v2 v2.0.1/go.mod h1:47LKunwvDZki/uRVD6NImtyk712yFzIs3UF3KlHohGw= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= @@ -541,13 +517,11 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -587,7 +561,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -604,7 +577,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -647,7 +619,6 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= @@ -738,12 +709,10 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -816,7 +785,6 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -831,7 +799,6 @@ google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.25.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= @@ -886,7 +853,6 @@ google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -906,7 +872,6 @@ google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= @@ -926,7 +891,6 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c h1:TU4rFa5APdKTq0s6B7WTsH6Xmx0Knj86s6Biz56mErE= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -956,12 +920,10 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/examples v0.0.0-20201209011439-fd32f6a4fefe/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= -google.golang.org/grpc/examples v0.0.0-20210715165331-ce7bdf50abb1/go.mod h1:bF8wuZSAZTcbF7ZPKrDI/qY52toTP/yxLpRRY4Eu9Js= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -997,6 +959,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1007,4 +971,3 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/grpc_opa/.gitignore b/grpc_opa/.gitignore deleted file mode 100644 index 22d0d82..0000000 --- a/grpc_opa/.gitignore +++ /dev/null @@ -1 +0,0 @@ -vendor diff --git a/grpc_opa/acct_entitlements.go b/grpc_opa/acct_entitlements.go deleted file mode 100644 index db7306e..0000000 --- a/grpc_opa/acct_entitlements.go +++ /dev/null @@ -1,99 +0,0 @@ -package grpc_opa_middleware - -import ( - "context" - "fmt" - - "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" - logrus "github.com/sirupsen/logrus" -) - -const ( - // DefaultAcctEntitlementsApiPath is default OPA path to fetch acct entitlements - DefaultAcctEntitlementsApiPath = "v1/data/authz/rbac/acct_entitlements_api" -) - -// AcctEntitlementsApiInput is the input payload for acct_entitlements_api -type AcctEntitlementsApiInput struct { - AccountIDs []string `json:"acct_entitlements_acct_ids"` - ServiceNames []string `json:"acct_entitlements_services"` -} - -// AcctEntitlementsType is a convenience data type, returned by GetAcctEntitlements() -// (map of acct_id to map of service to array of features) -type AcctEntitlementsType map[string]map[string][]string - -// AcctEntitlementsApiResult is the data type json.Unmarshaled from OPA RESTAPI query to acct_entitlements_api -type AcctEntitlementsApiResult struct { - Result *AcctEntitlementsType `json:"result"` -} - -// GetAcctEntitlementsBytes queries account entitled features data -// for the specified account-ids and entitled-services. -// If both account-ids and entitled-services are empty, -// then data for all entitled-services in all accounts are returned. -// Returns the raw JSON string response -func (a *DefaultAuthorizer) GetAcctEntitlementsBytes(ctx context.Context, accountIDs, serviceNames []string) ([]byte, error) { - lgNtry := ctxlogrus.Extract(ctx) - - if accountIDs == nil { - accountIDs = []string{} - } - if serviceNames == nil { - serviceNames = []string{} - } - - opaReq := OPARequest{ - Input: &AcctEntitlementsApiInput{ - AccountIDs: accountIDs, - ServiceNames: serviceNames, - }, - } - - rawBytes, err := a.clienter.CustomQueryBytes(ctx, a.acctEntitlementsApi, opaReq) - if err != nil { - lgNtry.WithError(err).Error("get_acct_entitlements_raw_fail") - return nil, err - } - - lgNtry.WithFields(logrus.Fields{ - "rawBytes": string(rawBytes), - }).Trace("get_acct_entitlements_raw_okay") - - return rawBytes, nil -} - -// GetAcctEntitlements queries account entitled features data -// for the specified account-ids and entitled-services. -// If both account-ids and entitled-services are empty, -// then data for all entitled-services in all accounts are returned. -func (a *DefaultAuthorizer) GetAcctEntitlements(ctx context.Context, accountIDs, serviceNames []string) (*AcctEntitlementsType, error) { - lgNtry := ctxlogrus.Extract(ctx) - acctResult := AcctEntitlementsApiResult{} - - if accountIDs == nil { - accountIDs = []string{} - } - if serviceNames == nil { - serviceNames = []string{} - } - - opaReq := OPARequest{ - Input: &AcctEntitlementsApiInput{ - AccountIDs: accountIDs, - ServiceNames: serviceNames, - }, - } - - err := a.clienter.CustomQuery(ctx, a.acctEntitlementsApi, opaReq, &acctResult) - if err != nil { - lgNtry.WithError(err).Error("get_acct_entitlements_fail") - return nil, err - } - - lgNtry.WithFields(logrus.Fields{ - "acctResult": fmt.Sprintf("%#v", acctResult), - }).Trace("get_acct_entitlements_okay") - - return acctResult.Result, nil -} diff --git a/grpc_opa/acct_entitlements_test.go b/grpc_opa/acct_entitlements_test.go deleted file mode 100644 index 0755f11..0000000 --- a/grpc_opa/acct_entitlements_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package grpc_opa_middleware - -import ( - "context" - "encoding/json" - "io/ioutil" - "reflect" - "testing" - - "github.com/infobloxopen/atlas-authz-middleware/pkg/opa_client" - "github.com/infobloxopen/atlas-authz-middleware/utils_test" - - "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" - logrus "github.com/sirupsen/logrus" -) - -func TestGetAcctEntitlementsOpa(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 := NewDefaultAuthorizer("bogus_unused_application_value", - WithOpaClienter(cli), - ) - - rawBytes, err := auther.GetAcctEntitlementsBytes(ctx, nil, nil) - if err != nil { - t.Errorf("FAIL: GetAcctEntitlementsBytes() unexpected err=%v", err) - } - t.Logf("rawBytes=%s", string(rawBytes)) - - expectJson := `{"result":{"2001016":{"environment":["ac","heated-seats"],"wheel":["abs","alloy","tpms"]},"2001040":{"environment":["ac","side-mirror-defogger"],"powertrain":["automatic","turbo"]},"2001230":{"powertrain":["manual","v8"],"wheel":["run-flat"]}}}` - - if string(rawBytes) != expectJson { - t.Errorf("FAIL:\nrawBytes: %s\nexpectJson: %s", string(rawBytes), expectJson) - } - - var actualAcctResult AcctEntitlementsApiResult - err = json.Unmarshal(rawBytes, &actualAcctResult) - t.Logf("actualAcctResult.Result=%#v", actualAcctResult.Result) - - expectAcctResult := AcctEntitlementsApiResult{ - Result: &AcctEntitlementsType{ - "2001016": { - "environment": {"ac", "heated-seats"}, - "wheel": {"abs", "alloy", "tpms"}, - }, - "2001040": { - "environment": {"ac", "side-mirror-defogger"}, - "powertrain": {"automatic", "turbo"}, - }, - "2001230": { - "powertrain": {"manual", "v8"}, - "wheel": {"run-flat"}, - }, - }, - } - if !reflect.DeepEqual(actualAcctResult.Result, expectAcctResult.Result) { - t.Errorf("FAIL:\nactualAcctResult.Result: %#v\nexpectAcctResult.Result: %#v", - actualAcctResult.Result, expectAcctResult.Result) - } - - var actualOpaResp OPAResponse - err = json.Unmarshal(rawBytes, &actualOpaResp) - t.Logf("actualOpaResp=%#v", actualOpaResp) - - expectOpaResp := OPAResponse{ - "result": map[string]interface{}{ - "2001016": map[string]interface{}{ - "environment": []interface{}{"ac", "heated-seats"}, - "wheel": []interface{}{"abs", "alloy", "tpms"}, - }, - "2001040": map[string]interface{}{ - "environment": []interface{}{"ac", "side-mirror-defogger"}, - "powertrain": []interface{}{"automatic", "turbo"}, - }, - "2001230": map[string]interface{}{ - "powertrain": []interface{}{"manual", "v8"}, - "wheel": []interface{}{"run-flat"}, - }, - }, - } - if !reflect.DeepEqual(actualOpaResp, expectOpaResp) { - t.Errorf("FAIL:\nactualOpaResp: %#v\nexpectOpaResp: %#v", actualOpaResp, expectOpaResp) - } - - actualSpecific, err := auther.GetAcctEntitlements(ctx, - []string{"2001040", "2001230"}, []string{"powertrain", "wheel"}) - if err != nil { - t.Errorf("FAIL: GetAcctEntitlements() unexpected err=%v", err) - } - t.Logf("actualSpecific=%#v", actualSpecific) - - expectSpecific := &AcctEntitlementsType{ - "2001040": { - "powertrain": {"automatic", "turbo"}, - }, - "2001230": { - "powertrain": {"manual", "v8"}, - "wheel": {"run-flat"}, - }, - } - if !reflect.DeepEqual(actualSpecific, expectSpecific) { - t.Errorf("FAIL:\nactualSpecific: %#v\nexpectSpecific: %#v", - actualSpecific, expectSpecific) - } -} - -func TestGetAcctEntitlementsMockOpaClient(t *testing.T) { - testMap := []struct { - name string - regoRespJSON string - expectErr bool - expectedVal *AcctEntitlementsType - }{ - { - name: `valid result`, - regoRespJSON: `{ "result": { - "acct1": { "svc1a": [ "feat1a1", "feat1a2" ] }, - "acct2": { "svc2a": [ "feat2a1", "feat2a2" ], - "svc2b": [ "feat2b1", "feat2b2" ] } - }}`, - expectErr: false, - expectedVal: &AcctEntitlementsType{ - "acct1": {"svc1a": {"feat1a1", "feat1a2"}}, - "acct2": {"svc2a": {"feat2a1", "feat2a2"}, - "svc2b": {"feat2b1", "feat2b2"}}, - }, - }, - { - name: `null result ok`, - regoRespJSON: `{ "result": null }`, - expectErr: false, - expectedVal: nil, - }, - { - name: `null account entitled service ok`, - regoRespJSON: `{ "result": { - "acct1": { "svc1a": [ "feat1a1", "feat1a2" ] }, - "acct2": null - }}`, - expectErr: false, - expectedVal: &AcctEntitlementsType{ - "acct1": {"svc1a": {"feat1a1", "feat1a2"}}, - "acct2": nil, - }, - }, - { - name: `null service entitled features ok`, - regoRespJSON: `{ "result": { - "acct2": { "svc2a": null, - "svc2b": [ "feat2b1", "feat2b2" ] } - }}`, - expectErr: false, - expectedVal: &AcctEntitlementsType{ - "acct2": {"svc2a": nil, - "svc2b": {"feat2b1", "feat2b2"}}, - }, - }, - { - name: `incorrect result type`, - regoRespJSON: `[ null ]`, - expectErr: true, - expectedVal: nil, - }, - { - name: `no result key`, - regoRespJSON: `{ "rresult": null }`, - expectErr: false, - expectedVal: nil, - }, - { - name: `invalid result array`, - regoRespJSON: `{ "result": [ 1, 2 ] }`, - expectErr: true, - expectedVal: nil, - }, - { - name: `invalid account entitled service`, - regoRespJSON: `{ "result": { - "acct2": { "svc2a": [ "feat2a1", "feat2a2" ], - "svc2b": {} } - }}`, - expectErr: true, - expectedVal: nil, - }, - { - name: `invalid service entitled feature`, - regoRespJSON: `{ "result": { - "acct2": { "svc2a": [ "feat2a1", "feat2a2" ], - "svc2b": [ "feat2b1", 31415926 ] } - }}`, - expectErr: true, - expectedVal: nil, - }, - } - - stdLoggr := logrus.StandardLogger() - ctx := context.WithValue(context.Background(), utils_test.TestingTContextKey, t) - ctx = ctxlogrus.ToContext(ctx, logrus.NewEntry(stdLoggr)) - - for nth, tm := range testMap { - mockOpaClienter := MockOpaClienter{ - Loggr: stdLoggr, - RegoRespJSON: tm.regoRespJSON, - } - auther := NewDefaultAuthorizer("bogus_unused_application_value", - WithOpaClienter(&mockOpaClienter), - ) - - actualVal, actualErr := auther.GetAcctEntitlements(ctx, nil, nil) - t.Logf("%d: %q: actualErr=%#v, actualVal=%#v", nth, tm.name, actualVal, actualErr) - - if tm.expectErr && actualErr == nil { - t.Errorf("%d: %q: FAIL: expected err, but got no err", nth, tm.name) - } else if !tm.expectErr && actualErr != nil { - t.Errorf("%d: %q: FAIL: got unexpected err=%s", nth, tm.name, actualErr) - } - - if actualErr != nil && actualVal != nil { - t.Errorf("%d: %q: FAIL: returned val should be nil if err returned", nth, tm.name) - } - - if !reflect.DeepEqual(actualVal, tm.expectedVal) { - t.Errorf("%d: %q: FAIL: expectedVal=%#v actualVal=%#v", - nth, tm.name, tm.expectedVal, actualVal) - } - } -} diff --git a/grpc_opa/authorizer.go b/grpc_opa/authorizer.go deleted file mode 100644 index 82e81bb..0000000 --- a/grpc_opa/authorizer.go +++ /dev/null @@ -1,475 +0,0 @@ -package grpc_opa_middleware - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strings" - "time" - - "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" - log "github.com/sirupsen/logrus" - "go.opencensus.io/trace" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "github.com/infobloxopen/atlas-app-toolkit/requestid" - "github.com/infobloxopen/atlas-authz-middleware/pkg/opa_client" - atlas_claims "github.com/infobloxopen/atlas-claims" -) - -// ABACKey is a context.Context key type -type ABACKey string -type ObligationKey string - -const ( - // DefaultValidatePath is default OPA path to perform authz validation - DefaultValidatePath = "v1/data/authz/rbac/validate_v1" - - REDACTED = "redacted" - TypeKey = ABACKey("ABACType") - VerbKey = ABACKey("ABACVerb") - ObKey = ObligationKey("obligations") -) - -// Override to set your servicename -var ( - SERVICENAME = "opa" -) - -var ( - ErrForbidden = status.Errorf(codes.PermissionDenied, "Request forbidden: not authorized") - ErrUnknown = status.Errorf(codes.Unknown, "Unknown error") - ErrInvalidArg = status.Errorf(codes.InvalidArgument, "Invalid argument") -) - -// DecisionInput is app/service-specific data supplied by app/service ABAC requests -type DecisionInput struct { - Type string `json:"type"` // Object/resource-type to match - Verb string `json:"verb"` // Verb to match - SealCtx []interface{} `json:"ctx"` // Array of app/service-specific context data to match - DecisionDocument string `json:"-"` // OPA decision document to query, by default "", - // which is default decision document configured in OPA -} - -// fullMethod is of the form "Service.FullMethod" -type DecisionInputHandler interface { - // GetDecisionInput returns an app/service-specific DecisionInput. - // A nil DecisionInput should NOT be returned unless error. - GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*DecisionInput, error) -} - -// DefaultDecisionInputer is an example DecisionInputHandler that is used as default -type DefaultDecisionInputer struct{} - -func (m DefaultDecisionInputer) String() string { - return "grpc_opa_middleware.DefaultDecisionInputer{}" -} - -// GetDecisionInput is an example DecisionInputHandler that returns some decision input -// based on some incoming Context values. App/services will most likely supply their -// own DecisionInputHandler using WithDecisionInputHandler option. -func (m *DefaultDecisionInputer) GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*DecisionInput, error) { - var abacType string - if v, ok := ctx.Value(TypeKey).(string); ok { - abacType = v - } - - var abacVerb string - if v, ok := ctx.Value(VerbKey).(string); ok { - abacVerb = v - } - - decInp := DecisionInput{ - Type: abacType, - Verb: abacVerb, - } - return &decInp, nil -} - -var defDecisionInputer = new(DefaultDecisionInputer) - -// OpaEvaluator implements calling OPA with a request and receiving the raw response -type OpaEvaluator func(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error - -// Authorizer interface is implemented for making arbitrary requests to Opa. -type Authorizer interface { - // Evaluate is called with the grpc request's method passing the grpc request Context. - // If the handler is executed, the request will be sent to Opa. Opa's response - // will be unmarshaled using JSON into the provided response. - // Evaluate returns true if the request is authorized. The context - // will be passed to subsequent HTTP Handler. - Evaluate(ctx context.Context, fullMethod string, grpcReq interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) - - // OpaQuery executes query of the specified decisionDocument against OPA. - // If decisionDocument is "", then the query is executed against the default decision document configured in OPA. - OpaQuery(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error -} - -type AuthorizeFn func(ctx context.Context, fullMethodName string, grpcReq interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) - -func (a AuthorizeFn) OpaQuery(opaReq, opaResp interface{}) error { - return nil -} - -func (a AuthorizeFn) Evaluate(ctx context.Context, fullMethod string, grpcReq interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) { - return a(ctx, fullMethod, grpcReq, opaEvaluator) -} - -func NewDefaultAuthorizer(application string, opts ...Option) *DefaultAuthorizer { - cfg := &Config{ - address: opa_client.DefaultAddress, - decisionInputHandler: defDecisionInputer, - claimsVerifier: UnverifiedClaimFromBearers, - acctEntitlementsApi: DefaultAcctEntitlementsApiPath, - } - for _, opt := range opts { - opt(cfg) - } - - //log.Debugf("cfg=%+v", *cfg) - - clienter := cfg.clienter - if clienter == nil { - clienter = opa_client.New(cfg.address, opa_client.WithHTTPClient(cfg.httpCli)) - } - - a := DefaultAuthorizer{ - clienter: clienter, - opaEvaluator: cfg.opaEvaluator, - application: application, - decisionInputHandler: cfg.decisionInputHandler, - claimsVerifier: cfg.claimsVerifier, - entitledServices: cfg.entitledServices, - acctEntitlementsApi: cfg.acctEntitlementsApi, - } - return &a -} - -type DefaultAuthorizer struct { - application string - clienter opa_client.Clienter - opaEvaluator OpaEvaluator - decisionInputHandler DecisionInputHandler - claimsVerifier ClaimsVerifier - entitledServices []string - acctEntitlementsApi string -} - -type Config struct { - httpCli *http.Client - // address to opa - address string - - clienter opa_client.Clienter - opaEvaluator OpaEvaluator - authorizer []Authorizer - decisionInputHandler DecisionInputHandler - claimsVerifier ClaimsVerifier - entitledServices []string - acctEntitlementsApi string -} - -type ClaimsVerifier func([]string, []string) (string, []error) - -// FullMethod is the full RPC method string, i.e., /package.service/method. -// e.g. fullmethod: /service.TagService/ListRetiredTags PARGs endpoint: TagService.ListRetiredTags -func parseEndpoint(fullMethod string) string { - byPackage := strings.Split(fullMethod, ".") - endpoint := byPackage[len(byPackage)-1] - return strings.Replace(endpoint, "/", ".", -1) -} - -func (a DefaultAuthorizer) String() string { - return fmt.Sprintf(`grpc_opa_middleware.DefaultAuthorizer{application:"%s" clienter:%s decisionInputHandler:%s}`, - a.application, a.clienter, a.decisionInputHandler) -} - -func (a *DefaultAuthorizer) Evaluate(ctx context.Context, fullMethod string, grpcReq interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) { - - logger := ctxlogrus.Extract(ctx).WithFields(log.Fields{ - "application": a.application, - }) - - // 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 = UnverifiedClaimFromBearers - } - - rawJWT, errs := claimsVerifier([]string{bearer}, []string{newBearer}) - if len(errs) > 0 { - return false, ctx, fmt.Errorf("%q", errs) - } - - reqID, ok := requestid.FromContext(ctx) - if !ok { - reqID = "no-request-uuid" - } - - opaReq := Payload{ - Endpoint: parseEndpoint(fullMethod), - FullMethod: fullMethod, - Application: a.application, - // FIXME: implement atlas_claims.AuthBearersFromCtx - JWT: redactJWT(rawJWT), - RequestID: reqID, - EntitledServices: a.entitledServices, - } - - decisionInput, err := a.decisionInputHandler.GetDecisionInput(ctx, fullMethod, grpcReq) - if decisionInput == nil || err != nil { - logger.WithFields(log.Fields{ - "fullMethod": fullMethod, - }).WithError(err).Error("get_decision_input") - return false, ctx, ErrInvalidArg - } - //logger.Debugf("decisionInput=%+v", *decisionInput) - opaReq.DecisionInput = *decisionInput - - opaReqJSON, err := json.Marshal(opaReq) - if err != nil { - logger.WithFields(log.Fields{ - "opaReq": opaReq, - }).WithError(err).Error("opa_request_json_marshal") - return false, ctx, ErrInvalidArg - } - - now := time.Now() - obfuscatedOpaReq := shortenPayloadForDebug(opaReq) - logger.WithFields(log.Fields{ - "opaReq": obfuscatedOpaReq, - //"opaReqJSON": string(opaReqJSON), - }).Debug("opa_authorization_request") - - // To enable tracing, the context must have a tracer attached - // to it. See the tracing documentation on how to do this. - ctx, span := trace.StartSpan(ctx, fmt.Sprint(SERVICENAME, fullMethod)) - { - span.Annotate([]trace.Attribute{ - trace.StringAttribute("in", string(opaReqJSON)), - }, "in") - } - // FIXME: perhaps only inject these fields if this is the default handler - - // If DecisionDocument is empty, the default OPA-configured decision document is queried. - // In this case, the input payload MUST NOT be encapsulated inside "input". - // Otherwise for any other non-empty DecisionDocument, even if it's the same as the default - // OPA-configured decision document, the input payload MUST be encapsulated inside "input". - // (See comments in testdata/mock_system_main.rego) - var opaInput interface{} - opaInput = opaReq - if len(decisionInput.DecisionDocument) > 0 { - opaInput = OPARequest{Input: &opaReq} - } - - var opaResp OPAResponse - err = opaEvaluator(ctxlogrus.ToContext(ctx, logger), decisionInput.DecisionDocument, opaInput, &opaResp) - // Metrics, logging, tracing handler - defer func() { - // opencensus Status is based on gRPC status codes - // https://pkg.go.dev/go.opencensus.io/trace?tab=doc#Status - // err == nil will return {Code: 200, Message:""} - span.SetStatus(trace.Status{ - Code: int32(grpc.Code(err)), - Message: grpc.ErrorDesc(err), - }) - span.End() - logger.WithFields(log.Fields{ - "opaResp": opaResp, - "elapsed": time.Since(now), - }).Debug("authorization_result") - }() - if err != nil { - return false, ctx, err - } - - // When we POST query OPA without url path, it returns results NOT encapsulated inside "result": - // {"allow": true, ...} - // When we POST query OPA with explicit decision document, it returns results encapsulated inside "result": - // {"result":{"allow": true, ...}} - // (See comments in testdata/mock_system_main.rego) - // If the JSON result document is nested within "result" wrapper map, - // we extract the nested JSON document and throw away the "result" wrapper map. - nestedResultVal, resultIsNested := opaResp["result"] - if resultIsNested { - nestedResultMap, ok := nestedResultVal.(map[string]interface{}) - if ok { - opaResp = OPAResponse{} - for k, v := range nestedResultMap { - opaResp[k] = v - } - } - } - - // Log non-err opa responses - { - raw, _ := json.Marshal(opaResp) - span.Annotate([]trace.Attribute{ - trace.StringAttribute("out", string(raw)), - }, "out") - } - - // adding raw entitled_features data to context if present - ctx = opaResp.AddRawEntitledFeatures(ctx) - - // adding obligations data to context if present - ctx, err = addObligations(ctx, opaResp) - if err != nil { - logger.WithField("opaResp", fmt.Sprintf("%#v", opaResp)).WithError(err).Error("parse_obligations_error") - } - - if !opaResp.Allow() { - return false, ctx, ErrForbidden - } - - return true, ctx, nil -} - -func (a *DefaultAuthorizer) OpaQuery(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error { - if a.opaEvaluator != nil { - return a.opaEvaluator(ctx, decisionDocument, opaReq, opaResp) - } - - logger := ctxlogrus.Extract(ctx) - - // Empty document path is intentional - // DO NOT hardcode a path here - err := a.clienter.CustomQuery(ctx, decisionDocument, opaReq, opaResp) - // TODO: allow overriding logger - if err != nil { - grpcErr := opa_client.GRPCError(err) - logger.WithError(grpcErr).Error("opa_policy_engine_request_error") - return opaqueError(grpcErr) - } - - logger.WithField("opaResp", opaResp).Debug("opa_policy_engine_response") - return err -} - -// AffirmAuthorization makes an authz request to sidecar-OPA. -// If authorization is permitted, error returned is nil, -// and a new context is returned, possibly containing obligations. -// Caller must further evaluate obligations if required. -func (a *DefaultAuthorizer) AffirmAuthorization(ctx context.Context, fullMethod string, grpcReq interface{}) (context.Context, error) { - logger := ctxlogrus.Extract(ctx) - var ( - ok bool - newCtx context.Context - err error - ) - - ok, newCtx, err = a.Evaluate(ctx, fullMethod, grpcReq, a.OpaQuery) - if err != nil { - logger.WithError(err).WithField("authorizer", a).Error("unable_authorize") - return nil, err - } - - if !ok { - err = opa_client.ErrUndefined - logger.WithError(err).Error("policy engine returned undefined response") - return nil, err - } - - return newCtx, nil -} - -var ( - errUnavailable = status.Error(codes.Unavailable, `Post http://localhost:8181/: dial tcp: connection refused`) -) - -// opaqueError trims some privileged information from errors -// as these get sent directly as grpc responses -func opaqueError(err error) error { - - switch status.Code(err) { - case codes.Unavailable: - return opa_client.ErrServiceUnavailable - case codes.Unknown: - return opa_client.ErrUnknown - } - - return err -} - -type Payload struct { - Endpoint string `json:"endpoint"` - Application string `json:"application"` - // FullMethod is the full RPC method string, i.e., /package.service/method. - FullMethod string `json:"full_method"` - JWT string `json:"jwt"` - RequestID string `json:"request_id"` - EntitledServices []string `json:"entitled_services"` - DecisionInput -} - -// OPARequest is used to query OPA -type OPARequest struct { - // OPA expects field called "input" to contain input payload - Input interface{} `json:"input"` -} - -// OPAResponse unmarshals the response from OPA into a generic untyped structure -type OPAResponse map[string]interface{} - -// Allow determine if policy is allowed -func (o OPAResponse) Allow() bool { - allow, ok := o["allow"].(bool) - if !ok { - return false - } - return allow -} - -// Obligations parses the returned obligations and returns them in standard format -func (o OPAResponse) Obligations() (*ObligationsNode, error) { - if obIfc, ok := o[string(ObKey)]; ok { - return parseOPAObligations(obIfc) - } - return nil, nil -} - -func redactJWT(jwt string) string { - parts := strings.Split(jwt, ".") - if len(parts) > 0 { - parts[len(parts)-1] = REDACTED - } - return strings.Join(parts, ".") -} - -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 - for i := range parts { - parts[i] = parts[i][:min(len(parts[i]), 16)] + "/" + REDACTED - } - return strings.Join(parts, ".") -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} - -func shortenPayloadForDebug(full Payload) Payload { - // This is a shallow copy - shorten := Payload(full) - shorten.JWT = redactJWTForDebug(shorten.JWT) - return shorten -} - -func addObligations(ctx context.Context, opaResp OPAResponse) (context.Context, error) { - ob, err := opaResp.Obligations() - if ob != nil { - ctx = context.WithValue(ctx, ObKey, ob) - } - return ctx, err -} diff --git a/grpc_opa/authorizer_test.go b/grpc_opa/authorizer_test.go deleted file mode 100644 index 50f4ed2..0000000 --- a/grpc_opa/authorizer_test.go +++ /dev/null @@ -1,466 +0,0 @@ -package grpc_opa_middleware - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "reflect" - "strings" - "testing" - - "github.com/infobloxopen/atlas-authz-middleware/pkg/opa_client" - "github.com/infobloxopen/atlas-authz-middleware/utils_test" - - "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" - logrus "github.com/sirupsen/logrus" - logrustesthook "github.com/sirupsen/logrus/hooks/test" -) - -func TestRedactJWT(t *testing.T) { - token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" - - if redacted := redactJWT(token); !strings.HasSuffix(redacted, REDACTED) { - - t.Errorf("got: %s, wanted: %s", redacted, REDACTED) - } -} - -func Test_parseEndpoint(t *testing.T) { - tests := []struct { - fullMethod string - endpoint string - }{ - { - fullMethod: "/service.TagService/ListRetiredTags", - endpoint: "TagService.ListRetiredTags", - }, - { - fullMethod: "/TagService/ListRetiredTags", - endpoint: ".TagService.ListRetiredTags", - }, - { - fullMethod: ".TagService.ListRetiredTags", - endpoint: "ListRetiredTags", - }, - { - fullMethod: "TagService/ListRetiredTags", - endpoint: "TagService.ListRetiredTags", - }, - { - fullMethod: "TagService.ListRetiredTags", - endpoint: "ListRetiredTags", - }, - { - fullMethod: "/ListRetiredTags", - endpoint: ".ListRetiredTags", - }, - { - fullMethod: ".ListRetiredTags", - endpoint: "ListRetiredTags", - }, - { - fullMethod: "ListRetiredTags", - endpoint: "ListRetiredTags", - }, - } - - for _, tst := range tests { - gotEndpoint := parseEndpoint(tst.fullMethod) - if gotEndpoint != tst.endpoint { - t.Errorf("parseEndpoint(%s)='%s', wanted='%s'", - tst.fullMethod, gotEndpoint, tst.endpoint) - } - } -} - -func Test_addObligations(t *testing.T) { - for idx, tst := range obligationsNodeTests { - ctx := context.Background() - var opaResp OPAResponse - - err := json.Unmarshal([]byte(tst.regoRespJSON), &opaResp) - if err != nil { - t.Errorf("tst#%d: err=%s trying to json.Unmarshal: %s", - idx, err, tst.regoRespJSON) - continue - } - - t.Logf("tst#%d: opaResp=%#v", idx, opaResp) - newCtx, actualErr := addObligations(ctx, opaResp) - - if actualErr != tst.expectedErr { - t.Errorf("tst#%d: expectedErr=%s actualErr=%s", - idx, tst.expectedErr, actualErr) - } - - actualVal, _ := newCtx.Value(ObKey).(*ObligationsNode) - if actualVal != nil { - t.Logf("tst#%d: before DeepSort: %s", idx, actualVal) - actualVal.DeepSort() - } - if !reflect.DeepEqual(actualVal, tst.expectedVal) { - // nil interface{} (untyped) does not compare equal with a nil typed value - // https://www.calhoun.io/when-nil-isnt-equal-to-nil/ - // https://stackoverflow.com/questions/13476349/check-for-nil-and-nil-interface-in-go - if actualVal != nil || tst.expectedVal != nil { - t.Errorf("tst#%d: expectedVal=%s actualVal=%s", - idx, tst.expectedVal, actualVal) - } - } - } -} - -func TestOPAResponseObligations(t *testing.T) { - for idx, tst := range obligationsNodeTests { - var opaResp OPAResponse - - err := json.Unmarshal([]byte(tst.regoRespJSON), &opaResp) - if err != nil { - t.Errorf("tst#%d: err=%s trying to json.Unmarshal: %s", - idx, err, tst.regoRespJSON) - continue - } - - t.Logf("tst#%d: opaResp=%#v", idx, opaResp) - actualVal, actualErr := opaResp.Obligations() - - if actualErr != tst.expectedErr { - t.Errorf("tst#%d: expectedErr=%s actualErr=%s", - idx, tst.expectedErr, actualErr) - } - - if actualVal != nil { - t.Logf("tst#%d: before DeepSort: %s", idx, actualVal) - actualVal.DeepSort() - } - if !reflect.DeepEqual(actualVal, tst.expectedVal) { - t.Errorf("tst#%d: expectedVal=%s actualVal=%s", - idx, tst.expectedVal, actualVal) - } - } -} - -func TestAffirmAuthorizationOpa(t *testing.T) { - testMap := []struct { - name string - application string - fullMethod string - expectErr bool - }{ - { - name: "permitted", - application: "automobile", - fullMethod: "/service.Vehicle/StompGasPedal", - expectErr: false, - }, - { - name: "denied, incorrect application", - application: "train", - fullMethod: "/service.Vehicle/StompGasPedal", - expectErr: true, - }, - { - name: "denied, incorrect endpoint", - application: "automobile", - fullMethod: "/service.Vehicle/SteerLeft", - expectErr: true, - }, - } - - 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_system_main.rego") - if err != nil { - t.Fatalf("ReadFile fatal err: %#v", err) - return - } - - var resp interface{} - err = cli.UploadRegoPolicy(ctx, "mock_system_main_policyid", policyRego, resp) - if err != nil { - t.Fatalf("OpaUploadPolicy fatal err: %#v", err) - return - } - - // DecisionInputHandler with explicitly set decision document - var decInputr MockDecisionInputr - decInputr.DecisionInput.DecisionDocument = "v1/data/system/main" - - for nth, tm := range testMap { - // Test without explicitly set decision document - authzr := NewDefaultAuthorizer(tm.application, - WithOpaClienter(cli), - WithClaimsVerifier(NullClaimsVerifier), - ) - - _, actualErr := authzr.AffirmAuthorization(ctx, tm.fullMethod, nil) - if !tm.expectErr && actualErr != nil { - t.Errorf("%d: %s: AffirmAuthorization(explicit) FAIL: unexpected DENY, err=%#v", nth, tm.name, err) - } else if tm.expectErr && actualErr == nil { - t.Errorf("%d: %s: AffirmAuthorization(explicit) FAIL: unexpected PERMIT", nth, tm.name) - } - - // Test with explicitly set decision document - authzr = NewDefaultAuthorizer(tm.application, - WithOpaClienter(cli), - WithDecisionInputHandler(&decInputr), - WithClaimsVerifier(NullClaimsVerifier), - ) - - _, actualErr = authzr.AffirmAuthorization(ctx, tm.fullMethod, nil) - if !tm.expectErr && actualErr != nil { - t.Errorf("%d: %s: AffirmAuthorization(explicit) FAIL: unexpected DENY, err=%#v", nth, tm.name, err) - } else if tm.expectErr && actualErr == nil { - t.Errorf("%d: %s: AffirmAuthorization(explicit) FAIL: unexpected PERMIT", nth, tm.name) - } - } -} - -func TestAffirmAuthorizationMockOpaEvaluator(t *testing.T) { - ErrBoom := errors.New("boom") - - testMap := []struct { - name string - opaEvaltor OpaEvaluator - expectCtx bool - expectedErr error - }{ - { - name: "authz permitted, nil opa error", - opaEvaltor: func(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error { - respJSON := fmt.Sprintf(`{"allow": %s}`, "true") - json.Unmarshal([]byte(respJSON), opaResp) - return nil - }, - expectCtx: true, - expectedErr: nil, - }, - { - name: "authz denied, nil opa error", - opaEvaltor: func(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error { - respJSON := fmt.Sprintf(`{"allow": %s}`, "false") - json.Unmarshal([]byte(respJSON), opaResp) - return nil - }, - expectCtx: false, - expectedErr: ErrForbidden, - }, - { - name: "bogus opa response, nil opa error", - opaEvaltor: func(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error { - respJSON := fmt.Sprintf(`{"bogus_opa_response_field": %s}`, "true") - json.Unmarshal([]byte(respJSON), opaResp) - return nil - }, - expectCtx: false, - expectedErr: ErrForbidden, - }, - { - name: "opa error", - opaEvaltor: func(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error { - respJSON := fmt.Sprintf(`{"allow": %s}`, "true") - json.Unmarshal([]byte(respJSON), opaResp) - return ErrBoom - }, - expectCtx: false, - expectedErr: ErrBoom, - }, - } - - ctx := context.WithValue(context.Background(), utils_test.TestingTContextKey, t) - ctx = ctxlogrus.ToContext(ctx, logrus.NewEntry(logrus.StandardLogger())) - - for nth, tm := range testMap { - auther := NewDefaultAuthorizer("app", - WithOpaEvaluator(tm.opaEvaltor), - WithClaimsVerifier(NullClaimsVerifier), - ) - - resultCtx, resultErr := auther.AffirmAuthorization(ctx, "FakeMethod", nil) - if resultErr != tm.expectedErr { - t.Errorf("%d: %q: got error: %s, wanted error: %s", nth, tm.name, resultErr, tm.expectedErr) - } - if resultErr == nil && resultCtx == nil { - t.Errorf("%d: %q: returned ctx should not be nil if no err returned", nth, tm.name) - } - if resultErr != nil && resultCtx != nil { - t.Errorf("%d: %q: returned ctx should be nil if err returned", nth, tm.name) - } - } -} - -func TestDebugLogging(t *testing.T) { - testMap := []struct { - name string - regoRespJSON string - expectErr bool - qryLogLvl logrus.Level - qryLogMsg string - qryOpaResp string - evalLogLvl logrus.Level - evalLogMsg string - evalOpaResp string - }{ - { - name: `valid rego object and valid response json object`, - regoRespJSON: `{"allow": true, "obligations": {"policy1": {}}}`, - expectErr: false, - qryLogLvl: logrus.DebugLevel, - qryLogMsg: `opa_policy_engine_response`, - qryOpaResp: `&map[allow:true obligations:map[policy1:map[]]]`, - evalLogLvl: logrus.DebugLevel, - evalLogMsg: `authorization_result`, - evalOpaResp: `map[allow:true obligations:map[policy1:map[]]]`, - }, - { - name: `valid rego set but invalid json set`, - regoRespJSON: `{"allow": true, "obligations": {"policy1", "policy2"}}`, - expectErr: true, - qryLogLvl: logrus.ErrorLevel, - qryLogMsg: `opa_policy_engine_request_error`, - qryOpaResp: ``, - evalLogLvl: logrus.DebugLevel, - evalLogMsg: `authorization_result`, - evalOpaResp: `map[]`, - }, - { - name: `valid rego array and valid response json array`, - regoRespJSON: `{"allow": true, "obligations": []}`, - expectErr: false, - qryLogLvl: logrus.DebugLevel, - qryLogMsg: `opa_policy_engine_response`, - qryOpaResp: `&map[allow:true obligations:[]]`, - evalLogLvl: logrus.DebugLevel, - evalLogMsg: `authorization_result`, - evalOpaResp: `map[allow:true obligations:[]]`, - }, - } - - loggertesthook := logrustesthook.NewGlobal() - stdLoggr := logrus.StandardLogger() - ctx := context.WithValue(context.Background(), utils_test.TestingTContextKey, t) - ctx = ctxlogrus.ToContext(ctx, logrus.NewEntry(stdLoggr)) - - for nth, tm := range testMap { - mockOpaClienter := MockOpaClienter{ - Loggr: stdLoggr, - RegoRespJSON: tm.regoRespJSON, - } - auther := NewDefaultAuthorizer("app", - WithOpaClienter(&mockOpaClienter), - WithClaimsVerifier(NullClaimsVerifier), - ) - loggertesthook.Reset() - - _, resultErr := auther.AffirmAuthorization(ctx, "FakeMethod", nil) - if !tm.expectErr && resultErr != nil { - t.Errorf("%d: %q: got unexpected error: %s", nth, tm.name, resultErr) - } - if tm.expectErr && resultErr == nil { - t.Errorf("%d: %q: expected error, but got no error", nth, tm.name) - } - - gotOpaQryLogMsg := false - gotOpaEvalLogMsg := false - for eth, entry := range loggertesthook.AllEntries() { - t.Logf("%d: %q: [%d]logrus.Entry.Level: %s", nth, tm.name, eth, entry.Level) - t.Logf("%d: %q: [%d]logrus.Entry.Message: %s", nth, tm.name, eth, entry.Message) - t.Logf("%d: %q: [%d]logrus.Entry.Data: %s", nth, tm.name, eth, entry.Data) - - opaResp, gotOpaResp := entry.Data["opaResp"] - entryOpaRespStr := fmt.Sprint(opaResp) - if gotOpaResp { - t.Logf("%d: %q: [%d]logrus.Entry.Data[opaResp]: %s", nth, tm.name, eth, entryOpaRespStr) - } - - if entry.Level == tm.qryLogLvl && entry.Message == tm.qryLogMsg { - gotOpaQryLogMsg = true - if len(tm.qryOpaResp) > 0 && gotOpaResp && entryOpaRespStr != tm.qryOpaResp { - gotOpaQryLogMsg = false - } - continue - } - - if entry.Level == tm.evalLogLvl && entry.Message == tm.evalLogMsg { - gotOpaEvalLogMsg = true - if len(tm.evalOpaResp) > 0 && gotOpaResp && entryOpaRespStr != tm.evalOpaResp { - gotOpaEvalLogMsg = false - } - continue - } - } - - if !gotOpaQryLogMsg { - t.Errorf("%d: %q: Did not get OpaQuery logrus.Entry.Level/Message/opaResp: %s/`%s`/`%s`", - nth, tm.name, tm.qryLogLvl, tm.qryLogMsg, tm.qryOpaResp) - } - - if !gotOpaEvalLogMsg { - t.Errorf("%d: %q: Did not get Evaluate logrus.Entry.Level/Message/opaResp: %s/`%s`/`%s`", - nth, tm.name, tm.evalLogLvl, tm.evalLogMsg, tm.evalOpaResp) - } - } -} - -type MockOpaClienter struct { - Loggr *logrus.Logger - RegoRespJSON string -} - -func (m MockOpaClienter) String() string { - return fmt.Sprintf(`MockOpaClienter{RegoRespJSON:"%s"}`, m.RegoRespJSON) -} - -func (m MockOpaClienter) Address() string { - return "http://localhost:8181" -} - -func (m MockOpaClienter) Health() error { - return nil -} - -func (m MockOpaClienter) Query(ctx context.Context, reqData, resp interface{}) error { - return m.CustomQuery(ctx, "", reqData, resp) -} - -func (m MockOpaClienter) CustomQueryStream(ctx context.Context, document string, postReqBody []byte, respRdrFn opa_client.StreamReaderFn) error { - return nil -} - -func (m MockOpaClienter) CustomQueryBytes(ctx context.Context, document string, reqData interface{}) ([]byte, error) { - return []byte(m.RegoRespJSON), nil -} - -func (m MockOpaClienter) CustomQuery(ctx context.Context, document string, reqData, resp interface{}) error { - err := json.Unmarshal([]byte(m.RegoRespJSON), resp) - m.Loggr.Debugf("CustomQuery: resp=%#v", resp) - return err -} - -type MockDecisionInputr struct { - DecisionInput -} - -func (d MockDecisionInputr) GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*DecisionInput, error) { - return &d.DecisionInput, nil -} diff --git a/grpc_opa/doc.go b/grpc_opa/doc.go deleted file mode 100644 index 9d8c9fe..0000000 --- a/grpc_opa/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package grpc_opa_middleware implements a gRPC middleware for sending -// requests to Opa. -// -package grpc_opa_middleware diff --git a/grpc_opa/entitled_features.go b/grpc_opa/entitled_features.go deleted file mode 100644 index c11841a..0000000 --- a/grpc_opa/entitled_features.go +++ /dev/null @@ -1,169 +0,0 @@ -package grpc_opa_middleware - -import ( - "context" - "fmt" - "sort" - "strings" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -// EntitledFeaturesKeyType is the type of the entitled_features key stored in the caller's context -type EntitledFeaturesKeyType string - -// EntitledFeaturesKey is the entitled_features key stored in the caller's context. -// It is also the entitled_features key in the OPA response. -const EntitledFeaturesKey = EntitledFeaturesKeyType("entitled_features") - -// ErrInvalidEntitledFeatures is returned upon invalid entitled_features -var ErrInvalidEntitledFeatures = status.Errorf(codes.Internal, "Invalid entitled_features") - -// AddRawEntitledFeatures adds raw entitled_features (if they exist) from OPAResponse to context -// The raw JSON-unmarshaled entitled_features is of the form: -// map[string]interface {}{"lic":[]interface {}{"dhcp", "ipam"}, "rpz":[]interface {}{"bogon", "malware"}}} -func (o OPAResponse) AddRawEntitledFeatures(ctx context.Context) context.Context { - efIfc, ok := o[string(EntitledFeaturesKey)] - if ok { - ctx = context.WithValue(ctx, EntitledFeaturesKey, efIfc) - } - return ctx -} - -// FlattenRawEntitledFeatures flattens raw entitled_features into an array -// The raw JSON-unmarshaled entitled_features is of the form: -// map[string]interface {}{"lic":[]interface {}{"dhcp", "ipam"}, "rpz":[]interface {}{"bogon", "malware"}}} -// Returns flattened array of the form: -// []string{"lic.dhcp", "lic.ipam", "rpz.bogon", "rpz.malware"} -func FlattenRawEntitledFeatures(efIfc interface{}) ([]string, error) { - if IsNilInterface(efIfc) { - return nil, nil - } - - efMapIfc, ok := efIfc.(map[string]interface{}) - if !ok { - return nil, ErrInvalidEntitledFeatures - } - - result := []string{} - for svcName, featIfc := range efMapIfc { - if IsNilInterface(featIfc) { - continue - } - - featArrIfc, ok := featIfc.([]interface{}) - if !ok { - return nil, ErrInvalidEntitledFeatures - } - - for _, oneFeatIfc := range featArrIfc { - if IsNilInterface(oneFeatIfc) { - continue - } - - oneFeatStr, ok := oneFeatIfc.(string) - if !ok { - return nil, ErrInvalidEntitledFeatures - } - - flatten := svcName + "." + oneFeatStr - result = append(result, flatten) - } - } - - return result, nil -} - -type opbench struct { - val map[string][]string - err error - log string -} - -// EntitlementsCtxOp is a simple operator to manipulate -// over entitled features in context. It retrieves entitled -// features from the context and converts them to a structure -// that is easy to operate on. Returned value includes only -// unimported fields and is designed to chain other methods -// that it has. -func EntitlementsCtxOp(ctx context.Context) *opbench { - op := opbench{ - val: map[string][]string{}, - log: fmt.Sprintf("ctx (%s) -> ", EntitledFeaturesKey), - } - switch v := ctx.Value(EntitledFeaturesKey).(type) { - case map[string]interface{}: - op.log += fmt.Sprintf("map[string]interface{} (%+v) -> ", v) - for k, vs := range v { - switch vv := vs.(type) { - case []interface{}: - op.log += k + ":[]interface{" - div := "" - for i, f := range vv { - if i > 0 { - div = ", " - } - - switch vvv := f.(type) { - case string: - op.log += fmt.Sprintf("%s%q", div, vvv) - op.val[k] = append(op.val[k], vvv) - case nil: - op.log += "nil (missing string value for key: " + k + ")" - default: - op.log += fmt.Sprintf("%T (unimplemented type for key: %v)", vv, k) - op.err = ErrInvalidEntitledFeatures - return &op - } - } - op.log += "} -> " - case nil: - op.log += "nil (missing value for key: " + k + ")" - default: - op.log += fmt.Sprintf("%T (unimplemented type for key: %v)", vv, k) - op.err = ErrInvalidEntitledFeatures - return &op - } - } - case nil: - op.log += "nil (missing)" - default: - op.log += fmt.Sprintf("%T (unimplemented type)", v) - op.err = ErrInvalidEntitledFeatures - } - return &op -} - -// ToJSONBArrStmt converts entitled features to an array -// statement for futher use with existence JSONB operator. -// Array elements are sorted in increasing order for easier -// testing and reading. The result is finally processed by -// PostgreSQL as '{license.td,license.se}'::text[]. -// The method also returns an error and a trace log entry -// reflecting all data manipulations up to the end result. -// Example of successful log entry: -// ctx (entitled_features) -> map[string]interface{} -// (map[license:[td]]) -> license:[]interface{"td"} -> -// array['license.td'] -func (op *opbench) ToJSONBArrStmt() (string, string, error) { - switch { - case op.err != nil: - return "", op.log, op.err - case len(op.val) == 0: - return "", op.log, nil - } - - var s []string - for k, vs := range op.val { - for _, v := range vs { - s = append(s, "'"+k+"."+v+"'") - } - } - - sort.Strings(s) - a := "array[" + strings.Join(s, ", ") + "]" - - op.log += a - return a, op.log, nil -} diff --git a/grpc_opa/entitled_features_test.go b/grpc_opa/entitled_features_test.go deleted file mode 100644 index 2c841ad..0000000 --- a/grpc_opa/entitled_features_test.go +++ /dev/null @@ -1,273 +0,0 @@ -package grpc_opa_middleware - -import ( - "context" - "encoding/json" - "fmt" - "reflect" - "sort" - "testing" -) - -const ( - succeed = "\u2713" - failed = "\u2717" - red = "\033[31m" - green = "\033[32m" - reset = "\033[0m" -) - -func Test_entitled_features_context(t *testing.T) { - for idx, tst := range entitledFeaturesTest { - var opaResp OPAResponse - - jsonErr := json.Unmarshal([]byte(tst.regoRespJSON), &opaResp) - if jsonErr != nil { - t.Errorf("tst#%d: FAIL: err=%s trying to json.Unmarshal: %s", - idx, jsonErr, tst.regoRespJSON) - continue - } - - t.Logf("tst#%d: opaResp=%#v", idx, opaResp) - - ctx := context.Background() - newCtx := opaResp.AddRawEntitledFeatures(ctx) - - efIfc := newCtx.Value(EntitledFeaturesKey) - if tst.expectNilCtxVal && efIfc != nil { - t.Errorf("tst#%d: FAIL: Got unexpected context.Value(%s): %#v", - idx, string(EntitledFeaturesKey), efIfc) - } else if !tst.expectNilCtxVal && efIfc == nil { - t.Errorf("tst#%d: FAIL: Expected non-nil context.Value(%s), but got nil", - idx, string(EntitledFeaturesKey)) - } - - efArr, flattenErr := FlattenRawEntitledFeatures(efIfc) - if !tst.expectFlattenErr && flattenErr != nil { - t.Errorf("tst#%d: FAIL: Got unexpected FlattenRawEntitledFeatures(%#v) err: %s", idx, efIfc, flattenErr) - } else if tst.expectFlattenErr && flattenErr == nil { - t.Errorf("tst#%d: FAIL: Expected FlattenRawEntitledFeatures(%#v) err, but got nil err", idx, efIfc) - } - - if efArr == nil { - continue - } - - sort.Strings(efArr) - sort.Strings(tst.expectFlattenVal) - if !reflect.DeepEqual(tst.expectFlattenVal, efArr) { - t.Errorf("tst#%d: FAIL: expectFlattenVal=%s\nefArr=%s", - idx, tst.expectFlattenVal, efArr) - } - } -} - -var entitledFeaturesTest = []struct { - regoRespJSON string - expectNilCtxVal bool - expectFlattenErr bool - expectFlattenVal []string -}{ - { - regoRespJSON: `{ - "allow": true - }`, - expectNilCtxVal: true, - expectFlattenErr: false, - expectFlattenVal: nil, - }, - { - regoRespJSON: `{ - "allow": true, - "entitled_features": null - }`, - expectNilCtxVal: true, - expectFlattenErr: false, - expectFlattenVal: nil, - }, - { - regoRespJSON: `{ - "allow": true, - "entitled_features": {} - }`, - expectNilCtxVal: false, - expectFlattenErr: false, - expectFlattenVal: []string{}, - }, - { - regoRespJSON: `{ - "allow": true, - "entitled_features": {"null": null} - }`, - expectNilCtxVal: false, - expectFlattenErr: false, - expectFlattenVal: []string{}, - }, - { - regoRespJSON: `{ - "allow": true, - "entitled_features": "bad entitled_features value" - }`, - expectNilCtxVal: false, - expectFlattenErr: true, - expectFlattenVal: nil, - }, - { - regoRespJSON: `{ - "allow": true, - "entitled_features": {"str": "str"} - }`, - expectNilCtxVal: false, - expectFlattenErr: true, - expectFlattenVal: nil, - }, - { - regoRespJSON: `{ - "allow": true, - "entitled_features": { - "null": null, - "lic": [ "dhcp", null, "ipam" ], - "rpz": [ "bogon", null, "malware" ] - } - }`, - expectNilCtxVal: false, - expectFlattenErr: false, - expectFlattenVal: []string{"lic.dhcp", "lic.ipam", "rpz.bogon", "rpz.malware"}, - }, -} - -func Test_opbench_ToJSONBArrStmt(t *testing.T) { - tests := []struct { - name string - rego string - want string - wantErr bool - }{ - { - name: "OneSvcOneFeatureOk", - rego: `{ - "allow": true, - "entitled_features": { - "license": [ "td" ] - } - }`, - want: "array['license.td']", - wantErr: false, - }, - { - name: "OneSvcManyFeaturesOk", - rego: `{ - "allow": true, - "entitled_features": { - "license": [ "td", "dhcp", "dns" ] - } - }`, - want: "array['license.dhcp', 'license.dns', 'license.td']", - wantErr: false, - }, - { - name: "ManySvcsManyFeaturesOk", - rego: `{ - "allow": true, - "entitled_features": { - "license": [ "td", "dhcp", "dns" ], - "license2": [ "td" ] - } - }`, - want: "array['license.dhcp', 'license.dns', 'license.td', 'license2.td']", - wantErr: false, - }, - { - name: "ManySvcsEmptyFeaturesOk", - rego: `{ - "allow": true, - "entitled_features": { - "license": [], - "license2": [] - } - }`, - want: "", - wantErr: false, - }, - { - name: "EmptyEntitlsOk", - rego: `{ - "allow": true, - "entitled_features": {} - }`, - want: "", - wantErr: false, - }, - { - name: "NulEntitlsOk", - rego: `{ - "allow": true, - "entitled_features": null - }`, - want: "", - wantErr: false, - }, - { - name: "MissingEntitlsOk", - rego: `{ - "allow": true - }`, - want: "", - wantErr: false, - }, - { - name: "EntitlsNulSvcOk", - rego: `{ - "allow": true, - "entitled_features": {"null": null} - }`, - want: "", - wantErr: false, - }, - { - name: "InvalidValErr", - rego: `{ - "allow": true, - "entitled_features": "invalid val" - }`, - want: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var opaResp OPAResponse - jsonErr := json.Unmarshal([]byte(tt.rego), &opaResp) - if jsonErr != nil { - t.Fatalf("JSON unmarshal error passing data: %s", tt.rego) - } - ctx := opaResp.AddRawEntitledFeatures(context.Background()) - - got, log, err := EntitlementsCtxOp(ctx).ToJSONBArrStmt() - t.Log(log) - - if err != nil { - if tt.wantErr { - t.Logf("\t%s test is passed", succeed) - return - } else { - t.Errorf("\t%s unexpected error"+ - "\nGot: "+red+"%v"+reset+"\nWant: "+green+"%t"+reset, - failed, err, tt.wantErr) - return - } - } - - if !reflect.DeepEqual(got, tt.want) { - vs := fmt.Sprintf("\t%s difference in got vs want statment"+ - "\nGot: "+red+" \n\n%s\n\n "+reset+"\nWant: "+green+"\n\n%s\n\n"+reset, - failed, got, tt.want) - t.Errorf(vs) - return - } else { - t.Logf("\t%s test is passed", succeed) - } - }) - } -} diff --git a/grpc_opa/example_test.go b/grpc_opa/example_test.go deleted file mode 100644 index 995bf04..0000000 --- a/grpc_opa/example_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package grpc_opa_middleware - -// // Initialization shows a relatively complex initialization sequence. -// func Example_initialization() { -// _ = grpc.NewServer( -// grpc_middleware.WithUnaryServerChain( -// UnaryServerInterceptor("app"), -// ), -// ) -// } - -// func Example_custom_querier() { - -// customQuerier := QueryFn(func(ctx context.Context, fullMethodName string, cli *opa_client.Client) (interface{}, bool, error) { -// return &struct{}{}, false, nil -// }) - -// _ = grpc.NewServer( -// grpc_middleware.WithUnaryServerChain( -// UnaryServerInterceptor("app", -// WithQuerier(customQuerier), -// WithHTTPClient(http.DefaultClient), -// WithAddress(opa_client.DefaultAddress), -// ), -// ), -// ) - -// } diff --git a/grpc_opa/makefile b/grpc_opa/makefile deleted file mode 100644 index 69da23c..0000000 --- a/grpc_opa/makefile +++ /dev/null @@ -1,5 +0,0 @@ -export GOPRIVATE += ,github.com/Infoblox-CTO - -.PHONY: vendor -vendor: - go mod tidy -v diff --git a/grpc_opa/options.go b/grpc_opa/options.go deleted file mode 100644 index 562e023..0000000 --- a/grpc_opa/options.go +++ /dev/null @@ -1,81 +0,0 @@ -package grpc_opa_middleware - -import ( - "net/http" - - "github.com/infobloxopen/atlas-authz-middleware/pkg/opa_client" -) - -type Option func(c *Config) - -// WithAddress -func WithAddress(address string) Option { - return func(c *Config) { - c.address = address - } -} - -// WithHTTPClient overrides the http.Client used to call Opa -func WithHTTPClient(client *http.Client) Option { - return func(c *Config) { - if client != nil { - c.httpCli = client - } - } -} - -// WithOpaClienter overrides the Clienter used to call Opa. -// This option takes precedence over WithHTTPClient. -func WithOpaClienter(clienter opa_client.Clienter) Option { - return func(c *Config) { - if clienter != nil { - c.clienter = clienter - } - } -} - -// WithOpaEvaluator overrides the OpaEvaluator use to -// evaluate authorization against OPA. -func WithOpaEvaluator(opaEvaluator OpaEvaluator) Option { - return func(c *Config) { - c.opaEvaluator = opaEvaluator - } -} - -// WithAuthorizer overrides the request/response -// processing of OPA. Multiple authorizers can be passed -func WithAuthorizer(auther ...Authorizer) Option { - return func(c *Config) { - c.authorizer = auther - } -} - -// WithDecisionInputHandler supplies optional DecisionInputHandler -// for DefaultAuthorizer to obtain additional input for OPA -// ABAC decision processing. -func WithDecisionInputHandler(decisionHandler DecisionInputHandler) Option { - return func(c *Config) { - c.decisionInputHandler = decisionHandler - } -} - -// WithClaimsVerifier overrides default ClaimsVerifier -func WithClaimsVerifier(claimsVerifier ClaimsVerifier) Option { - return func(c *Config) { - c.claimsVerifier = claimsVerifier - } -} - -// WithEntitledServices overrides default EntitledServices -func WithEntitledServices(entitledServices ...string) Option { - return func(c *Config) { - c.entitledServices = entitledServices - } -} - -// WithAcctEntitlementsApiPath overrides default AcctEntitlementsApiPath -func WithAcctEntitlementsApiPath(acctEntitlementsApi string) Option { - return func(c *Config) { - c.acctEntitlementsApi = acctEntitlementsApi - } -} diff --git a/grpc_opa/options_test.go b/grpc_opa/options_test.go deleted file mode 100644 index 59a59a1..0000000 --- a/grpc_opa/options_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package grpc_opa_middleware - -import ( - "context" - "encoding/json" - "fmt" - "reflect" - "testing" - - "github.com/infobloxopen/atlas-authz-middleware/pkg/opa_client" - "github.com/infobloxopen/atlas-authz-middleware/utils_test" -) - -func Test_WithEntitledServices_payload(t *testing.T) { - var uninitializedStrSlice []string - withEntitledServicesTests := []struct { - name string - inputEntitledServices interface{} - expectEntitledServices []string - }{ - { - name: `dont-call-WithEntitledServices`, - inputEntitledServices: `dont-call-WithEntitledServices`, - expectEntitledServices: nil, - }, - { - name: `WithEntitledServices(nil)`, - inputEntitledServices: nil, - expectEntitledServices: nil, - }, - { - name: `WithEntitledServices(uninitializedStrSlice)`, - inputEntitledServices: uninitializedStrSlice, - expectEntitledServices: nil, - }, - { - name: `WithEntitledServices([])`, - inputEntitledServices: []string{}, - expectEntitledServices: []string{}, - }, - { - name: `WithEntitledServices(["lic"])`, - inputEntitledServices: []string{"lic"}, - expectEntitledServices: []string{"lic"}, - }, - { - name: `WithEntitledServices(["lic","rpz"])`, - inputEntitledServices: []string{"lic", "rpz"}, - expectEntitledServices: []string{"lic", "rpz"}, - }, - } - - testingTCtx := context.WithValue(context.Background(), utils_test.TestingTContextKey, t) - - for idx, tstc := range withEntitledServicesTests { - tcCtx := context.WithValue(testingTCtx, utils_test.TestCaseIndexContextKey, idx) - tcCtx = context.WithValue(tcCtx, utils_test.TestCaseNameContextKey, tstc.name) - - mockOpaClienter := optionsMockOpaClienter{ - VerifyEntitledServices: true, - ExpectEntitledServices: tstc.expectEntitledServices, - } - - auther := NewDefaultAuthorizer("app", - WithOpaClienter(&mockOpaClienter), - WithClaimsVerifier(NullClaimsVerifier), - ) - - inputEntitledServices, ok := tstc.inputEntitledServices.([]string) - if tstc.inputEntitledServices == nil || ok { - t.Logf("tst#%d: name=%s; calling option WithEntitledServices(%#v)", - idx, tstc.name, inputEntitledServices) - auther = NewDefaultAuthorizer("app", - WithOpaClienter(&mockOpaClienter), - WithClaimsVerifier(NullClaimsVerifier), - WithEntitledServices(inputEntitledServices...), - ) - } - - auther.AffirmAuthorization(tcCtx, "FakeMethod", nil) - } -} - -type optionsMockOpaClienter struct { - VerifyEntitledServices bool - ExpectEntitledServices []string -} - -func (m optionsMockOpaClienter) String() string { - return fmt.Sprintf(`optionsMockOpaClienter{VerifyEntitledServices:%v,ExpectEntitledServices:%#v}`, - m.VerifyEntitledServices, m.ExpectEntitledServices) -} - -func (m optionsMockOpaClienter) Address() string { - return "http://optionsMockOpaClienter:8181" -} - -func (m optionsMockOpaClienter) Health() error { - return nil -} - -func (m optionsMockOpaClienter) Query(ctx context.Context, reqData, resp interface{}) error { - return m.CustomQuery(ctx, "", reqData, resp) -} - -func (m optionsMockOpaClienter) CustomQueryStream(ctx context.Context, document string, postReqBody []byte, respRdrFn opa_client.StreamReaderFn) error { - return nil -} - -func (m optionsMockOpaClienter) CustomQueryBytes(ctx context.Context, document string, reqData interface{}) ([]byte, error) { - return []byte(`{"allow": true}`), nil -} - -func (m optionsMockOpaClienter) CustomQuery(ctx context.Context, document string, reqData, resp interface{}) error { - 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) - if m.VerifyEntitledServices && !reflect.DeepEqual(payload.EntitledServices, m.ExpectEntitledServices) { - t.Errorf("tst#%d: FAIL: name=%s; not equal: payload.EntitledServices=%#v; m.ExpectEntitledServices=%#v", - tcIdx, tcName, payload.EntitledServices, m.ExpectEntitledServices) - } - return json.Unmarshal([]byte(`{"allow": true}`), resp) -} diff --git a/grpc_opa/server_interceptor.go b/grpc_opa/server_interceptor.go deleted file mode 100644 index e5656f5..0000000 --- a/grpc_opa/server_interceptor.go +++ /dev/null @@ -1,149 +0,0 @@ -package grpc_opa_middleware - -import ( - "context" - "errors" - - "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" - "github.com/sirupsen/logrus" - "google.golang.org/grpc" - - "github.com/infobloxopen/atlas-authz-middleware/pkg/opa_client" -) - -const ( - authZKey = key("grpc-authz-key") -) - -var ( - // Application is set at initization - Application string - ErrNoCredentials = errors.New("no credentials found") -) - -type key string - -// NewDefaultConfig returns a new default Config. -// If WithAuthorizer() option is not specified, -// then a new DefaultAuthorizer is used. -func NewDefaultConfig(application string, opts ...Option) *Config { - cfg := &Config{ - address: opa_client.DefaultAddress, - } - - for _, opt := range opts { - opt(cfg) - } - - opts = append([]Option{WithOpaClienter(opa_client.New(cfg.address, opa_client.WithHTTPClient(cfg.httpCli)))}, opts...) - - authorizer := NewDefaultAuthorizer(application, opts...) - - if cfg.authorizer == nil { - logrus.Info("authorizers empty, using default authorizer") - cfg.authorizer = []Authorizer{authorizer} - } - - return cfg -} - -// UnaryServerInterceptor returns a new unary client interceptor that optionally logs the execution of external gRPC calls. -func UnaryServerInterceptor(application string, opts ...Option) grpc.UnaryServerInterceptor { - cfg := NewDefaultConfig(application, opts...) - - return func(ctx context.Context, grpcReq interface{}, info *grpc.UnaryServerInfo, grpcUnaryHandler grpc.UnaryHandler) (interface{}, error) { - logger := ctxlogrus.Extract(ctx) - - var ( - ok bool - newCtx context.Context - err error - ) - - for _, auther := range cfg.authorizer { - ok, newCtx, err = auther.Evaluate(ctx, info.FullMethod, grpcReq, auther.OpaQuery) - if err != nil { - logger.WithError(err).WithField("authorizer", auther).Error("unable_authorize") - } - if ok { - break - } - } - - if err != nil { - return nil, err - } - - if !ok { - logger.WithError(opa_client.ErrUndefined).Error("policy engine returned undefined response") - return nil, opa_client.ErrUndefined - } - - // TODO: pass along authz information through context - return grpcUnaryHandler(newCtx, grpcReq) - } -} - -// StreamServerInterceptor returns a new Stream client interceptor that optionally logs the execution of external gRPC calls. -func StreamServerInterceptor(application string, opts ...Option) grpc.StreamServerInterceptor { - cfg := NewDefaultConfig(application, opts...) - - return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, grpcStreamHandler grpc.StreamHandler) error { - logger := ctxlogrus.Extract(stream.Context()) - - var ( - ok bool - newCtx context.Context - err error - ) - - for _, auther := range cfg.authorizer { - ok, newCtx, err = auther.Evaluate(stream.Context(), info.FullMethod, info, auther.OpaQuery) - if err != nil { - logger.WithError(err).WithField("authorizer", auther).Error("unable_authorize") - } - if ok { - break - } - } - - if err != nil { - return err - } - - if !ok { - logger.WithError(opa_client.ErrUndefined).Error("policy engine returned undefined response") - return opa_client.ErrUndefined - } - - // TODO: pass along authz information through context - wrapped := wrapServerStream(stream) - wrapped.WrappedCtx = newCtx - return grpcStreamHandler(srv, wrapped) - } -} - -// FromContext retrieves authZ information from the Context -func FromContext(ctx context.Context) interface{} { - return ctx.Value(authZKey) -} - -// WrappedSrvStream allows modifying context. -type WrappedSrvStream struct { - grpc.ServerStream - // It is wrapper's own Context. - WrappedCtx context.Context -} - -// Context returns the wrapper's WrappedCtx -func (w *WrappedSrvStream) Context() context.Context { - return w.WrappedCtx -} - -// wrapServerStream returns a ServerStream that has the ability to overwrite context. -func wrapServerStream(stream grpc.ServerStream) *WrappedSrvStream { - if existing, ok := stream.(*WrappedSrvStream); ok { - return existing - } - return &WrappedSrvStream{ServerStream: stream, WrappedCtx: stream.Context()} -} diff --git a/grpc_opa/server_interceptor_test.go b/grpc_opa/server_interceptor_test.go deleted file mode 100644 index 5b104a8..0000000 --- a/grpc_opa/server_interceptor_test.go +++ /dev/null @@ -1,395 +0,0 @@ -package grpc_opa_middleware - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net" - "net/http" - "syscall" - "testing" - "time" - - "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" - logrus "github.com/sirupsen/logrus" - logrustesthook "github.com/sirupsen/logrus/hooks/test" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - - "github.com/infobloxopen/atlas-authz-middleware/pkg/opa_client" - "github.com/infobloxopen/atlas-authz-middleware/utils_test" -) - -var netDialErr = &net.OpError{Op: "dial", Net: "tcp", Err: syscall.ECONNREFUSED} - -type connFailTransport struct { - httpReq *http.Request -} - -func (t *connFailTransport) RoundTrip(httpReq *http.Request) (httpResp *http.Response, err error) { - t.httpReq = httpReq - return nil, netDialErr -} - -func init() { - logrus.SetLevel(logrus.TraceLevel) -} - -func TestConnFailure(t *testing.T) { - grpcUnaryHandler := func(ctx context.Context, grpcReq interface{}) (interface{}, error) { - return nil, nil - } - interceptor := UnaryServerInterceptor("app", - WithHTTPClient(&http.Client{Transport: &connFailTransport{},}), - WithClaimsVerifier(NullClaimsVerifier), - ) - - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Second)) - ctx = context.WithValue(ctx, utils_test.TestingTContextKey, t) - defer cancel() - grpcResp, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{FullMethod: "FakeMethod"}, grpcUnaryHandler) - if grpcResp != nil { - t.Errorf("unexpected grpcResp: %#v", grpcResp) - } - - if e := opa_client.ErrServiceUnavailable; !errors.Is(err, e) { - t.Errorf("got: %s wanted: %s", err, e) - } -} - -func TestMockOPA(t *testing.T) { - grpcUnaryHandler := func(ctx context.Context, grpcReq interface{}) (interface{}, error) { - return nil, nil - } - - mock := new(mockAuthorizer) - interceptor := UnaryServerInterceptor("app", - WithAuthorizer(mock), - WithClaimsVerifier(NullClaimsVerifier), - ) - - deadline := time.Now().Add(3 * time.Second) - ctx, cancel := context.WithDeadline(context.Background(), deadline) - ctx = context.WithValue(ctx, utils_test.TestingTContextKey, t) - defer cancel() - testMap := []struct { - code codes.Code - fn AuthorizeFn - errMsg string - }{ - { - code: codes.Internal, - fn: func(ctx context.Context, fullMethodName string, grpcReq interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) { - return false, ctx, grpc.Errorf(codes.Internal, "boom") - }, - errMsg: "boom", - }, - } - - for _, tm := range testMap { - mock.evaluate = tm.fn - _, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{FullMethod: "FakeMethod"}, grpcUnaryHandler) - if e := tm.code; e != grpc.Code(err) { - t.Errorf("got: %s wanted: %s", grpc.Code(err), e) - } - if e := tm.errMsg; e != grpc.ErrorDesc(err) { - t.Errorf("got: %s wanted: %s", err, e) - } - } -} - -type mockAuthorizer struct { - DefaultAuthorizer - evaluate func(ctx context.Context, fullMethod string, grpcReq interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) -} - -func (m *mockAuthorizer) Evaluate(ctx context.Context, fullMethod string, grpcReq interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) { - return m.evaluate(ctx, fullMethod, grpcReq, opaEvaluator) -} - -func TestStreamServerInterceptorMockAuthorizer(t *testing.T) { - grpcStreamHandler := func(srv interface{}, stream grpc.ServerStream) error { - return nil - } - - mock := new(mockAuthorizer) - interceptor := StreamServerInterceptor("app", - WithAuthorizer(mock), - WithClaimsVerifier(NullClaimsVerifier), - ) - - deadline := time.Now().Add(3 * time.Second) - ctx, cancel := context.WithDeadline(context.Background(), deadline) - ctx = context.WithValue(ctx, utils_test.TestingTContextKey, t) - defer cancel() - testMap := []struct { - code codes.Code - fn AuthorizeFn - errMsg string - }{ - { - code: codes.Internal, - fn: func(ctx context.Context, fullMethodName string, grpcReq interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) { - return false, ctx, grpc.Errorf(codes.Internal, "boom") - }, - errMsg: "boom", - }, - } - - srvStream := WrappedSrvStream{ - WrappedCtx: context.Background(), - } - - for _, tm := range testMap { - mock.evaluate = tm.fn - err := interceptor(ctx, &srvStream, &grpc.StreamServerInfo{FullMethod: "FakeMethod"}, grpcStreamHandler) - - if e := tm.code; e != grpc.Code(err) { - t.Errorf("got: %s wanted: %s", grpc.Code(err), e) - } - if e := tm.errMsg; e != grpc.ErrorDesc(err) { - t.Errorf("got: %s wanted: %s", err, e) - } - } -} - -type mockAuthorizerWithAllowOpaEvaluator struct { - defAuther *DefaultAuthorizer -} - -func (m mockAuthorizerWithAllowOpaEvaluator) String() string { - return fmt.Sprintf("mockAuthorizerWithAllowOpaEvaluator{defAuther:%s}", m.defAuther.String()) -} - -func (a *mockAuthorizerWithAllowOpaEvaluator) Evaluate(ctx context.Context, fullMethod string, grpcReq interface{}, opaEvaluator OpaEvaluator) (bool, context.Context, error) { - return a.defAuther.Evaluate(ctx, fullMethod, grpcReq, opaEvaluator) -} - -func (m *mockAuthorizerWithAllowOpaEvaluator) OpaQuery(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error { - t, _ := ctx.Value(utils_test.TestingTContextKey).(*testing.T) - _, ok := opaReq.(Payload) - allow := "true" - if !ok { - allow = "false" - t.Errorf("invalid opa payload (type: %T)", opaReq) - } - respJSON := fmt.Sprintf(`{"allow": %s}`, allow) - return json.Unmarshal([]byte(respJSON), opaResp) -} - -func newMockAuthorizerWithAllowOpaEvaluator(application string, opts ...Option) *mockAuthorizerWithAllowOpaEvaluator { - a := new(mockAuthorizerWithAllowOpaEvaluator) - a.defAuther = NewDefaultAuthorizer(application, opts...) - return a -} - -type badDecisionInputer struct{} - -func (m badDecisionInputer) String() string { - return "badDecisionInputer{}" -} - -func (m *badDecisionInputer) GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*DecisionInput, error) { - return nil, fmt.Errorf("badDecisionInputer") -} - -type jsonMarshalableInputer struct{} - -func (m jsonMarshalableInputer) String() string { - return "jsonMarshalableInputer{}" -} - -func (m *jsonMarshalableInputer) GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*DecisionInput, error) { - var sealctx []interface{} - sealctx = append(sealctx, map[string]interface{}{ - "id": "guid1", - "name": "ClassA", - "tags": map[string]string{ - "dept": "finance", - "zone": "apj", - }, - }) - sealctx = append(sealctx, map[string]interface{}{ - "id": "guid2", - "name": "ClassB", - "tags": map[string]string{ - "dept": "sales", - "zone": "emea", - }, - }) - - inp, _ := defDecisionInputer.GetDecisionInput(ctx, fullMethod, grpcReq) - inp.SealCtx = sealctx - - //logrus.Debugf("inp=%+v", *inp) - return inp, nil -} - -type jsonNonMarshalableInputer struct{} - -func (m jsonNonMarshalableInputer) String() string { - return "jsonNonMarshalableInputer{}" -} - -func (m *jsonNonMarshalableInputer) GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*DecisionInput, error) { - var sealctx []interface{} - sealctx = append(sealctx, NullClaimsVerifier) // NullClaimsVerifier is a non-json-marshalable fn) - - inp, _ := defDecisionInputer.GetDecisionInput(ctx, fullMethod, grpcReq) - inp.SealCtx = sealctx - //logrus.Debugf("inp=%+v", *inp) - return inp, nil -} - -func TestDecisionInput(t *testing.T) { - testMap := []struct { - err error - abacType string - abacVerb string - inputer DecisionInputHandler - jwtHeader string - errLogMsg string - authorizerField string - }{ - { - err: nil, - abacType: "electron", - abacVerb: "run", - inputer: new(jsonMarshalableInputer), - // fake jwt with fake signature - jwtHeader: "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJvZ3VzQGZvby5jb20iLCJhcGlfdG9rZW4iOiIwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZiIsImFjY291bnRfaWQiOiI0MDQiLCJncm91cHMiOlsiZm9vX2FkbWluIiwiYWRtaW4iXSwiYXVkIjoiZm9vLWF1ZGllbmNlIiwiZXhwIjoyMzk4MzY3NTQwLCJpYXQiOjE1MzQzNjc1NDAsImlzcyI6ImZvby1pc3N1ZXIiLCJuYmYiOjE1MzQzNjc1NDB9.fhwZBaz7TkRbcPcM1M_l1B_S1UZSvro3jwc4EhV37IA", - }, - { - err: ErrInvalidArg, - abacType: "proton", - abacVerb: "jump", - inputer: new(jsonNonMarshalableInputer), - // fake svc-svc jwt with fake signature - jwtHeader: "bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlIjoiZm9vLXNlcnZpY2UiLCJhdWQiOiJmb28tYXVkaWVuY2UiLCJleHAiOjIzOTg4NzI3NzgsImp0aSI6ImZvby1qdGkiLCJpYXQiOjE1MzUzMjE0MDcsImlzcyI6ImZvby1pc3N1ZXIiLCJuYmYiOjE1MzUzMjE0MDd9.4zcNzRrhIXN3s6jNYWIbe6TRBaOwTh_Yy1iSCqVW9H4pT3p2c23TSsLq6R2zs-xmsZ5jTUvalpQgPJwbFmdvxA", - errLogMsg: `unable_authorize`, - authorizerField: `mockAuthorizerWithAllowOpaEvaluator{defAuther:grpc_opa_middleware.DefaultAuthorizer{application:"myapplication" clienter:opa_client.Client{address:"http://localhost:8181"} decisionInputHandler:jsonNonMarshalableInputer{}}}`, - }, - { - err: ErrInvalidArg, - abacType: "neutron", - abacVerb: "swim", - inputer: new(badDecisionInputer), - // empty jwt with fake signature - jwtHeader: "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.A5mVf-_pE0XM6RlWnNx4YBzFWqYIcsc3_j1g9I2768c", - errLogMsg: `unable_authorize`, - authorizerField: `mockAuthorizerWithAllowOpaEvaluator{defAuther:grpc_opa_middleware.DefaultAuthorizer{application:"myapplication" clienter:opa_client.Client{address:"http://localhost:8181"} decisionInputHandler:badDecisionInputer{}}}`, - }, - } - - loggertesthook := logrustesthook.NewGlobal() - - for nth, tm := range testMap { - grpcUnaryHandler := func(ctx context.Context, grpcReq interface{}) (interface{}, error) { - return nil, nil - } - - mockInputer := tm.inputer - mockAuthzer := newMockAuthorizerWithAllowOpaEvaluator("myapplication", WithDecisionInputHandler(mockInputer)) - interceptor := UnaryServerInterceptor("app", - WithAuthorizer(mockAuthzer), - WithDecisionInputHandler(mockInputer), - WithClaimsVerifier(NullClaimsVerifier), - ) - - headers := map[string]string{ - "authorization": tm.jwtHeader, - } - - ctx := context.Background() - ctx = context.WithValue(ctx, utils_test.TestingTContextKey, t) - ctx = context.WithValue(ctx, TypeKey, tm.abacType) - ctx = context.WithValue(ctx, VerbKey, tm.abacVerb) - ctx = ctxlogrus.ToContext(ctx, logrus.NewEntry(logrus.StandardLogger())) - ctx = metadata.NewIncomingContext(ctx, metadata.New(headers)) - - loggertesthook.Reset() - - _, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{FullMethod: "FakeService.FakeMethod"}, grpcUnaryHandler) - //t.Logf("err=%+v tm.err=%+v", err, tm.err) - if err != tm.err { - t.Errorf("%d: got: %s wanted: %s", nth, err, tm.err) - } else if err != nil { - gotExpectedErrLogMsg := false - for _, entry := range loggertesthook.AllEntries() { - t.Logf("%d: logrus.Entry.Message: %s", nth, entry.Message) - t.Logf("%d: logrus.Entry.Data: %s", nth, entry.Data) - authorizerFieldVal := entry.Data["authorizer"] - authorizerField := fmt.Sprint(authorizerFieldVal) - if entry.Message == tm.errLogMsg { - if authorizerField == tm.authorizerField { - gotExpectedErrLogMsg = true - } else { - t.Errorf("%d: Did not\n get authorizerField: `%s`\n got authorizerField: `%s`", - nth, tm.authorizerField, authorizerField) - } - break - } - } - if !gotExpectedErrLogMsg { - t.Errorf("%d: Did not get logrus.Entry.Message: `%s`", nth, tm.errLogMsg) - } - } - } -} - -func TestStreamServerInterceptorMockOpaClient(t *testing.T) { - testMap := []struct { - regoRespJSON string - expErr error - }{ - { - regoRespJSON: `{"allow": false, "obligations": {}}`, - expErr: ErrForbidden, - }, - { - regoRespJSON: `{"allow": true, "obligations": {}}`, - expErr: nil, - }, - { - regoRespJSON: `invalid json result`, - expErr: opa_client.ErrUnknown, - }, - } - - stdLoggr := logrus.StandardLogger() - ctx := context.WithValue(context.Background(), utils_test.TestingTContextKey, t) - ctx = ctxlogrus.ToContext(ctx, logrus.NewEntry(stdLoggr)) - - srvStream := WrappedSrvStream{ - WrappedCtx: ctx, - } - - grpcStreamHandler := func(srv interface{}, stream grpc.ServerStream) error { - return nil - } - - for idx, tm := range testMap { - mockOpaClienter := MockOpaClienter{ - Loggr: stdLoggr, - RegoRespJSON: tm.regoRespJSON, - } - interceptor := StreamServerInterceptor("app", - WithOpaClienter(mockOpaClienter), - WithClaimsVerifier(NullClaimsVerifier), - ) - - gotErr := interceptor(ctx, &srvStream, - &grpc.StreamServerInfo{FullMethod: "FakeMethod"}, - grpcStreamHandler) - t.Logf("%d: gotErr=%s", idx, gotErr) - - if (tm.expErr == nil) && gotErr != nil { - t.Errorf("%d: expErr=nil, gotErr=%s", idx, gotErr) - } else if (tm.expErr != nil) && (tm.expErr != gotErr) { - t.Errorf("%d: expErr=%s, gotErr=%s", idx, tm.expErr, gotErr) - } - } -} diff --git a/grpc_opa/testdata/mock_authz_policy.rego b/grpc_opa/testdata/mock_authz_policy.rego deleted file mode 100644 index ead8d65..0000000 --- a/grpc_opa/testdata/mock_authz_policy.rego +++ /dev/null @@ -1,125 +0,0 @@ -package authz.rbac - -validate_v1 = { - "allow": true, -} - -acct_entitlements_acct_ids_is_empty { - not input.acct_entitlements_acct_ids -} - -acct_entitlements_acct_ids_is_empty { - is_array(input.acct_entitlements_acct_ids) - count(input.acct_entitlements_acct_ids) == 0 -} - -acct_entitlements_services_is_empty { - not input.acct_entitlements_services -} - -acct_entitlements_services_is_empty { - is_array(input.acct_entitlements_services) - count(input.acct_entitlements_services) == 0 -} - -acct_entitlements_api = acct_ent_result { - # No filtering, get all acct_entitlements for all acct_entitlements_acct_ids - acct_entitlements_acct_ids_is_empty - acct_entitlements_services_is_empty - acct_ent_result := account_service_features -} else = acct_ent_result { - acct_ent_result := acct_entitlements_filtered_api -} - -# Get acct_entitlements by specific acct_entitlements_acct_ids -# and specific acct_entitlements_services -acct_entitlements_filtered_api[acct_id] = acct_ent { - is_array(input.acct_entitlements_acct_ids) - count(input.acct_entitlements_acct_ids) > 0 - is_array(input.acct_entitlements_services) - count(input.acct_entitlements_services) > 0 - acct_id := input.acct_entitlements_acct_ids[_] - acct_ent := {ent_svc_name: ent_svc_feats | - ent_svc_name := input.acct_entitlements_services[_] - ent_svc_feats := account_service_features[acct_id][ent_svc_name] - } -} - -account_service_features := { - "2001016": { - "environment": [ - "ac", - "heated-seats", - ], - "wheel": [ - "abs", - "alloy", - "tpms", - ], - }, - "2001040": { - "environment": [ - "ac", - "side-mirror-defogger", - ], - "powertrain": [ - "automatic", - "turbo", - ], - }, - "2001230": { - "powertrain": [ - "manual", - "v8", - ], - "wheel": [ - "run-flat", - ], - }, -} - -test_acct_entitlements_api_no_input { - results := acct_entitlements_api - trace(sprintf("results: %v", [results])) - results == account_service_features -} - -test_acct_entitlements_api_empty_input { - results := acct_entitlements_api with input as { - "acct_entitlements_acct_ids": [], - "acct_entitlements_services": [], - } - trace(sprintf("results: %v", [results])) - results == account_service_features -} - -test_acct_entitlements_api_with_input { - results := acct_entitlements_api with input as { - "acct_entitlements_acct_ids": ["2001040", "2001230"], - "acct_entitlements_services": ["powertrain", "wheel"], - } - trace(sprintf("results: %v", [results])) - results == { - "2001040": { - "powertrain": [ - "automatic", - "turbo", - ], - }, - "2001230": { - "powertrain": [ - "manual", - "v8", - ], - "wheel": [ - "run-flat", - ], - }, - } -} - -# 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 . -# curl -X POST -H 'Content-Type: application/json' http://localhost:8181/v1/data/authz/rbac/acct_entitlements_api | jq . - diff --git a/grpc_opa/testdata/mock_system_main.rego b/grpc_opa/testdata/mock_system_main.rego deleted file mode 100644 index 9ed1925..0000000 --- a/grpc_opa/testdata/mock_system_main.rego +++ /dev/null @@ -1,42 +0,0 @@ -# OPA POST query without url path will query OPA's configured default decision document. -# By default the default decision document is /data/system/main. -# ( See https://www.openpolicyagent.org/docs/v0.29.4/rest-api/#query-api ) -# -# This test rego defines a dummy authz policy for /data/system/main. -# It verifies that queries with and without url path: -# - requires different input document formats -# - returns different response document formats -# -# $ opa run --server mock_system_main.rego -# -# POST query WITHOUT url path against unspecified default decision document -# Notice that: -# - the input document must NOT be encapsulated inside "input" -# - the return document is NOT encapsulated inside "result" -# $ curl -X POST -H 'Content-Type: application/json' http://localhost:8181/ -d '{"application": "automobile", "endpoint": "Vehicle.StompGasPedal"}' | jq . -# ==> returns {"allow": true} -# $ curl -X POST -H 'Content-Type: application/json' http://localhost:8181/ -d '{"input": {"application": "automobile", "endpoint": "Vehicle.StompGasPedal"}}' | jq . -# ==> returns {"allow": false} -# -# POST query WITH url path against any explicitly specified decision document -# Notice that: -# - the input document MUST be encapsulated inside "input" -# - the return document is ALWAYS encapsulated inside "result" -# $ curl -X POST -H 'Content-Type: application/json' http://localhost:8181/v1/data/system/main -d '{"application": "automobile", "endpoint": "Vehicle.StompGasPedal"}' | jq . -# ==> returns {"result": {"allow": false}} -# $ curl -X POST -H 'Content-Type: application/json' http://localhost:8181/v1/data/system/main -d '{"input": {"application": "automobile", "endpoint": "Vehicle.StompGasPedal"}}' | jq . -# ==> returns {"result": {"allow": true}} -# - -package system - -default allow = false - -allow { - input.application == "automobile" - input.endpoint == "Vehicle.StompGasPedal" -} - -main = { - "allow": allow, -} diff --git a/http_opa/authorizer.go b/http_opa/authorizer.go new file mode 100644 index 0000000..0e16541 --- /dev/null +++ b/http_opa/authorizer.go @@ -0,0 +1,261 @@ +// Package httpopa provides an implementation of the az.Authorizer interface for HTTP-based authorization using OPA (Open Policy Agent). +package httpopa + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" + "github.com/infobloxopen/atlas-authz-middleware/v2/common" + 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" + "github.com/infobloxopen/atlas-authz-middleware/v2/http_opa/exception" + "github.com/infobloxopen/atlas-authz-middleware/v2/http_opa/util" + "github.com/infobloxopen/atlas-authz-middleware/v2/pkg/opa_client" + log "github.com/sirupsen/logrus" +) + +// SERVICENAME is the name of the OPA service. +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 + endpointModifier *EndpointModifier +} + +var defDecisionInputer = new(az.DefaultDecisionInputer) + +// NewHttpAuthorizer creates a new instance of httpAuthorizer with the given application name and options. +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: common.DefaultAcctEntitlementsApiPath, + } + for _, opt := range opts { + opt(cfg) + } + + clienter := cfg.clienter + if clienter == nil { + clienter = opa_client.New(cfg.address, opa_client.WithHTTPClient(cfg.httpCli)) + } + + a := httpAuthorizer{ + clienter: clienter, + opaEvaluator: cfg.opaEvaluator, + application: application, + decisionInputHandler: cfg.decisionInputHandler, + claimsVerifier: cfg.claimsVerifier, + entitledServices: cfg.entitledServices, + acctEntitlementsApi: cfg.acctEntitlementsApi, + endpointModifier: cfg.endpointModifier, + } + return &a +} + +// Evaluate evaluates the authorization for the given endpoint and request. +func (a *httpAuthorizer) Evaluate(ctx context.Context, endpoint string, req interface{}, opaEvaluator az.OpaEvaluator) (bool, context.Context, error) { + // Extract the logger from the context + logger := ctxlogrus.Extract(ctx).WithFields(log.Fields{ + "application": a.application, + }) + + // Get the bearer token from the request + bearer, err := util.GetBearerFromRequest(req.(*http.Request)) + if err != nil { + logger.WithError(err).Error("get_bearer_from_request") + return false, ctx, exception.ErrForbidden + } + + // Verify the bearer token and get the raw JWT + claimsVerifier := a.claimsVerifier + if claimsVerifier == nil { + claimsVerifier = commonClaim.UnverifiedClaimFromBearers + } + rawJWT, errs := claimsVerifier([]string{bearer}, nil) + if len(errs) > 0 { + return false, ctx, exception.NewHttpError( + exception.WithError(errors.Join(errs...)), + exception.WithHttpStatus(http.StatusUnauthorized)) + } + + // Get the request ID from the request + reqID := util.GetRequestIdFromRequest(req.(*http.Request)) + + // Modify the endpoint if necessary + pargsEndpoint := endpoint + if a.endpointModifier != nil { + pargsEndpoint = a.endpointModifier.getModifiedEndpoint(pargsEndpoint) + } + + // Prepare the OPA request payload + opaReq := opautil.Payload{ + Endpoint: pargsEndpoint, + FullMethod: endpoint, + Application: a.application, + JWT: opautil.RedactJWT(rawJWT), + RequestID: reqID, + EntitledServices: a.entitledServices, + } + + // Get the decision input for the endpoint and request + decisionInput, err := a.decisionInputHandler.GetDecisionInput(ctx, endpoint, req) + if decisionInput == nil || err != nil { + logger.WithFields(log.Fields{ + "endpoint": endpoint, + }).WithError(err).Error("get_decision_input") + return false, ctx, exception.ErrInvalidArg + } + + opaReq.DecisionInput = *decisionInput + + // TODO: Add tracing for the middleware + // opaReqJSON, err := json.Marshal(opaReq) + // if err != nil { + // logger.WithFields(log.Fields{ + // "opaReq": opaReq, + // }).WithError(err).Error("opa_request_json_marshal") + // return false, ctx, exception.ErrInvalidArg + // } + + now := time.Now() + obfuscatedOpaReq := opautil.ShortenPayloadForDebug(opaReq) + logger.WithFields(log.Fields{ + "opaReq": obfuscatedOpaReq, + }).Debug("opa_authorization_request") + + // TODO: Add tracing for the middleware + // Start a new trace span + // ctx, span := trace.StartSpan(ctx, fmt.Sprint(SERVICENAME, endpoint)) + // { + // span.Annotate([]trace.Attribute{ + // trace.StringAttribute("in", string(opaReqJSON)), + // }, "in") + // } + + // Prepare the OPA input based on the decision document + var opaInput interface{} + opaInput = opaReq + if len(decisionInput.DecisionDocument) > 0 { + opaInput = opautil.OPARequest{Input: &opaReq} + } + + var opaResp opautil.OPAResponse + err = opaEvaluator(ctxlogrus.ToContext(ctx, logger), decisionInput.DecisionDocument, opaInput, &opaResp) + defer func() { + // TODO: Add tracing for the middleware + // span.SetStatus(trace.Status{ + // Code: int32(grpc.Code(err)), + // Message: grpc.ErrorDesc(err), + // }) + // span.End() + logger.WithFields(log.Fields{ + "opaResp": opaResp, + "elapsed": time.Since(now), + }).Debug("authorization_result") + }() + if err != nil { + return false, ctx, err + } + + // Extract the nested result if present + nestedResultVal, resultIsNested := opaResp["result"] + if resultIsNested { + nestedResultMap, ok := nestedResultVal.(map[string]interface{}) + if ok { + opaResp = opautil.OPAResponse{} + for k, v := range nestedResultMap { + opaResp[k] = v + } + } + } + + // Log non-error OPA responses + { + // TODO: Add tracing for the middleware + // raw, _ := json.Marshal(opaResp) + // span.Annotate([]trace.Attribute{ + // trace.StringAttribute("out", string(raw)), + // }, "out") + } + + // Add raw entitled_features data to the context + //REVIEW: is it needed for http? + ctx = opaResp.AddRawEntitledFeatures(ctx) + + // Add obligations data to the context + //REVIEW: is it needed for http? + ctx, err = opautil.AddObligations(ctx, opaResp) + if err != nil { + logger.WithField("opaResp", fmt.Sprintf("%#v", opaResp)).WithError(err).Error("parse_obligations_error") + } + + // Check if the authorization is allowed + if !opaResp.Allow() { + return false, ctx, exception.ErrForbidden + } + + return true, ctx, nil +} + +// OpaQuery executes a custom OPA query with the given decision document, request, and response. +func (a *httpAuthorizer) OpaQuery(ctx context.Context, decisionDocument string, opaReq, opaResp interface{}) error { + if a.opaEvaluator != nil { + return a.opaEvaluator(ctx, decisionDocument, opaReq, opaResp) + } + + logger := ctxlogrus.Extract(ctx) + + // Empty document path is intentional + // DO NOT hardcode a path here + err := a.clienter.CustomQuery(ctx, decisionDocument, opaReq, opaResp) + // TODO: allow overriding logger + if err != nil { + httpErr := exception.GrpcToHttpError(err) + logger.WithError(httpErr).Error("opa_policy_engine_request_error") + return exception.AbstractError(httpErr) + } + logger.WithField("opaResp", opaResp).Debug("opa_policy_engine_response") + return nil +} + +// AffirmAuthorization makes an authz request to sidecar-OPA. +// If authorization is permitted, error returned is nil, +// and a new context is returned, possibly containing obligations. +// Caller must further evaluate obligations if required. +func (a *httpAuthorizer) AffirmAuthorization(ctx context.Context, fullMethod string, req interface{}) (context.Context, error) { + logger := ctxlogrus.Extract(ctx) + var ( + ok bool + newCtx context.Context + err error + ) + + ok, newCtx, err = a.Evaluate(ctx, fullMethod, req, a.OpaQuery) + if err != nil { + logger.WithError(err).WithField("authorizer", a).Error("unable_authorize") + return nil, err + } + + if !ok { + err = opa_client.ErrUndefined + logger.WithError(err).Error("policy engine returned undefined response") + return nil, err + } + + return newCtx, nil +} diff --git a/http_opa/config.go b/http_opa/config.go new file mode 100644 index 0000000..c875df8 --- /dev/null +++ b/http_opa/config.go @@ -0,0 +1,162 @@ +package httpopa + +import ( + "net/http" + "slices" + "strings" + + az "github.com/infobloxopen/atlas-authz-middleware/v2/common/authorizer" + "github.com/infobloxopen/atlas-authz-middleware/v2/pkg/opa_client" + "github.com/sirupsen/logrus" +) + +type DefaultModifyConfig struct { + SegmentsNeeded int + SegmentStart int + Prefix string +} + +type EndpointModifier struct { + DefaultModifyConfig + Modify func(string) string +} + +func (e *EndpointModifier) defaultModify(endpoint string) string { + segments := strings.Split(endpoint, "/") + verb := segments[0] + shortSegments := segments[e.SegmentStart : e.SegmentStart+e.SegmentsNeeded] + if len(e.Prefix) > 0 { + shortSegments = slices.Insert(shortSegments, 0, e.Prefix) + } + shortSegments = slices.Insert(shortSegments, 0, verb) + return strings.Join(shortSegments, "/") +} + +func (e *EndpointModifier) getModifiedEndpoint(endpoint string) string { + if e.Modify == nil { + return e.defaultModify(endpoint) + } + return e.Modify(endpoint) +} + +type Config struct { + httpCli *http.Client + // 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 + endpointModifier *EndpointModifier +} + +func (c Config) GetAuthorizer() []az.Authorizer { + return c.authorizer +} + +// NewDefaultConfig returns a new default Config for Http Authorizer. +func NewDefaultConfig(application string, opts ...Option) *Config { + cfg := &Config{ + address: opa_client.DefaultAddress, + } + + for _, opt := range opts { + opt(cfg) + } + + opts = append([]Option{WithOpaClienter(opa_client.New(cfg.address, opa_client.WithHTTPClient(cfg.httpCli)))}, opts...) + + authorizer := NewHttpAuthorizer(application, opts...) + + if cfg.authorizer == nil { + logrus.Info("authorizers empty, using default authorizer") + cfg.authorizer = []az.Authorizer{authorizer} + } + + return cfg +} + +type Option func(c *Config) + +// WithAddress +func WithAddress(address string) Option { + return func(c *Config) { + c.address = address + } +} + +// WithHTTPClient overrides the http.Client used to call Opa +func WithHTTPClient(client *http.Client) Option { + return func(c *Config) { + if client != nil { + c.httpCli = client + } + } +} + +// WithOpaClienter overrides the Clienter used to call Opa. +// This option takes precedence over WithHTTPClient. +func WithOpaClienter(clienter opa_client.Clienter) Option { + return func(c *Config) { + if clienter != nil { + c.clienter = clienter + } + } +} + +// WithOpaEvaluator overrides the OpaEvaluator use to +// evaluate authorization against OPA. +func WithOpaEvaluator(opaEvaluator az.OpaEvaluator) Option { + return func(c *Config) { + c.opaEvaluator = opaEvaluator + } +} + +// WithAuthorizer overrides the request/response +// processing of OPA. Multiple authorizers can be passed +func WithAuthorizer(auther ...az.Authorizer) Option { + return func(c *Config) { + c.authorizer = auther + } +} + +// WithDecisionInputHandler supplies optional DecisionInputHandler +// for DefaultAuthorizer to obtain additional input for OPA +// ABAC decision processing. +func WithDecisionInputHandler(decisionHandler az.DecisionInputHandler) Option { + return func(c *Config) { + c.decisionInputHandler = decisionHandler + } +} + +// WithClaimsVerifier overrides default ClaimsVerifier +func WithClaimsVerifier(claimsVerifier az.ClaimsVerifier) Option { + return func(c *Config) { + c.claimsVerifier = claimsVerifier + } +} + +// WithEntitledServices overrides default EntitledServices +func WithEntitledServices(entitledServices ...string) Option { + return func(c *Config) { + c.entitledServices = entitledServices + } +} + +// WithAcctEntitlementsApiPath overrides default AcctEntitlementsApiPath +func WithAcctEntitlementsApiPath(acctEntitlementsApi string) Option { + return func(c *Config) { + c.acctEntitlementsApi = acctEntitlementsApi + } +} + +// WithAcctSegmentsNeeded overrides default 0 +func WithEndpointModifier(modifier *EndpointModifier) Option { + return func(c *Config) { + c.endpointModifier = modifier + } +} diff --git a/http_opa/config_test.go b/http_opa/config_test.go new file mode 100644 index 0000000..52f8ecb --- /dev/null +++ b/http_opa/config_test.go @@ -0,0 +1,71 @@ +package httpopa + +import "testing" + +func TestEndpointModifier_defaultModify(t *testing.T) { + type fields struct { + DefaultModifyConfig + Modify func(string) string + } + type args struct { + endpoint string + } + tests := []struct { + name string + fields fields + args args + want string + }{ + { + name: "success with prefix", + fields: fields{ + DefaultModifyConfig: DefaultModifyConfig{ + SegmentsNeeded: 2, + SegmentStart: 3, + Prefix: "wapi", + }, + }, + args: args{ + endpoint: "GET /nios/v1.2/grid/id", + }, + want: "GET /wapi/grid/id", + }, + { + name: "success without prefix", + fields: fields{ + DefaultModifyConfig: DefaultModifyConfig{ + SegmentsNeeded: 2, + SegmentStart: 3, + }, + }, + args: args{ + endpoint: "GET /nios/v1.2/grid/id", + }, + want: "GET /grid/id", + }, + { + name: "success without segment", + fields: fields{ + DefaultModifyConfig: DefaultModifyConfig{ + SegmentsNeeded: 0, + SegmentStart: 0, + }, + }, + args: args{ + endpoint: "GET /nios/v1.2/grid/id", + }, + want: "GET ", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &EndpointModifier{ + DefaultModifyConfig: tt.fields.DefaultModifyConfig, + Modify: tt.fields.Modify, + } + if got := e.defaultModify(tt.args.endpoint); got != tt.want { + t.Errorf("EndpointModifier.defaultModify() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/http_opa/exception/exception.go b/http_opa/exception/exception.go new file mode 100644 index 0000000..31de3d6 --- /dev/null +++ b/http_opa/exception/exception.go @@ -0,0 +1,22 @@ +package exception + +import ( + "fmt" +) + +var ( + //abstract errors + ErrAbstrForbidden = generateAbstrErr("request forbidden", "not authorized") + ErrAbstrUnknown = generateAbstrErr("unknown", "unknown error") + ErrAbstrInvalidArg = generateAbstrErr("invalid", "invalid argument") + ErrAbstrInvalidEndpoint = generateAbstrErr("invalid", "invalid endpoint to parse") + ErrAbstrServiceUnavailable = generateAbstrErr("serviceUnvailable", "connection refused") + ErrAbstrInternal = generateAbstrErr("internal", "internal error") + ErrAbstrAuthHeaderMissing = generateAbstrErr("authHeaderMissing", "Authorization header is missing") + ErrAbstrAuthHeaderMalformed = generateAbstrErr("authHeaderMalformed", "Authorization header is malformed") + ErrAbstrInvalidObligations = generateAbstrErr("internal", "invalid obligations") +) + +func generateAbstrErr(code, msg string) error { + return fmt.Errorf("code: %s, message: %s", code, msg) +} diff --git a/http_opa/exception/http_exception.go b/http_opa/exception/http_exception.go new file mode 100644 index 0000000..93fcc14 --- /dev/null +++ b/http_opa/exception/http_exception.go @@ -0,0 +1,94 @@ +package exception + +import ( + "net/http" + + "github.com/infobloxopen/atlas-authz-middleware/v2/pkg/opa_client" + "github.com/open-policy-agent/opa/server/types" +) + +var ( + ErrForbidden = NewHttpError(WithError(ErrAbstrForbidden), WithHttpStatus(http.StatusUnauthorized)) + ErrUnknown = NewHttpError(WithError(ErrAbstrUnknown), WithHttpStatus(http.StatusInternalServerError)) + ErrInvalidArg = NewHttpError(WithError(ErrAbstrInvalidArg), WithHttpStatus(http.StatusBadRequest)) + ErrServiceUnavailable = NewHttpError(WithError(ErrAbstrServiceUnavailable), WithHttpStatus(http.StatusServiceUnavailable)) +) + +type HttpError struct { + error + Status int +} + +func NewHttpError(opts ...ErrOpt) *HttpError { + he := &HttpError{ + Status: http.StatusInternalServerError, + error: ErrAbstrInternal, + } + for _, opt := range opts { + opt(he) + } + return he +} + +type ErrOpt func(he *HttpError) + +func WithHttpStatus(status int) ErrOpt { + return func(he *HttpError) { + he.Status = status + } +} + +func WithError(err error) ErrOpt { + return func(he *HttpError) { + he.error = err + } +} + +func WithCode(code string) ErrOpt { + return WithHttpStatus(httpStatusFromOPACode(code)) +} + +// HttpError translates opa errors to http status errors +func GrpcToHttpError(err error) *HttpError { + switch tErr := err.(type) { + case *types.ErrorV1: + return opaErrTHttpErr(WithCode(tErr.Code), WithError(tErr)) + case *opa_client.ErrorV1: + return opaErrTHttpErr(WithCode(tErr.Code), WithError(tErr)) + } + return opaErrTHttpErr(WithError(err)) +} + +func opaErrTHttpErr(opts ...ErrOpt) *HttpError { + return NewHttpError(opts...) +} + +var codeToHttpStatus = map[string]int{ + "": http.StatusOK, + types.CodeInternal: http.StatusInternalServerError, + types.CodeEvaluation: http.StatusInternalServerError, + types.CodeUnauthorized: http.StatusUnauthorized, + types.CodeInvalidParameter: http.StatusBadRequest, + types.CodeInvalidOperation: http.StatusBadRequest, + types.CodeResourceNotFound: http.StatusNotFound, + types.CodeResourceConflict: http.StatusNotFound, + types.CodeUndefinedDocument: http.StatusNotFound, + http.StatusText(http.StatusServiceUnavailable): http.StatusServiceUnavailable, +} + +func httpStatusFromOPACode(code string) int { + if status, ok := codeToHttpStatus[code]; ok { + return status + } + return http.StatusInternalServerError +} + +// AbstractError trims some privileged information from errors +// as these get sent directly as grpc responses +func AbstractError(err *HttpError) *HttpError { + switch err.Status { + case http.StatusServiceUnavailable: + return ErrServiceUnavailable + } + return err +} diff --git a/http_opa/middleware.go b/http_opa/middleware.go new file mode 100644 index 0000000..ea50da9 --- /dev/null +++ b/http_opa/middleware.go @@ -0,0 +1,67 @@ +package httpopa + +import ( + "net/http" + "strings" + + "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" + + "github.com/infobloxopen/atlas-authz-middleware/v2/http_opa/exception" +) + +type ContextKey string + +var ( + EndPointKey = ContextKey("endpoint") +) + +// NewServerAuthzMiddleware evaluate the OPA policy against the requested endpoint, and aborts the request if not authorized. +func NewServerAuthzMiddleware(application string, opts ...Option) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + cfg := NewDefaultConfig(application, opts...) + logger := ctxlogrus.Extract(ctx) + + var ( + ok bool + err error + ) + + authorizers := cfg.GetAuthorizer() + for _, auther := range authorizers { + //REVIEW: identify if returned ctx is needed or not + ok, _, err = auther.Evaluate(ctx, getEndpoint(r), r, auther.OpaQuery) + if err != nil { + logger.WithError(err).WithField("authorizer", auther).Error("unable_authorize") + } + if ok { + break + } + } + if err != nil || !ok { + if err == nil { + err = exception.ErrUnknown + } + logger.WithError(err).Error("policy engine returned an error") + he := err.(*exception.HttpError) + http.Error(w, "unable to authorize", he.Status) + return + } + + next.ServeHTTP(w, r) + + }) + } +} + +func getVerbAndEndpointHTTP(r *http.Request) string { + return strings.Join([]string{r.Method, r.URL.Path}, " ") +} + +func getEndpoint(r *http.Request) string { + if ep, ok := r.Context().Value(EndPointKey).(string); ok { + return ep + } + return getVerbAndEndpointHTTP(r) +} diff --git a/http_opa/middleware_test.go b/http_opa/middleware_test.go new file mode 100644 index 0000000..61d3768 --- /dev/null +++ b/http_opa/middleware_test.go @@ -0,0 +1,72 @@ +package httpopa + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/golang/mock/gomock" + "github.com/infobloxopen/atlas-authz-middleware/v2/common/authorizer" + "github.com/infobloxopen/atlas-authz-middleware/v2/http_opa/exception" + "gotest.tools/v3/assert" +) + +// TestAuthzMiddleware is a unit test function that tests the behavior of the AuthzMiddleware function. +// It uses a table-driven approach to test different scenarios of authorized and unauthorized requests. +// The function takes a testing.T parameter for test assertions. +// The test cases include authorized and unauthorized requests with different application names and authorizer modifications. +// For each test case, it creates a new mock authorizer, modifies it based on the test case, and then runs the AuthzMiddleware function with the modified authorizer. +// Finally, it asserts the expected authorization result based on the test case. +func TestAuthzMiddleware(t *testing.T) { + testCases := []struct { + name string + application string + opts []Option + expectAuth bool + modifyAuthorizer func(*authorizer.MockAuthorizer) + }{ + { + name: "Authorized request", + application: "testApp", + modifyAuthorizer: func(ma *authorizer.MockAuthorizer) { + ma.EXPECT().Evaluate(context.Background(), gomock.Any(), gomock.Any(), gomock.Any()).Return(true, context.Background(), nil) + }, + expectAuth: true, + }, + { + name: "Unauthorized request", + application: "testApp", + modifyAuthorizer: func(ma *authorizer.MockAuthorizer) { + ma.EXPECT().Evaluate(context.Background(), gomock.Any(), gomock.Any(), gomock.Any()).Return(false, context.Background(), exception.ErrForbidden) + }, + expectAuth: false, + }, + // Add more test cases as needed + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ma := authorizer.NewMockAuthorizer(ctrl) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + }) + + tc.modifyAuthorizer(ma) + + tc.opts = append(tc.opts, WithAuthorizer(ma)) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + + middleware := NewServerAuthzMiddleware(tc.application, tc.opts...) + rr := httptest.NewRecorder() + + middleware(handler).ServeHTTP(rr, request) + + assert.Equal(t, tc.expectAuth, rr.Code == http.StatusOK, "Expected authorized request") + + }) + } +} diff --git a/http_opa/util/util.go b/http_opa/util/util.go new file mode 100644 index 0000000..6dc2df0 --- /dev/null +++ b/http_opa/util/util.go @@ -0,0 +1,35 @@ +package util + +import ( + "net/http" + "strings" + + "github.com/google/uuid" + "github.com/infobloxopen/atlas-authz-middleware/v2/http_opa/exception" +) + +const ( + DefaultRequestIDKey = "X-Request-ID" +) + +// GetRequestIdFromRequest fetches requestid from http request +func GetRequestIdFromRequest(r *http.Request) string { + reqId := r.Header.Get(DefaultRequestIDKey) + if len(reqId) != 0 { + return reqId + } + return uuid.NewString() +} + +// GetBearerFromRequest fetches requestid from http request +func GetBearerFromRequest(r *http.Request) (string, error) { + authHead := r.Header.Get("Authorization") + if len(authHead) == 0 { + return authHead, exception.ErrAbstrAuthHeaderMissing + } + authHeadArr := strings.Split(authHead, " ") + if len(authHeadArr) != 2 { + return authHead, exception.ErrAbstrAuthHeaderMalformed + } + return authHeadArr[1], nil +} diff --git a/makefile b/makefile index d1ada96..c39ddc6 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,6 @@ .PHONY: test test: + go mod vendor go vet ./... go test ./... diff --git a/pkg/opa_client/http.go b/pkg/opa_client/http.go index 7950359..00feb24 100644 --- a/pkg/opa_client/http.go +++ b/pkg/opa_client/http.go @@ -89,7 +89,7 @@ func (c Client) String() string { // Address retrieves the protocol://address of server func (c *Client) Address() string { - return fmt.Sprintf("%s", c.address) + return c.address } func (c *Client) Health() error { diff --git a/pkg/opa_client/http_test.go b/pkg/opa_client/http_test.go index 5c67abe..57e7f06 100644 --- a/pkg/opa_client/http_test.go +++ b/pkg/opa_client/http_test.go @@ -4,13 +4,16 @@ import ( "context" "errors" "io/ioutil" + "net/http" "reflect" "syscall" "testing" - opamw "github.com/infobloxopen/atlas-authz-middleware/grpc_opa" - "github.com/infobloxopen/atlas-authz-middleware/pkg/opa_client" - "github.com/infobloxopen/atlas-authz-middleware/utils_test" + "github.com/infobloxopen/atlas-authz-middleware/v2/common/authorizer" + "github.com/infobloxopen/atlas-authz-middleware/v2/common/claim" + httpopa "github.com/infobloxopen/atlas-authz-middleware/v2/http_opa" + "github.com/infobloxopen/atlas-authz-middleware/v2/pkg/opa_client" + "github.com/infobloxopen/atlas-authz-middleware/v2/utils_test" "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" logrus "github.com/sirupsen/logrus" @@ -92,15 +95,15 @@ func TestPolicyReturningRegoSet(t *testing.T) { } mockDecInp := &MockDecisionInputer{} - auther := opamw.NewDefaultAuthorizer("app", - opamw.WithOpaClienter(cli), - opamw.WithDecisionInputHandler(mockDecInp), - opamw.WithClaimsVerifier(opamw.NullClaimsVerifier), + auther := httpopa.NewHttpAuthorizer("app", + httpopa.WithOpaClienter(cli), + httpopa.WithDecisionInputHandler(mockDecInp), + httpopa.WithClaimsVerifier(claim.NullClaimsVerifier), ) // If authorization is permitted, then this verifies that the OPA JSON results were correctly decoded, // and this verifies that the rego set result is returned by OPA as a JSON array result. - resultCtx, resultErr := auther.AffirmAuthorization(ctx, "FakeMethod", nil) + resultCtx, resultErr := auther.AffirmAuthorization(ctx, "FakeMethod", getHttpRequest()) if resultErr != nil { t.Errorf("AffirmAuthorization err: %#v", resultErr) } @@ -109,6 +112,14 @@ func TestPolicyReturningRegoSet(t *testing.T) { } } +func getHttpRequest() *http.Request { + req, _ := http.NewRequest("GET", "http://example.com", nil) + req.Header.Set("Authorization", "Bearer token") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Request-Id", "123") + return req +} + func TestCustomQuery(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) ctx = context.WithValue(ctx, utils_test.TestingTContextKey, t) @@ -220,8 +231,8 @@ func (m MockDecisionInputer) String() string { return "opa_client_test.MockDecisionInputer{}" } -func (m *MockDecisionInputer) GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*opamw.DecisionInput, error) { - decInp := opamw.DecisionInput{ +func (m *MockDecisionInputer) GetDecisionInput(ctx context.Context, fullMethod string, grpcReq interface{}) (*authorizer.DecisionInput, error) { + decInp := authorizer.DecisionInput{ DecisionDocument: "/v1/data/policy_returning_set/get_results", } return &decInp, nil diff --git a/utils_test/opa_server.go b/utils_test/opa_server.go index d512592..a92e9c6 100644 --- a/utils_test/opa_server.go +++ b/utils_test/opa_server.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/infobloxopen/atlas-authz-middleware/pkg/opa_client" + "github.com/infobloxopen/atlas-authz-middleware/v2/pkg/opa_client" "github.com/open-policy-agent/opa/plugins" "github.com/open-policy-agent/opa/server"