/* * 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 exposes methods to manage authorization within gRPC. // // Experimental // // Notice: This package is EXPERIMENTAL and may be changed or removed // in a later release. package authz import ( "bytes" "encoding/json" "fmt" "strings" v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" ) type header struct { Key string Values []string } type peer struct { Principals []string } type request struct { Paths []string Headers []header } type rule struct { Name string Source peer Request request } // Represents the SDK authorization policy provided by user. type authorizationPolicy struct { Name string DenyRules []rule `json:"deny_rules"` AllowRules []rule `json:"allow_rules"` } func principalOr(principals []*v3rbacpb.Principal) *v3rbacpb.Principal { return &v3rbacpb.Principal{ Identifier: &v3rbacpb.Principal_OrIds{ OrIds: &v3rbacpb.Principal_Set{ Ids: principals, }, }, } } func permissionOr(permission []*v3rbacpb.Permission) *v3rbacpb.Permission { return &v3rbacpb.Permission{ Rule: &v3rbacpb.Permission_OrRules{ OrRules: &v3rbacpb.Permission_Set{ Rules: permission, }, }, } } func permissionAnd(permission []*v3rbacpb.Permission) *v3rbacpb.Permission { return &v3rbacpb.Permission{ Rule: &v3rbacpb.Permission_AndRules{ AndRules: &v3rbacpb.Permission_Set{ Rules: permission, }, }, } } func getStringMatcher(value string) *v3matcherpb.StringMatcher { switch { case value == "*": return &v3matcherpb.StringMatcher{ MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{ SafeRegex: &v3matcherpb.RegexMatcher{Regex: ".+"}}, } case strings.HasSuffix(value, "*"): prefix := strings.TrimSuffix(value, "*") return &v3matcherpb.StringMatcher{ MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: prefix}, } case strings.HasPrefix(value, "*"): suffix := strings.TrimPrefix(value, "*") return &v3matcherpb.StringMatcher{ MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: suffix}, } default: return &v3matcherpb.StringMatcher{ MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: value}, } } } func getHeaderMatcher(key, value string) *v3routepb.HeaderMatcher { switch { case value == "*": return &v3routepb.HeaderMatcher{ Name: key, HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{ SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: ".+"}}, } case strings.HasSuffix(value, "*"): prefix := strings.TrimSuffix(value, "*") return &v3routepb.HeaderMatcher{ Name: key, HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: prefix}, } case strings.HasPrefix(value, "*"): suffix := strings.TrimPrefix(value, "*") return &v3routepb.HeaderMatcher{ Name: key, HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SuffixMatch{SuffixMatch: suffix}, } default: return &v3routepb.HeaderMatcher{ Name: key, HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: value}, } } } func parsePrincipalNames(principalNames []string) []*v3rbacpb.Principal { ps := make([]*v3rbacpb.Principal, 0, len(principalNames)) for _, principalName := range principalNames { newPrincipalName := &v3rbacpb.Principal{ Identifier: &v3rbacpb.Principal_Authenticated_{ Authenticated: &v3rbacpb.Principal_Authenticated{ PrincipalName: getStringMatcher(principalName), }, }} ps = append(ps, newPrincipalName) } return ps } func parsePeer(source peer) *v3rbacpb.Principal { if len(source.Principals) == 0 { return &v3rbacpb.Principal{ Identifier: &v3rbacpb.Principal_Any{ Any: true, }, } } return principalOr(parsePrincipalNames(source.Principals)) } func parsePaths(paths []string) []*v3rbacpb.Permission { ps := make([]*v3rbacpb.Permission, 0, len(paths)) for _, path := range paths { newPath := &v3rbacpb.Permission{ Rule: &v3rbacpb.Permission_UrlPath{ UrlPath: &v3matcherpb.PathMatcher{ Rule: &v3matcherpb.PathMatcher_Path{Path: getStringMatcher(path)}}}} ps = append(ps, newPath) } return ps } func parseHeaderValues(key string, values []string) []*v3rbacpb.Permission { vs := make([]*v3rbacpb.Permission, 0, len(values)) for _, value := range values { newHeader := &v3rbacpb.Permission{ Rule: &v3rbacpb.Permission_Header{ Header: getHeaderMatcher(key, value)}} vs = append(vs, newHeader) } return vs } var unsupportedHeaders = map[string]bool{ "host": true, "connection": true, "keep-alive": true, "proxy-authenticate": true, "proxy-authorization": true, "te": true, "trailer": true, "transfer-encoding": true, "upgrade": true, } func unsupportedHeader(key string) bool { return key[0] == ':' || strings.HasPrefix(key, "grpc-") || unsupportedHeaders[key] } func parseHeaders(headers []header) ([]*v3rbacpb.Permission, error) { hs := make([]*v3rbacpb.Permission, 0, len(headers)) for i, header := range headers { if header.Key == "" { return nil, fmt.Errorf(`"headers" %d: "key" is not present`, i) } header.Key = strings.ToLower(header.Key) if unsupportedHeader(header.Key) { return nil, fmt.Errorf(`"headers" %d: unsupported "key" %s`, i, header.Key) } if len(header.Values) == 0 { return nil, fmt.Errorf(`"headers" %d: "values" is not present`, i) } values := parseHeaderValues(header.Key, header.Values) hs = append(hs, permissionOr(values)) } return hs, nil } func parseRequest(request request) (*v3rbacpb.Permission, error) { var and []*v3rbacpb.Permission if len(request.Paths) > 0 { and = append(and, permissionOr(parsePaths(request.Paths))) } if len(request.Headers) > 0 { headers, err := parseHeaders(request.Headers) if err != nil { return nil, err } and = append(and, permissionAnd(headers)) } if len(and) > 0 { return permissionAnd(and), nil } return &v3rbacpb.Permission{ Rule: &v3rbacpb.Permission_Any{ Any: true, }, }, nil } func parseRules(rules []rule, prefixName string) (map[string]*v3rbacpb.Policy, error) { policies := make(map[string]*v3rbacpb.Policy) for i, rule := range rules { if rule.Name == "" { return policies, fmt.Errorf(`%d: "name" is not present`, i) } permission, err := parseRequest(rule.Request) if err != nil { return nil, fmt.Errorf("%d: %v", i, err) } policyName := prefixName + "_" + rule.Name policies[policyName] = &v3rbacpb.Policy{ Principals: []*v3rbacpb.Principal{parsePeer(rule.Source)}, Permissions: []*v3rbacpb.Permission{permission}, } } return policies, nil } // translatePolicy translates SDK authorization policy in JSON format to two // 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, fmt.Errorf(`"name" is not present`) } if len(policy.AllowRules) == 0 { return nil, fmt.Errorf(`"allow_rules" is not present`) } rbacs := make([]*v3rbacpb.RBAC, 0, 2) if len(policy.DenyRules) > 0 { denyPolicies, err := parseRules(policy.DenyRules, policy.Name) if err != nil { return nil, fmt.Errorf(`"deny_rules" %v`, err) } denyRBAC := &v3rbacpb.RBAC{ Action: v3rbacpb.RBAC_DENY, Policies: denyPolicies, } rbacs = append(rbacs, denyRBAC) } 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 }