Skip to content

Commit

Permalink
authz: Rbac engine audit logging (#6225)
Browse files Browse the repository at this point in the history
add the functionality to actually do audit logging in rbac_engine.go and associated tests for that functionality.
  • Loading branch information
gtcooke94 committed May 17, 2023
1 parent 52fef6d commit 390c392
Show file tree
Hide file tree
Showing 8 changed files with 1,407 additions and 186 deletions.
4 changes: 2 additions & 2 deletions authz/grpc_authz_server_interceptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ type StaticInterceptor struct {
// NewStatic returns a new StaticInterceptor from a static authorization policy
// JSON string.
func NewStatic(authzPolicy string) (*StaticInterceptor, error) {
rbacs, err := translatePolicy(authzPolicy)
rbacs, policyName, err := translatePolicy(authzPolicy)
if err != nil {
return nil, err
}
chainEngine, err := rbac.NewChainEngine(rbacs)
chainEngine, err := rbac.NewChainEngine(rbacs, policyName)
if err != nil {
return nil, err
}
Expand Down
43 changes: 21 additions & 22 deletions authz/rbac_translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import (

// 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 typedURLPrefix = "grpc.authz.audit_logging/"
const typeURLPrefix = "grpc.authz.audit_logging/"

type header struct {
Key string
Expand All @@ -62,14 +62,14 @@ type rule struct {
}

type auditLogger struct {
Name string `json:"name"`
Config *structpb.Struct `json:"config"`
IsOptional bool `json:"is_optional"`
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"`
AuditCondition string `json:"audit_condition"`
AuditLoggers []*auditLogger `json:"audit_loggers"`
}

// Represents the SDK authorization policy provided by user.
Expand Down Expand Up @@ -302,14 +302,13 @@ func (options *auditLoggingOptions) toProtos() (allow *v3rbacpb.RBAC_AuditLoggin
deny.AuditCondition = toDenyCondition(v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition(rbacCondition))
}

for i := range options.AuditLoggers {
config := &options.AuditLoggers[i]
if config.Config == nil {
return nil, nil, fmt.Errorf("AuditLogger Config field cannot be nil")
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)
}
typedStruct := &v1xdsudpatypepb.TypedStruct{
TypeUrl: typedURLPrefix + config.Name,
Value: config.Config,
TypeUrl: typeURLPrefix + config.Name,
Value: &config.Config,
}
customConfig, err := anypb.New(typedStruct)
if err != nil {
Expand Down Expand Up @@ -355,30 +354,30 @@ func toDenyCondition(condition v3rbacpb.RBAC_AuditLoggingOptions_AuditCondition)

// 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. If the input policy cannot be parsed or is invalid, an error
// will be returned.
func translatePolicy(policyStr string) ([]*v3rbacpb.RBAC, error) {
// 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)
return nil, "", fmt.Errorf("failed to unmarshal policy: %v", err)
}
if policy.Name == "" {
return nil, fmt.Errorf(`"name" is not present`)
return nil, "", fmt.Errorf(`"name" is not present`)
}
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()
if err != nil {
return nil, err
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)
return nil, "", fmt.Errorf(`"deny_rules" %v`, err)
}
denyRBAC := &v3rbacpb.RBAC{
Action: v3rbacpb.RBAC_DENY,
Expand All @@ -389,8 +388,8 @@ func translatePolicy(policyStr string) ([]*v3rbacpb.RBAC, error) {
}
allowPolicies, err := parseRules(policy.AllowRules, policy.Name)
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}
return append(rbacs, allowRBAC), nil
return append(rbacs, allowRBAC), policy.Name, nil
}
128 changes: 110 additions & 18 deletions authz/rbac_translator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ import (

func TestTranslatePolicy(t *testing.T) {
tests := map[string]struct {
authzPolicy string
wantErr string
wantPolicies []*v3rbacpb.RBAC
authzPolicy string
wantErr string
wantPolicies []*v3rbacpb.RBAC
wantPolicyName string
}{
"valid policy": {
authzPolicy: `{
Expand Down Expand Up @@ -210,6 +211,7 @@ func TestTranslatePolicy(t *testing.T) {
AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{},
},
},
wantPolicyName: "authz",
},
"allow authenticated": {
authzPolicy: `{
Expand Down Expand Up @@ -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": {
authzPolicy: `{"random": 123}`,
wantErr: "failed to unmarshal policy",
Expand Down Expand Up @@ -897,7 +994,7 @@ func TestTranslatePolicy(t *testing.T) {
}`,
wantErr: `failed to unmarshal policy`,
},
"missing custom config audit logger": {
"missing audit logger name": {
authzPolicy: `{
"name": "authz",
"allow_rules": [
Expand All @@ -907,37 +1004,32 @@ func TestTranslatePolicy(t *testing.T) {
"principals":["*", ""]
}
}],
"deny_rules": [
{
"name": "deny_policy_1",
"source": {
"principals":[
"spiffe://foo.abc"
]
}
}],
"audit_logging_options": {
"audit_condition": "ON_DENY",
"audit_condition": "NONE",
"audit_loggers": [
{
"name": "stdout_logger",
"name": "",
"config": {},
"is_optional": false
}
]
}
}`,
wantErr: "AuditLogger Config field cannot be nil",
wantErr: `missing required field: name`,
},
}
for name, test := range tests {
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) {
t.Fatalf("unexpected error\nwant:%v\ngot:%v", test.wantErr, gotErr)
}
if diff := cmp.Diff(gotPolicies, test.wantPolicies, protocmp.Transform()); 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)
}
})
}
}
Expand All @@ -946,7 +1038,7 @@ func anyPbHelper(t *testing.T, in map[string]interface{}, name string) *anypb.An
t.Helper()
pb, err := structpb.NewStruct(in)
typedStruct := &v1xdsudpatypepb.TypedStruct{
TypeUrl: typedURLPrefix + name,
TypeUrl: typeURLPrefix + name,
Value: pb,
}
if err != nil {
Expand Down
98 changes: 98 additions & 0 deletions internal/xds/rbac/converter.go
Original file line number Diff line number Diff line change
@@ -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 comments on commit 390c392

Please sign in to comment.