diff --git a/base/audit/auditd_descriptor_test.go b/base/audit/auditd_descriptor_test.go index 2a0cf5d9b8..dc7906e58b 100644 --- a/base/audit/auditd_descriptor_test.go +++ b/base/audit/auditd_descriptor_test.go @@ -14,9 +14,9 @@ import ( "github.com/stretchr/testify/require" ) -// TestGenerateAuditdModuleDescriptor outputs a generated auditd module descriptor for sgAuditEvents. +// TestGenerateAuditdModuleDescriptor outputs a generated auditd module descriptor for SGAuditEvents. func TestGenerateAuditdModuleDescriptor(t *testing.T) { - b, err := generateAuditdModuleDescriptor(sgAuditEvents) + b, err := generateAuditdModuleDescriptor(SGAuditEvents) require.NoError(t, err) t.Log(string(b)) } diff --git a/base/audit/auditd_types.go b/base/audit/auditd_types.go index 3addaec44d..c545227022 100644 --- a/base/audit/auditd_types.go +++ b/base/audit/auditd_types.go @@ -27,17 +27,17 @@ type auditdEventDescriptor struct { OptionalFields map[string]any `json:"optional_fields,omitempty"` } -// toAuditdEventDescriptor converts an eventDescriptor to an auditdEventDescriptor. +// toAuditdEventDescriptor converts an EventDescriptor to an auditdEventDescriptor. // These are _mostly_ the same, but each event holds its own ID in an array in the JSON format. -func toAuditdEventDescriptor(id ID, e eventDescriptor) auditdEventDescriptor { +func toAuditdEventDescriptor(id ID, e EventDescriptor) auditdEventDescriptor { return auditdEventDescriptor{ ID: id, - Name: e.name, - Description: e.description, - Enabled: e.enabledByDefault, - FilteringPermitted: e.filteringPermitted, - MandatoryFields: toAuditdFieldType(e.mandatoryFields), - OptionalFields: toAuditdFieldType(e.optionalFields), + Name: e.Name, + Description: e.Description, + Enabled: e.EnabledByDefault, + FilteringPermitted: e.FilteringPermitted, + MandatoryFields: toAuditdFieldType(e.MandatoryFields), + OptionalFields: toAuditdFieldType(e.OptionalFields), } } diff --git a/base/audit/events.go b/base/audit/events.go index 158ec97dc2..3f86cec5cd 100644 --- a/base/audit/events.go +++ b/base/audit/events.go @@ -15,21 +15,21 @@ const ( IDPlaceholder ID = 54000 ) -var sgAuditEvents = events{ +var SGAuditEvents = events{ IDPlaceholder: { - name: "Placeholder audit event", - description: "This is a placeholder.", - mandatoryFields: map[string]any{ + Name: "Placeholder audit event", + Description: "This is a placeholder.", + MandatoryFields: map[string]any{ "context": map[string]any{ "provider": "example provider", "username": "alice", }, }, - optionalFields: map[string]any{ + OptionalFields: map[string]any{ "operationID": 123, "isSomething": false, }, - filteringPermitted: false, - eventType: eventTypeAdmin, + FilteringPermitted: false, + EventType: eventTypeAdmin, }, } diff --git a/base/audit/events_test.go b/base/audit/events_test.go index 6540f7b671..0da7ae5fec 100644 --- a/base/audit/events_test.go +++ b/base/audit/events_test.go @@ -25,14 +25,14 @@ const ( func TestValidateAuditEvents(t *testing.T) { // Ensures that the above audit event IDs are within the allocated range and are valid. - require.NoError(t, validateAuditEvents(sgAuditEvents)) + require.NoError(t, validateAuditEvents(SGAuditEvents)) } func validateAuditEvents(e events) error { for id, descriptor := range e { if id < auditdSyncGatewayStartID || id > auditdSyncGatewayEndID { return fmt.Errorf("invalid audit event ID: %d %q (allowed range: %d-%d)", - id, descriptor.name, auditdSyncGatewayStartID, auditdSyncGatewayEndID) + id, descriptor.Name, auditdSyncGatewayStartID, auditdSyncGatewayEndID) } } return nil diff --git a/base/audit/types.go b/base/audit/types.go index bde97c4715..1223b5cb02 100644 --- a/base/audit/types.go +++ b/base/audit/types.go @@ -8,28 +8,35 @@ package audit +import "strconv" + // ID is a unique identifier for an audit event. type ID uint +// String implements Stringer +func (i ID) String() string { + return strconv.FormatUint(uint64(i), 10) +} + // events is a map of audit event IDs to event descriptors. -type events map[ID]eventDescriptor - -// eventDescriptor is an audit event. The fields closely (but not exactly) follows kv_engine's auditd descriptor implementation. -type eventDescriptor struct { - // name is a short textual name of the event - name string - // description is a longer name / description of the event - description string - // enabledByDefault indicates whether the event should be enabled by default - enabledByDefault bool - // filteringPermitted indicates whether the event can be filtered or not - filteringPermitted bool - // mandatoryFields describe field(s) required for a valid instance of the event - mandatoryFields map[string]any - // optionalFields describe optional field(s) valid in an instance of the event - optionalFields map[string]any - // eventType represents a type of event. Used only for documentation categorization. - eventType eventType +type events map[ID]EventDescriptor + +// EventDescriptor is an audit event. The fields closely (but not exactly) follows kv_engine's auditd descriptor implementation. +type EventDescriptor struct { + // Name is a short textual Name of the event + Name string + // Description is a longer Name / Description of the event + Description string + // EnabledByDefault indicates whether the event should be enabled by default + EnabledByDefault bool + // FilteringPermitted indicates whether the event can be filtered or not + FilteringPermitted bool + // MandatoryFields describe field(s) required for a valid instance of the event + MandatoryFields map[string]any + // OptionalFields describe optional field(s) valid in an instance of the event + OptionalFields map[string]any + // EventType represents a type of event. Used only for documentation categorization. + EventType eventType } const ( diff --git a/base/logging_config.go b/base/logging_config.go index 8b36c3334d..49c7de44c1 100644 --- a/base/logging_config.go +++ b/base/logging_config.go @@ -241,6 +241,12 @@ type LoggingConfig struct { Debug *FileLoggerConfig `json:"debug,omitempty"` Trace *FileLoggerConfig `json:"trace,omitempty"` Stats *FileLoggerConfig `json:"stats,omitempty"` + Audit *AuditLoggerConfig `json:"audit,omitempty"` +} + +type AuditLoggerConfig struct { + FileLoggerConfig + AuditLogFilePath *string `json:"audit_log_file_path,omitempty"` // If set, overrides the output path for the audit log files } func BuildLoggingConfigFromLoggers(redactionLevel RedactionLevel, LogFilePath string) *LoggingConfig { @@ -256,6 +262,8 @@ func BuildLoggingConfigFromLoggers(redactionLevel RedactionLevel, LogFilePath st config.Debug = debugLogger.getFileLoggerConfig() config.Trace = traceLogger.getFileLoggerConfig() config.Stats = statsLogger.getFileLoggerConfig() + // FIXME(bbrks): Once AuditLogger is implemented + config.Audit = &AuditLoggerConfig{} return &config } diff --git a/docs/api/admin.yaml b/docs/api/admin.yaml index 145b752805..c70e006d5b 100644 --- a/docs/api/admin.yaml +++ b/docs/api/admin.yaml @@ -96,6 +96,8 @@ paths: $ref: ./paths/admin/_post_upgrade.yaml '/{db}/_config': $ref: './paths/admin/db-_config.yaml' + '/{db}/_config/audit': + $ref: './paths/admin/db-_config-audit.yaml' '/{keyspace}/_config/sync': $ref: './paths/admin/keyspace-_config-sync.yaml' '/{keyspace}/_config/import_filter': diff --git a/docs/api/components/schemas.yaml b/docs/api/components/schemas.yaml index 1dc8e92051..16313b013f 100644 --- a/docs/api/components/schemas.yaml +++ b/docs/api/components/schemas.yaml @@ -1835,7 +1835,81 @@ Database: items: type: string example: ["CRUD", "HTTP", "Query"] + audit: + description: Audit logging configuration. + type: object + properties: + enabled: + description: Whether audit logging is enabled. + type: boolean + default: false + enabled_events: + description: List of enabled audit events + type: array + items: + type: number + example: [1234, 5678] + disabled_users: + description: List of users for which audit logging is disabled + type: array + items: + type: object + properties: + domain: + description: The domain of the user for which audit logging is disabled. + type: string + name: + description: The name of the user for which audit logging is disabled. + type: string + disabled_roles: + description: List of roles for which audit logging is disabled + type: array + items: + type: object + properties: + domain: + description: The domain of the role for which audit logging is disabled. + type: string + name: + description: The name of the role for which audit logging is disabled. + type: string title: Database-config +Database-audit: + title: Database-audit + description: A map of audit events and whether they are enabled or not. + type: object + additionalProperties: + x-additionalPropertiesName: audit_id + description: The audit event ID and whether it is enabled or not. + type: boolean +Database-audit-verbose: + title: Database-audit-verbose + description: A map of detailed audit events. + type: object + additionalProperties: + x-additionalPropertiesName: audit_id + description: The audit event ID and whether it is enabled or not. + $ref: '#/AuditEventVerbose' +AuditEventVerbose: + title: audit-event-verbose + description: Detailed information about an audit event. + type: object + properties: + name: + type: string + description: "The name of the audit event." + readOnly: true + description: + type: string + description: "The description of the audit event." + readOnly: true + enabled: + type: boolean + description: "Whether this audit event is currently enabled or not." + filterable: + type: boolean + description: "Whether this audit event can be disabled. Some audit events are always on." + readOnly: true Event-config: type: object properties: @@ -2162,6 +2236,8 @@ Startup-config: $ref: '#/File-logging-config' stats: $ref: '#/File-logging-config' + audit: + $ref: '#/File-logging-config' auth: type: object properties: @@ -2284,6 +2360,8 @@ Runtime-config: $ref: '#/File-logging-config' stats: $ref: '#/File-logging-config' + audit: + $ref: '#/Audit-logging-config' max_concurrent_replications: description: Maximum number of concurrent replication connections allowed. If set to 0 this limit will be ignored. type: integer @@ -2302,6 +2380,23 @@ File-logging-config: type: integer readOnly: true title: File-logging-config +Audit-logging-config: + type: object + properties: + enabled: + description: Toggle for this log output + type: boolean + rotation: + $ref: '#/Log-rotation-config-readonly' + audit_log_file_path: + description: The path to write audit log files to + type: string + readOnly: true + collation_buffer_size: + description: The size of the log collation buffer + type: integer + readOnly: true + title: File-logging-config Log-rotation-config-readonly: type: object properties: diff --git a/docs/api/paths/admin/db-_config-audit.yaml b/docs/api/paths/admin/db-_config-audit.yaml new file mode 100644 index 0000000000..a6b9dc28f2 --- /dev/null +++ b/docs/api/paths/admin/db-_config-audit.yaml @@ -0,0 +1,107 @@ +# Copyright 2022-Present Couchbase, Inc. +# +# Use of this software is governed by the Business Source License included +# in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +# in that file, in accordance with the Business Source License, use of this +# software will be governed by the Apache License, Version 2.0, included in +# the file licenses/APL2.txt. +get: + summary: Get database audit configuration + description: |- + Retrieve the audit configuration for the database specified. + + Required Sync Gateway RBAC roles: + + * TODO + parameters: + - name: verbose + in: query + description: Whether to show name and description with each audit event. + schema: + type: boolean + default: false + - name: filterable + in: query + description: Whether to show only filterable audit events. + schema: + type: boolean + default: false + responses: + '200': + description: Successfully retrieved database configuration + content: + application/json: + schema: + oneOf: + - $ref: ../../components/schemas.yaml#/Database-audit + - $ref: ../../components/schemas.yaml#/Database-audit-verbose + '404': + $ref: ../../components/responses.yaml#/Not-found + tags: + - Database Configuration + operationId: get_db-_config-audit +put: + summary: Replace database audit configuration + description: |- + Replaces the database audit configuration with the one sent in the request. + + Unspecified audit events will be reset to their default enabled value. Use POST if you want upsert-style semantics. + + Required Sync Gateway RBAC roles: + + * TODO + requestBody: + description: The new database audit configuration to use + content: + application/json: + schema: + oneOf: + - $ref: ../../components/schemas.yaml#/Database-audit + - $ref: ../../components/schemas.yaml#/Database-audit-verbose + responses: + '200': + description: Database audit configuration successfully updated + '400': + $ref: ../../components/responses.yaml#/request-problem + '404': + $ref: ../../components/responses.yaml#/Not-found + tags: + - Database Configuration + operationId: put_db-_config-audit +post: + summary: Update database audit configuration + description: |- + This is used to update the database configuration fields specified. Only the fields specified in the request will have their values replaced. + + Unspecified audit events will be unaffected. Use PUT if you want to reset events to their default state. + + Required Sync Gateway RBAC roles: + + * TODO + parameters: + - name: verbose + in: query + description: Whether to show name and description with each audit event. + schema: + type: boolean + default: false + requestBody: + description: The database configuration fields to update + content: + application/json: + schema: + oneOf: + - $ref: ../../components/schemas.yaml#/Database-audit + - $ref: ../../components/schemas.yaml#/Database-audit-verbose + responses: + '200': + description: Database audit configuration successfully updated + '400': + $ref: ../../components/responses.yaml#/request-problem + '404': + description: Not Found + tags: + - Database Configuration + operationId: post_db-_config-audit +parameters: + - $ref: ../../components/parameters.yaml#/db diff --git a/rest/admin_api.go b/rest/admin_api.go index dd3a5e9b44..c437ac3455 100644 --- a/rest/admin_api.go +++ b/rest/admin_api.go @@ -21,6 +21,7 @@ import ( "github.com/couchbase/sync_gateway/auth" "github.com/couchbase/sync_gateway/base" + "github.com/couchbase/sync_gateway/base/audit" "github.com/couchbase/sync_gateway/db" "github.com/google/uuid" "github.com/gorilla/mux" @@ -658,6 +659,50 @@ func (h *handler) handlePutDbConfig() (err error) { } +// GET audit config for database +func (h *handler) handleGetDbAuditConfig() error { + h.assertAdminOnly() + + showOnlyFilterable := h.getBoolQuery("filterable") + verbose := h.getBoolQuery("verbose") + + // TODO: Move to structs + events := make(map[string]interface{}, len(audit.SGAuditEvents)) + for id, descriptor := range audit.SGAuditEvents { + if showOnlyFilterable && !descriptor.FilteringPermitted { + continue + } + idStr := id.String() + if verbose { + events[idStr] = map[string]interface{}{ + "name": descriptor.Name, + "description": descriptor.Description, + "enabled": descriptor.EnabledByDefault, // TODO: Switch to actual configuration + "filterable": descriptor.FilteringPermitted, + } + } else { + events[idStr] = descriptor.EnabledByDefault // TODO: Switch to actual configuration + } + } + + h.writeJSON(events) + + return nil +} + +// PUT/POST audit config for database +func (h *handler) handlePutDbAuditConfig() error { + h.assertAdminOnly() + + // interface can be either bool or object for verbose-format + var body map[string]interface{} + if err := h.readJSONInto(&body); err != nil { + return err + } + + return nil +} + // GET collection config sync function func (h *handler) handleGetCollectionConfigSync() error { h.assertAdminOnly() diff --git a/rest/config.go b/rest/config.go index c02610b3f8..b8059afa65 100644 --- a/rest/config.go +++ b/rest/config.go @@ -266,6 +266,7 @@ type ChannelCacheConfig struct { // DbLoggingConfig allows per-database logging overrides type DbLoggingConfig struct { Console *DbConsoleLoggingConfig `json:"console,omitempty"` + Audit *DbAuditLoggingConfig `json:"audit,omitempty"` } // DbConsoleLoggingConfig are per-db options configurable for console logging @@ -274,6 +275,19 @@ type DbConsoleLoggingConfig struct { LogKeys []string `json:"log_keys,omitempty"` } +// DbAuditLoggingConfig are per-db options configurable for audit logging +type DbAuditLoggingConfig struct { + Enabled *bool `json:"enabled,omitempty"` // Whether audit logging is enabled for this database + EnabledEvents []uint `json:"enabled_events,omitempty"` // List of audit event IDs that are enabled + DisabledUsers []AuditLoggingPrincipal `json:"disabled_users,omitempty"` // List of users to disable audit logging for + DisabledRoles []AuditLoggingPrincipal `json:"disabled_roles,omitempty"` // List of roles to disable audit logging for +} + +type AuditLoggingPrincipal struct { + Domain string `json:"domain,omitempty"` + Name string `json:"name,omitempty"` +} + func GetTLSVersionFromString(stringV *string) uint16 { if stringV != nil { switch *stringV { diff --git a/rest/config_flags.go b/rest/config_flags.go index 18bfb492cf..6f32a3ed7a 100644 --- a/rest/config_flags.go +++ b/rest/config_flags.go @@ -127,6 +127,15 @@ func registerConfigFlags(config *StartupConfig, fs *flag.FlagSet) map[string]con "logging.stats.rotation.rotation_interval": {&config.Logging.Stats.Rotation.RotationInterval, fs.String("logging.stats.rotation.rotation_interval", "", "")}, "logging.stats.collation_buffer_size": {&config.Logging.Stats.CollationBufferSize, fs.Int("logging.stats.collation_buffer_size", 0, "")}, + "logging.audit.enabled": {&config.Logging.Audit.Enabled, fs.Bool("logging.audit.enabled", false, "")}, + "logging.audit.rotation.max_size": {&config.Logging.Audit.Rotation.MaxSize, fs.Int("logging.audit.rotation.max_size", 0, "")}, + "logging.audit.rotation.max_age": {&config.Logging.Audit.Rotation.MaxAge, fs.Int("logging.audit.rotation.max_age", 0, "")}, + "logging.audit.rotation.localtime": {&config.Logging.Audit.Rotation.LocalTime, fs.Bool("logging.audit.rotation.localtime", false, "")}, + "logging.audit.rotation.rotated_logs_size_limit": {&config.Logging.Audit.Rotation.RotatedLogsSizeLimit, fs.Int("logging.audit.rotation.rotated_logs_size_limit", 0, "")}, + "logging.audit.collation_buffer_size": {&config.Logging.Audit.CollationBufferSize, fs.Int("logging.audit.collation_buffer_size", 0, "")}, + "logging.audit.rotation.rotation_interval": {&config.Logging.Audit.Rotation.RotationInterval, fs.String("logging.audit.rotation.rotation_interval", "", "")}, + "logging.audit.audit_log_file_path": {&config.Logging.Audit.AuditLogFilePath, fs.String("logging.audit.audit_log_file_path", "", "")}, + "auth.bcrypt_cost": {&config.Auth.BcryptCost, fs.Int("auth.bcrypt_cost", 0, "Cost to use for bcrypt password hashes")}, "replicator.max_heartbeat": {&config.Replicator.MaxHeartbeat, fs.String("replicator.max_heartbeat", "", "Max heartbeat value for _changes request")}, @@ -135,16 +144,17 @@ func registerConfigFlags(config *StartupConfig, fs *flag.FlagSet) map[string]con "replicator.max_concurrent_changes_batches": {&config.Replicator.MaxConcurrentChangesBatches, fs.Int("replicator.max_concurrent_changes_batches", 0, "Maximum number of changes batches to process concurrently per replication")}, "replicator.max_concurrent_revs": {&config.Replicator.MaxConcurrentRevs, fs.Int("replicator.max_concurrent_revs", 0, "Maximum number of revs to process concurrently per replication")}, - "unsupported.diagnostic_interface": {&config.Unsupported.DiagnosticInterface, fs.String("unsupported.diagnostic_interface", "", "Network interface to bind diagnostic API to")}, - "unsupported.stats_log_frequency": {&config.Unsupported.StatsLogFrequency, fs.String("unsupported.stats_log_frequency", "", "How often should stats be written to stats logs")}, - "unsupported.use_stdlib_json": {&config.Unsupported.UseStdlibJSON, fs.Bool("unsupported.use_stdlib_json", false, "Bypass the jsoniter package and use Go's stdlib instead")}, - "unsupported.http2.enabled": {&config.Unsupported.HTTP2.Enabled, fs.Bool("unsupported.http2.enabled", false, "Whether HTTP2 support is enabled")}, - "unsupported.serverless.enabled": {&config.Unsupported.Serverless.Enabled, fs.Bool("unsupported.serverless.enabled", false, "Settings for running Sync Gateway in serverless mode.")}, - "unsupported.serverless.min_config_fetch_interval": {&config.Unsupported.Serverless.MinConfigFetchInterval, fs.String("unsupported.serverless.min_config_fetch_interval", "", "How long to cache configs fetched from the buckets for. This cache is used for requested databases that SG does not know about.")}, - "unsupported.use_xattr_config": {&config.Unsupported.UseXattrConfig, fs.Bool("unsupported.use_xattr_config", false, "Store database configurations in system xattrs")}, - "unsupported.allow_dbconfig_env_vars": {&config.Unsupported.AllowDbConfigEnvVars, fs.Bool("unsupported.allow_dbconfig_env_vars", true, "Can be set to false to skip environment variable expansion in database configs")}, - - "unsupported.user_queries": {&config.Unsupported.UserQueries, fs.Bool("unsupported.user_queries", false, "Whether user-query APIs are enabled")}, + "unsupported.diagnostic_interface": {&config.Unsupported.DiagnosticInterface, fs.String("unsupported.diagnostic_interface", "", "Network interface to bind diagnostic API to")}, + "unsupported.stats_log_frequency": {&config.Unsupported.StatsLogFrequency, fs.String("unsupported.stats_log_frequency", "", "How often should stats be written to stats logs")}, + "unsupported.use_stdlib_json": {&config.Unsupported.UseStdlibJSON, fs.Bool("unsupported.use_stdlib_json", false, "Bypass the jsoniter package and use Go's stdlib instead")}, + "unsupported.http2.enabled": {&config.Unsupported.HTTP2.Enabled, fs.Bool("unsupported.http2.enabled", false, "Whether HTTP2 support is enabled")}, + "unsupported.serverless.enabled": {&config.Unsupported.Serverless.Enabled, fs.Bool("unsupported.serverless.enabled", false, "Settings for running Sync Gateway in serverless mode.")}, + "unsupported.serverless.min_config_fetch_interval": {&config.Unsupported.Serverless.MinConfigFetchInterval, fs.String("unsupported.serverless.min_config_fetch_interval", "", "How long to cache configs fetched from the buckets for. This cache is used for requested databases that SG does not know about.")}, + "unsupported.use_xattr_config": {&config.Unsupported.UseXattrConfig, fs.Bool("unsupported.use_xattr_config", false, "Store database configurations in system xattrs")}, + "unsupported.allow_dbconfig_env_vars": {&config.Unsupported.AllowDbConfigEnvVars, fs.Bool("unsupported.allow_dbconfig_env_vars", true, "Can be set to false to skip environment variable expansion in database configs")}, + "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")}, "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.")}, @@ -171,23 +181,51 @@ func fillConfigWithFlags(fs *flag.FlagSet, flags map[string]configFlag) error { } switch rval.Interface().(type) { - case *string: - *val.config.(*string) = *val.flagValue.(*string) - case *[]string: - list := strings.Split(*val.flagValue.(*string), ",") - *val.config.(*[]string) = list - case *uint: - *val.config.(*uint) = *val.flagValue.(*uint) - case *uint64: - *val.config.(*uint64) = *val.flagValue.(*uint64) + case *bool: + if pointer { + rval.Set(reflect.ValueOf(val.flagValue)) + } else { + *val.config.(*bool) = *val.flagValue.(*bool) + } case *int: if pointer { rval.Set(reflect.ValueOf(val.flagValue)) } else { *val.config.(*int) = *val.flagValue.(*int) } - case *bool: - rval.Set(reflect.ValueOf(val.flagValue)) + case *int64: + if pointer { + rval.Set(reflect.ValueOf(val.flagValue)) + } else { + *val.config.(*int64) = *val.flagValue.(*int64) + } + case *uint: + if pointer { + rval.Set(reflect.ValueOf(val.flagValue)) + } else { + *val.config.(*uint) = *val.flagValue.(*uint) + } + case *uint64: + if pointer { + rval.Set(reflect.ValueOf(val.flagValue)) + } else { + *val.config.(*uint64) = *val.flagValue.(*uint64) + } + case *string: + if pointer { + rval.Set(reflect.ValueOf(val.flagValue)) + } else { + *val.config.(*string) = *val.flagValue.(*string) + } + case *float64: + if pointer { + rval.Set(reflect.ValueOf(val.flagValue)) + } else { + *val.config.(*float64) = *val.flagValue.(*float64) + } + case *[]string: + list := strings.Split(*val.flagValue.(*string), ",") + *val.config.(*[]string) = list case *base.ConfigDuration: duration, err := time.ParseDuration(*val.flagValue.(*string)) if err != nil { diff --git a/rest/config_flags_test.go b/rest/config_flags_test.go index 9612857476..6e3f79e8d3 100644 --- a/rest/config_flags_test.go +++ b/rest/config_flags_test.go @@ -54,8 +54,10 @@ func TestAllConfigFlags(t *testing.T) { flags = append(flags, "-"+name+"=true") case uint, uint64: flags = append(flags, "-"+name, "1234") - case int: + case int, int64: flags = append(flags, "-"+name, "-5678") + case float64: + flags = append(flags, "-"+name, "123.456") default: assert.Failf(t, "Unknown flag type", "value type %v for flag %v", rFlagVal.Interface(), name) } @@ -142,7 +144,7 @@ func TestAllConfigOptionsAsFlags(t *testing.T) { cfg := NewEmptyStartupConfig() cfgFieldsNum := countFields(cfg) flagsNum := registerConfigFlags(&cfg, flag.NewFlagSet("test", flag.ContinueOnError)) - assert.Equalf(t, len(flagsNum), cfgFieldsNum, "Number of cli flags and startup config properties did not match! Did you forget to add a new config option in registerConfigFlags?") + assert.Lenf(t, flagsNum, cfgFieldsNum, "Number of cli flags and startup config properties did not match! Did you forget to add a new config option in registerConfigFlags?") } func countFields(cfg interface{}) (fields int) { diff --git a/rest/config_startup.go b/rest/config_startup.go index 2952a55099..680e69cd41 100644 --- a/rest/config_startup.go +++ b/rest/config_startup.go @@ -151,14 +151,20 @@ 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"` + 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"` +} + +type AuditInfoProviderConfig struct { + GlobalInfoEnvVarName *string `json:"global_info_env_var_name,omitempty" help:"Environment variable name to get global audit event info from"` + RequestInfoHeaderName *string `json:"request_info_header_name,omitempty" help:"HTTP header name to get request audit event info from"` } type ServerlessConfig struct { @@ -235,9 +241,11 @@ func NewEmptyStartupConfig() StartupConfig { Debug: &base.FileLoggerConfig{}, Trace: &base.FileLoggerConfig{}, Stats: &base.FileLoggerConfig{}, + Audit: &base.AuditLoggerConfig{}, }, Unsupported: UnsupportedConfig{ - HTTP2: &HTTP2Config{}, + HTTP2: &HTTP2Config{}, + AuditInfoProvider: &AuditInfoProviderConfig{}, }, } } diff --git a/rest/routing.go b/rest/routing.go index 4142a0d589..8e703a861e 100644 --- a/rest/routing.go +++ b/rest/routing.go @@ -219,6 +219,9 @@ func CreateAdminRouter(sc *ServerContext) *mux.Router { dbr.Handle("/_config", makeOfflineHandler(sc, adminPrivs, []Permission{PermUpdateDb, PermConfigureSyncFn, PermConfigureAuth}, []Permission{PermUpdateDb, PermConfigureSyncFn, PermConfigureAuth}, (*handler).handlePutDbConfig)).Methods("PUT", "POST") + dbr.Handle("/_config/audit", makeHandler(sc, adminPrivs, []Permission{PermUpdateDb}, nil, (*handler).handleGetDbAuditConfig)).Methods("GET") + dbr.Handle("/_config/audit", makeHandler(sc, adminPrivs, []Permission{PermUpdateDb}, nil, (*handler).handlePutDbAuditConfig)).Methods("PUT", "POST") + keyspace.Handle("/_config/sync", makeOfflineHandler(sc, adminPrivs, []Permission{PermUpdateDb, PermConfigureSyncFn}, nil, (*handler).handleGetCollectionConfigSync)).Methods("GET") keyspace.Handle("/_config/sync",