123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398 |
- /*
- * 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
- }
|