diff --git a/authz/rbac_translator.go b/authz/rbac_translator.go index fe305a1adce..039d76bc99d 100644 --- a/authz/rbac_translator.go +++ b/authz/rbac_translator.go @@ -23,6 +23,7 @@ package authz import ( + "bytes" "encoding/json" "fmt" "strings" @@ -93,7 +94,7 @@ func getStringMatcher(value string) *v3matcherpb.StringMatcher { switch { case value == "*": return &v3matcherpb.StringMatcher{ - MatchPattern: &v3matcherpb.StringMatcher_Prefix{}, + MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{}, } case strings.HasSuffix(value, "*"): prefix := strings.TrimSuffix(value, "*") @@ -117,7 +118,7 @@ func getHeaderMatcher(key, value string) *v3routepb.HeaderMatcher { case value == "*": return &v3routepb.HeaderMatcher{ Name: key, - HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{}, + HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{}, } case strings.HasSuffix(value, "*"): prefix := strings.TrimSuffix(value, "*") @@ -268,34 +269,38 @@ func parseRules(rules []rule, prefixName string) (map[string]*v3rbacpb.Policy, e } // translatePolicy translates SDK authorization policy in JSON format to two -// Envoy RBAC polices (deny and allow policy). If the policy cannot be parsed -// or is invalid, an error will be returned. -func translatePolicy(policyStr string) (*v3rbacpb.RBAC, *v3rbacpb.RBAC, error) { - var policy authorizationPolicy - if err := json.Unmarshal([]byte(policyStr), &policy); err != nil { - return nil, nil, fmt.Errorf("failed to unmarshal policy: %v", err) +// Envoy RBAC polices (deny followed by allow policy) or only one Envoy RBAC +// allow policy. If the input policy cannot be parsed or is invalid, an error +// will be returned. +func translatePolicy(policyStr string) ([]*v3rbacpb.RBAC, error) { + policy := &authorizationPolicy{} + d := json.NewDecoder(bytes.NewReader([]byte(policyStr))) + d.DisallowUnknownFields() + if err := d.Decode(policy); err != nil { + return nil, fmt.Errorf("failed to unmarshal policy: %v", err) } if policy.Name == "" { - return nil, nil, fmt.Errorf(`"name" is not present`) + return nil, fmt.Errorf(`"name" is not present`) } if len(policy.AllowRules) == 0 { - return nil, nil, fmt.Errorf(`"allow_rules" is not present`) + return nil, fmt.Errorf(`"allow_rules" is not present`) } - allowPolicies, err := parseRules(policy.AllowRules, policy.Name) - if err != nil { - return nil, nil, fmt.Errorf(`"allow_rules" %v`, err) - } - allowRBAC := &v3rbacpb.RBAC{Action: v3rbacpb.RBAC_ALLOW, Policies: allowPolicies} - var denyRBAC *v3rbacpb.RBAC + rbacs := make([]*v3rbacpb.RBAC, 0, 2) if len(policy.DenyRules) > 0 { denyPolicies, err := parseRules(policy.DenyRules, policy.Name) if err != nil { - return nil, nil, fmt.Errorf(`"deny_rules" %v`, err) + return nil, fmt.Errorf(`"deny_rules" %v`, err) } - denyRBAC = &v3rbacpb.RBAC{ + denyRBAC := &v3rbacpb.RBAC{ Action: v3rbacpb.RBAC_DENY, Policies: denyPolicies, } + rbacs = append(rbacs, denyRBAC) } - return denyRBAC, allowRBAC, nil + allowPolicies, err := parseRules(policy.AllowRules, policy.Name) + if err != nil { + return nil, fmt.Errorf(`"allow_rules" %v`, err) + } + allowRBAC := &v3rbacpb.RBAC{Action: v3rbacpb.RBAC_ALLOW, Policies: allowPolicies} + return append(rbacs, allowRBAC), nil } diff --git a/authz/rbac_translator_test.go b/authz/rbac_translator_test.go index 425cae85b03..9a883e9d78d 100644 --- a/authz/rbac_translator_test.go +++ b/authz/rbac_translator_test.go @@ -32,10 +32,9 @@ import ( func TestTranslatePolicy(t *testing.T) { tests := map[string]struct { - authzPolicy string - wantErr string - wantDenyPolicy *v3rbacpb.RBAC - wantAllowPolicy *v3rbacpb.RBAC + authzPolicy string + wantErr string + wantPolicies []*v3rbacpb.RBAC }{ "valid policy": { authzPolicy: `{ @@ -82,81 +81,133 @@ func TestTranslatePolicy(t *testing.T) { } }] }`, - wantDenyPolicy: &v3rbacpb.RBAC{Action: v3rbacpb.RBAC_DENY, Policies: map[string]*v3rbacpb.Policy{ - "authz_deny_policy_1": { - Principals: []*v3rbacpb.Principal{ - {Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{ - Ids: []*v3rbacpb.Principal{ - {Identifier: &v3rbacpb.Principal_Authenticated_{ - Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{ - MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "spiffe://foo.abc"}}}}}, - {Identifier: &v3rbacpb.Principal_Authenticated_{ - Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{ - MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "spiffe://bar"}}}}}, - {Identifier: &v3rbacpb.Principal_Authenticated_{ - Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{ - MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: "baz"}}}}}, - {Identifier: &v3rbacpb.Principal_Authenticated_{ - Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{ - MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "spiffe://abc.*.com"}}}}}, - }}}}}, - Permissions: []*v3rbacpb.Permission{ - {Rule: &v3rbacpb.Permission_Any{Any: true}}}, - }, - }}, - wantAllowPolicy: &v3rbacpb.RBAC{Action: v3rbacpb.RBAC_ALLOW, Policies: map[string]*v3rbacpb.Policy{ - "authz_allow_policy_1": { - Principals: []*v3rbacpb.Principal{ - {Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{ - Ids: []*v3rbacpb.Principal{ - {Identifier: &v3rbacpb.Principal_Authenticated_{ - Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{ - MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: ""}}}}}, - }}}}}, - Permissions: []*v3rbacpb.Permission{ - {Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{ - Rules: []*v3rbacpb.Permission{ - {Rule: &v3rbacpb.Permission_OrRules{OrRules: &v3rbacpb.Permission_Set{ - Rules: []*v3rbacpb.Permission{ - {Rule: &v3rbacpb.Permission_UrlPath{ - UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{ - MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "path-foo"}}}}}}, - }}}}}}}}}, + wantPolicies: []*v3rbacpb.RBAC{ + { + Action: v3rbacpb.RBAC_DENY, + Policies: map[string]*v3rbacpb.Policy{ + "authz_deny_policy_1": { + Principals: []*v3rbacpb.Principal{ + {Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{ + Ids: []*v3rbacpb.Principal{ + {Identifier: &v3rbacpb.Principal_Authenticated_{ + Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{ + MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "spiffe://foo.abc"}, + }}, + }}, + {Identifier: &v3rbacpb.Principal_Authenticated_{ + Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{ + MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "spiffe://bar"}, + }}, + }}, + {Identifier: &v3rbacpb.Principal_Authenticated_{ + Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{ + MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: "baz"}, + }}, + }}, + {Identifier: &v3rbacpb.Principal_Authenticated_{ + Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{ + MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "spiffe://abc.*.com"}, + }}, + }}, + }, + }}}, + }, + Permissions: []*v3rbacpb.Permission{ + {Rule: &v3rbacpb.Permission_Any{Any: true}}, + }, + }, + }, }, - "authz_allow_policy_2": { - Principals: []*v3rbacpb.Principal{ - {Identifier: &v3rbacpb.Principal_Any{Any: true}}}, - Permissions: []*v3rbacpb.Permission{ - {Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{ - Rules: []*v3rbacpb.Permission{ - {Rule: &v3rbacpb.Permission_OrRules{OrRules: &v3rbacpb.Permission_Set{ - Rules: []*v3rbacpb.Permission{ - {Rule: &v3rbacpb.Permission_UrlPath{ - UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{ - MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "path-bar"}}}}}}, - {Rule: &v3rbacpb.Permission_UrlPath{ - UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{ - MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: "baz"}}}}}}, - }}}}, + { + Action: v3rbacpb.RBAC_ALLOW, + Policies: map[string]*v3rbacpb.Policy{ + "authz_allow_policy_1": { + Principals: []*v3rbacpb.Principal{ + {Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{ + Ids: []*v3rbacpb.Principal{ + {Identifier: &v3rbacpb.Principal_Authenticated_{ + Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{ + MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{}, + }}, + }}, + }, + }}}, + }, + Permissions: []*v3rbacpb.Permission{ {Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{ Rules: []*v3rbacpb.Permission{ {Rule: &v3rbacpb.Permission_OrRules{OrRules: &v3rbacpb.Permission_Set{ Rules: []*v3rbacpb.Permission{ - {Rule: &v3rbacpb.Permission_Header{ - Header: &v3routepb.HeaderMatcher{ - Name: "key-1", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "foo"}}}}, - {Rule: &v3rbacpb.Permission_Header{ - Header: &v3routepb.HeaderMatcher{ - Name: "key-1", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SuffixMatch{SuffixMatch: "bar"}}}}, - }}}}, + {Rule: &v3rbacpb.Permission_UrlPath{ + UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{ + MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "path-foo"}, + }}}, + }}, + }, + }}}, + }, + }}}, + }, + }, + "authz_allow_policy_2": { + Principals: []*v3rbacpb.Principal{ + {Identifier: &v3rbacpb.Principal_Any{Any: true}}, + }, + Permissions: []*v3rbacpb.Permission{ + {Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{ + Rules: []*v3rbacpb.Permission{ {Rule: &v3rbacpb.Permission_OrRules{OrRules: &v3rbacpb.Permission_Set{ Rules: []*v3rbacpb.Permission{ - {Rule: &v3rbacpb.Permission_Header{ - Header: &v3routepb.HeaderMatcher{ - Name: "key-2", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "baz"}}}}, - }}}}}}}}}}}}}, + {Rule: &v3rbacpb.Permission_UrlPath{ + UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{ + MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "path-bar"}, + }}}, + }}, + {Rule: &v3rbacpb.Permission_UrlPath{ + UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{ + MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: "baz"}, + }}}, + }}, + }, + }}}, + {Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{ + Rules: []*v3rbacpb.Permission{ + {Rule: &v3rbacpb.Permission_OrRules{OrRules: &v3rbacpb.Permission_Set{ + Rules: []*v3rbacpb.Permission{ + {Rule: &v3rbacpb.Permission_Header{ + Header: &v3routepb.HeaderMatcher{ + Name: "key-1", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "foo"}, + }, + }}, + {Rule: &v3rbacpb.Permission_Header{ + Header: &v3routepb.HeaderMatcher{ + Name: "key-1", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SuffixMatch{SuffixMatch: "bar"}, + }, + }}, + }, + }}}, + {Rule: &v3rbacpb.Permission_OrRules{OrRules: &v3rbacpb.Permission_Set{ + Rules: []*v3rbacpb.Permission{ + {Rule: &v3rbacpb.Permission_Header{ + Header: &v3routepb.HeaderMatcher{ + Name: "key-2", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "baz"}, + }, + }}, + }, + }}}, + }, + }}}, + }, + }}}, + }, + }, + }, }, - }}, + }, + }, + "unknown field": { + authzPolicy: `{"random": 123}`, + wantErr: "failed to unmarshal policy", }, "missing name field": { authzPolicy: `{}`, @@ -167,10 +218,8 @@ func TestTranslatePolicy(t *testing.T) { wantErr: "failed to unmarshal policy", }, "missing allow rules field": { - authzPolicy: `{"name": "authz-foo"}`, - wantErr: `"allow_rules" is not present`, - wantDenyPolicy: nil, - wantAllowPolicy: nil, + authzPolicy: `{"name": "authz-foo"}`, + wantErr: `"allow_rules" is not present`, }, "missing rule name field": { authzPolicy: `{ @@ -210,18 +259,14 @@ func TestTranslatePolicy(t *testing.T) { wantErr: `"allow_rules" 0: "headers" 0: unsupported "key" :method`, }, } - for name, test := range tests { t.Run(name, func(t *testing.T) { - gotDenyPolicy, gotAllowPolicy, gotErr := translatePolicy(test.authzPolicy) + gotPolicies, gotErr := translatePolicy(test.authzPolicy) if gotErr != nil && !strings.HasPrefix(gotErr.Error(), test.wantErr) { t.Fatalf("unexpected error\nwant:%v\ngot:%v", test.wantErr, gotErr) } - if diff := cmp.Diff(gotDenyPolicy, test.wantDenyPolicy, protocmp.Transform()); diff != "" { - t.Fatalf("unexpected deny policy\ndiff (-want +got):\n%s", diff) - } - if diff := cmp.Diff(gotAllowPolicy, test.wantAllowPolicy, protocmp.Transform()); diff != "" { - t.Fatalf("unexpected allow policy\ndiff (-want +got):\n%s", diff) + if diff := cmp.Diff(gotPolicies, test.wantPolicies, protocmp.Transform()); diff != "" { + t.Fatalf("unexpected policy\ndiff (-want +got):\n%s", diff) } }) } diff --git a/authz/sdk_end2end_test.go b/authz/sdk_end2end_test.go new file mode 100644 index 00000000000..92a5e4f4b21 --- /dev/null +++ b/authz/sdk_end2end_test.go @@ -0,0 +1,307 @@ +/* + * + * Copyright 2021 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package authz_test + +import ( + "context" + "io" + "net" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/authz" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + pb "google.golang.org/grpc/test/grpc_testing" +) + +type testServer struct { + pb.UnimplementedTestServiceServer +} + +func (s *testServer) UnaryCall(ctx context.Context, req *pb.SimpleRequest) (*pb.SimpleResponse, error) { + return &pb.SimpleResponse{}, nil +} + +func (s *testServer) StreamingInputCall(stream pb.TestService_StreamingInputCallServer) error { + for { + _, err := stream.Recv() + if err == io.EOF { + return stream.SendAndClose(&pb.StreamingInputCallResponse{}) + } + if err != nil { + return err + } + } +} + +func TestSDKEnd2End(t *testing.T) { + tests := map[string]struct { + authzPolicy string + md metadata.MD + wantStatusCode codes.Code + wantErr string + }{ + "DeniesRpcRequestMatchInDenyNoMatchInAllow": { + authzPolicy: `{ + "name": "authz", + "allow_rules": + [ + { + "name": "allow_StreamingOutputCall", + "request": { + "paths": + [ + "/grpc.testing.TestService/StreamingOutputCall" + ] + } + } + ], + "deny_rules": + [ + { + "name": "deny_TestServiceCalls", + "request": { + "paths": + [ + "/grpc.testing.TestService/UnaryCall", + "/grpc.testing.TestService/StreamingInputCall" + ], + "headers": + [ + { + "key": "key-abc", + "values": + [ + "val-abc", + "val-def" + ] + } + ] + } + } + ] + }`, + md: metadata.Pairs("key-abc", "val-abc"), + wantStatusCode: codes.PermissionDenied, + wantErr: "unauthorized RPC request rejected", + }, + "DeniesRpcRequestMatchInDenyAndAllow": { + authzPolicy: `{ + "name": "authz", + "allow_rules": + [ + { + "name": "allow_TestServiceCalls", + "request": { + "paths": + [ + "/grpc.testing.TestService/*" + ] + } + } + ], + "deny_rules": + [ + { + "name": "deny_TestServiceCalls", + "request": { + "paths": + [ + "/grpc.testing.TestService/*" + ] + } + } + ] + }`, + wantStatusCode: codes.PermissionDenied, + wantErr: "unauthorized RPC request rejected", + }, + "AllowsRpcRequestNoMatchInDenyMatchInAllow": { + authzPolicy: `{ + "name": "authz", + "allow_rules": + [ + { + "name": "allow_all" + } + ], + "deny_rules": + [ + { + "name": "deny_TestServiceCalls", + "request": { + "paths": + [ + "/grpc.testing.TestService/UnaryCall", + "/grpc.testing.TestService/StreamingInputCall" + ], + "headers": + [ + { + "key": "key-abc", + "values": + [ + "val-abc", + "val-def" + ] + } + ] + } + } + ] + }`, + md: metadata.Pairs("key-xyz", "val-xyz"), + wantStatusCode: codes.OK, + }, + "AllowsRpcRequestNoMatchInDenyAndAllow": { + authzPolicy: `{ + "name": "authz", + "allow_rules": + [ + { + "name": "allow_some_user", + "source": { + "principals": + [ + "some_user" + ] + } + } + ], + "deny_rules": + [ + { + "name": "deny_StreamingOutputCall", + "request": { + "paths": + [ + "/grpc.testing.TestService/StreamingOutputCall" + ] + } + } + ] + }`, + wantStatusCode: codes.PermissionDenied, + wantErr: "unauthorized RPC request rejected", + }, + "AllowsRpcRequestEmptyDenyMatchInAllow": { + authzPolicy: `{ + "name": "authz", + "allow_rules": + [ + { + "name": "allow_UnaryCall", + "request": + { + "paths": + [ + "/grpc.testing.TestService/UnaryCall" + ] + } + }, + { + "name": "allow_StreamingInputCall", + "request": + { + "paths": + [ + "/grpc.testing.TestService/StreamingInputCall" + ] + } + } + ] + }`, + wantStatusCode: codes.OK, + }, + "DeniesRpcRequestEmptyDenyNoMatchInAllow": { + authzPolicy: `{ + "name": "authz", + "allow_rules": + [ + { + "name": "allow_StreamingOutputCall", + "request": + { + "paths": + [ + "/grpc.testing.TestService/StreamingOutputCall" + ] + } + } + ] + }`, + wantStatusCode: codes.PermissionDenied, + wantErr: "unauthorized RPC request rejected", + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + // Start a gRPC server with SDK unary and stream server interceptors. + i, _ := authz.NewStatic(test.authzPolicy) + lis, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("error listening: %v", err) + } + s := grpc.NewServer( + grpc.ChainUnaryInterceptor(i.UnaryInterceptor), + grpc.ChainStreamInterceptor(i.StreamInterceptor)) + pb.RegisterTestServiceServer(s, &testServer{}) + go s.Serve(lis) + + // Establish a connection to the server. + clientConn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure()) + if err != nil { + t.Fatalf("grpc.Dial(%v) failed: %v", lis.Addr().String(), err) + } + defer clientConn.Close() + client := pb.NewTestServiceClient(clientConn) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ctx = metadata.NewOutgoingContext(ctx, test.md) + + // Verifying authorization decision for Unary RPC. + _, err = client.UnaryCall(ctx, &pb.SimpleRequest{}) + if got := status.Convert(err); got.Code() != test.wantStatusCode || got.Message() != test.wantErr { + t.Fatalf("[UnaryCall] error want:{%v %v} got:{%v %v}", test.wantStatusCode, test.wantErr, got.Code(), got.Message()) + } + + // Verifying authorization decision for Streaming RPC. + stream, err := client.StreamingInputCall(ctx) + if err != nil { + t.Fatalf("failed StreamingInputCall err: %v", err) + } + req := &pb.StreamingInputCallRequest{ + Payload: &pb.Payload{ + Body: []byte("hi"), + }, + } + if err := stream.Send(req); err != nil && err != io.EOF { + t.Fatalf("failed stream.Send err: %v", err) + } + _, err = stream.CloseAndRecv() + if got := status.Convert(err); got.Code() != test.wantStatusCode || got.Message() != test.wantErr { + t.Fatalf("[StreamingCall] error want:{%v %v} got:{%v %v}", test.wantStatusCode, test.wantErr, got.Code(), got.Message()) + } + }) + } +} diff --git a/authz/sdk_server_interceptors.go b/authz/sdk_server_interceptors.go new file mode 100644 index 00000000000..a2f992b5f26 --- /dev/null +++ b/authz/sdk_server_interceptors.go @@ -0,0 +1,75 @@ +/* + * Copyright 2021 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package authz + +import ( + "context" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/internal/xds/rbac" + "google.golang.org/grpc/status" +) + +// StaticInterceptor contains engines used to make authorization decisions. It +// either contains two engines deny engine followed by an allow engine or only +// one allow engine. +type StaticInterceptor struct { + engines rbac.ChainEngine +} + +// NewStatic returns a new StaticInterceptor from a static authorization policy +// JSON string. +func NewStatic(authzPolicy string) (*StaticInterceptor, error) { + rbacs, err := translatePolicy(authzPolicy) + if err != nil { + return nil, err + } + chainEngine, err := rbac.NewChainEngine(rbacs) + if err != nil { + return nil, err + } + return &StaticInterceptor{*chainEngine}, nil +} + +// UnaryInterceptor intercepts incoming Unary RPC requests. +// Only authorized requests are allowed to pass. Otherwise, an unauthorized +// error is returned to the client. +func (i *StaticInterceptor) UnaryInterceptor(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + err := i.engines.IsAuthorized(ctx) + if err != nil { + if status.Code(err) == codes.PermissionDenied { + return nil, status.Errorf(codes.PermissionDenied, "unauthorized RPC request rejected") + } + return nil, err + } + return handler(ctx, req) +} + +// StreamInterceptor intercepts incoming Stream RPC requests. +// Only authorized requests are allowed to pass. Otherwise, an unauthorized +// error is returned to the client. +func (i *StaticInterceptor) StreamInterceptor(srv interface{}, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + err := i.engines.IsAuthorized(ss.Context()) + if err != nil { + if status.Code(err) == codes.PermissionDenied { + return status.Errorf(codes.PermissionDenied, "unauthorized RPC request rejected") + } + return err + } + return handler(srv, ss) +} diff --git a/authz/sdk_server_interceptors_test.go b/authz/sdk_server_interceptors_test.go new file mode 100644 index 00000000000..e2c1072e7d8 --- /dev/null +++ b/authz/sdk_server_interceptors_test.go @@ -0,0 +1,56 @@ +/* + * + * Copyright 2021 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package authz_test + +import ( + "testing" + + "google.golang.org/grpc/authz" +) + +func TestNewStatic(t *testing.T) { + tests := map[string]struct { + authzPolicy string + wantErr bool + }{ + "InvalidPolicyFailsToCreateInterceptor": { + authzPolicy: `{}`, + wantErr: true, + }, + "ValidPolicyCreatesInterceptor": { + authzPolicy: `{ + "name": "authz", + "allow_rules": + [ + { + "name": "allow_all" + } + ] + }`, + wantErr: false, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if _, err := authz.NewStatic(test.authzPolicy); (err != nil) != test.wantErr { + t.Fatalf("NewStatic(%v) returned err: %v, want err: %v", test.authzPolicy, err, test.wantErr) + } + }) + } +} diff --git a/internal/xds/rbac/rbac_engine.go b/internal/xds/rbac/rbac_engine.go index 269edabeaa6..1642d26b1ae 100644 --- a/internal/xds/rbac/rbac_engine.go +++ b/internal/xds/rbac/rbac_engine.go @@ -32,12 +32,15 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/grpclog" "google.golang.org/grpc/internal/transport" "google.golang.org/grpc/metadata" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" ) +var logger = grpclog.Component("rbac") + var getConnection = transport.GetConnection // ChainEngine represents a chain of RBAC Engines, used to make authorization @@ -69,7 +72,8 @@ func (cre *ChainEngine) IsAuthorized(ctx context.Context) error { // and then be used for the whole chain of RBAC Engines. rpcData, err := newRPCData(ctx) if err != nil { - return status.Errorf(codes.InvalidArgument, "missing fields in ctx %+v: %v", ctx, err) + logger.Errorf("newRPCData: %v", err) + return status.Errorf(codes.Internal, "gRPC RBAC: %v", err) } for _, engine := range cre.chainedEngines { matchingPolicyName, ok := engine.findMatchingPolicy(rpcData) @@ -86,7 +90,7 @@ func (cre *ChainEngine) IsAuthorized(ctx context.Context) error { // If the incoming RPC gets through all of the engines successfully (i.e. // doesn't not match an allow or match a deny engine), the RPC is authorized // to proceed. - return status.Error(codes.OK, "") + return nil } // engine is used for matching incoming RPCs to policies.