authz: create interceptors for gRPC security policy API (#4664)

* Static Authorization Interceptor
This commit is contained in:
Ashitha Santhosh 2021-09-02 11:22:07 -07:00 committed by GitHub
parent d6a5f5f4f3
commit b189f5e1bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 595 additions and 103 deletions

View File

@ -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
}

View File

@ -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}}},
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}},
},
},
},
},
}},
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"}}}}}},
}}}}}}}}},
},
"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)
}
})
}

307
authz/sdk_end2end_test.go Normal file
View File

@ -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())
}
})
}
}

View File

@ -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)
}

View File

@ -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)
}
})
}
}

View File

@ -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.