mirror of https://github.com/grpc/grpc-go.git
authz: create interceptors for gRPC security policy API (#4664)
* Static Authorization Interceptor
This commit is contained in:
parent
d6a5f5f4f3
commit
b189f5e1bc
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue