| /* |
| * 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" |
| |
| v1xdsudpatypepb "github.com/cncf/xds/go/udpa/type/v1" |
| v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" |
| 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" |
| "google.golang.org/protobuf/types/known/anypb" |
| "google.golang.org/protobuf/types/known/structpb" |
| ) |
| |
| // This is used when converting a custom config from raw JSON to a TypedStruct |
| // The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/<name>" |
| const typeURLPrefix = "grpc.authz.audit_logging/" |
| |
| 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 |
| } |
| |
| type auditLogger struct { |
| Name string `json:"name"` |
| Config *structpb.Struct `json:"config"` |
| IsOptional bool `json:"is_optional"` |
| } |
| |
| type auditLoggingOptions struct { |
| AuditCondition string `json:"audit_condition"` |
| AuditLoggers []*auditLogger `json:"audit_loggers"` |
| } |
| |
| // Represents the SDK authorization policy provided by user. |
| type authorizationPolicy struct { |
| Name string |
| DenyRules []rule `json:"deny_rules"` |
| AllowRules []rule `json:"allow_rules"` |
| AuditLoggingOptions auditLoggingOptions `json:"audit_logging_options"` |
| } |
| |
| 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 |
| } |
| |
| // Parse auditLoggingOptions to the associated RBAC protos. The single |
| // auditLoggingOptions results in two different parsed protos, one for the allow |
| // policy and one for the deny policy |
| func (options *auditLoggingOptions) toProtos() (allow *v3rbacpb.RBAC_AuditLoggingOptions, deny *v3rbacpb.RBAC_AuditLoggingOptions, err error) { |
| allow = &v3rbacpb.RBAC_AuditLoggingOptions{} |
| deny = &v3rbacpb.RBAC_AuditLoggingOptions{} |
| |
| if options.AuditCondition != "" { |
| rbacCondition, ok := v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition_value[options.AuditCondition] |
| if !ok { |
| return nil, nil, fmt.Errorf("failed to parse AuditCondition %v. Allowed values {NONE, ON_DENY, ON_ALLOW, ON_DENY_AND_ALLOW}", options.AuditCondition) |
| } |
| allow.AuditCondition = v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition) |
| deny.AuditCondition = toDenyCondition(v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition)) |
| } |
| |
| for i, config := range options.AuditLoggers { |
| if config.Name == "" { |
| return nil, nil, fmt.Errorf("missing required field: name in audit_logging_options.audit_loggers[%v]", i) |
| } |
| if config.Config == nil { |
| config.Config = &structpb.Struct{} |
| } |
| typedStruct := &v1xdsudpatypepb.TypedStruct{ |
| TypeUrl: typeURLPrefix + config.Name, |
| Value: config.Config, |
| } |
| customConfig, err := anypb.New(typedStruct) |
| if err != nil { |
| return nil, nil, fmt.Errorf("error parsing custom audit logger config: %v", err) |
| } |
| |
| logger := &v3corepb.TypedExtensionConfig{Name: config.Name, TypedConfig: customConfig} |
| rbacConfig := v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{ |
| IsOptional: config.IsOptional, |
| AuditLogger: logger, |
| } |
| allow.LoggerConfigs = append(allow.LoggerConfigs, &rbacConfig) |
| deny.LoggerConfigs = append(deny.LoggerConfigs, &rbacConfig) |
| } |
| |
| return allow, deny, nil |
| } |
| |
| // Maps the AuditCondition coming from AuditLoggingOptions to the proper |
| // condition for the deny policy RBAC proto |
| func toDenyCondition(condition v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition) v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition { |
| // Mapping the overall policy AuditCondition to what it must be for the Deny and Allow RBAC |
| // See gRPC A59 for details - https://github.com/grpc/proposal/pull/346/files |
| // |Authorization Policy |DENY RBAC |ALLOW RBAC | |
| // |----------------------|-------------------|---------------------| |
| // |NONE |NONE |NONE | |
| // |ON_DENY |ON_DENY |ON_DENY | |
| // |ON_ALLOW |NONE |ON_ALLOW | |
| // |ON_DENY_AND_ALLOW |ON_DENY |ON_DENY_AND_ALLOW | |
| switch condition { |
| case v3rbacpb.RBAC_AuditLoggingOptions_NONE: |
| return v3rbacpb.RBAC_AuditLoggingOptions_NONE |
| case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY: |
| return v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY |
| case v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW: |
| return v3rbacpb.RBAC_AuditLoggingOptions_NONE |
| case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY_AND_ALLOW: |
| return v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY |
| default: |
| return v3rbacpb.RBAC_AuditLoggingOptions_NONE |
| } |
| } |
| |
| // 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. Also returns the overall policy name. If the input policy |
| // cannot be parsed or is invalid, an error will be returned. |
| func translatePolicy(policyStr string) ([]*v3rbacpb.RBAC, string, 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`) |
| } |
| allowLogger, denyLogger, err := policy.AuditLoggingOptions.toProtos() |
| if err != nil { |
| return nil, "", err |
| } |
| 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, |
| AuditLoggingOptions: denyLogger, |
| } |
| 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, AuditLoggingOptions: allowLogger} |
| return append(rbacs, allowRBAC), policy.Name, nil |
| } |