Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CBG-4064: stamp effective user ID from header #6984

Merged
merged 4 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions base/logger_audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const (
AuditFieldCompactionReset = "reset"
AuditFieldPostUpgradePreview = "preview"
AuditFieldAuthMethod = "auth_method"
AuditEffectiveUserID = "effective_userid"
)

// expandFields populates data with information from the id, context and additionalData.
Expand Down Expand Up @@ -75,6 +76,14 @@ func expandFields(id AuditID, ctx context.Context, globalFields AuditFields, add
"user": userName,
}
}
effectiveDomain := logCtx.EffectiveDomain
effectiveUser := logCtx.EffectiveUserID
if effectiveDomain != "" || effectiveUser != "" {
fields[AuditEffectiveUserID] = map[string]any{
"domain": effectiveDomain,
"user": effectiveUser,
}
}
if logCtx.RequestHost != "" {
host, port, err := net.SplitHostPort(logCtx.RequestHost)
if err != nil {
Expand Down
20 changes: 20 additions & 0 deletions base/logging_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ type LogContext struct {

// implicitDefaultCollection is set to true when the context represents the default collection, but we want to omit that value from logging to prevent verbosity.
implicitDefaultCollection bool

// Effective user ID from HTTP header
EffectiveUserID string

// Domain defined in the HTTP request header
EffectiveDomain string
}

// DbLogConfig can be used to customise the logging for logs associated with this database.
Expand Down Expand Up @@ -135,6 +141,8 @@ func (lc *LogContext) getCopy() LogContext {
UserDomain: lc.UserDomain,
RequestHost: lc.RequestHost,
RequestRemoteAddr: lc.RequestRemoteAddr,
EffectiveUserID: lc.EffectiveUserID,
EffectiveDomain: lc.EffectiveDomain,
}
}

Expand Down Expand Up @@ -213,6 +221,13 @@ func AuditLogCtx(parent context.Context, additionalAuditFields map[string]any) c
return LogContextWith(parent, &newCtx)
}

func EffectiveUserIDLogCtx(parent context.Context, domain, userID string) context.Context {
newCtx := getLogCtx(parent)
newCtx.EffectiveDomain = domain
newCtx.EffectiveUserID = userID
return LogContextWith(parent, &newCtx)
}

// KeyspaceLogCtx extends the parent context with a fully qualified keyspace (bucket.scope.collection)
func KeyspaceLogCtx(parent context.Context, bucketName, scopeName, collectionName string) context.Context {
newCtx := getLogCtx(parent)
Expand All @@ -222,6 +237,11 @@ func KeyspaceLogCtx(parent context.Context, bucketName, scopeName, collectionNam
return LogContextWith(parent, &newCtx)
}

type EffectiveUserPair struct {
UserID string `json:"user"`
Domain string `json:"domain"`
}

type userIDDomain string

const (
Expand Down
54 changes: 54 additions & 0 deletions rest/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package rest

import (
"bytes"
"fmt"
"net/http"
"testing"

Expand Down Expand Up @@ -415,3 +416,56 @@ func TestRedactConfigAsStr(t *testing.T) {
})
}
}

func TestEffectiveUserID(t *testing.T) {
tempdir := t.TempDir()
base.ResetGlobalTestLogging(t)
base.InitializeMemoryLoggers()
const (
user = "user"
domain = "domain"
cnfDomain = "myDomain"
cnfUser = "bob"
realUser = "alice"
realDomain = "sgw"
)
reqHeaders := map[string]string{
"user_header": fmt.Sprintf(`{"%s": "%s", "%s":"%s"}`, domain, cnfDomain, user, cnfUser),
"Authorization": getBasicAuthHeader(realUser, RestTesterDefaultUserPassword),
}

rt := NewRestTester(t, &RestTesterConfig{
GuestEnabled: true,
PersistentConfig: true,
MutateStartupConfig: func(config *StartupConfig) {
config.Unsupported.EffectiveUserHeaderName = base.StringPtr("user_header")
config.Logging = base.LoggingConfig{
LogFilePath: tempdir,
Audit: &base.AuditLoggerConfig{
FileLoggerConfig: base.FileLoggerConfig{
Enabled: base.BoolPtr(true),
},
},
}
require.NoError(t, config.SetupAndValidateLogging(base.TestCtx(t)))
},
})
defer rt.Close()
RequireStatus(t, rt.CreateDatabase("db", rt.NewDbConfig()), http.StatusCreated)
rt.CreateUser(realUser, nil)

action := func(t testing.TB) {
RequireStatus(t, rt.SendRequestWithHeaders(http.MethodGet, "/{{.db}}/", "", reqHeaders), http.StatusOK)
}
output := base.AuditLogContents(t, action)
events := jsonLines(t, output)

for _, event := range events {
effective := event[base.AuditEffectiveUserID].(map[string]any)
assert.Equal(t, cnfDomain, effective[domain])
assert.Equal(t, cnfUser, effective[user])
realUserEvent := event[base.AuditFieldRealUserID].(map[string]any)
assert.Equal(t, realDomain, realUserEvent[domain])
assert.Equal(t, realUser, realUserEvent[user])
}
}
1 change: 1 addition & 0 deletions rest/config_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ func registerConfigFlags(config *StartupConfig, fs *flag.FlagSet) map[string]con
"unsupported.user_queries": {&config.Unsupported.UserQueries, fs.Bool("unsupported.user_queries", false, "Whether user-query APIs are enabled")},
"unsupported.audit_info_provider.global_info_env_var_name": {&config.Unsupported.AuditInfoProvider.GlobalInfoEnvVarName, fs.String("unsupported.audit_info_provider.global_info_env_var_name", "", "Environment variable name to get global audit event info from")},
"unsupported.audit_info_provider.request_info_header_name": {&config.Unsupported.AuditInfoProvider.RequestInfoHeaderName, fs.String("unsupported.audit_info_provider.request_info_header_name", "", "Header name to get request audit event info from")},
"unsupported.effective_user_header_name": {&config.Unsupported.EffectiveUserHeaderName, fs.String("unsupported.effective_user_header_name", "", "HTTP header name to get effective user id from")},

