mirror of https://github.com/grpc/grpc-go.git
authz: Rbac engine audit logging (#6225)
add the functionality to actually do audit logging in rbac_engine.go and associated tests for that functionality.
This commit is contained in:
parent
52fef6da12
commit
390c392f84
|
|
@ -44,11 +44,11 @@ type StaticInterceptor struct {
|
||||||
// NewStatic returns a new StaticInterceptor from a static authorization policy
|
// NewStatic returns a new StaticInterceptor from a static authorization policy
|
||||||
// JSON string.
|
// JSON string.
|
||||||
func NewStatic(authzPolicy string) (*StaticInterceptor, error) {
|
func NewStatic(authzPolicy string) (*StaticInterceptor, error) {
|
||||||
rbacs, err := translatePolicy(authzPolicy)
|
rbacs, policyName, err := translatePolicy(authzPolicy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
chainEngine, err := rbac.NewChainEngine(rbacs)
|
chainEngine, err := rbac.NewChainEngine(rbacs, policyName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ import (
|
||||||
|
|
||||||
// This is used when converting a custom config from raw JSON to a TypedStruct
|
// 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>"
|
// The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/<name>"
|
||||||
const typedURLPrefix = "grpc.authz.audit_logging/"
|
const typeURLPrefix = "grpc.authz.audit_logging/"
|
||||||
|
|
||||||
type header struct {
|
type header struct {
|
||||||
Key string
|
Key string
|
||||||
|
|
@ -63,13 +63,13 @@ type rule struct {
|
||||||
|
|
||||||
type auditLogger struct {
|
type auditLogger struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Config *structpb.Struct `json:"config"`
|
Config structpb.Struct `json:"config"`
|
||||||
IsOptional bool `json:"is_optional"`
|
IsOptional bool `json:"is_optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type auditLoggingOptions struct {
|
type auditLoggingOptions struct {
|
||||||
AuditCondition string `json:"audit_condition"`
|
AuditCondition string `json:"audit_condition"`
|
||||||
AuditLoggers []auditLogger `json:"audit_loggers"`
|
AuditLoggers []*auditLogger `json:"audit_loggers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Represents the SDK authorization policy provided by user.
|
// Represents the SDK authorization policy provided by user.
|
||||||
|
|
@ -302,14 +302,13 @@ func (options *auditLoggingOptions) toProtos() (allow *v3rbacpb.RBAC_AuditLoggin
|
||||||
deny.AuditCondition = toDenyCondition(v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition))
|
deny.AuditCondition = toDenyCondition(v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition))
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range options.AuditLoggers {
|
for i, config := range options.AuditLoggers {
|
||||||
config := &options.AuditLoggers[i]
|
if config.Name == "" {
|
||||||
if config.Config == nil {
|
return nil, nil, fmt.Errorf("missing required field: name in audit_logging_options.audit_loggers[%v]", i)
|
||||||
return nil, nil, fmt.Errorf("AuditLogger Config field cannot be nil")
|
|
||||||
}
|
}
|
||||||
typedStruct := &v1xdsudpatypepb.TypedStruct{
|
typedStruct := &v1xdsudpatypepb.TypedStruct{
|
||||||
TypeUrl: typedURLPrefix + config.Name,
|
TypeUrl: typeURLPrefix + config.Name,
|
||||||
Value: config.Config,
|
Value: &config.Config,
|
||||||
}
|
}
|
||||||
customConfig, err := anypb.New(typedStruct)
|
customConfig, err := anypb.New(typedStruct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -355,30 +354,30 @@ func toDenyCondition(condition v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition)
|
||||||
|
|
||||||
// translatePolicy translates SDK authorization policy in JSON format to two
|
// translatePolicy translates SDK authorization policy in JSON format to two
|
||||||
// Envoy RBAC polices (deny followed by allow policy) or only one Envoy RBAC
|
// Envoy RBAC polices (deny followed by allow policy) or only one Envoy RBAC
|
||||||
// allow policy. If the input policy cannot be parsed or is invalid, an error
|
// allow policy. Also returns the overall policy name. If the input policy
|
||||||
// will be returned.
|
// cannot be parsed or is invalid, an error will be returned.
|
||||||
func translatePolicy(policyStr string) ([]*v3rbacpb.RBAC, error) {
|
func translatePolicy(policyStr string) ([]*v3rbacpb.RBAC, string, error) {
|
||||||
policy := &authorizationPolicy{}
|
policy := &authorizationPolicy{}
|
||||||
d := json.NewDecoder(bytes.NewReader([]byte(policyStr)))
|
d := json.NewDecoder(bytes.NewReader([]byte(policyStr)))
|
||||||
d.DisallowUnknownFields()
|
d.DisallowUnknownFields()
|
||||||
if err := d.Decode(policy); err != nil {
|
if err := d.Decode(policy); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal policy: %v", err)
|
return nil, "", fmt.Errorf("failed to unmarshal policy: %v", err)
|
||||||
}
|
}
|
||||||
if policy.Name == "" {
|
if policy.Name == "" {
|
||||||
return nil, fmt.Errorf(`"name" is not present`)
|
return nil, "", fmt.Errorf(`"name" is not present`)
|
||||||
}
|
}
|
||||||
if len(policy.AllowRules) == 0 {
|
if len(policy.AllowRules) == 0 {
|
||||||
return nil, fmt.Errorf(`"allow_rules" is not present`)
|
return nil, "", fmt.Errorf(`"allow_rules" is not present`)
|
||||||
}
|
}
|
||||||
allowLogger, denyLogger, err := policy.AuditLoggingOptions.toProtos()
|
allowLogger, denyLogger, err := policy.AuditLoggingOptions.toProtos()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
rbacs := make([]*v3rbacpb.RBAC, 0, 2)
|
rbacs := make([]*v3rbacpb.RBAC, 0, 2)
|
||||||
if len(policy.DenyRules) > 0 {
|
if len(policy.DenyRules) > 0 {
|
||||||
denyPolicies, err := parseRules(policy.DenyRules, policy.Name)
|
denyPolicies, err := parseRules(policy.DenyRules, policy.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`"deny_rules" %v`, err)
|
return nil, "", fmt.Errorf(`"deny_rules" %v`, err)
|
||||||
}
|
}
|
||||||
denyRBAC := &v3rbacpb.RBAC{
|
denyRBAC := &v3rbacpb.RBAC{
|
||||||
Action: v3rbacpb.RBAC_DENY,
|
Action: v3rbacpb.RBAC_DENY,
|
||||||
|
|
@ -389,8 +388,8 @@ func translatePolicy(policyStr string) ([]*v3rbacpb.RBAC, error) {
|
||||||
}
|
}
|
||||||
allowPolicies, err := parseRules(policy.AllowRules, policy.Name)
|
allowPolicies, err := parseRules(policy.AllowRules, policy.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`"allow_rules" %v`, err)
|
return nil, "", fmt.Errorf(`"allow_rules" %v`, err)
|
||||||
}
|
}
|
||||||
allowRBAC := &v3rbacpb.RBAC{Action: v3rbacpb.RBAC_ALLOW, Policies: allowPolicies, AuditLoggingOptions: allowLogger}
|
allowRBAC := &v3rbacpb.RBAC{Action: v3rbacpb.RBAC_ALLOW, Policies: allowPolicies, AuditLoggingOptions: allowLogger}
|
||||||
return append(rbacs, allowRBAC), nil
|
return append(rbacs, allowRBAC), policy.Name, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ func TestTranslatePolicy(t *testing.T) {
|
||||||
authzPolicy string
|
authzPolicy string
|
||||||
wantErr string
|
wantErr string
|
||||||
wantPolicies []*v3rbacpb.RBAC
|
wantPolicies []*v3rbacpb.RBAC
|
||||||
|
wantPolicyName string
|
||||||
}{
|
}{
|
||||||
"valid policy": {
|
"valid policy": {
|
||||||
authzPolicy: `{
|
authzPolicy: `{
|
||||||
|
|
@ -210,6 +211,7 @@ func TestTranslatePolicy(t *testing.T) {
|
||||||
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{},
|
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
wantPolicyName: "authz",
|
||||||
},
|
},
|
||||||
"allow authenticated": {
|
"allow authenticated": {
|
||||||
authzPolicy: `{
|
authzPolicy: `{
|
||||||
|
|
@ -798,6 +800,101 @@ func TestTranslatePolicy(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"missing custom config audit logger": {
|
||||||
|
authzPolicy: `{
|
||||||
|
"name": "authz",
|
||||||
|
"allow_rules": [
|
||||||
|
{
|
||||||
|
"name": "allow_authenticated",
|
||||||
|
"source": {
|
||||||
|
"principals":["*", ""]
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"deny_rules": [
|
||||||
|
{
|
||||||
|
"name": "deny_policy_1",
|
||||||
|
"source": {
|
||||||
|
"principals":[
|
||||||
|
"spiffe://foo.abc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"audit_logging_options": {
|
||||||
|
"audit_condition": "ON_DENY",
|
||||||
|
"audit_loggers": [
|
||||||
|
{
|
||||||
|
"name": "stdout_logger",
|
||||||
|
"is_optional": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
wantPolicies: []*v3rbacpb.RBAC{
|
||||||
|
{
|
||||||
|
Action: v3rbacpb.RBAC_DENY,
|
||||||
|
Policies: map[string]*v3rbacpb.Policy{
|
||||||
|
"authz_deny_policy_1": {
|
||||||
|
Principals: []*v3rbacpb.Principal{
|
||||||
|
{Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{
|
||||||
|
Ids: []*v3rbacpb.Principal{
|
||||||
|
{Identifier: &v3rbacpb.Principal_Authenticated_{
|
||||||
|
Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{
|
||||||
|
MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "spiffe://foo.abc"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
Permissions: []*v3rbacpb.Permission{
|
||||||
|
{Rule: &v3rbacpb.Permission_Any{Any: true}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
|
||||||
|
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY,
|
||||||
|
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
|
||||||
|
{AuditLogger: &v3corepb.TypedExtensionConfig{Name: "stdout_logger", TypedConfig: anyPbHelper(t, map[string]interface{}{}, "stdout_logger")},
|
||||||
|
IsOptional: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: v3rbacpb.RBAC_ALLOW,
|
||||||
|
Policies: map[string]*v3rbacpb.Policy{
|
||||||
|
"authz_allow_authenticated": {
|
||||||
|
Principals: []*v3rbacpb.Principal{
|
||||||
|
{Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{
|
||||||
|
Ids: []*v3rbacpb.Principal{
|
||||||
|
{Identifier: &v3rbacpb.Principal_Authenticated_{
|
||||||
|
Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{
|
||||||
|
MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{SafeRegex: &v3matcherpb.RegexMatcher{Regex: ".+"}},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
{Identifier: &v3rbacpb.Principal_Authenticated_{
|
||||||
|
Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{
|
||||||
|
MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: ""},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
Permissions: []*v3rbacpb.Permission{
|
||||||
|
{Rule: &v3rbacpb.Permission_Any{Any: true}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
|
||||||
|
AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_DENY,
|
||||||
|
LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
|
||||||
|
{AuditLogger: &v3corepb.TypedExtensionConfig{Name: "stdout_logger", TypedConfig: anyPbHelper(t, map[string]interface{}{}, "stdout_logger")},
|
||||||
|
IsOptional: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"unknown field": {
|
"unknown field": {
|
||||||
authzPolicy: `{"random": 123}`,
|
authzPolicy: `{"random": 123}`,
|
||||||
wantErr: "failed to unmarshal policy",
|
wantErr: "failed to unmarshal policy",
|
||||||
|
|
@ -897,7 +994,7 @@ func TestTranslatePolicy(t *testing.T) {
|
||||||
}`,
|
}`,
|
||||||
wantErr: `failed to unmarshal policy`,
|
wantErr: `failed to unmarshal policy`,
|
||||||
},
|
},
|
||||||
"missing custom config audit logger": {
|
"missing audit logger name": {
|
||||||
authzPolicy: `{
|
authzPolicy: `{
|
||||||
"name": "authz",
|
"name": "authz",
|
||||||
"allow_rules": [
|
"allow_rules": [
|
||||||
|
|
@ -907,37 +1004,32 @@ func TestTranslatePolicy(t *testing.T) {
|
||||||
"principals":["*", ""]
|
"principals":["*", ""]
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
"deny_rules": [
|
|
||||||
{
|
|
||||||
"name": "deny_policy_1",
|
|
||||||
"source": {
|
|
||||||
"principals":[
|
|
||||||
"spiffe://foo.abc"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"audit_logging_options": {
|
"audit_logging_options": {
|
||||||
"audit_condition": "ON_DENY",
|
"audit_condition": "NONE",
|
||||||
"audit_loggers": [
|
"audit_loggers": [
|
||||||
{
|
{
|
||||||
"name": "stdout_logger",
|
"name": "",
|
||||||
|
"config": {},
|
||||||
"is_optional": false
|
"is_optional": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
wantErr: "AuditLogger Config field cannot be nil",
|
wantErr: `missing required field: name`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for name, test := range tests {
|
for name, test := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
gotPolicies, gotErr := translatePolicy(test.authzPolicy)
|
gotPolicies, gotPolicyName, gotErr := translatePolicy(test.authzPolicy)
|
||||||
if gotErr != nil && !strings.HasPrefix(gotErr.Error(), test.wantErr) {
|
if gotErr != nil && !strings.HasPrefix(gotErr.Error(), test.wantErr) {
|
||||||
t.Fatalf("unexpected error\nwant:%v\ngot:%v", test.wantErr, gotErr)
|
t.Fatalf("unexpected error\nwant:%v\ngot:%v", test.wantErr, gotErr)
|
||||||
}
|
}
|
||||||
if diff := cmp.Diff(gotPolicies, test.wantPolicies, protocmp.Transform()); diff != "" {
|
if diff := cmp.Diff(gotPolicies, test.wantPolicies, protocmp.Transform()); diff != "" {
|
||||||
t.Fatalf("unexpected policy\ndiff (-want +got):\n%s", diff)
|
t.Fatalf("unexpected policy\ndiff (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
|
if test.wantPolicyName != "" && gotPolicyName != test.wantPolicyName {
|
||||||
|
t.Fatalf("unexpected policy name\nwant:%v\ngot:%v", test.wantPolicyName, gotPolicyName)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -946,7 +1038,7 @@ func anyPbHelper(t *testing.T, in map[string]interface{}, name string) *anypb.An
|
||||||
t.Helper()
|
t.Helper()
|
||||||
pb, err := structpb.NewStruct(in)
|
pb, err := structpb.NewStruct(in)
|
||||||
typedStruct := &v1xdsudpatypepb.TypedStruct{
|
typedStruct := &v1xdsudpatypepb.TypedStruct{
|
||||||
TypeUrl: typedURLPrefix + name,
|
TypeUrl: typeURLPrefix + name,
|
||||||
Value: pb,
|
Value: pb,
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
"google.golang.org/grpc/authz/audit"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const udpaTypedStuctType = "type.googleapis.com/udpa.type.v1.TypedStruct"
|
||||||
|
const xdsTypedStuctType = "type.googleapis.com/xds.type.v3.TypedStruct"
|
||||||
|
|
||||||
|
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) {
|
||||||
|
switch config.GetTypeUrl() {
|
||||||
|
case udpaTypedStuctType:
|
||||||
|
typedStruct := &v1xdsudpatypepb.TypedStruct{}
|
||||||
|
if err := config.UnmarshalTo(typedStruct); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to unmarshal resource: %v", err)
|
||||||
|
}
|
||||||
|
return convertCustomConfig(typedStruct.TypeUrl, typedStruct.Value)
|
||||||
|
case xdsTypedStuctType:
|
||||||
|
typedStruct := &v3xdsxdstypepb.TypedStruct{}
|
||||||
|
if err := config.UnmarshalTo(typedStruct); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to unmarshal resource: %v", err)
|
||||||
|
}
|
||||||
|
return convertCustomConfig(typedStruct.TypeUrl, typedStruct.Value)
|
||||||
|
}
|
||||||
|
return nil, "", fmt.Errorf("custom config not implemented for type [%v]", config.GetTypeUrl())
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
|
v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
|
||||||
|
"google.golang.org/grpc/authz/audit"
|
||||||
|
"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: &anypb.Any{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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]interface{}{}, ""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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]interface{}{}, "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]interface{}{"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]interface{}{}, "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)
|
||||||
|
} else {
|
||||||
|
if logger != test.expectedLogger {
|
||||||
|
t.Fatalf("expected logger: %v. got logger: %v", test.expectedLogger, logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ import (
|
||||||
|
|
||||||
v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
|
v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/authz/audit"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/grpclog"
|
"google.golang.org/grpc/grpclog"
|
||||||
|
|
@ -51,10 +52,10 @@ type ChainEngine struct {
|
||||||
|
|
||||||
// NewChainEngine returns a chain of RBAC engines, used to make authorization
|
// NewChainEngine returns a chain of RBAC engines, used to make authorization
|
||||||
// decisions on incoming RPCs. Returns a non-nil error for invalid policies.
|
// decisions on incoming RPCs. Returns a non-nil error for invalid policies.
|
||||||
func NewChainEngine(policies []*v3rbacpb.RBAC) (*ChainEngine, error) {
|
func NewChainEngine(policies []*v3rbacpb.RBAC, policyName string) (*ChainEngine, error) {
|
||||||
engines := make([]*engine, 0, len(policies))
|
engines := make([]*engine, 0, len(policies))
|
||||||
for _, policy := range policies {
|
for _, policy := range policies {
|
||||||
engine, err := newEngine(policy)
|
engine, err := newEngine(policy, policyName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -94,13 +95,16 @@ func (cre *ChainEngine) IsAuthorized(ctx context.Context) error {
|
||||||
switch {
|
switch {
|
||||||
case engine.action == v3rbacpb.RBAC_ALLOW && !ok:
|
case engine.action == v3rbacpb.RBAC_ALLOW && !ok:
|
||||||
cre.logRequestDetails(rpcData)
|
cre.logRequestDetails(rpcData)
|
||||||
|
engine.doAuditLogging(rpcData, matchingPolicyName, false)
|
||||||
return status.Errorf(codes.PermissionDenied, "incoming RPC did not match an allow policy")
|
return status.Errorf(codes.PermissionDenied, "incoming RPC did not match an allow policy")
|
||||||
case engine.action == v3rbacpb.RBAC_DENY && ok:
|
case engine.action == v3rbacpb.RBAC_DENY && ok:
|
||||||
cre.logRequestDetails(rpcData)
|
cre.logRequestDetails(rpcData)
|
||||||
|
engine.doAuditLogging(rpcData, matchingPolicyName, false)
|
||||||
return status.Errorf(codes.PermissionDenied, "incoming RPC matched a deny policy %q", matchingPolicyName)
|
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
|
// Every policy in the engine list must be queried. Thus, iterate to the
|
||||||
// next policy.
|
// next policy.
|
||||||
|
engine.doAuditLogging(rpcData, matchingPolicyName, true)
|
||||||
}
|
}
|
||||||
// If the incoming RPC gets through all of the engines successfully (i.e.
|
// 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
|
// doesn't not match an allow or match a deny engine), the RPC is authorized
|
||||||
|
|
@ -110,14 +114,18 @@ func (cre *ChainEngine) IsAuthorized(ctx context.Context) error {
|
||||||
|
|
||||||
// engine is used for matching incoming RPCs to policies.
|
// engine is used for matching incoming RPCs to policies.
|
||||||
type engine struct {
|
type engine struct {
|
||||||
|
// TODO(gtcooke94) - differentiate between `policyName`, `policies`, and `rules`
|
||||||
|
policyName string
|
||||||
policies map[string]*policyMatcher
|
policies map[string]*policyMatcher
|
||||||
// action must be ALLOW or DENY.
|
// action must be ALLOW or DENY.
|
||||||
action v3rbacpb.RBAC_Action
|
action v3rbacpb.RBAC_Action
|
||||||
|
auditLoggers []audit.Logger
|
||||||
|
auditCondition v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition
|
||||||
}
|
}
|
||||||
|
|
||||||
// newEngine creates an RBAC Engine based on the contents of policy. Returns a
|
// newEngine creates an RBAC Engine based on the contents of a policy. Returns a
|
||||||
// non-nil error if the policy is invalid.
|
// non-nil error if the policy is invalid.
|
||||||
func newEngine(config *v3rbacpb.RBAC) (*engine, error) {
|
func newEngine(config *v3rbacpb.RBAC, policyName string) (*engine, error) {
|
||||||
a := config.GetAction()
|
a := config.GetAction()
|
||||||
if a != v3rbacpb.RBAC_ALLOW && a != v3rbacpb.RBAC_DENY {
|
if a != v3rbacpb.RBAC_ALLOW && a != v3rbacpb.RBAC_DENY {
|
||||||
return nil, fmt.Errorf("unsupported action %s", config.Action)
|
return nil, fmt.Errorf("unsupported action %s", config.Action)
|
||||||
|
|
@ -131,18 +139,47 @@ func newEngine(config *v3rbacpb.RBAC) (*engine, error) {
|
||||||
}
|
}
|
||||||
policies[name] = matcher
|
policies[name] = matcher
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auditLoggers, auditCondition, err := parseAuditOptions(config.GetAuditLoggingOptions())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &engine{
|
return &engine{
|
||||||
|
policyName: policyName,
|
||||||
policies: policies,
|
policies: policies,
|
||||||
action: a,
|
action: a,
|
||||||
|
auditLoggers: auditLoggers,
|
||||||
|
auditCondition: auditCondition,
|
||||||
}, nil
|
}, 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
|
// 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
|
// 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
|
// to specify that there was a matching policy found. It returns false in
|
||||||
// the case of not finding a matching policy.
|
// the case of not finding a matching policy.
|
||||||
func (r *engine) findMatchingPolicy(rpcData *rpcData) (string, bool) {
|
func (e *engine) findMatchingPolicy(rpcData *rpcData) (string, bool) {
|
||||||
for policy, matcher := range r.policies {
|
for policy, matcher := range e.policies {
|
||||||
if matcher.match(rpcData) {
|
if matcher.match(rpcData) {
|
||||||
return policy, true
|
return policy, true
|
||||||
}
|
}
|
||||||
|
|
@ -238,3 +275,43 @@ type rpcData struct {
|
||||||
// handshake.
|
// handshake.
|
||||||
certs []*x509.Certificate
|
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 && rpcData.peerInfo.AuthInfo != nil && rpcData.peerInfo.AuthInfo.AuthType() == "tls" {
|
||||||
|
// If AuthType = tls, then we can cast AuthInfo to TLSInfo.
|
||||||
|
tlsInfo := rpcData.peerInfo.AuthInfo.(credentials.TLSInfo)
|
||||||
|
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
|
|
@ -126,7 +126,10 @@ func parseConfig(rbacCfg *rpb.RBAC) (httpfilter.FilterConfig, error) {
|
||||||
return config{}, nil
|
return config{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ce, err := rbac.NewChainEngine([]*v3rbacpb.RBAC{rbacCfg.GetRules()})
|
// TODO(gregorycooke) - change the call chain to here so we have the filter
|
||||||
|
// name to input here instead of an empty string. It will come from here:
|
||||||
|
// https://github.com/grpc/grpc-go/blob/eff0942e95d93112921414aee758e619ec86f26f/xds/internal/xdsclient/xdsresource/unmarshal_lds.go#L199
|
||||||
|
ce, err := rbac.NewChainEngine([]*v3rbacpb.RBAC{rbacCfg.GetRules()}, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// "At this time, if the RBAC.action is Action.LOG then the policy will be
|
// "At this time, if the RBAC.action is Action.LOG then the policy will be
|
||||||
// completely ignored, as if RBAC was not configurated." - A41
|
// completely ignored, as if RBAC was not configurated." - A41
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue