...

Source file src/google.golang.org/grpc/authz/rbac_translator.go

Documentation: google.golang.org/grpc/authz

     1  /*
     2   * Copyright 2021 gRPC authors.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  // Package authz exposes methods to manage authorization within gRPC.
    18  //
    19  // # Experimental
    20  //
    21  // Notice: This package is EXPERIMENTAL and may be changed or removed
    22  // in a later release.
    23  package authz
    24  
    25  import (
    26  	"bytes"
    27  	"encoding/json"
    28  	"fmt"
    29  	"strings"
    30  
    31  	v1xdsudpatypepb "github.com/cncf/xds/go/udpa/type/v1"
    32  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    33  	v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
    34  	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    35  	v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
    36  	"google.golang.org/protobuf/types/known/anypb"
    37  	"google.golang.org/protobuf/types/known/structpb"
    38  )
    39  
    40  // This is used when converting a custom config from raw JSON to a TypedStruct
    41  // The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/<name>"
    42  const typeURLPrefix = "grpc.authz.audit_logging/"
    43  
    44  type header struct {
    45  	Key    string
    46  	Values []string
    47  }
    48  
    49  type peer struct {
    50  	Principals []string
    51  }
    52  
    53  type request struct {
    54  	Paths   []string
    55  	Headers []header
    56  }
    57  
    58  type rule struct {
    59  	Name    string
    60  	Source  peer
    61  	Request request
    62  }
    63  
    64  type auditLogger struct {
    65  	Name       string           `json:"name"`
    66  	Config     *structpb.Struct `json:"config"`
    67  	IsOptional bool             `json:"is_optional"`
    68  }
    69  
    70  type auditLoggingOptions struct {
    71  	AuditCondition string         `json:"audit_condition"`
    72  	AuditLoggers   []*auditLogger `json:"audit_loggers"`
    73  }
    74  
    75  // Represents the SDK authorization policy provided by user.
    76  type authorizationPolicy struct {
    77  	Name                string
    78  	DenyRules           []rule              `json:"deny_rules"`
    79  	AllowRules          []rule              `json:"allow_rules"`
    80  	AuditLoggingOptions auditLoggingOptions `json:"audit_logging_options"`
    81  }
    82  
    83  func principalOr(principals []*v3rbacpb.Principal) *v3rbacpb.Principal {
    84  	return &v3rbacpb.Principal{
    85  		Identifier: &v3rbacpb.Principal_OrIds{
    86  			OrIds: &v3rbacpb.Principal_Set{
    87  				Ids: principals,
    88  			},
    89  		},
    90  	}
    91  }
    92  
    93  func permissionOr(permission []*v3rbacpb.Permission) *v3rbacpb.Permission {
    94  	return &v3rbacpb.Permission{
    95  		Rule: &v3rbacpb.Permission_OrRules{
    96  			OrRules: &v3rbacpb.Permission_Set{
    97  				Rules: permission,
    98  			},
    99  		},
   100  	}
   101  }
   102  
   103  func permissionAnd(permission []*v3rbacpb.Permission) *v3rbacpb.Permission {
   104  	return &v3rbacpb.Permission{
   105  		Rule: &v3rbacpb.Permission_AndRules{
   106  			AndRules: &v3rbacpb.Permission_Set{
   107  				Rules: permission,
   108  			},
   109  		},
   110  	}
   111  }
   112  
   113  func getStringMatcher(value string) *v3matcherpb.StringMatcher {
   114  	switch {
   115  	case value == "*":
   116  		return &v3matcherpb.StringMatcher{
   117  			MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{
   118  				SafeRegex: &v3matcherpb.RegexMatcher{Regex: ".+"}},
   119  		}
   120  	case strings.HasSuffix(value, "*"):
   121  		prefix := strings.TrimSuffix(value, "*")
   122  		return &v3matcherpb.StringMatcher{
   123  			MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: prefix},
   124  		}
   125  	case strings.HasPrefix(value, "*"):
   126  		suffix := strings.TrimPrefix(value, "*")
   127  		return &v3matcherpb.StringMatcher{
   128  			MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: suffix},
   129  		}
   130  	default:
   131  		return &v3matcherpb.StringMatcher{
   132  			MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: value},
   133  		}
   134  	}
   135  }
   136  
   137  func getHeaderMatcher(key, value string) *v3routepb.HeaderMatcher {
   138  	switch {
   139  	case value == "*":
   140  		return &v3routepb.HeaderMatcher{
   141  			Name: key,
   142  			HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{
   143  				SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: ".+"}},
   144  		}
   145  	case strings.HasSuffix(value, "*"):
   146  		prefix := strings.TrimSuffix(value, "*")
   147  		return &v3routepb.HeaderMatcher{
   148  			Name:                 key,
   149  			HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: prefix},
   150  		}
   151  	case strings.HasPrefix(value, "*"):
   152  		suffix := strings.TrimPrefix(value, "*")
   153  		return &v3routepb.HeaderMatcher{
   154  			Name:                 key,
   155  			HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SuffixMatch{SuffixMatch: suffix},
   156  		}
   157  	default:
   158  		return &v3routepb.HeaderMatcher{
   159  			Name:                 key,
   160  			HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: value},
   161  		}
   162  	}
   163  }
   164  
   165  func parsePrincipalNames(principalNames []string) []*v3rbacpb.Principal {
   166  	ps := make([]*v3rbacpb.Principal, 0, len(principalNames))
   167  	for _, principalName := range principalNames {
   168  		newPrincipalName := &v3rbacpb.Principal{
   169  			Identifier: &v3rbacpb.Principal_Authenticated_{
   170  				Authenticated: &v3rbacpb.Principal_Authenticated{
   171  					PrincipalName: getStringMatcher(principalName),
   172  				},
   173  			}}
   174  		ps = append(ps, newPrincipalName)
   175  	}
   176  	return ps
   177  }
   178  
   179  func parsePeer(source peer) *v3rbacpb.Principal {
   180  	if len(source.Principals) == 0 {
   181  		return &v3rbacpb.Principal{
   182  			Identifier: &v3rbacpb.Principal_Any{
   183  				Any: true,
   184  			},
   185  		}
   186  	}
   187  	return principalOr(parsePrincipalNames(source.Principals))
   188  }
   189  
   190  func parsePaths(paths []string) []*v3rbacpb.Permission {
   191  	ps := make([]*v3rbacpb.Permission, 0, len(paths))
   192  	for _, path := range paths {
   193  		newPath := &v3rbacpb.Permission{
   194  			Rule: &v3rbacpb.Permission_UrlPath{
   195  				UrlPath: &v3matcherpb.PathMatcher{
   196  					Rule: &v3matcherpb.PathMatcher_Path{Path: getStringMatcher(path)}}}}
   197  		ps = append(ps, newPath)
   198  	}
   199  	return ps
   200  }
   201  
   202  func parseHeaderValues(key string, values []string) []*v3rbacpb.Permission {
   203  	vs := make([]*v3rbacpb.Permission, 0, len(values))
   204  	for _, value := range values {
   205  		newHeader := &v3rbacpb.Permission{
   206  			Rule: &v3rbacpb.Permission_Header{
   207  				Header: getHeaderMatcher(key, value)}}
   208  		vs = append(vs, newHeader)
   209  	}
   210  	return vs
   211  }
   212  
   213  var unsupportedHeaders = map[string]bool{
   214  	"host":                true,
   215  	"connection":          true,
   216  	"keep-alive":          true,
   217  	"proxy-authenticate":  true,
   218  	"proxy-authorization": true,
   219  	"te":                  true,
   220  	"trailer":             true,
   221  	"transfer-encoding":   true,
   222  	"upgrade":             true,
   223  }
   224  
   225  func unsupportedHeader(key string) bool {
   226  	return key[0] == ':' || strings.HasPrefix(key, "grpc-") || unsupportedHeaders[key]
   227  }
   228  
   229  func parseHeaders(headers []header) ([]*v3rbacpb.Permission, error) {
   230  	hs := make([]*v3rbacpb.Permission, 0, len(headers))
   231  	for i, header := range headers {
   232  		if header.Key == "" {
   233  			return nil, fmt.Errorf(`"headers" %d: "key" is not present`, i)
   234  		}
   235  		header.Key = strings.ToLower(header.Key)
   236  		if unsupportedHeader(header.Key) {
   237  			return nil, fmt.Errorf(`"headers" %d: unsupported "key" %s`, i, header.Key)
   238  		}
   239  		if len(header.Values) == 0 {
   240  			return nil, fmt.Errorf(`"headers" %d: "values" is not present`, i)
   241  		}
   242  		values := parseHeaderValues(header.Key, header.Values)
   243  		hs = append(hs, permissionOr(values))
   244  	}
   245  	return hs, nil
   246  }
   247  
   248  func parseRequest(request request) (*v3rbacpb.Permission, error) {
   249  	var and []*v3rbacpb.Permission
   250  	if len(request.Paths) > 0 {
   251  		and = append(and, permissionOr(parsePaths(request.Paths)))
   252  	}
   253  	if len(request.Headers) > 0 {
   254  		headers, err := parseHeaders(request.Headers)
   255  		if err != nil {
   256  			return nil, err
   257  		}
   258  		and = append(and, permissionAnd(headers))
   259  	}
   260  	if len(and) > 0 {
   261  		return permissionAnd(and), nil
   262  	}
   263  	return &v3rbacpb.Permission{
   264  		Rule: &v3rbacpb.Permission_Any{
   265  			Any: true,
   266  		},
   267  	}, nil
   268  }
   269  
   270  func parseRules(rules []rule, prefixName string) (map[string]*v3rbacpb.Policy, error) {
   271  	policies := make(map[string]*v3rbacpb.Policy)
   272  	for i, rule := range rules {
   273  		if rule.Name == "" {
   274  			return policies, fmt.Errorf(`%d: "name" is not present`, i)
   275  		}
   276  		permission, err := parseRequest(rule.Request)
   277  		if err != nil {
   278  			return nil, fmt.Errorf("%d: %v", i, err)
   279  		}
   280  		policyName := prefixName + "_" + rule.Name
   281  		policies[policyName] = &v3rbacpb.Policy{
   282  			Principals:  []*v3rbacpb.Principal{parsePeer(rule.Source)},
   283  			Permissions: []*v3rbacpb.Permission{permission},
   284  		}
   285  	}
   286  	return policies, nil
   287  }
   288  
   289  // Parse auditLoggingOptions to the associated RBAC protos. The single
   290  // auditLoggingOptions results in two different parsed protos, one for the allow
   291  // policy and one for the deny policy
   292  func (options *auditLoggingOptions) toProtos() (allow *v3rbacpb.RBAC_AuditLoggingOptions, deny *v3rbacpb.RBAC_AuditLoggingOptions, err error) {
   293  	allow = &v3rbacpb.RBAC_AuditLoggingOptions{}
   294  	deny = &v3rbacpb.RBAC_AuditLoggingOptions{}
   295  
   296  	if options.AuditCondition != "" {
   297  		rbacCondition, ok := v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition_value[options.AuditCondition]
   298  		if !ok {
   299  			return nil, nil, fmt.Errorf("failed to parse AuditCondition %v. Allowed values {NONE, ON_DENY, ON_ALLOW, ON_DENY_AND_ALLOW}", options.AuditCondition)
   300  		}
   301  		allow.AuditCondition = v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition)
   302  		deny.AuditCondition = toDenyCondition(v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition))
   303  	}
   304  
   305  	for i, config := range options.AuditLoggers {
   306  		if config.Name == "" {
   307  			return nil, nil, fmt.Errorf("missing required field: name in audit_logging_options.audit_loggers[%v]", i)
   308  		}
   309  		if config.Config == nil {
   310  			config.Config = &structpb.Struct{}
   311  		}
   312  		typedStruct := &v1xdsudpatypepb.TypedStruct{
   313  			TypeUrl: typeURLPrefix + config.Name,
   314  			Value:   config.Config,
   315  		}
   316  		customConfig, err := anypb.New(typedStruct)
   317  		if err != nil {
   318  			return nil, nil, fmt.Errorf("error parsing custom audit logger config: %v", err)
   319  		}
   320  
   321  		logger := &v3corepb.TypedExtensionConfig{Name: config.Name, TypedConfig: customConfig}
   322  		rbacConfig := v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
   323  			IsOptional:  config.IsOptional,
   324  			AuditLogger: logger,
   325  		}
   326  		allow.LoggerConfigs = append(allow.LoggerConfigs, &rbacConfig)
   327  		deny.LoggerConfigs = append(deny.LoggerConfigs, &rbacConfig)
   328  	}
   329  
   330  	return allow, deny, nil
   331  }
   332  
   333  // Maps the AuditCondition coming from AuditLoggingOptions to the proper
   334  // condition for the deny policy RBAC proto
   335  func toDenyCondition(condition v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition) v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition {
   336  	// Mapping the overall policy AuditCondition to what it must be for the Deny and Allow RBAC
   337  	// See gRPC A59 for details - https://github.com/grpc/proposal/pull/346/files
   338  	// |Authorization Policy  |DENY RBAC          |ALLOW RBAC           |
   339  	// |----------------------|-------------------|---------------------|
   340  	// |NONE                  |NONE               |NONE                 |
   341  	// |ON_DENY               |ON_DENY            |ON_DENY              |
   342  	// |ON_ALLOW              |NONE               |ON_ALLOW             |
   343  	// |ON_DENY_AND_ALLOW     |ON_DENY            |ON_DENY_AND_ALLOW    |
   344  	switch condition {
   345  	case v3rbacpb.RBAC_AuditLoggingOptions_NONE:
   346  		return v3rbacpb.RBAC_AuditLoggingOptions_NONE
   347  	case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY:
   348  		return v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY
   349  	case v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW:
   350  		return v3rbacpb.RBAC_AuditLoggingOptions_NONE
   351  	case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY_AND_ALLOW:
   352  		return v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY
   353  	default:
   354  		return v3rbacpb.RBAC_AuditLoggingOptions_NONE
   355  	}
   356  }
   357  
   358  // translatePolicy translates SDK authorization policy in JSON format to two
   359  // Envoy RBAC polices (deny followed by allow policy) or only one Envoy RBAC
   360  // allow policy. Also returns the overall policy name. If the input policy
   361  // cannot be parsed or is invalid, an error will be returned.
   362  func translatePolicy(policyStr string) ([]*v3rbacpb.RBAC, string, error) {
   363  	policy := &authorizationPolicy{}
   364  	d := json.NewDecoder(bytes.NewReader([]byte(policyStr)))
   365  	d.DisallowUnknownFields()
   366  	if err := d.Decode(policy); err != nil {
   367  		return nil, "", fmt.Errorf("failed to unmarshal policy: %v", err)
   368  	}
   369  	if policy.Name == "" {
   370  		return nil, "", fmt.Errorf(`"name" is not present`)
   371  	}
   372  	if len(policy.AllowRules) == 0 {
   373  		return nil, "", fmt.Errorf(`"allow_rules" is not present`)
   374  	}
   375  	allowLogger, denyLogger, err := policy.AuditLoggingOptions.toProtos()
   376  	if err != nil {
   377  		return nil, "", err
   378  	}
   379  	rbacs := make([]*v3rbacpb.RBAC, 0, 2)
   380  	if len(policy.DenyRules) > 0 {
   381  		denyPolicies, err := parseRules(policy.DenyRules, policy.Name)
   382  		if err != nil {
   383  			return nil, "", fmt.Errorf(`"deny_rules" %v`, err)
   384  		}
   385  		denyRBAC := &v3rbacpb.RBAC{
   386  			Action:              v3rbacpb.RBAC_DENY,
   387  			Policies:            denyPolicies,
   388  			AuditLoggingOptions: denyLogger,
   389  		}
   390  		rbacs = append(rbacs, denyRBAC)
   391  	}
   392  	allowPolicies, err := parseRules(policy.AllowRules, policy.Name)
   393  	if err != nil {
   394  		return nil, "", fmt.Errorf(`"allow_rules" %v`, err)
   395  	}
   396  	allowRBAC := &v3rbacpb.RBAC{Action: v3rbacpb.RBAC_ALLOW, Policies: allowPolicies, AuditLoggingOptions: allowLogger}
   397  	return append(rbacs, allowRBAC), policy.Name, nil
   398  }
   399  

View as plain text