Skip to content

Commit

Permalink
CBG-4064: stamp effective user ID from header (#6984)
Browse files Browse the repository at this point in the history
* CBG-4064: stamp effective user ID from header

* test updates

* add test case for real user

* better veriable names
  • Loading branch information
gregns1 authored Jul 19, 2024
1 parent 677e5aa commit 09feb24
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 9 deletions.
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 @@ -59,6 +59,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 @@ -144,6 +150,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 @@ -222,6 +230,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 @@ -231,6 +246,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
53 changes: 53 additions & 0 deletions rest/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,3 +463,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

0 comments on commit 09feb24

Please sign in to comment.