snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2023 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 rbac
import (
"encoding/json"
"fmt"
"strings"
v1xdsudpatypepb "github.com/cncf/xds/go/udpa/type/v1"
v3xdsxdstypepb "github.com/cncf/xds/go/xds/type/v3"
v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
v3auditloggersstreampb "github.com/envoyproxy/go-control-plane/envoy/extensions/rbac/audit_loggers/stream/v3"
"google.golang.org/grpc/authz/audit"
"google.golang.org/grpc/authz/audit/stdout"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/structpb"
)
func buildLogger(loggerConfig *v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig) (audit.Logger, error) {
if loggerConfig.GetAuditLogger().GetTypedConfig() == nil {
return nil, fmt.Errorf("missing required field: TypedConfig")
}
customConfig, loggerName, err := getCustomConfig(loggerConfig.AuditLogger.TypedConfig)
if err != nil {
return nil, err
}
if loggerName == "" {
return nil, fmt.Errorf("field TypedConfig.TypeURL cannot be an empty string")
}
factory := audit.GetLoggerBuilder(loggerName)
if factory == nil {
if loggerConfig.IsOptional {
return nil, nil
}
return nil, fmt.Errorf("no builder registered for %v", loggerName)
}
auditLoggerConfig, err := factory.ParseLoggerConfig(customConfig)
if err != nil {
return nil, fmt.Errorf("custom config could not be parsed by registered factory. error: %v", err)
}
auditLogger := factory.Build(auditLoggerConfig)
return auditLogger, nil
}
func getCustomConfig(config *anypb.Any) (json.RawMessage, string, error) {
c, err := config.UnmarshalNew()
if err != nil {
return nil, "", err
}
switch m := c.(type) {
case *v1xdsudpatypepb.TypedStruct:
return convertCustomConfig(m.TypeUrl, m.Value)
case *v3xdsxdstypepb.TypedStruct:
return convertCustomConfig(m.TypeUrl, m.Value)
case *v3auditloggersstreampb.StdoutAuditLog:
return convertStdoutConfig(m)
}
return nil, "", fmt.Errorf("custom config not implemented for type [%v]", config.GetTypeUrl())
}
func convertStdoutConfig(config *v3auditloggersstreampb.StdoutAuditLog) (json.RawMessage, string, error) {
json, err := protojson.Marshal(config)
return json, stdout.Name, err
}
func convertCustomConfig(typeURL string, s *structpb.Struct) (json.RawMessage, string, error) {
// The gRPC policy name will be the "type name" part of the value of the
// type_url field in the TypedStruct. We get this by using the part after
// the last / character. Can assume a valid type_url from the control plane.
urls := strings.Split(typeURL, "/")
if len(urls) == 0 {
return nil, "", fmt.Errorf("error converting custom audit logger %v for %v: typeURL must have a url-like format with the typeName being the value after the last /", typeURL, s)
}
name := urls[len(urls)-1]
rawJSON := []byte("{}")
var err error
if s != nil {
rawJSON, err = json.Marshal(s)
if err != nil {
return nil, "", fmt.Errorf("error converting custom audit logger %v for %v: %v", typeURL, s, err)
}
}
return rawJSON, name, nil
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2023 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 rbac
import (
"reflect"
"strings"
"testing"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
v3auditloggersstreampb "github.com/envoyproxy/go-control-plane/envoy/extensions/rbac/audit_loggers/stream/v3"
"google.golang.org/grpc/authz/audit"
"google.golang.org/grpc/authz/audit/stdout"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/protobuf/types/known/anypb"
)
func (s) TestBuildLoggerErrors(t *testing.T) {
tests := []struct {
name string
loggerConfig *v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig
expectedLogger audit.Logger
expectedError string
}{
{
name: "nil typed config",
loggerConfig: &v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
AuditLogger: &v3corepb.TypedExtensionConfig{
TypedConfig: nil,
},
},
expectedError: "missing required field: TypedConfig",
},
{
name: "Unsupported Type",
loggerConfig: &v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: testutils.MarshalAny(t, &v3rbacpb.RBAC_AuditLoggingOptions{}),
},
},
expectedError: "custom config not implemented for type ",
},
{
name: "Empty name",
loggerConfig: &v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerBuffer",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, ""),
},
},
expectedError: "field TypedConfig.TypeURL cannot be an empty string",
},
{
name: "No registered logger",
loggerConfig: &v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "UnregisteredLogger",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "UnregisteredLogger"),
},
IsOptional: false,
},
expectedError: "no builder registered for UnregisteredLogger",
},
{
name: "fail to parse custom config",
loggerConfig: &v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "TestAuditLoggerCustomConfig",
TypedConfig: createUDPATypedStruct(t, map[string]any{"abc": "BADVALUE", "xyz": "123"}, "fail to parse custom config_TestAuditLoggerCustomConfig")},
IsOptional: false,
},
expectedError: "custom config could not be parsed",
},
{
name: "no registered logger but optional passes",
loggerConfig: &v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
AuditLogger: &v3corepb.TypedExtensionConfig{
Name: "UnregisteredLogger",
TypedConfig: createUDPATypedStruct(t, map[string]any{}, "no registered logger but optional passes_UnregisteredLogger"),
},
IsOptional: true,
},
expectedLogger: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
b := TestAuditLoggerCustomConfigBuilder{testName: test.name}
audit.RegisterLoggerBuilder(&b)
logger, err := buildLogger(test.loggerConfig)
if err != nil && !strings.HasPrefix(err.Error(), test.expectedError) {
t.Fatalf("expected error: %v. got error: %v", test.expectedError, err)
}
if logger != test.expectedLogger {
t.Fatalf("expected logger: %v. got logger: %v", test.expectedLogger, logger)
}
})
}
}
func (s) TestBuildLoggerKnownTypes(t *testing.T) {
tests := []struct {
name string
loggerConfig *v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig
expectedType reflect.Type
}{
{
name: "stdout logger",
loggerConfig: &v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
AuditLogger: &v3corepb.TypedExtensionConfig{
Name: stdout.Name,
TypedConfig: createStdoutPb(t),
},
IsOptional: false,
},
expectedType: reflect.TypeOf(audit.GetLoggerBuilder(stdout.Name).Build(nil)),
},
{
name: "stdout logger with generic TypedConfig",
loggerConfig: &v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
AuditLogger: &v3corepb.TypedExtensionConfig{
Name: stdout.Name,
TypedConfig: createXDSTypedStruct(t, map[string]any{}, stdout.Name),
},
IsOptional: false,
},
expectedType: reflect.TypeOf(audit.GetLoggerBuilder(stdout.Name).Build(nil)),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
logger, err := buildLogger(test.loggerConfig)
if err != nil {
t.Fatalf("expected success. got error: %v", err)
}
loggerType := reflect.TypeOf(logger)
if test.expectedType != loggerType {
t.Fatalf("logger not of expected type. want: %v got: %v", test.expectedType, loggerType)
}
})
}
}
// Builds stdout config for audit logger proto.
func createStdoutPb(t *testing.T) *anypb.Any {
t.Helper()
pb := &v3auditloggersstreampb.StdoutAuditLog{}
customConfig, err := anypb.New(pb)
if err != nil {
t.Fatalf("createStdoutPb failed during anypb.New: %v", err)
}
return customConfig
}

