Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

authz: create interceptors for gRPC security policy API #4664

Merged
merged 14 commits into from Sep 2, 2021
43 changes: 24 additions & 19 deletions authz/rbac_translator.go
Expand Up @@ -23,6 +23,7 @@
package authz

import (
"bytes"
"encoding/json"
"fmt"
"strings"
Expand Down Expand Up @@ -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, "*")
Expand All @@ -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, "*")
Expand Down Expand Up @@ -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
}
209 changes: 127 additions & 82 deletions authz/rbac_translator_test.go
Expand Up @@ -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: `{
Expand Down Expand Up @@ -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: `{}`,
Expand All @@ -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: `{
Expand Down Expand Up @@ -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)
}
})
}
Expand Down