mirror of https://github.com/grpc/grpc-go.git
399 lines
13 KiB
Go
399 lines
13 KiB
Go
/*
|
|
* 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
|
|
}
|