View File

@@ -0,0 +1,505 @@
/*
* 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 rbac
import (
"errors"
"fmt"
"net"
"net/netip"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
v3route_componentspb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
internalmatcher "google.golang.org/grpc/internal/xds/matcher"
)
// matcher is an interface that takes data about incoming RPC's and returns
// whether it matches with whatever matcher implements this interface.
type matcher interface {
match(data *rpcData) bool
}
// policyMatcher helps determine whether an incoming RPC call matches a policy.
// A policy is a logical role (e.g. Service Admin), which is comprised of
// permissions and principals. A principal is an identity (or identities) for a
// downstream subject which are assigned the policy (role), and a permission is
// an action(s) that a principal(s) can take. A policy matches if both a
// permission and a principal match, which will be determined by the child or
// permissions and principal matchers. policyMatcher implements the matcher
// interface.
type policyMatcher struct {
permissions *orMatcher
principals *orMatcher
}
func newPolicyMatcher(policy *v3rbacpb.Policy) (*policyMatcher, error) {
permissions, err := matchersFromPermissions(policy.Permissions)
if err != nil {
return nil, err
}
principals, err := matchersFromPrincipals(policy.Principals)
if err != nil {
return nil, err
}
return &policyMatcher{
permissions: &orMatcher{matchers: permissions},
principals: &orMatcher{matchers: principals},
}, nil
}
func (pm *policyMatcher) match(data *rpcData) bool {
// A policy matches if and only if at least one of its permissions match the
// action taking place AND at least one if its principals match the
// downstream peer.
return pm.permissions.match(data) && pm.principals.match(data)
}
// matchersFromPermissions takes a list of permissions (can also be
// a single permission, e.g. from a not matcher which is logically !permission)
// and returns a list of matchers which correspond to that permission. This will
// be called in many instances throughout the initial construction of the RBAC
// engine from the AND and OR matchers and also from the NOT matcher.
func matchersFromPermissions(permissions []*v3rbacpb.Permission) ([]matcher, error) {
var matchers []matcher
for _, permission := range permissions {
switch p := permission.GetRule().(type) {
case *v3rbacpb.Permission_AndRules:
mList, err := matchersFromPermissions(permission.GetAndRules().Rules)
if err != nil {
return nil, err
}
matchers = append(matchers, &andMatcher{matchers: mList})
case *v3rbacpb.Permission_OrRules:
mList, err := matchersFromPermissions(permission.GetOrRules().Rules)
if err != nil {
return nil, err
}
matchers = append(matchers, &orMatcher{matchers: mList})
case *v3rbacpb.Permission_Any:
matchers = append(matchers, &alwaysMatcher{})
case *v3rbacpb.Permission_Header:
m, err := newHeaderMatcher(permission.GetHeader())
if err != nil {
return nil, err
}
matchers = append(matchers, m)
case *v3rbacpb.Permission_UrlPath:
m, err := newURLPathMatcher(permission.GetUrlPath())
if err != nil {
return nil, err
}
matchers = append(matchers, m)
case *v3rbacpb.Permission_DestinationIp:
// Due to this being on server side, the destination IP is the local
// IP.
m, err := newLocalIPMatcher(permission.GetDestinationIp())
if err != nil {
return nil, err
}
matchers = append(matchers, m)
case *v3rbacpb.Permission_DestinationPort:
matchers = append(matchers, newPortMatcher(permission.GetDestinationPort()))
case *v3rbacpb.Permission_NotRule:
mList, err := matchersFromPermissions([]*v3rbacpb.Permission{{Rule: permission.GetNotRule().Rule}})
if err != nil {
return nil, err
}
if len(mList) != 1 {
return nil, fmt.Errorf("NotRule must contain exactly one rule")
}
matchers = append(matchers, &notMatcher{matcherToNot: mList[0]})
case *v3rbacpb.Permission_Metadata:
if permission.GetMetadata().GetInvert() {
matchers = append(matchers, &alwaysMatcher{})
} else {
matchers = append(matchers, &neverMatcher{})
}
case *v3rbacpb.Permission_RequestedServerName:
m, err := newRequestedServerNameMatcher(permission.GetRequestedServerName())
if err != nil {
return nil, err
}
matchers = append(matchers, m)
default:
return nil, fmt.Errorf("unsupported permission rule type: %T", p)
}
}
return matchers, nil
}
func matchersFromPrincipals(principals []*v3rbacpb.Principal) ([]matcher, error) {
var matchers []matcher
for _, principal := range principals {
switch p := principal.GetIdentifier().(type) {
case *v3rbacpb.Principal_AndIds:
mList, err := matchersFromPrincipals(principal.GetAndIds().Ids)
if err != nil {
return nil, err
}
matchers = append(matchers, &andMatcher{matchers: mList})
case *v3rbacpb.Principal_OrIds:
mList, err := matchersFromPrincipals(principal.GetOrIds().Ids)
if err != nil {
return nil, err
}
matchers = append(matchers, &orMatcher{matchers: mList})
case *v3rbacpb.Principal_Any:
matchers = append(matchers, &alwaysMatcher{})
case *v3rbacpb.Principal_Authenticated_:
authenticatedMatcher, err := newAuthenticatedMatcher(principal.GetAuthenticated())
if err != nil {
return nil, err
}
matchers = append(matchers, authenticatedMatcher)
case *v3rbacpb.Principal_DirectRemoteIp:
m, err := newRemoteIPMatcher(principal.GetDirectRemoteIp())
if err != nil {
return nil, err
}
matchers = append(matchers, m)
case *v3rbacpb.Principal_Header:
// Do we need an error here?
m, err := newHeaderMatcher(principal.GetHeader())
if err != nil {
return nil, err
}
matchers = append(matchers, m)
case *v3rbacpb.Principal_UrlPath:
m, err := newURLPathMatcher(principal.GetUrlPath())
if err != nil {
return nil, err
}
matchers = append(matchers, m)
case *v3rbacpb.Principal_Metadata:
if principal.GetMetadata().GetInvert() {
matchers = append(matchers, &alwaysMatcher{})
} else {
matchers = append(matchers, &neverMatcher{})
}
case *v3rbacpb.Principal_NotId:
mList, err := matchersFromPrincipals([]*v3rbacpb.Principal{{Identifier: principal.GetNotId().Identifier}})
if err != nil {
return nil, err
}
if len(mList) != 1 {
return nil, fmt.Errorf("NotId must contain exactly one identifier")
}
matchers = append(matchers, &notMatcher{matcherToNot: mList[0]})
case *v3rbacpb.Principal_SourceIp:
// The source ip principal identifier is deprecated, but gRPC RBAC
// treats it as equivalent to direct_remote_ip as per A41.
m, err := newRemoteIPMatcher(principal.GetSourceIp())
if err != nil {
return nil, err
}
matchers = append(matchers, m)
case *v3rbacpb.Principal_RemoteIp:
// RBAC in gRPC treats direct_remote_ip and remote_ip as logically
// equivalent, as per A41.
m, err := newRemoteIPMatcher(principal.GetRemoteIp())
if err != nil {
return nil, err
}
matchers = append(matchers, m)
default:
return nil, fmt.Errorf("unsupported principal identifier type: %T", p)
}
}
return matchers, nil
}
// orMatcher is a matcher where it successfully matches if one of it's
// children successfully match. It also logically represents a principal or
// permission, but can also be it's own entity further down the tree of
// matchers. orMatcher implements the matcher interface.
type orMatcher struct {
matchers []matcher
}
func (om *orMatcher) match(data *rpcData) bool {
// Range through child matchers and pass in data about incoming RPC, and
// only one child matcher has to match to be logically successful.
for _, m := range om.matchers {
if m.match(data) {
return true
}
}
return false
}
// andMatcher is a matcher that is successful if every child matcher
// matches. andMatcher implements the matcher interface.
type andMatcher struct {
matchers []matcher
}
func (am *andMatcher) match(data *rpcData) bool {
for _, m := range am.matchers {
if !m.match(data) {
return false
}
}
return true
}
// alwaysMatcher is a matcher that will always match. This logically
// represents an any rule for a permission or a principal. alwaysMatcher
// implements the matcher interface.
type alwaysMatcher struct {
}
func (am *alwaysMatcher) match(*rpcData) bool {
return true
}
// neverMatcher is a matcher that will never match. This logically represents a
// permission or principal that is unsupported in gRPC. neverMatcher implements
// the matcher interface.
type neverMatcher struct {
}
func (nm *neverMatcher) match(*rpcData) bool {
return false
}
// notMatcher is a matcher that nots an underlying matcher. notMatcher
// implements the matcher interface.
type notMatcher struct {
matcherToNot matcher
}
func (nm *notMatcher) match(data *rpcData) bool {
return !nm.matcherToNot.match(data)
}
// headerMatcher is a matcher that matches on incoming HTTP Headers present
// in the incoming RPC. headerMatcher implements the matcher interface.
type headerMatcher struct {
matcher internalmatcher.HeaderMatcher
}
func newHeaderMatcher(headerMatcherConfig *v3route_componentspb.HeaderMatcher) (*headerMatcher, error) {
var m internalmatcher.HeaderMatcher
switch headerMatcherConfig.HeaderMatchSpecifier.(type) {
case *v3route_componentspb.HeaderMatcher_ExactMatch:
m = internalmatcher.NewHeaderExactMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetExactMatch(), headerMatcherConfig.InvertMatch)
case *v3route_componentspb.HeaderMatcher_SafeRegexMatch:
regex, err := internalmatcher.CompileSafeRegex(headerMatcherConfig.GetSafeRegexMatch().GetRegex())
if err != nil {
return nil, err
}
m = internalmatcher.NewHeaderRegexMatcher(headerMatcherConfig.Name, regex, headerMatcherConfig.InvertMatch)
case *v3route_componentspb.HeaderMatcher_RangeMatch:
m = internalmatcher.NewHeaderRangeMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetRangeMatch().Start, headerMatcherConfig.GetRangeMatch().End, headerMatcherConfig.InvertMatch)
case *v3route_componentspb.HeaderMatcher_PresentMatch:
m = internalmatcher.NewHeaderPresentMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetPresentMatch(), headerMatcherConfig.InvertMatch)
case *v3route_componentspb.HeaderMatcher_PrefixMatch:
m = internalmatcher.NewHeaderPrefixMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetPrefixMatch(), headerMatcherConfig.InvertMatch)
case *v3route_componentspb.HeaderMatcher_SuffixMatch:
m = internalmatcher.NewHeaderSuffixMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetSuffixMatch(), headerMatcherConfig.InvertMatch)
case *v3route_componentspb.HeaderMatcher_ContainsMatch:
m = internalmatcher.NewHeaderContainsMatcher(headerMatcherConfig.Name, headerMatcherConfig.GetContainsMatch(), headerMatcherConfig.InvertMatch)
case *v3route_componentspb.HeaderMatcher_StringMatch:
sm, err := internalmatcher.StringMatcherFromProto(headerMatcherConfig.GetStringMatch())
if err != nil {
return nil, fmt.Errorf("invalid string matcher %+v: %v", headerMatcherConfig.GetStringMatch(), err)
}
m = internalmatcher.NewHeaderStringMatcher(headerMatcherConfig.Name, sm, headerMatcherConfig.InvertMatch)
default:
return nil, errors.New("unknown header matcher type")
}
return &headerMatcher{matcher: m}, nil
}
func (hm *headerMatcher) match(data *rpcData) bool {
return hm.matcher.Match(data.md)
}
// urlPathMatcher matches on the URL Path of the incoming RPC. In gRPC, this
// logically maps to the full method name the RPC is calling on the server side.
// urlPathMatcher implements the matcher interface.
type urlPathMatcher struct {
stringMatcher internalmatcher.StringMatcher
}
func newURLPathMatcher(pathMatcher *v3matcherpb.PathMatcher) (*urlPathMatcher, error) {
stringMatcher, err := internalmatcher.StringMatcherFromProto(pathMatcher.GetPath())
if err != nil {
return nil, err
}
return &urlPathMatcher{stringMatcher: stringMatcher}, nil
}
func (upm *urlPathMatcher) match(data *rpcData) bool {
return upm.stringMatcher.Match(data.fullMethod)
}
// remoteIPMatcher and localIPMatcher both are matchers that match against
// a CIDR Range. Two different matchers are needed as the remote and destination
// ip addresses come from different parts of the data about incoming RPC's
// passed in. Matching a CIDR Range means to determine whether the IP Address
// falls within the CIDR Range or not. They both implement the matcher
// interface.
type remoteIPMatcher struct {
// ipNet represents the CidrRange that this matcher was configured with.
// This is what will remote and destination IP's will be matched against.
ipNet netip.Prefix
}
func newRemoteIPMatcher(cidrRange *v3corepb.CidrRange) (*remoteIPMatcher, error) {
// Convert configuration to a cidrRangeString, as Go standard library has
// methods that parse cidr string.
cidrRangeString := fmt.Sprintf("%s/%d", cidrRange.GetAddressPrefix(), cidrRange.GetPrefixLen().GetValue())
ipNet, err := netip.ParsePrefix(cidrRangeString)
if err != nil {
return nil, err
}
return &remoteIPMatcher{ipNet: ipNet.Masked()}, nil
}
func (sim *remoteIPMatcher) match(data *rpcData) bool {
host, _, err := net.SplitHostPort(data.peerInfo.Addr.String())
if err != nil {
// Fallback for addresses without a port.
host = data.peerInfo.Addr.String()
}
ip, err := netip.ParseAddr(host)
if err != nil {
return false
}
return sim.ipNet.Contains(ip)
}
type localIPMatcher struct {
ipNet netip.Prefix
}
func newLocalIPMatcher(cidrRange *v3corepb.CidrRange) (*localIPMatcher, error) {
cidrRangeString := fmt.Sprintf("%s/%d", cidrRange.GetAddressPrefix(), cidrRange.GetPrefixLen().GetValue())
ipNet, err := netip.ParsePrefix(cidrRangeString)
if err != nil {
return nil, err
}
return &localIPMatcher{ipNet: ipNet.Masked()}, nil
}
func (dim *localIPMatcher) match(data *rpcData) bool {
host, _, err := net.SplitHostPort(data.localAddr.String())
if err != nil {
// Fallback for addresses without a port.
host = data.localAddr.String()
}
ip, err := netip.ParseAddr(host)
if err != nil {
return false
}
return dim.ipNet.Contains(ip)
}
// portMatcher matches on whether the destination port of the RPC matches the
// destination port this matcher was instantiated with. portMatcher
// implements the matcher interface.
type portMatcher struct {
destinationPort uint32
}
func newPortMatcher(destinationPort uint32) *portMatcher {
return &portMatcher{destinationPort: destinationPort}
}
func (pm *portMatcher) match(data *rpcData) bool {
return data.destinationPort == pm.destinationPort
}
// requestedServerNameMatcher matches on if the given string matcher matches
// on "", as per A41-xds-rbac.md. requestedServerNameMatcher implements
// the matcher interface.
type requestedServerNameMatcher struct {
stringMatcher internalmatcher.StringMatcher
}
func newRequestedServerNameMatcher(stringMatcherProto *v3matcherpb.StringMatcher) (*requestedServerNameMatcher, error) {
stringMatcher, err := internalmatcher.StringMatcherFromProto(stringMatcherProto)
if err != nil {
return nil, err
}
return &requestedServerNameMatcher{stringMatcher: stringMatcher}, nil
}
func (r *requestedServerNameMatcher) match(*rpcData) bool {
return r.stringMatcher.Match("")
}
// authenticatedMatcher matches on the name of the Principal. If set, the URI
// SAN or DNS SAN in that order is used from the certificate, otherwise the
// subject field is used. If unset, it applies to any user that is
// authenticated. authenticatedMatcher implements the matcher interface.
type authenticatedMatcher struct {
stringMatcher *internalmatcher.StringMatcher
}
func newAuthenticatedMatcher(authenticatedMatcherConfig *v3rbacpb.Principal_Authenticated) (*authenticatedMatcher, error) {
// Represents this line in the RBAC documentation = "If unset, it applies to
// any user that is authenticated" (see package-level comments).
if authenticatedMatcherConfig.PrincipalName == nil {
return &authenticatedMatcher{}, nil
}
stringMatcher, err := internalmatcher.StringMatcherFromProto(authenticatedMatcherConfig.PrincipalName)
if err != nil {
return nil, err
}
return &authenticatedMatcher{stringMatcher: &stringMatcher}, nil
}
func (am *authenticatedMatcher) match(data *rpcData) bool {
if data.authType != "tls" {
// Connection is not authenticated.
return false
}
if am.stringMatcher == nil {
// Allows any authenticated user.
return true
}
// "If there is no client certificate (thus no SAN nor Subject), check if ""
// (empty string) matches. If it matches, the principal_name is said to
// match" - A41
if len(data.certs) == 0 {
return am.stringMatcher.Match("")
}
cert := data.certs[0]
// Use the first non-empty identity source in priority order:
// URI SANs, then DNS SANs, then Subject.
if len(cert.URIs) > 0 {
for _, uriSAN := range cert.URIs {
if am.stringMatcher.Match(uriSAN.String()) {
return true
}
}
return false
}
if len(cert.DNSNames) > 0 {
for _, dnsSAN := range cert.DNSNames {
if am.stringMatcher.Match(dnsSAN) {
return true
}
}
return false
}
return am.stringMatcher.Match(cert.Subject.String())
}

View File

@@ -0,0 +1,319 @@
/*
* 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 rbac provides service-level and method-level access control for a
// service. See
// https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/rbac/v3/rbac.proto#role-based-access-control-rbac
// for documentation.
package rbac
import (
"context"
"crypto/x509"
"errors"
"fmt"
"net"
"strconv"
"google.golang.org/grpc"
"google.golang.org/grpc/authz/audit"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/internal/transport"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
)
var logger = grpclog.Component("rbac")
var getConnection = transport.GetConnection
// ChainEngine represents a chain of RBAC Engines, used to make authorization
// decisions on incoming RPCs.
type ChainEngine struct {
chainedEngines []*engine
}
// NewChainEngine returns a chain of RBAC engines, used to make authorization
// decisions on incoming RPCs. Returns a non-nil error for invalid policies.
func NewChainEngine(policies []*v3rbacpb.RBAC, policyName string) (*ChainEngine, error) {
engines := make([]*engine, 0, len(policies))
for _, policy := range policies {
engine, err := newEngine(policy, policyName)
if err != nil {
return nil, err
}
engines = append(engines, engine)
}
return &ChainEngine{chainedEngines: engines}, nil
}
func (cre *ChainEngine) logRequestDetails(rpcData *rpcData) {
if logger.V(2) {
logger.Infof("checking request: url path=%s", rpcData.fullMethod)
if len(rpcData.certs) > 0 {
cert := rpcData.certs[0]
logger.Infof("uri sans=%q, dns sans=%q, subject=%v", cert.URIs, cert.DNSNames, cert.Subject)
}
}
}
// IsAuthorized determines if an incoming RPC is authorized based on the chain of RBAC
// engines and their associated actions.
//
// Errors returned by this function are compatible with the status package.
func (cre *ChainEngine) IsAuthorized(ctx context.Context) error {
// This conversion step (i.e. pulling things out of ctx) can be done once,
// and then be used for the whole chain of RBAC Engines.
rpcData, err := newRPCData(ctx)
if err != nil {
logger.Errorf("newRPCData: %v", err)
return status.Errorf(codes.Internal, "gRPC RBAC: %v", err)
}
for _, engine := range cre.chainedEngines {
matchingPolicyName, ok := engine.findMatchingPolicy(rpcData)
if logger.V(2) && ok {
logger.Infof("incoming RPC matched to policy %v in engine with action %v", matchingPolicyName, engine.action)
}
switch {
case engine.action == v3rbacpb.RBAC_ALLOW && !ok:
cre.logRequestDetails(rpcData)
engine.doAuditLogging(rpcData, matchingPolicyName, false)
return status.Errorf(codes.PermissionDenied, "incoming RPC did not match an allow policy")
case engine.action == v3rbacpb.RBAC_DENY && ok:
cre.logRequestDetails(rpcData)
engine.doAuditLogging(rpcData, matchingPolicyName, false)
return status.Errorf(codes.PermissionDenied, "incoming RPC matched a deny policy %q", matchingPolicyName)
}
// Every policy in the engine list must be queried. Thus, iterate to the
// next policy.
engine.doAuditLogging(rpcData, matchingPolicyName, true)
}
// If the incoming RPC gets through all of the engines successfully (i.e.
// doesn't not match an allow or match a deny engine), the RPC is authorized
// to proceed.
return nil
}
// engine is used for matching incoming RPCs to policies.
type engine struct {
// TODO(gtcooke94) - differentiate between `policyName`, `policies`, and `rules`
policyName string
policies map[string]*policyMatcher
// action must be ALLOW or DENY.
action v3rbacpb.RBAC_Action
auditLoggers []audit.Logger
auditCondition v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition
}
// newEngine creates an RBAC Engine based on the contents of a policy. Returns a
// non-nil error if the policy is invalid.
func newEngine(config *v3rbacpb.RBAC, policyName string) (*engine, error) {
a := config.GetAction()
if a != v3rbacpb.RBAC_ALLOW && a != v3rbacpb.RBAC_DENY {
return nil, fmt.Errorf("unsupported action %s", config.Action)
}
policies := make(map[string]*policyMatcher, len(config.GetPolicies()))
for name, policy := range config.GetPolicies() {
matcher, err := newPolicyMatcher(policy)
if err != nil {
return nil, err
}
policies[name] = matcher
}
auditLoggers, auditCondition, err := parseAuditOptions(config.GetAuditLoggingOptions())
if err != nil {
return nil, err
}
return &engine{
policyName: policyName,
policies: policies,
action: a,
auditLoggers: auditLoggers,
auditCondition: auditCondition,
}, nil
}
func parseAuditOptions(opts *v3rbacpb.RBAC_AuditLoggingOptions) ([]audit.Logger, v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition, error) {
if opts == nil {
return nil, v3rbacpb.RBAC_AuditLoggingOptions_NONE, nil
}
var auditLoggers []audit.Logger
for _, logger := range opts.LoggerConfigs {
auditLogger, err := buildLogger(logger)
if err != nil {
return nil, v3rbacpb.RBAC_AuditLoggingOptions_NONE, err
}
if auditLogger == nil {
// This occurs when the audit logger is not registered but also
// marked optional.
continue
}
auditLoggers = append(auditLoggers, auditLogger)
}
return auditLoggers, opts.GetAuditCondition(), nil
}
// findMatchingPolicy determines if an incoming RPC matches a policy. On a
// successful match, it returns the name of the matching policy and a true bool
// to specify that there was a matching policy found. It returns false in
// the case of not finding a matching policy.
func (e *engine) findMatchingPolicy(rpcData *rpcData) (string, bool) {
for policy, matcher := range e.policies {
if matcher.match(rpcData) {
return policy, true
}
}
return "", false
}
// newRPCData takes an incoming context (should be a context representing state
// needed for server RPC Call with metadata, peer info (used for source ip/port
// and TLS information) and connection (used for destination ip/port) piped into
// it) and the method name of the Service being called server side and populates
// an rpcData struct ready to be passed to the RBAC Engine to find a matching
// policy.
func newRPCData(ctx context.Context) (*rpcData, error) {
// The caller should populate all of these fields (i.e. for empty headers,
// pipe an empty md into context).
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errors.New("missing metadata in incoming context")
}
// ":method can be hard-coded to POST if unavailable" - A41
md[":method"] = []string{"POST"}
// "If the transport exposes TE in Metadata, then RBAC must special-case the
// header to treat it as not present." - A41
delete(md, "TE")
pi, ok := peer.FromContext(ctx)
if !ok {
return nil, errors.New("missing peer info in incoming context")
}
// The methodName will be available in the passed in ctx from a unary or streaming
// interceptor, as grpc.Server pipes in a transport stream which contains the methodName
// into contexts available in both unary or streaming interceptors.
mn, ok := grpc.Method(ctx)
if !ok {
return nil, errors.New("missing method in incoming context")
}
// gRPC-Go strips :path from the headers given to the application, but RBAC should be
// able to match against it.
md[":path"] = []string{mn}
// The connection is needed in order to find the destination address and
// port of the incoming RPC Call.
conn := getConnection(ctx)
if conn == nil {
return nil, errors.New("missing connection in incoming context")
}
_, dPort, err := net.SplitHostPort(conn.LocalAddr().String())
if err != nil {
return nil, fmt.Errorf("error parsing local address: %v", err)
}
dp, err := strconv.ParseUint(dPort, 10, 32)
if err != nil {
return nil, fmt.Errorf("error parsing local address: %v", err)
}
var authType string
var peerCertificates []*x509.Certificate
if tlsInfo, ok := pi.AuthInfo.(credentials.TLSInfo); ok {
authType = pi.AuthInfo.AuthType()
peerCertificates = tlsInfo.State.PeerCertificates
}
return &rpcData{
md: md,
peerInfo: pi,
fullMethod: mn,
destinationPort: uint32(dp),
localAddr: conn.LocalAddr(),
authType: authType,
certs: peerCertificates,
}, nil
}
// rpcData wraps data pulled from an incoming RPC that the RBAC engine needs to
// find a matching policy.
type rpcData struct {
// md is the HTTP Headers that are present in the incoming RPC.
md metadata.MD
// peerInfo is information about the downstream peer.
peerInfo *peer.Peer
// fullMethod is the method name being called on the upstream service.
fullMethod string
// destinationPort is the port that the RPC is being sent to on the
// server.
destinationPort uint32
// localAddr is the address that the RPC is being sent to.
localAddr net.Addr
// authType is the type of authentication e.g. "tls".
authType string
// certs are the certificates presented by the peer during a TLS
// handshake.
certs []*x509.Certificate
}
func (e *engine) doAuditLogging(rpcData *rpcData, rule string, authorized bool) {
// In the RBAC world, we need to have a SPIFFE ID as the principal for this
// to be meaningful
principal := ""
if rpcData.peerInfo != nil {
// If AuthType = tls, then we can cast AuthInfo to TLSInfo.
if tlsInfo, ok := rpcData.peerInfo.AuthInfo.(credentials.TLSInfo); ok {
if tlsInfo.SPIFFEID != nil {
principal = tlsInfo.SPIFFEID.String()
}
}
}
// TODO(gtcooke94) check if we need to log before creating the event
event := &audit.Event{
FullMethodName: rpcData.fullMethod,
Principal: principal,
PolicyName: e.policyName,
MatchedRule: rule,
Authorized: authorized,
}
for _, logger := range e.auditLoggers {
switch e.auditCondition {
case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY:
if !authorized {
logger.Log(event)
}
case v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW:
if authorized {
logger.Log(event)
}
case v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY_AND_ALLOW:
logger.Log(event)
}
}
}
// 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/"

File diff suppressed because it is too large Load Diff