"database_credentials": {&config.DatabaseCredentials, fs.String("database_credentials", "null", "JSON-encoded per-database credentials, that can be used instead of the bootstrap ones. This will override bucket_credentials that target the bucket that the database is in.")},
"bucket_credentials": {&config.BucketCredentials, fs.String("bucket_credentials", "null", "JSON-encoded per-bucket credentials, that can be used instead of the bootstrap ones.")},
Expand Down
19 changes: 10 additions & 9 deletions rest/config_startup.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,16 @@ type ReplicatorConfig struct {
}

type UnsupportedConfig struct {
StatsLogFrequency *base.ConfigDuration `json:"stats_log_frequency,omitempty" help:"How often should stats be written to stats logs"`
UseStdlibJSON *bool `json:"use_stdlib_json,omitempty" help:"Bypass the jsoniter package and use Go's stdlib instead"`
Serverless ServerlessConfig `json:"serverless,omitempty"`
HTTP2 *HTTP2Config `json:"http2,omitempty"`
UserQueries *bool `json:"user_queries,omitempty" help:"Feature flag for user N1QL/JS queries"`
UseXattrConfig *bool `json:"use_xattr_config,omitempty" help:"Store database configurations in system xattrs"`
AllowDbConfigEnvVars *bool `json:"allow_dbconfig_env_vars,omitempty" help:"Can be set to false to skip environment variable expansion in database configs"`
DiagnosticInterface string `json:"diagnostic_interface,omitempty" help:"Network interface to bind diagnostic API to"`
AuditInfoProvider *AuditInfoProviderConfig `json:"audit_info_provider,omitempty" help:"Configuration for audit info provider"`
StatsLogFrequency *base.ConfigDuration `json:"stats_log_frequency,omitempty" help:"How often should stats be written to stats logs"`
UseStdlibJSON *bool `json:"use_stdlib_json,omitempty" help:"Bypass the jsoniter package and use Go's stdlib instead"`
Serverless ServerlessConfig `json:"serverless,omitempty"`
HTTP2 *HTTP2Config `json:"http2,omitempty"`
UserQueries *bool `json:"user_queries,omitempty" help:"Feature flag for user N1QL/JS queries"`
UseXattrConfig *bool `json:"use_xattr_config,omitempty" help:"Store database configurations in system xattrs"`
AllowDbConfigEnvVars *bool `json:"allow_dbconfig_env_vars,omitempty" help:"Can be set to false to skip environment variable expansion in database configs"`
DiagnosticInterface string `json:"diagnostic_interface,omitempty" help:"Network interface to bind diagnostic API to"`
EffectiveUserHeaderName *string `json:"effective_user_header_name,omitempty" help:"HTTP header name to get effective user id from"`
AuditInfoProvider *AuditInfoProviderConfig `json:"audit_info_provider,omitempty" help:"Configuration for audit info provider"`
}

type AuditInfoProviderConfig struct {
Expand Down
13 changes: 13 additions & 0 deletions rest/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,19 @@ func (h *handler) validateAndWriteHeaders(method handlerMethod, accessPermission
}
}

if h.server.Config.Unsupported.EffectiveUserHeaderName != nil {
effectiveUserFields := h.rq.Header.Get(*h.server.Config.Unsupported.EffectiveUserHeaderName)
if effectiveUserFields != "" {
var fields base.EffectiveUserPair
err := base.JSONUnmarshal([]byte(effectiveUserFields), &fields)
if err != nil {
base.WarnfCtx(h.ctx(), "Error unmarshalling effective user header fields: %v", err)
} else {
h.rqCtx = base.EffectiveUserIDLogCtx(h.rqCtx, fields.Domain, fields.UserID)
}
}
}

switch h.rq.Header.Get("Content-Encoding") {
case "":
h.requestBody = NewReaderCounter(h.rq.Body)
Expand Down
Loading