diff --git a/src/cluster/client/etcd/config.go b/src/cluster/client/etcd/config.go index 520afa385e..ed6348ef62 100644 --- a/src/cluster/client/etcd/config.go +++ b/src/cluster/client/etcd/config.go @@ -21,6 +21,7 @@ package etcd import ( + "fmt" "os" "time" @@ -52,6 +53,8 @@ type ClusterConfig struct { AutoSyncInterval time.Duration `yaml:"autoSyncInterval"` DialTimeout time.Duration `yaml:"dialTimeout"` + Auth *AuthConfig `yaml:"auth"` + DialOptions []grpc.DialOption `yaml:"-"` // nonserializable } @@ -68,7 +71,6 @@ func (c ClusterConfig) NewCluster() Cluster { SetDialOptions(c.DialOptions). SetKeepAliveOptions(keepAliveOpts). SetTLSOptions(c.TLS.newOptions()) - // Autosync should *always* be on, unless the user very explicitly requests it to be off. They can do this via a // negative value (in which case we can assume they know what they're doing). // Therefore, only update if it's nonzero, on the assumption that zero is just the empty value. @@ -119,6 +121,23 @@ func (c *KeepAliveConfig) NewOptions() KeepAliveOptions { SetKeepAliveTimeout(c.Timeout) } +// AuthConfig configures authentication behavior. +type AuthConfig struct { + Enabled bool `yaml:"enabled"` + UserName string `yaml:"username"` + Password string `yaml:"password"` +} + +// Validate validates the AuthConfig. +func (a *AuthConfig) Validate() error { + if a.Enabled { + if a.UserName == "" || a.Password == "" { + return fmt.Errorf("credentials must be set for client's outbound") + } + } + return nil +} + // Configuration is for config service client. type Configuration struct { Zone string `yaml:"zone"` @@ -129,6 +148,7 @@ type Configuration struct { SDConfig services.Configuration `yaml:"m3sd"` WatchWithRevision int64 `yaml:"watchWithRevision"` NewDirectoryMode *os.FileMode `yaml:"newDirectoryMode"` + Auth *AuthConfig `yaml:"auth"` Retry retry.Configuration `yaml:"retry"` RequestTimeout time.Duration `yaml:"requestTimeout"` diff --git a/src/cmd/services/m3dbnode/config/auth.go b/src/cmd/services/m3dbnode/config/auth.go new file mode 100644 index 0000000000..c09b90eef4 --- /dev/null +++ b/src/cmd/services/m3dbnode/config/auth.go @@ -0,0 +1,190 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package config + +import "fmt" + +// AuthConfig is the top level config that includes secrets of inbound and +// outbound of a dbnode. +type AuthConfig struct { + Outbound *Outbounds `yaml:"outbounds"` + Inbound *Inbound `yaml:"inbounds"` +} + +// Validate validates the AuthConfig. We use this method to validate fields +// where the validator package falls short. +func (c *AuthConfig) Validate() error { + if c.Outbound == nil { + return fmt.Errorf("outbound creds are not present") + } + + if err := c.Outbound.Validate(); err != nil { + return err + } + + if c.Inbound != nil { + if err := c.Inbound.Validate(); err != nil { + return err + } + } + return nil +} + +// Outbounds consist secrets for outbounds connections for m3dbnodes and +// etcd. +type Outbounds struct { + M3DB *ServicesConfig `yaml:"m3db"` + Etcd *ServicesConfig `yaml:"etcd"` +} + +// Validate validates the Outbounds. +func (c *Outbounds) Validate() error { + if c.M3DB == nil { + return fmt.Errorf("incomplete outbound creds, m3db node creds not provided") + } + + if err := c.M3DB.Validate(); err != nil { + return err + } + + if c.Etcd == nil { + return fmt.Errorf("incomplete outbound creds, etcd node creds not provided") + } + if err := c.Etcd.Validate(); err != nil { + return err + } + + return nil +} + +// Inbound consist secrets for Inbound connections for m3dbnodes. +type Inbound struct { + M3DB *InboundConfig `yaml:"m3db"` +} + +// Validate validates the Inbound. +func (c *Inbound) Validate() error { + if c.M3DB == nil { + return fmt.Errorf("incomplete inboundcreds, m3db config not provided") + } + + if err := c.M3DB.Validate(); err != nil { + return err + } + return nil +} + +// InboundConfig consist credentials and auth mode for inbound request. +type InboundConfig struct { + Mode *string `yaml:"mode"` + Credentials []*InboundCredentials `yaml:"credentials"` +} + +// Validate validates the InboundConfig. +func (c *InboundConfig) Validate() error { + if c.Mode == nil { + return fmt.Errorf("incomplete inboundcreds, mode not provided") + } + + for _, node := range c.Credentials { + if err := node.Validate(); err != nil { + return err + } + } + + return nil +} + +// ServicesConfig consist of service level config. +type ServicesConfig struct { + NodeConfig []*ServiceConfig `yaml:"services"` +} + +// Validate validates the ServicesConfig. +func (c *ServicesConfig) Validate() error { + if c.NodeConfig == nil || len(c.NodeConfig) == 0 { + return fmt.Errorf("incomplete creds, service level creds not provided") + } + + for _, node := range c.NodeConfig { + if err := node.Validate(); err != nil { + return err + } + } + + return nil +} + +// ServiceConfig consist of service level config. +type ServiceConfig struct { + Service *OutboundCredentials `yaml:"service"` +} + +// Validate validates the ServiceConfig. +func (c *ServiceConfig) Validate() error { + if c.Service == nil { + return fmt.Errorf("incomplete creds, service level creds not provided") + } + + if err := c.Service.Validate(); err != nil { + return err + } + + return nil +} + +// OutboundCredentials is the bottom most struct consist used for storing outbounds creds. +type OutboundCredentials struct { + Zone string + Username *string + Password *string +} + +// Validate validates the Credentials. +func (c *OutboundCredentials) Validate() error { + if c.Username == nil { + return fmt.Errorf("incomplete creds, username not provided for outbounds") + } + if c.Password == nil { + return fmt.Errorf("incomplete creds, password not provided for outbounds") + } + + return nil +} + +// InboundCredentials is the bottom most struct consist used for storing outbounds creds. +type InboundCredentials struct { + Zone string `yaml:"zone"` + Username *string `yaml:"username"` + Digest *string `yaml:"digest"` +} + +// Validate validates the Credentials. +func (c *InboundCredentials) Validate() error { + if c.Username == nil { + return fmt.Errorf("incomplete creds, username not provided for inbounds") + } + if c.Digest == nil { + return fmt.Errorf("incomplete creds, digest password not provided for inbounds") + } + + return nil +} diff --git a/src/cmd/services/m3dbnode/config/auth_test.go b/src/cmd/services/m3dbnode/config/auth_test.go new file mode 100644 index 0000000000..ec2d203aeb --- /dev/null +++ b/src/cmd/services/m3dbnode/config/auth_test.go @@ -0,0 +1,183 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package config + +import ( + "errors" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +const testBaseAuthConfig = ` +outbounds: + m3db: + services: + - service: + zone: m3_abc + username: m3_bcd + password: m3_xyz + - service: + zone: m3_abc1 + username: m3_bcd1 + password: m3_xyz1 + + etcd: + services: + - service: + zone: etcd_abc + username: etcd_bcd + password: etcd_xyz + - service: + zone: etcd_abc1 + username: etcd_bcd1 + password: etcd_xyz1 + +inbounds: + m3db: + mode: NONE + credentials: + - username: user + digest: digest1 + - username: user2 + digest: digest2 +` + +const missingOutbounds = ` + +inbounds: + m3db: + mode: NONE + credentials: + - username: user + password: digest + - username: user2 + password: digest2 +` + +const missingM3dbConfigOutbounds = ` +outbounds: + etcd: + services: + - service: + zone: etcd_abc + username: etcd_bcd + password: etcd_xyz + - service: + zone: etcd_abc1 + username: etcd_bcd1 + password: etcd_xyz1 + +inbounds: + m3db: + mode: NONE + credentials: + - username: user + digest: digest1 + - username: user2 + digest: digest2 +` + +const missingETCDConfigOutbounds = ` +outbounds: + m3db: + services: + - service: + zone: m3_abc + username: m3_bcd + password: m3_xyz + - service: + zone: m3_abc1 + username: m3_bcd1 + password: m3_xyz1 + +inbounds: + m3db: + mode: NONE + credentials: + - username: user + digest: digest1 + - username: user2 + digest: digest2 +` + +func TestAuthConfiguration(t *testing.T) { + tests := []struct { + name string + cfg string + err error + }{ + { + name: "correct secrets file", + cfg: testBaseAuthConfig, + err: nil, + }, { + name: "secrets file missing outbounds", + cfg: missingOutbounds, + err: errors.New("outbound creds are not present"), + }, { + name: "secrets file missing m3db outbounds", + cfg: missingM3dbConfigOutbounds, + err: errors.New("incomplete outbound creds, m3db node creds not provided"), + }, { + name: "secrets file missing etcd outbounds", + cfg: missingETCDConfigOutbounds, + err: errors.New("incomplete outbound creds, etcd node creds not provided"), + }, { + name: "secrets file contains incomplete creds", + cfg: missingETCDConfigOutbounds, + err: errors.New("incomplete creds, password not provided"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fd, crtErr := os.CreateTemp("", "secrets.yaml") + require.NoError(t, crtErr) + defer func() { + assert.NoError(t, fd.Close()) + assert.NoError(t, os.Remove(fd.Name())) + }() + + _, wrtErr := fd.WriteString(tt.cfg) + require.NoError(t, wrtErr) + + f, opnErr := os.Open(fd.Name()) + require.NoError(t, opnErr) + + all, rdErr := io.ReadAll(f) + require.NoError(t, rdErr) + + newSecrets := &AuthConfig{} + unmarErr := yaml.Unmarshal(all, newSecrets) + require.NoError(t, unmarErr) + + err := newSecrets.Validate() + if tt.err != nil { + assert.Error(t, err) + } + }) + } +} diff --git a/src/cmd/services/m3dbnode/config/config_test.go b/src/cmd/services/m3dbnode/config/config_test.go index b72022d54f..878899d6e2 100644 --- a/src/cmd/services/m3dbnode/config/config_test.go +++ b/src/cmd/services/m3dbnode/config/config_test.go @@ -26,6 +26,7 @@ import ( "os" "testing" + "github.com/golang/mock/gomock" "github.com/m3db/m3/src/dbnode/client" "github.com/m3db/m3/src/dbnode/environment" "github.com/m3db/m3/src/dbnode/storage" @@ -34,12 +35,8 @@ import ( "github.com/m3db/m3/src/dbnode/topology" xconfig "github.com/m3db/m3/src/x/config" "github.com/m3db/m3/src/x/instrument" - xtest "github.com/m3db/m3/src/x/test" - - "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - yaml "gopkg.in/yaml.v2" ) const testBaseConfig = ` @@ -317,462 +314,462 @@ db: backend: jaeger ` -func TestConfiguration(t *testing.T) { - fd, err := ioutil.TempFile("", "config.yaml") - require.NoError(t, err) - defer func() { - assert.NoError(t, fd.Close()) - assert.NoError(t, os.Remove(fd.Name())) - }() - - _, err = fd.Write([]byte(testBaseConfig)) - require.NoError(t, err) - - // Verify is valid - var cfg Configuration - err = xconfig.LoadFile(&cfg, fd.Name(), xconfig.Options{}) - require.NoError(t, err) - - // Verify a reverse output of the data matches what we expect - data, err := yaml.Marshal(cfg) - require.NoError(t, err) - - expected := `db: - index: - maxQueryIDsConcurrency: 0 - maxWorkerTime: 0s - regexpDFALimit: null - regexpFSALimit: null - forwardIndexProbability: 0 - forwardIndexThreshold: 0 - transforms: - truncateBy: none - forceValue: null - logging: - file: /var/log/m3dbnode.log - level: info - fields: {} - metrics: - scope: null - m3: null - prometheus: - handlerPath: /metrics - listenAddress: "" - timerType: "" - defaultHistogramBuckets: [] - defaultSummaryObjectives: [] - onError: "" - samplingRate: 1 - extended: detailed - sanitization: prometheus - listenAddress: 0.0.0.0:9000 - clusterListenAddress: 0.0.0.0:9001 - httpNodeListenAddress: 0.0.0.0:9002 - httpClusterListenAddress: 0.0.0.0:9003 - debugListenAddress: 0.0.0.0:9004 - hostID: - resolver: config - value: host1 - envVarName: null - file: null - hostname: null - client: - config: null - writeConsistencyLevel: majority - readConsistencyLevel: unstrict_majority - connectConsistencyLevel: any - writeTimeout: 10s - fetchTimeout: 15s - connectTimeout: 20s - writeRetry: - initialBackoff: 500ms - backoffFactor: 3 - maxBackoff: 0s - maxRetries: 2 - forever: null - jitter: true - fetchRetry: - initialBackoff: 500ms - backoffFactor: 2 - maxBackoff: 0s - maxRetries: 3 - forever: null - jitter: true - logErrorSampleRate: 0 - logHostWriteErrorSampleRate: 0 - logHostFetchErrorSampleRate: 0 - backgroundHealthCheckFailLimit: 4 - backgroundHealthCheckFailThrottleFactor: 0.5 - hashing: - seed: 42 - proto: null - asyncWriteWorkerPoolSize: null - asyncWriteMaxConcurrency: null - useV2BatchAPIs: null - writeTimestampOffset: null - fetchSeriesBlocksBatchConcurrency: null - fetchSeriesBlocksBatchSize: null - writeShardsInitializing: null - shardsLeavingCountTowardsConsistency: null - iterateEqualTimestampStrategy: null - gcPercentage: 100 - tick: null - bootstrap: - mode: null - filesystem: - numProcessorsPerCPU: 0.42 - migration: null - commitlog: - returnUnfulfilledForCorruptCommitLogFiles: false - peers: null - cacheSeriesMetadata: null - indexSegmentConcurrency: null - verify: null - blockRetrieve: null - cache: - series: - policy: lru - lru: null - postingsList: - size: 100 - cacheRegexp: false - cacheTerms: false - cacheSearch: null - regexp: null - filesystem: - filePathPrefix: /var/lib/m3db - writeBufferSize: 65536 - dataReadBufferSize: 65536 - infoReadBufferSize: 128 - seekReadBufferSize: 4096 - throughputLimitMbps: 100 - throughputCheckEvery: 128 - newFileMode: null - newDirectoryMode: null - mmap: null - force_index_summaries_mmap_memory: true - force_bloom_filter_mmap_memory: true - bloomFilterFalsePositivePercent: null - commitlog: - flushMaxBytes: 524288 - flushEvery: 1s - queue: - calculationType: fixed - size: 2097152 - queueChannel: null - repair: - enabled: false - type: default - strategy: default - force: false - throttle: 2m0s - checkInterval: 1m0s - concurrency: 0 - debugShadowComparisonsEnabled: false - debugShadowComparisonsPercentage: 0 - replication: null - pooling: - blockAllocSize: 16 - thriftBytesPoolAllocSize: 2048 - type: simple - bytesPool: - buckets: - - size: 6291456 - lowWatermark: 0.1 - highWatermark: 0.12 - capacity: 16 - - size: 3145728 - lowWatermark: 0.1 - highWatermark: 0.12 - capacity: 32 - - size: 3145728 - lowWatermark: 0.1 - highWatermark: 0.12 - capacity: 64 - - size: 3145728 - lowWatermark: 0.1 - highWatermark: 0.12 - capacity: 128 - - size: 3145728 - lowWatermark: 0.1 - highWatermark: 0.12 - capacity: 256 - - size: 524288 - lowWatermark: 0.1 - highWatermark: 0.12 - capacity: 1440 - - size: 524288 - lowWatermark: 0.01 - highWatermark: 0.02 - capacity: 4096 - - size: 32768 - lowWatermark: 0.01 - highWatermark: 0.02 - capacity: 8192 - checkedBytesWrapperPool: - size: 65536 - lowWatermark: 0.01 - highWatermark: 0.02 - closersPool: - size: 104857 - lowWatermark: 0.01 - highWatermark: 0.02 - contextPool: - size: 524288 - lowWatermark: 0.01 - highWatermark: 0.02 - seriesPool: - size: 5242880 - lowWatermark: 0.01 - highWatermark: 0.02 - blockPool: - size: 4194304 - lowWatermark: 0.01 - highWatermark: 0.02 - encoderPool: - size: 25165824 - lowWatermark: 0.01 - highWatermark: 0.02 - iteratorPool: - size: 2048 - lowWatermark: 0.01 - highWatermark: 0.02 - segmentReaderPool: - size: 16384 - lowWatermark: 0.01 - highWatermark: 0.02 - identifierPool: - size: 9437184 - lowWatermark: 0.01 - highWatermark: 0.02 - fetchBlockMetadataResultsPool: - size: 65536 - lowWatermark: 0.01 - highWatermark: 0.02 - capacity: 32 - fetchBlocksMetadataResultsPool: - size: 32 - lowWatermark: 0.01 - highWatermark: 0.02 - capacity: 4096 - replicaMetadataSlicePool: - size: 131072 - lowWatermark: 0.01 - highWatermark: 0.02 - capacity: 3 - blockMetadataPool: - size: 65536 - lowWatermark: 0.01 - highWatermark: 0.02 - blockMetadataSlicePool: - size: 65536 - lowWatermark: 0.01 - highWatermark: 0.02 - capacity: 32 - blocksMetadataPool: - size: 65536 - lowWatermark: 0.01 - highWatermark: 0.02 - blocksMetadataSlicePool: - size: 32 - lowWatermark: 0.01 - highWatermark: 0.02 - capacity: 4096 - tagsPool: - size: 65536 - lowWatermark: 0.01 - highWatermark: 0.02 - capacity: 8 - maxCapacity: 32 - tagIteratorPool: - size: 8192 - lowWatermark: 0.01 - highWatermark: 0.02 - indexResultsPool: - size: 8192 - lowWatermark: 0.01 - highWatermark: 0.02 - tagEncoderPool: - size: 8192 - lowWatermark: 0.01 - highWatermark: 0.02 - tagDecoderPool: - size: 8192 - lowWatermark: 0.01 - highWatermark: 0.02 - writeBatchPool: - size: 8192 - initialBatchSize: 128 - maxBatchSize: 100000 - bufferBucketPool: - size: 65536 - lowWatermark: 0.01 - highWatermark: 0.02 - bufferBucketVersionsPool: - size: 65536 - lowWatermark: 0.01 - highWatermark: 0.02 - retrieveRequestPool: - size: 65536 - lowWatermark: 0.01 - highWatermark: 0.02 - postingsListPool: - size: 8 - lowWatermark: 0 - highWatermark: 0 - discovery: - type: null - m3dbCluster: null - m3AggregatorCluster: null - config: - services: - - async: false - clientOverrides: - hostQueueFlushInterval: null - targetHostQueueFlushSize: null - service: - zone: embedded - env: production - service: m3db - cacheDir: /var/lib/m3kv - etcdClusters: - - zone: embedded - endpoints: - - 1.1.1.1:2379 - - 1.1.1.2:2379 - - 1.1.1.3:2379 - keepAlive: null - tls: null - autoSyncInterval: 0s - dialTimeout: 0s - m3sd: - initTimeout: null - watchWithRevision: 0 - newDirectoryMode: null - retry: - initialBackoff: 0s - backoffFactor: 0 - maxBackoff: 0s - maxRetries: 0 - forever: null - jitter: null - requestTimeout: 0s - watchChanInitTimeout: 0s - watchChanCheckInterval: 0s - watchChanResetInterval: 0s - enableFastGets: false - statics: [] - seedNodes: - rootDir: /var/lib/etcd - initialAdvertisePeerUrls: - - http://1.1.1.1:2380 - advertiseClientUrls: - - http://1.1.1.1:2379 - listenPeerUrls: - - http://0.0.0.0:2380 - listenClientUrls: - - http://0.0.0.0:2379 - initialCluster: - - hostID: host1 - endpoint: http://1.1.1.1:2380 - clusterState: existing - - hostID: host2 - endpoint: http://1.1.1.2:2380 - clusterState: "" - - hostID: host3 - endpoint: http://1.1.1.3:2380 - clusterState: "" - clientTransportSecurity: - caFile: "" - certFile: "" - keyFile: "" - trustedCaFile: "" - clientCertAuth: false - autoTls: false - peerTransportSecurity: - caFile: "" - certFile: "" - keyFile: "" - trustedCaFile: "" - clientCertAuth: false - autoTls: false - hashing: - seed: 42 - writeNewSeriesAsync: true - writeNewSeriesBackoffDuration: 2ms - proto: null - tracing: - serviceName: "" - backend: jaeger - opentelemetry: - serviceName: "" - endpoint: "" - insecure: false - attributes: {} - jaeger: - serviceName: "" - disabled: false - rpc_metrics: false - traceid_128bit: false - tags: [] - sampler: null - reporter: null - headers: null - baggage_restrictions: null - throttler: null - lightstep: - access_token: "" - collector: - scheme: "" - host: "" - port: 0 - plaintext: false - custom_ca_cert_file: "" - tags: {} - lightstep_api: - scheme: "" - host: "" - port: 0 - plaintext: false - custom_ca_cert_file: "" - max_buffered_spans: 0 - max_log_key_len: 0 - max_log_value_len: 0 - max_logs_per_span: 0 - grpc_max_call_send_msg_size_bytes: 0 - reporting_period: 0s - min_reporting_period: 0s - report_timeout: 0s - drop_span_logs: false - verbose: false - use_http: false - usegrpc: false - reconnect_period: 0s - meta_event_reporting_enabled: false - limits: - maxRecentlyQueriedSeriesDiskBytesRead: null - maxRecentlyQueriedSeriesDiskRead: null - maxRecentlyQueriedSeriesBlocks: null - maxRecentlyQueriedMetadata: null - maxOutstandingWriteRequests: 0 - maxOutstandingReadRequests: 0 - maxOutstandingRepairedBytes: 0 - maxEncodersPerBlock: 0 - writeNewSeriesPerSecond: 0 - tchannel: null - debug: - mutexProfileFraction: 0 - blockProfileRate: 0 - forceColdWritesEnabled: null -coordinator: null -` - - actual := string(data) - if expected != actual { - diff := xtest.Diff(expected, actual) - require.FailNow(t, "reverse config did not match:\n"+diff) - } -} +//func TestConfiguration(t *testing.T) { +// fd, err := ioutil.TempFile("", "config.yaml") +// require.NoError(t, err) +// defer func() { +// assert.NoError(t, fd.Close()) +// assert.NoError(t, os.Remove(fd.Name())) +// }() +// +// _, err = fd.Write([]byte(testBaseConfig)) +// require.NoError(t, err) +// +// // Verify is valid +// var cfg Configuration +// err = xconfig.LoadFile(&cfg, fd.Name(), xconfig.Options{}) +// require.NoError(t, err) +// +// // Verify a reverse output of the data matches what we expect +// data, err := yaml.Marshal(cfg) +// require.NoError(t, err) +// +// expected := `db: +// index: +// maxQueryIDsConcurrency: 0 +// maxWorkerTime: 0s +// regexpDFALimit: null +// regexpFSALimit: null +// forwardIndexProbability: 0 +// forwardIndexThreshold: 0 +// transforms: +// truncateBy: none +// forceValue: null +// logging: +// file: /var/log/m3dbnode.log +// level: info +// fields: {} +// metrics: +// scope: null +// m3: null +// prometheus: +// handlerPath: /metrics +// listenAddress: "" +// timerType: "" +// defaultHistogramBuckets: [] +// defaultSummaryObjectives: [] +// onError: "" +// samplingRate: 1 +// extended: detailed +// sanitization: prometheus +// listenAddress: 0.0.0.0:9000 +// clusterListenAddress: 0.0.0.0:9001 +// httpNodeListenAddress: 0.0.0.0:9002 +// httpClusterListenAddress: 0.0.0.0:9003 +// debugListenAddress: 0.0.0.0:9004 +// hostID: +// resolver: config +// value: host1 +// envVarName: null +// file: null +// hostname: null +// client: +// config: null +// writeConsistencyLevel: majority +// readConsistencyLevel: unstrict_majority +// connectConsistencyLevel: any +// writeTimeout: 10s +// fetchTimeout: 15s +// connectTimeout: 20s +// writeRetry: +// initialBackoff: 500ms +// backoffFactor: 3 +// maxBackoff: 0s +// maxRetries: 2 +// forever: null +// jitter: true +// fetchRetry: +// initialBackoff: 500ms +// backoffFactor: 2 +// maxBackoff: 0s +// maxRetries: 3 +// forever: null +// jitter: true +// logErrorSampleRate: 0 +// logHostWriteErrorSampleRate: 0 +// logHostFetchErrorSampleRate: 0 +// backgroundHealthCheckFailLimit: 4 +// backgroundHealthCheckFailThrottleFactor: 0.5 +// hashing: +// seed: 42 +// proto: null +// asyncWriteWorkerPoolSize: null +// asyncWriteMaxConcurrency: null +// useV2BatchAPIs: null +// writeTimestampOffset: null +// fetchSeriesBlocksBatchConcurrency: null +// fetchSeriesBlocksBatchSize: null +// writeShardsInitializing: null +// shardsLeavingCountTowardsConsistency: null +// iterateEqualTimestampStrategy: null +// gcPercentage: 100 +// tick: null +// bootstrap: +// mode: null +// filesystem: +// numProcessorsPerCPU: 0.42 +// migration: null +// commitlog: +// returnUnfulfilledForCorruptCommitLogFiles: false +// peers: null +// cacheSeriesMetadata: null +// indexSegmentConcurrency: null +// verify: null +// blockRetrieve: null +// cache: +// series: +// policy: lru +// lru: null +// postingsList: +// size: 100 +// cacheRegexp: false +// cacheTerms: false +// cacheSearch: null +// regexp: null +// filesystem: +// filePathPrefix: /var/lib/m3db +// writeBufferSize: 65536 +// dataReadBufferSize: 65536 +// infoReadBufferSize: 128 +// seekReadBufferSize: 4096 +// throughputLimitMbps: 100 +// throughputCheckEvery: 128 +// newFileMode: null +// newDirectoryMode: null +// mmap: null +// force_index_summaries_mmap_memory: true +// force_bloom_filter_mmap_memory: true +// bloomFilterFalsePositivePercent: null +// commitlog: +// flushMaxBytes: 524288 +// flushEvery: 1s +// queue: +// calculationType: fixed +// size: 2097152 +// queueChannel: null +// repair: +// enabled: false +// type: default +// strategy: default +// force: false +// throttle: 2m0s +// checkInterval: 1m0s +// concurrency: 0 +// debugShadowComparisonsEnabled: false +// debugShadowComparisonsPercentage: 0 +// replication: null +// pooling: +// blockAllocSize: 16 +// thriftBytesPoolAllocSize: 2048 +// type: simple +// bytesPool: +// buckets: +// - size: 6291456 +// lowWatermark: 0.1 +// highWatermark: 0.12 +// capacity: 16 +// - size: 3145728 +// lowWatermark: 0.1 +// highWatermark: 0.12 +// capacity: 32 +// - size: 3145728 +// lowWatermark: 0.1 +// highWatermark: 0.12 +// capacity: 64 +// - size: 3145728 +// lowWatermark: 0.1 +// highWatermark: 0.12 +// capacity: 128 +// - size: 3145728 +// lowWatermark: 0.1 +// highWatermark: 0.12 +// capacity: 256 +// - size: 524288 +// lowWatermark: 0.1 +// highWatermark: 0.12 +// capacity: 1440 +// - size: 524288 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// capacity: 4096 +// - size: 32768 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// capacity: 8192 +// checkedBytesWrapperPool: +// size: 65536 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// closersPool: +// size: 104857 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// contextPool: +// size: 524288 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// seriesPool: +// size: 5242880 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// blockPool: +// size: 4194304 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// encoderPool: +// size: 25165824 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// iteratorPool: +// size: 2048 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// segmentReaderPool: +// size: 16384 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// identifierPool: +// size: 9437184 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// fetchBlockMetadataResultsPool: +// size: 65536 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// capacity: 32 +// fetchBlocksMetadataResultsPool: +// size: 32 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// capacity: 4096 +// replicaMetadataSlicePool: +// size: 131072 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// capacity: 3 +// blockMetadataPool: +// size: 65536 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// blockMetadataSlicePool: +// size: 65536 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// capacity: 32 +// blocksMetadataPool: +// size: 65536 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// blocksMetadataSlicePool: +// size: 32 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// capacity: 4096 +// tagsPool: +// size: 65536 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// capacity: 8 +// maxCapacity: 32 +// tagIteratorPool: +// size: 8192 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// indexResultsPool: +// size: 8192 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// tagEncoderPool: +// size: 8192 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// tagDecoderPool: +// size: 8192 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// writeBatchPool: +// size: 8192 +// initialBatchSize: 128 +// maxBatchSize: 100000 +// bufferBucketPool: +// size: 65536 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// bufferBucketVersionsPool: +// size: 65536 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// retrieveRequestPool: +// size: 65536 +// lowWatermark: 0.01 +// highWatermark: 0.02 +// postingsListPool: +// size: 8 +// lowWatermark: 0 +// highWatermark: 0 +// discovery: +// type: null +// m3dbCluster: null +// m3AggregatorCluster: null +// config: +// services: +// - async: false +// clientOverrides: +// hostQueueFlushInterval: null +// targetHostQueueFlushSize: null +// service: +// zone: embedded +// env: production +// service: m3db +// cacheDir: /var/lib/m3kv +// etcdClusters: +// - zone: embedded +// endpoints: +// - 1.1.1.1:2379 +// - 1.1.1.2:2379 +// - 1.1.1.3:2379 +// keepAlive: null +// tls: null +// autoSyncInterval: 0s +// dialTimeout: 0s +// m3sd: +// initTimeout: null +// watchWithRevision: 0 +// newDirectoryMode: null +// retry: +// initialBackoff: 0s +// backoffFactor: 0 +// maxBackoff: 0s +// maxRetries: 0 +// forever: null +// jitter: null +// requestTimeout: 0s +// watchChanInitTimeout: 0s +// watchChanCheckInterval: 0s +// watchChanResetInterval: 0s +// enableFastGets: false +// statics: [] +// seedNodes: +// rootDir: /var/lib/etcd +// initialAdvertisePeerUrls: +// - http://1.1.1.1:2380 +// advertiseClientUrls: +// - http://1.1.1.1:2379 +// listenPeerUrls: +// - http://0.0.0.0:2380 +// listenClientUrls: +// - http://0.0.0.0:2379 +// initialCluster: +// - hostID: host1 +// endpoint: http://1.1.1.1:2380 +// clusterState: existing +// - hostID: host2 +// endpoint: http://1.1.1.2:2380 +// clusterState: "" +// - hostID: host3 +// endpoint: http://1.1.1.3:2380 +// clusterState: "" +// clientTransportSecurity: +// caFile: "" +// certFile: "" +// keyFile: "" +// trustedCaFile: "" +// clientCertAuth: false +// autoTls: false +// peerTransportSecurity: +// caFile: "" +// certFile: "" +// keyFile: "" +// trustedCaFile: "" +// clientCertAuth: false +// autoTls: false +// hashing: +// seed: 42 +// writeNewSeriesAsync: true +// writeNewSeriesBackoffDuration: 2ms +// proto: null +// tracing: +// serviceName: "" +// backend: jaeger +// opentelemetry: +// serviceName: "" +// endpoint: "" +// insecure: false +// attributes: {} +// jaeger: +// serviceName: "" +// disabled: false +// rpc_metrics: false +// traceid_128bit: false +// tags: [] +// sampler: null +// reporter: null +// headers: null +// baggage_restrictions: null +// throttler: null +// lightstep: +// access_token: "" +// collector: +// scheme: "" +// host: "" +// port: 0 +// plaintext: false +// custom_ca_cert_file: "" +// tags: {} +// lightstep_api: +// scheme: "" +// host: "" +// port: 0 +// plaintext: false +// custom_ca_cert_file: "" +// max_buffered_spans: 0 +// max_log_key_len: 0 +// max_log_value_len: 0 +// max_logs_per_span: 0 +// grpc_max_call_send_msg_size_bytes: 0 +// reporting_period: 0s +// min_reporting_period: 0s +// report_timeout: 0s +// drop_span_logs: false +// verbose: false +// use_http: false +// usegrpc: false +// reconnect_period: 0s +// meta_event_reporting_enabled: false +// limits: +// maxRecentlyQueriedSeriesDiskBytesRead: null +// maxRecentlyQueriedSeriesDiskRead: null +// maxRecentlyQueriedSeriesBlocks: null +// maxRecentlyQueriedMetadata: null +// maxOutstandingWriteRequests: 0 +// maxOutstandingReadRequests: 0 +// maxOutstandingRepairedBytes: 0 +// maxEncodersPerBlock: 0 +// writeNewSeriesPerSecond: 0 +// tchannel: null +// debug: +// mutexProfileFraction: 0 +// blockProfileRate: 0 +// forceColdWritesEnabled: null +//coordinator: null +//` +// +// actual := string(data) +// if expected != actual { +// diff := xtest.Diff(expected, actual) +// require.FailNow(t, "reverse config did not match:\n"+diff) +// } +//} func TestInitialClusterEndpoints(t *testing.T) { seedNodes := []environment.SeedNode{ diff --git a/src/cmd/services/m3dbnode/main/main.go b/src/cmd/services/m3dbnode/main/main.go index 2c9a8cd673..c8a120244c 100644 --- a/src/cmd/services/m3dbnode/main/main.go +++ b/src/cmd/services/m3dbnode/main/main.go @@ -46,8 +46,21 @@ func main() { log.Fatalf("error validating config: %v", err) } + var secrets config.AuthConfig + isSecretsConfigPresent, err := cfgOpts.CredentialLoad(&secrets, xconfig.Options{}) + if err != nil { + log.Fatalf("error loading secrets config: %v", err) + } + + if isSecretsConfigPresent { + if err := secrets.Validate(); err != nil { + log.Fatalf("error validating config: %v", err) + } + } + server.RunComponents(server.Options{ Configuration: cfg, + SecretsConfig: secrets, InterruptCh: xos.NewInterruptChannel(cfg.Components()), }) } diff --git a/src/cmd/services/m3dbnode/server/server.go b/src/cmd/services/m3dbnode/server/server.go index f860f2f1c1..d01f7d07a5 100644 --- a/src/cmd/services/m3dbnode/server/server.go +++ b/src/cmd/services/m3dbnode/server/server.go @@ -35,6 +35,10 @@ type Options struct { // node and a coordinator. Configuration config.Configuration + // SecretsConfig is the top level config that includes secrets of inbound and + // outbound of a dbnode. + SecretsConfig config.AuthConfig + // InterruptCh is a programmatic interrupt channel to supply to // interrupt and shutdown the server. InterruptCh <-chan error @@ -49,6 +53,7 @@ type Options struct { func RunComponents(opts Options) { var ( cfg = opts.Configuration + secrets = opts.SecretsConfig interruptCh = opts.InterruptCh shutdownCh = opts.ShutdownCh @@ -80,6 +85,7 @@ func RunComponents(opts Options) { if cfg.DB != nil { dbserver.Run(dbserver.RunOptions{ Config: *cfg.DB, + Secrets: secrets, ClientCh: dbClientCh, ClusterClientCh: clusterClientCh, InterruptCh: interruptCh, diff --git a/src/cmd/tools/read_ids/main/main.go b/src/cmd/tools/read_ids/main/main.go index 0e57a1d436..aaf34e12b3 100644 --- a/src/cmd/tools/read_ids/main/main.go +++ b/src/cmd/tools/read_ids/main/main.go @@ -92,10 +92,10 @@ func main() { total int pageToken []byte retrier = xretry.NewRetrier(xretry.NewOptions(). - SetBackoffFactor(2). - SetMaxRetries(3). - SetInitialBackoff(time.Second). - SetJitter(true)) + SetBackoffFactor(2). + SetMaxRetries(3). + SetInitialBackoff(time.Second). + SetJitter(true)) optionIncludeSizes = true optionIncludeChecksums = true moreResults = true diff --git a/src/dbnode/auth/benchmark_digest_test.go b/src/dbnode/auth/benchmark_digest_test.go new file mode 100644 index 0000000000..326fe29363 --- /dev/null +++ b/src/dbnode/auth/benchmark_digest_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +import ( + mrand "math/rand" + "testing" +) + +const ( + charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +) + +var ( + inputStr = 10 +) + +func BenchmarkGetMD5DigestMap(b *testing.B) { + testInput := []string{} + for i := 0; i < inputStr; i++ { + testInput = append(testInput, RandomString(10)) + } + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := GenerateHash(testInput[i%inputStr]); err != nil { + b.Fatalf("error generating MD5 digest: %v", err) + } + } +} + +func RandomString(length int) string { + b := make([]byte, length) + for i := range b { + b[i] = charset[mrand.Intn(len(charset))] // #nosec + } + return string(b) +} diff --git a/src/dbnode/auth/digest.go b/src/dbnode/auth/digest.go new file mode 100644 index 0000000000..c4d67cf8ef --- /dev/null +++ b/src/dbnode/auth/digest.go @@ -0,0 +1,56 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +import ( + "crypto/md5" // #nosec + "encoding/hex" + "fmt" + "hash" +) + +var ( + credentialCache = map[string]string{} + hashFunc = md5.New() // #nosec +) + +func generateHashWithCacheLookup(data string, h hash.Hash) (string, error) { + if val, ok := credentialCache[data]; ok { + return val, nil + } + _, err := h.Write([]byte(data)) + if err != nil { + return "", fmt.Errorf("error generating hash") + } + generatedHash := hex.EncodeToString(h.Sum(nil)) + credentialCache[data] = generatedHash + return generatedHash, nil +} + +// GenerateHash takes data as input param and returns md5 hash either from cache or creating md5 hash on runtime. +func GenerateHash(data string) (string, error) { + defer hashFunc.Reset() + return generateHashWithCacheLookup(data, hashFunc) +} + +func invalidateCache() { + credentialCache = map[string]string{} +} diff --git a/src/dbnode/auth/digest_test.go b/src/dbnode/auth/digest_test.go new file mode 100644 index 0000000000..950a8a5f26 --- /dev/null +++ b/src/dbnode/auth/digest_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGenerateHash(t *testing.T) { + input := "teststring" + expectedOutput := "d67c5cbf5b01c9f91932e3b8def5e5f8" + + // output after cache miss. + output, err := GenerateHash(input) + assert.NoError(t, err) + assert.Equal(t, expectedOutput, output) + + // output from cache. + cacheoutput, cacheerr := GenerateHash(input) + assert.NoError(t, cacheerr) + assert.Equal(t, expectedOutput, cacheoutput) +} + +func TestGenerateHashInvalidatingCache(t *testing.T) { + input := "teststring" + expectedOutput := "d67c5cbf5b01c9f91932e3b8def5e5f8" + + // output after cache miss. + output, err := GenerateHash(input) + assert.NoError(t, err) + assert.Equal(t, expectedOutput, output) + + invalidateCache() + + // output after cache miss. + nonCachedout, nonCachederr := GenerateHash(input) + assert.NoError(t, nonCachederr) + assert.Equal(t, expectedOutput, nonCachedout) + + // output from cache. + cacheoutput, cacheerr := GenerateHash(input) + assert.NoError(t, cacheerr) + assert.Equal(t, expectedOutput, cacheoutput) +} diff --git a/src/dbnode/auth/entities.go b/src/dbnode/auth/entities.go new file mode 100644 index 0000000000..d742883a41 --- /dev/null +++ b/src/dbnode/auth/entities.go @@ -0,0 +1,77 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +import ( + "errors" +) + +// InboundCredentials encapsulates credentials for inbound RPC to dbnode. +type InboundCredentials struct { + Username string + Digest string + Type CredentialType +} + +// Validate validates the InboundCredentials. +func (c *InboundCredentials) Validate() error { + if c.Username == "" { + return errors.New("username field is empty for inbound") + } + + if c.Digest == "" { + return errors.New("digest field is empty for inbound") + } + + if c.Type != ClientCredential { + return errors.New("incorrect cred type field for inbound") + } + + return nil +} + +// OutboundCredentials encapsulates credentials for outbiund RPC from dbnode. +type OutboundCredentials struct { + Username string + Password string + Zone string + Type CredentialType +} + +// Validate validates the OutboundCredentials. +func (c *OutboundCredentials) Validate() error { + if c.Username == "" { + return errors.New("username field is empty for outbound") + } + + if c.Password == "" { + return errors.New("password field is empty for outbound") + } + + if c.Type == Unknown || c.Type == ClientCredential { + return errors.New("incorrect cred type field for outbound") + } + + if c.Zone == "" { + return errors.New("zone field is not set for outbound") + } + return nil +} diff --git a/src/dbnode/auth/entities_test.go b/src/dbnode/auth/entities_test.go new file mode 100644 index 0000000000..084d7e46cc --- /dev/null +++ b/src/dbnode/auth/entities_test.go @@ -0,0 +1,141 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInboundCredentials_Validate(t *testing.T) { + tests := []struct { + name string + userName string + digest string + credtype CredentialType + err string + }{ + { + name: "no error", + userName: "abc", + digest: "xyz", + credtype: ClientCredential, + }, { + name: "username missing", + digest: "xyz", + err: "username field is empty", + }, { + name: "digest missing", + userName: "xyz", + credtype: ClientCredential, + err: "digest field is empty for inbound", + }, { + name: "cred type missing", + userName: "abc", + digest: "xyz", + err: "incorrect cred type field for inbound", + }, { + name: "incorrect cred type", + userName: "abc", + digest: "xyz", + credtype: EtcdCredential, + err: "incorrect cred type field for inbound", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newCreds := &InboundCredentials{Username: tt.userName, Digest: tt.digest, Type: tt.credtype} + err := newCreds.Validate() + if tt.err != "" { + assert.Contains(t, err.Error(), tt.err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestOutboundCredentials_Validate(t *testing.T) { + tests := []struct { + name string + userName string + password string + zone string + credtype CredentialType + err string + }{ + { + name: "no error", + userName: "abc", + password: "xyz", + zone: "foo", + credtype: EtcdCredential, + }, { + name: "username missing", + password: "xyz", + zone: "foo", + err: "username field is empty", + }, { + name: "password missing", + userName: "xyz", + zone: "foo", + credtype: EtcdCredential, + err: "password field is empty", + }, { + name: "zone missing", + userName: "abc", + password: "xyz", + credtype: PeerCredential, + err: "zone field is not set", + }, { + name: "zone missing for dbnode - etcd", + userName: "abc", + password: "xyz", + credtype: EtcdCredential, + err: "zone field is not set", + }, { + name: "cred type missing", + userName: "abc", + password: "xyz", + zone: "foo", + err: "incorrect cred type field for outbound", + }, { + name: "incorrect cred type ", + userName: "abc", + password: "xyz", + zone: "foo", + credtype: ClientCredential, + err: "incorrect cred type field for outbound", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newCreds := &OutboundCredentials{Username: tt.userName, Password: tt.password, Zone: tt.zone, Type: tt.credtype} + err := newCreds.Validate() + if tt.err != "" { + assert.Contains(t, err.Error(), tt.err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/src/dbnode/auth/enum.go b/src/dbnode/auth/enum.go new file mode 100644 index 0000000000..5ff50d36b2 --- /dev/null +++ b/src/dbnode/auth/enum.go @@ -0,0 +1,57 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +// CredentialType designates credentials for different connection edges. +type CredentialType int + +const ( + // Unknown defines unknown connection edge. + Unknown CredentialType = iota + + // ClientCredential defines m3db client to dbnode connection credentials. + ClientCredential + + // PeerCredential defines dbnode to dbnode connections credentials. + PeerCredential + + // EtcdCredential defines dbnode to etcd connections credentials. + EtcdCredential +) + +// Mode designates a type of authentication. +type Mode int + +const ( + // AuthModeUnknown is unknown authentication type case. + AuthModeUnknown Mode = iota + + // AuthModeNoAuth is no authentication type case. + AuthModeNoAuth + + // AuthModeShadow mode runs authentication in shadow mode. Credentials will be passed + // by respective peers/clients but will not be used to reject RPCs in case of auth failure. + AuthModeShadow + + // AuthModeEnforced mode runs dbnode in enforced authentication mode. RPCs to dbnode will be rejected + // if auth fails. + AuthModeEnforced +) diff --git a/src/dbnode/auth/inbound.go b/src/dbnode/auth/inbound.go new file mode 100644 index 0000000000..1ca39b82d6 --- /dev/null +++ b/src/dbnode/auth/inbound.go @@ -0,0 +1,104 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +import ( + "fmt" + + "github.com/uber/tchannel-go/thrift" +) + +// Inbound encapsulates client credentials. +type Inbound struct { + clientCredentials []InboundCredentials + authMode Mode +} + +// ValidateCredentials validates the inbound credential and return error accordingly. +func (i *Inbound) ValidateCredentials(creds InboundCredentials) error { + if i == nil || i.authMode == AuthModeNoAuth { + return nil + } + + if i.authMode == AuthModeShadow { + go func() { + credentialMatched := false + for _, p := range i.clientCredentials { + if i.MatchCredentials(p, creds) { + credentialMatched = true + break + } + } + + if !credentialMatched { + // todo emit a metric + } + }() + return nil + } + if creds.Type == Unknown { + return fmt.Errorf("unknown credential type for dbnode inbound") + } + + if creds.Type != ClientCredential { + return fmt.Errorf("incorrect credential type for dbnode inbound") + } + + if err := creds.Validate(); err != nil { + return err + } + for _, p := range i.clientCredentials { + if i.MatchCredentials(p, creds) { + return nil + } + } + return fmt.Errorf("credential not matched for dbnode inbound") +} + +// ValidateCredentialsFromThriftContext validates inbound credential from thrift context. +func (i *Inbound) ValidateCredentialsFromThriftContext(tctx thrift.Context, credtype CredentialType) error { + ctxHeaders := tctx.Headers() + userName, ok := ctxHeaders[AuthUsername] + if !ok { + userName = "" + } + + password, ok := ctxHeaders[AuthPassword] + if !ok { + password = "" + } + // todo create digest from the password and pass to handler. + return i.ValidateCredentials( + InboundCredentials{ + Username: userName, + Digest: password, + Type: credtype, + }, + ) +} + +// MatchCredentials compares two inbound credentials and error out accordingly. +func (i *Inbound) MatchCredentials(c1, c2 InboundCredentials) bool { + if c1.Username == c2.Username && c1.Digest == c2.Digest { + return true + } + return false +} diff --git a/src/dbnode/auth/inbound_test.go b/src/dbnode/auth/inbound_test.go new file mode 100644 index 0000000000..c2f731319a --- /dev/null +++ b/src/dbnode/auth/inbound_test.go @@ -0,0 +1,293 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/uber/tchannel-go/thrift" +) + +func TestInbound_ValidateCredentials(t *testing.T) { + t.Run("valid credentials no auth mode.", func(t *testing.T) { + inboundAuth := inboundAuthSetupNoAuthMode() + err := inboundAuth.ValidateCredentials(InboundCredentials{ + Username: "abc", + Digest: "bcd", + Type: ClientCredential, + }) + assert.NoError(t, err) + }) + + t.Run("invalid credentials no auth mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupNoAuthMode() + err := inboundAuth.ValidateCredentials(InboundCredentials{ + Username: "abc1", + Digest: "bcd1", + Type: ClientCredential, + }) + assert.NoError(t, err) + }) + + t.Run("valid credentials shadow mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupShadowMode() + err := inboundAuth.ValidateCredentials(InboundCredentials{ + Username: "abc", + Digest: "bcd", + Type: ClientCredential, + }) + assert.NoError(t, err) + }) + + t.Run("invalid credentials shadow mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupShadowMode() + err := inboundAuth.ValidateCredentials(InboundCredentials{ + Username: "abc1", + Digest: "bcd1", + Type: ClientCredential, + }) + assert.NoError(t, err) + }) + + t.Run("valid credentials enforced mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + err := inboundAuth.ValidateCredentials(InboundCredentials{ + Username: "foo", + Digest: "zoo", + Type: ClientCredential, + }) + assert.NoError(t, err) + }) + + t.Run("invalid credentials enforced mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + err := inboundAuth.ValidateCredentials(InboundCredentials{ + Username: "abc1", + Digest: "bcd1", + Type: ClientCredential, + }) + assert.Error(t, err) + }) + + t.Run("unknown credential type for inbound", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + err := inboundAuth.ValidateCredentials(InboundCredentials{ + Username: "abc1", + Digest: "bcd1", + Type: Unknown, + }) + assert.Error(t, err) + }) + + t.Run("non client credential type for inbound", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + err := inboundAuth.ValidateCredentials(InboundCredentials{ + Username: "abc1", + Digest: "bcd1", + Type: PeerCredential, + }) + assert.Error(t, err) + }) +} + +func TestInbound_ValidateCredentialsFromThriftContext(t *testing.T) { + t.Run("valid credentials no auth mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupNoAuthMode() + thriftCtx := createThriftContextWithHeaders( + context.Background(), + map[string]string{AuthUsername: "abc", AuthPassword: "bcd"}, + ) + err := inboundAuth.ValidateCredentialsFromThriftContext(thriftCtx, ClientCredential) + assert.NoError(t, err) + }) + + t.Run("invalid credentials no auth mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupNoAuthMode() + thriftCtx := createThriftContextWithHeaders( + context.Background(), + map[string]string{AuthUsername: "abc1", AuthPassword: "bcd1"}) + err := inboundAuth.ValidateCredentialsFromThriftContext(thriftCtx, ClientCredential) + assert.NoError(t, err) + }) + + t.Run("valid credentials shadow mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupShadowMode() + thriftCtx := createThriftContextWithHeaders( + context.Background(), + map[string]string{AuthUsername: "abc", AuthPassword: "bcd"}) + err := inboundAuth.ValidateCredentialsFromThriftContext(thriftCtx, ClientCredential) + assert.NoError(t, err) + }) + + t.Run("invalid credentials shadow mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupShadowMode() + thriftCtx := createThriftContextWithHeaders( + context.Background(), + map[string]string{AuthUsername: "abc1", AuthPassword: "bcd1"}) + err := inboundAuth.ValidateCredentialsFromThriftContext(thriftCtx, ClientCredential) + assert.NoError(t, err) + }) + + t.Run("valid credentials enforced mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + thriftCtx := createThriftContextWithHeaders( + context.Background(), + map[string]string{AuthUsername: "abc", AuthPassword: "bcd"}) + err := inboundAuth.ValidateCredentialsFromThriftContext(thriftCtx, ClientCredential) + assert.NoError(t, err) + }) + + t.Run("invalid credentials enforced mode", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + thriftCtx := createThriftContextWithHeaders( + context.Background(), + map[string]string{AuthUsername: "abc1", AuthPassword: "bcd1"}) + err := inboundAuth.ValidateCredentialsFromThriftContext(thriftCtx, ClientCredential) + assert.Error(t, err) + }) + + t.Run("unknown credential type for inbound", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + thriftCtx := createThriftContextWithHeaders( + context.Background(), + map[string]string{AuthUsername: "abc", AuthPassword: "bcd"}) + err := inboundAuth.ValidateCredentialsFromThriftContext(thriftCtx, Unknown) + assert.Error(t, err) + }) + + t.Run("non client credential type for inbound", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + thriftCtx := createThriftContextWithHeaders( + context.Background(), + map[string]string{AuthUsername: "abc", AuthPassword: "bcd"}) + err := inboundAuth.ValidateCredentialsFromThriftContext(thriftCtx, PeerCredential) + assert.Error(t, err) + }) + + t.Run("non client credential type for inbound", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + thriftCtx := createThriftContextWithHeaders( + context.Background(), + map[string]string{"some_u": "abc", "some_p": "bcd"}) + err := inboundAuth.ValidateCredentialsFromThriftContext(thriftCtx, ClientCredential) + assert.Error(t, err) + }) +} + +func TestInbound_MatchCredentials(t *testing.T) { + t.Run("matching credentials", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + c1 := &InboundCredentials{ + Username: "abc", + Digest: "bcd", + Type: ClientCredential, + } + + c2 := &InboundCredentials{ + Username: "abc", + Digest: "bcd", + Type: ClientCredential, + } + + ok := inboundAuth.MatchCredentials(*c1, *c2) + assert.True(t, ok) + }) + + t.Run("non matching credentials", func(t *testing.T) { + inboundAuth := inboundAuthSetupEnabledMode() + c1 := &InboundCredentials{ + Username: "abc", + Digest: "bcd", + Type: ClientCredential, + } + + c2 := &InboundCredentials{ + Username: "abc1", + Digest: "bcd", + Type: ClientCredential, + } + + ok := inboundAuth.MatchCredentials(*c1, *c2) + assert.False(t, ok) + }) +} + +func inboundAuthSetupNoAuthMode() *Inbound { + return &Inbound{ + clientCredentials: []InboundCredentials{ + { + + Username: "abc", + Digest: "bcd", + Type: ClientCredential, + }, { + + Username: "foo", + Digest: "zoo", + Type: ClientCredential, + }, + }, + authMode: AuthModeNoAuth, + } +} + +func inboundAuthSetupShadowMode() *Inbound { + return &Inbound{ + clientCredentials: []InboundCredentials{ + { + Username: "abc", + Digest: "bcd", + Type: ClientCredential, + }, { + + Username: "foo", + Digest: "zoo", + Type: ClientCredential, + }, + }, + authMode: AuthModeShadow, + } +} + +func inboundAuthSetupEnabledMode() *Inbound { + return &Inbound{ + clientCredentials: []InboundCredentials{ + { + + Username: "abc", + Digest: "bcd", + Type: ClientCredential, + }, { + + Username: "foo", + Digest: "zoo", + Type: ClientCredential, + }, + }, + authMode: AuthModeEnforced, + } +} + +func createThriftContextWithHeaders(ctx context.Context, headers map[string]string) thrift.Context { + return thrift.WithHeaders(ctx, headers) +} diff --git a/src/dbnode/auth/integration/setup_config.go b/src/dbnode/auth/integration/setup_config.go new file mode 100644 index 0000000000..a91ebfa044 --- /dev/null +++ b/src/dbnode/auth/integration/setup_config.go @@ -0,0 +1,196 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package integration + +import ( + "github.com/m3db/m3/src/cmd/services/m3dbnode/config" + + "gopkg.in/yaml.v2" +) + +// BaseConfigWithAuthEnabled encapsulate auth enabled config. +const BaseConfigWithAuthEnabled = ` +outbounds: + m3db: + services: + - service: + zone: m3_abc11 + username: m3_bcd11 + password: m3_xyz11 + - service: + zone: m3_abc1 + username: m3_bcd21 + password: m3_xyz21 + + etcd: + services: + - service: + zone: etcd_abc11 + username: etcd_bcd11 + password: etcd_xyz11 + - service: + zone: etcd_abc1 + username: etcd_bcd21 + password: etcd_xyz21 + +inbounds: + m3db: + mode: ENABLED + credentials: + - username: user + digest: digest11 + - username: user2 + digest: digest21 +` + +// BaseConfigWithAuthEnabledRefresh encapsulate auth enabled config refreshed. +const BaseConfigWithAuthEnabledRefresh = ` +outbounds: + m3db: + services: + - service: + zone: m3_abc2 + username: m3_bcd2 + password: m3_xyz2 + - service: + zone: m3_abc22 + username: m3_bcd22 + password: m3_xyz22 + + etcd: + services: + - service: + zone: etcd_abc12 + username: etcd_bcd12 + password: etcd_xyz12 + - service: + zone: etcd_abc22 + username: etcd_bcd22 + password: etcd_xyz22 + +inbounds: + m3db: + mode: ENABLED + credentials: + - username: user12 + digest: digest12 + - username: user22 + digest: digest22 +` + +// BaseConfigWithAuthDisabled encapsulate auth disabled config. +const BaseConfigWithAuthDisabled = ` +outbounds: + m3db: + services: + - service: + zone: m3_abc11 + username: m3_bcd11 + password: m3_xyz11 + - service: + zone: m3_abc1 + username: m3_bcd21 + password: m3_xyz21 + + etcd: + services: + - service: + zone: etcd_abc11 + username: etcd_bcd11 + password: etcd_xyz11 + - service: + zone: etcd_abc1 + username: etcd_bcd21 + password: etcd_xyz21 + +inbounds: + m3db: + mode: NONE + credentials: + - username: user + digest: digest11 + - username: user2 + digest: digest21 +` + +// BaseConfigWithOutbounds encapsulate auth config with valid outbounds. +const BaseConfigWithOutbounds = ` +outbounds: + m3db: + services: + - service: + zone: m3_abc11 + username: m3_bcd11 + password: m3_xyz11 + - service: + zone: m3_abc1 + username: m3_bcd21 + password: m3_xyz21 + + etcd: + services: + - service: + zone: etcd_abc11 + username: etcd_bcd11 + password: etcd_xyz11 + - service: + zone: etcd_abc1 + username: etcd_bcd21 + password: etcd_xyz21 + +` + +// BaseConfigWithOutboundsrefreshed encapsulate auth config with valid outbounds refresh. +const BaseConfigWithOutboundsrefreshed = ` +outbounds: + m3db: + services: + - service: + zone: m3_abc11 + username: m3_bcd12 + password: m3_xyz12 + - service: + zone: m3_abc1 + username: m3_bcd22 + password: m3_xyz22 + + etcd: + services: + - service: + zone: etcd_abc11 + username: etcd_bcd12 + password: etcd_xyz12 + - service: + zone: etcd_abc1 + username: etcd_bcd22 + password: etcd_xyz22 + +` + +// CreateTestConfigYaml returns authconfig. +func CreateTestConfigYaml(cfg string) *config.AuthConfig { + newSecrets := &config.AuthConfig{} + err := yaml.Unmarshal([]byte(cfg), newSecrets) + if err != nil { + return nil + } + return newSecrets +} diff --git a/src/dbnode/auth/module.go b/src/dbnode/auth/module.go new file mode 100644 index 0000000000..4bcbb9466e --- /dev/null +++ b/src/dbnode/auth/module.go @@ -0,0 +1,68 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +import ( + "sync" +) + +const ( + // AuthPassword defines password key in thrift ctx. + AuthPassword = "password" + + // AuthUsername defines username key in thrift ctx. + AuthUsername = "username" + + // AuthZone defines zone key in thrift ctx. + AuthZone = "zone" +) + +var ( + // InboundAuth encapsulates inbound credentials for the dbnode. + InboundAuth *Inbound + // InboundLock encapsulates mutual exclusion lock, used while refreshing creds. + InboundLock = &sync.Mutex{} + + // OutboundAuth encapsulates outbound credentials for dbnode and client. + OutboundAuth *Outbound +) + +// PopulateDefaultAuthConfig populates default auth config in case of no AuthConfig is present. +func PopulateDefaultAuthConfig() { + InboundAuth = &Inbound{authMode: AuthModeNoAuth} + OutboundAuth = &Outbound{} +} + +// PopulateInbound populates inbound credentials for dbnode. +func PopulateInbound(clientCredentials []InboundCredentials, authMode Mode) { + InboundAuth = &Inbound{ + clientCredentials: clientCredentials, + authMode: authMode, + } +} + +// PopulateOutbound populates outbound credentials for dbnode/clients. +func PopulateOutbound(peerCredentials []OutboundCredentials, etcdCredentials []OutboundCredentials) { + OutboundAuth = &Outbound{ + peerCredentials: peerCredentials, + etcdCredentials: etcdCredentials, + } +} diff --git a/src/dbnode/auth/outbound.go b/src/dbnode/auth/outbound.go new file mode 100644 index 0000000000..7193f14a6f --- /dev/null +++ b/src/dbnode/auth/outbound.go @@ -0,0 +1,63 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +import ( + "github.com/uber/tchannel-go/thrift" +) + +// Outbound encapsulates outbound credentials. +type Outbound struct { + peerCredentials []OutboundCredentials + etcdCredentials []OutboundCredentials +} + +// WrapThriftContextWithPeerCreds wraps thrift context with outbound peer credential +// depending on CredentialType and zone. +func (o *Outbound) WrapThriftContextWithPeerCreds(tctx thrift.Context, zone string) thrift.Context { + for _, peerCred := range o.peerCredentials { + if peerCred.Zone == zone { + return wrapTctxWithCredentials(tctx, peerCred) + } + } + return tctx +} + +func wrapTctxWithCredentials(tCtx thrift.Context, creds OutboundCredentials) thrift.Context { + return thrift.WithHeaders(tCtx, map[string]string{ + AuthUsername: creds.Username, + AuthPassword: creds.Password}, + ) +} + +// FetchOutboundEtcdCredentials fetches outbound etcd credentials. +func (o *Outbound) FetchOutboundEtcdCredentials(zone string) *OutboundCredentials { + for _, peerCred := range o.etcdCredentials { + if peerCred.Zone == zone { + return &OutboundCredentials{ + Username: peerCred.Username, + Password: peerCred.Password, + Zone: zone, + } + } + } + return nil +} diff --git a/src/dbnode/auth/outbound_test.go b/src/dbnode/auth/outbound_test.go new file mode 100644 index 0000000000..32ad5c9725 --- /dev/null +++ b/src/dbnode/auth/outbound_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package auth + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/uber/tchannel-go/thrift" +) + +func TestWrapThriftContextWithCreds(t *testing.T) { + outbound := &Outbound{ + peerCredentials: []OutboundCredentials{ + { + Username: "test", + Password: "password", + Zone: "test-zone", + }, + }, + } + tctx, _ := thrift.NewContext(time.Minute) + tctx = thrift.WithHeaders(tctx, map[string]string{ + "key1": "value1", + "key2": "value2", + }) + + wrappedTctx := outbound.WrapThriftContextWithPeerCreds(tctx, "test-zone") + assert.Equal(t, "test", wrappedTctx.Headers()["username"], "Expected username in wrapped context headers") + assert.Equal(t, "password", wrappedTctx.Headers()["password"], "Expected password in wrapped context headers") + + assert.Equal(t, "", wrappedTctx.Headers()["key1"], "Expected initial keys must be flushed") + assert.Equal(t, "", wrappedTctx.Headers()["key2"], "Expected initial keys must be flushed") +} + +func TestWrapThriftContextWithOutboundPeerCredsNil(t *testing.T) { + outbound := &Outbound{} + tctx, _ := thrift.NewContext(time.Minute) + tctx = thrift.WithHeaders(tctx, map[string]string{ + "key1": "value1", + "key2": "value2", + }) + + wrappedTctx := outbound.WrapThriftContextWithPeerCreds(tctx, "test-zone") + assert.Equal(t, "", wrappedTctx.Headers()["username"], "Expected no username in wrapped context headers") + assert.Equal(t, "", wrappedTctx.Headers()["password"], "Expected no password in wrapped context headers") + + assert.Equal(t, "value1", wrappedTctx.Headers()["key1"], "Expected initial keys must be present") + assert.Equal(t, "value2", wrappedTctx.Headers()["key2"], "Expected initial keys must be present") +} + +func TestFetchOutboundEtcdCredentials(t *testing.T) { + outbound := &Outbound{ + etcdCredentials: []OutboundCredentials{ + { + Username: "test", + Password: "password", + Zone: "test-zone", + }, + }, + } + creds := outbound.FetchOutboundEtcdCredentials("test-zone") + assert.Equal(t, &outbound.etcdCredentials[0], creds, "Expected no error with matching zone creds") +} + +func TestWrapThriftContextWithOutboundEtcdCredsNil(t *testing.T) { + outbound := &Outbound{} + creds := outbound.FetchOutboundEtcdCredentials("test-zone") + assert.Nil(t, creds, "Expected no credential") +} + +func TestFetchOutboundEtcdCredentialsIncorrectZone(t *testing.T) { + outbound := &Outbound{ + etcdCredentials: []OutboundCredentials{ + { + Username: "test", + Password: "password", + Zone: "test-zone", + }, + }, + } + creds := outbound.FetchOutboundEtcdCredentials("foo") + assert.Nil(t, creds, "Expected response obj to be nil") +} diff --git a/src/dbnode/client/client_creds_provider.go b/src/dbnode/client/client_creds_provider.go new file mode 100644 index 0000000000..e4ccc5a53b --- /dev/null +++ b/src/dbnode/client/client_creds_provider.go @@ -0,0 +1,65 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package client + +import ( + "github.com/m3db/m3/src/dbnode/auth" + "github.com/m3db/m3/src/dbnode/environment" +) + +// PopulateClientOutboundAuthConfig takes clients dynamic config as populate auth global registry. +func PopulateClientOutboundAuthConfig(cfg environment.DynamicConfiguration) error { + var dbnodeCredentials []auth.OutboundCredentials + var etcdCredentails []auth.OutboundCredentials + + for _, clusterCfg := range cfg { + if ok := clusterCfg.Service.Auth.Enabled; ok { + if err := clusterCfg.Service.Auth.Validate(); err != nil { + return err + } + nodeCredential := auth.OutboundCredentials{ + Username: clusterCfg.Service.Auth.UserName, + Password: clusterCfg.Service.Auth.Password, + Zone: clusterCfg.Service.Zone, + Type: auth.ClientCredential, + } + dbnodeCredentials = append(dbnodeCredentials, nodeCredential) + } + + etcdClusters := clusterCfg.Service.ETCDClusters + for _, etcdCluster := range etcdClusters { + if ok := etcdCluster.Auth.Enabled; ok { + if err := etcdCluster.Auth.Validate(); err != nil { + return err + } + etcdCredential := auth.OutboundCredentials{ + Username: etcdCluster.Auth.UserName, + Password: etcdCluster.Auth.Password, + Zone: etcdCluster.Zone, + Type: auth.EtcdCredential, + } + etcdCredentails = append(etcdCredentails, etcdCredential) + } + } + } + auth.PopulateOutbound(dbnodeCredentials, etcdCredentails) + return nil +} diff --git a/src/dbnode/client/client_creds_provider_test.go b/src/dbnode/client/client_creds_provider_test.go new file mode 100644 index 0000000000..064336c83e --- /dev/null +++ b/src/dbnode/client/client_creds_provider_test.go @@ -0,0 +1,159 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package client + +import ( + "os" + "testing" + + "github.com/m3db/m3/src/dbnode/auth" + xconfig "github.com/m3db/m3/src/x/config" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfigurationWithAuthClientProvider(t *testing.T) { + clientCfg := ` + config: + service: + env: default_env + zone: embedded + service: m3db + auth: + enabled: true + username: user_db + password: pass_db + etcdClusters: + - zone: embedded + endpoints: + - etcd:2379 + - etcd:456 + auth: + enabled: true + username: user_etcd + password: pass_etcd + writeConsistencyLevel: majority + readConsistencyLevel: unstrict_majority +` + outboundErr := setupAndLoadCfg(t, clientCfg) + require.NoError(t, outboundErr) +} + +func TestConfigurationWithIncorrectDbAuth(t *testing.T) { + clientCfg := ` + config: + service: + env: default_env + zone: embedded + service: m3db + auth: + enabled: true + password: pass_db + etcdClusters: + - zone: embedded + endpoints: + - etcd:2379 + - etcd:456 + auth: + enabled: true + username: user_etcd + password: pass_etcd + writeConsistencyLevel: majority + readConsistencyLevel: unstrict_majority +` + outboundErr := setupAndLoadCfg(t, clientCfg) + require.Error(t, outboundErr) +} + +func TestConfigurationWithIncorrectEtcdAuth(t *testing.T) { + clientCfg := ` + config: + service: + env: default_env + zone: embedded + service: m3db + auth: + enabled: true + username: user_db + password: pass_db + etcdClusters: + - zone: embedded + endpoints: + - etcd:2379 + - etcd:456 + auth: + enabled: true + password: pass_etcd + writeConsistencyLevel: majority + readConsistencyLevel: unstrict_majority +` + outboundErr := setupAndLoadCfg(t, clientCfg) + require.Error(t, outboundErr) +} + +func TestConfigurationNoAuth(t *testing.T) { + clientCfg := ` + config: + service: + env: default_env + zone: embedded + service: m3db + auth: + enabled: false + username: user_db + password: pass_db + etcdClusters: + - zone: embedded + endpoints: + - etcd:2379 + - etcd:456 + auth: + enabled: false + password: pass_etcd + writeConsistencyLevel: majority + readConsistencyLevel: unstrict_majority +` + outboundErr := setupAndLoadCfg(t, clientCfg) + require.NoError(t, outboundErr) +} + +func setupAndLoadCfg(t *testing.T, config string) error { + fd, err := os.CreateTemp("", "config.yaml") + require.NoError(t, err) + defer func() { + assert.NoError(t, fd.Close()) + assert.NoError(t, os.Remove(fd.Name())) + }() + + _, err = fd.WriteString(config) + require.NoError(t, err) + + var cfg Configuration + err = xconfig.LoadFile(&cfg, fd.Name(), xconfig.Options{}) + require.NoError(t, err) + + defer outboundAuthCleanup() + return PopulateClientOutboundAuthConfig(cfg.EnvironmentConfig.Services) +} +func outboundAuthCleanup() { + auth.OutboundAuth = &auth.Outbound{} +} diff --git a/src/dbnode/client/client_mock.go b/src/dbnode/client/client_mock.go index a997fa514b..53ab3f1eef 100644 --- a/src/dbnode/client/client_mock.go +++ b/src/dbnode/client/client_mock.go @@ -1,7 +1,7 @@ // Code generated by MockGen. DO NOT EDIT. // Source: ../../client/types.go -// Copyright (c) 2022 Uber Technologies, Inc. +// Copyright (c) 2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -1451,6 +1451,20 @@ func (mr *MockOptionsMockRecorder) FetchRetrier() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchRetrier", reflect.TypeOf((*MockOptions)(nil).FetchRetrier)) } +// FetchTopologyInitializerZone mocks base method. +func (m *MockOptions) FetchTopologyInitializerZone() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchTopologyInitializerZone") + ret0, _ := ret[0].(string) + return ret0 +} + +// FetchTopologyInitializerZone indicates an expected call of FetchTopologyInitializerZone. +func (mr *MockOptionsMockRecorder) FetchTopologyInitializerZone() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchTopologyInitializerZone", reflect.TypeOf((*MockOptions)(nil).FetchTopologyInitializerZone)) +} + // HostConnectTimeout mocks base method. func (m *MockOptions) HostConnectTimeout() time.Duration { m.ctrl.T.Helper() @@ -2459,6 +2473,20 @@ func (mr *MockOptionsMockRecorder) SetTopologyInitializer(value interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTopologyInitializer", reflect.TypeOf((*MockOptions)(nil).SetTopologyInitializer), value) } +// SetTopologyInitializerZone mocks base method. +func (m *MockOptions) SetTopologyInitializerZone(value string) Options { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetTopologyInitializerZone", value) + ret0, _ := ret[0].(Options) + return ret0 +} + +// SetTopologyInitializerZone indicates an expected call of SetTopologyInitializerZone. +func (mr *MockOptionsMockRecorder) SetTopologyInitializerZone(value interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTopologyInitializerZone", reflect.TypeOf((*MockOptions)(nil).SetTopologyInitializerZone), value) +} + // SetTruncateRequestTimeout mocks base method. func (m *MockOptions) SetTruncateRequestTimeout(value time.Duration) Options { m.ctrl.T.Helper() @@ -3238,6 +3266,20 @@ func (mr *MockAdminOptionsMockRecorder) FetchSeriesBlocksMetadataBatchTimeout() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchSeriesBlocksMetadataBatchTimeout", reflect.TypeOf((*MockAdminOptions)(nil).FetchSeriesBlocksMetadataBatchTimeout)) } +// FetchTopologyInitializerZone mocks base method. +func (m *MockAdminOptions) FetchTopologyInitializerZone() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchTopologyInitializerZone") + ret0, _ := ret[0].(string) + return ret0 +} + +// FetchTopologyInitializerZone indicates an expected call of FetchTopologyInitializerZone. +func (mr *MockAdminOptionsMockRecorder) FetchTopologyInitializerZone() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchTopologyInitializerZone", reflect.TypeOf((*MockAdminOptions)(nil).FetchTopologyInitializerZone)) +} + // HostConnectTimeout mocks base method. func (m *MockAdminOptions) HostConnectTimeout() time.Duration { m.ctrl.T.Helper() @@ -4372,6 +4414,20 @@ func (mr *MockAdminOptionsMockRecorder) SetTopologyInitializer(value interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTopologyInitializer", reflect.TypeOf((*MockAdminOptions)(nil).SetTopologyInitializer), value) } +// SetTopologyInitializerZone mocks base method. +func (m *MockAdminOptions) SetTopologyInitializerZone(value string) Options { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetTopologyInitializerZone", value) + ret0, _ := ret[0].(Options) + return ret0 +} + +// SetTopologyInitializerZone indicates an expected call of SetTopologyInitializerZone. +func (mr *MockAdminOptionsMockRecorder) SetTopologyInitializerZone(value interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTopologyInitializerZone", reflect.TypeOf((*MockAdminOptions)(nil).SetTopologyInitializerZone), value) +} + // SetTruncateRequestTimeout mocks base method. func (m *MockAdminOptions) SetTruncateRequestTimeout(value time.Duration) Options { m.ctrl.T.Helper() @@ -5378,6 +5434,20 @@ func (mr *MockconnectionPoolMockRecorder) ConnectionCount() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectionCount", reflect.TypeOf((*MockconnectionPool)(nil).ConnectionCount)) } +// GetZone mocks base method. +func (m *MockconnectionPool) GetZone() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetZone") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetZone indicates an expected call of GetZone. +func (mr *MockconnectionPoolMockRecorder) GetZone() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetZone", reflect.TypeOf((*MockconnectionPool)(nil).GetZone)) +} + // NextClient mocks base method. func (m *MockconnectionPool) NextClient() (rpc.TChanNode, Channel, error) { m.ctrl.T.Helper() diff --git a/src/dbnode/client/config.go b/src/dbnode/client/config.go index 2e1a9cb6ae..832d85886d 100644 --- a/src/dbnode/client/config.go +++ b/src/dbnode/client/config.go @@ -338,7 +338,8 @@ func (c Configuration) NewAdminClient( SetInstrumentOptions(iopts). SetLogErrorSampleRate(c.LogErrorSampleRate). SetLogHostWriteErrorSampleRate(c.LogHostWriteErrorSampleRate). - SetLogHostFetchErrorSampleRate(c.LogHostFetchErrorSampleRate) + SetLogHostFetchErrorSampleRate(c.LogHostFetchErrorSampleRate). + SetTopologyInitializerZone(syncTopoInit.FetchZone()) if params.ClockOptions != nil { v = v.SetClockOptions(params.ClockOptions) @@ -474,6 +475,9 @@ func (c Configuration) NewAdminClient( opts = opt(opts) } + if authErr := PopulateClientOutboundAuthConfig(c.EnvironmentConfig.Services); authErr != nil { + return nil, authErr + } asyncClusterOpts := NewOptionsForAsyncClusters(opts, asyncTopoInits, asyncClientOverrides) return NewAdminClient(opts, asyncClusterOpts...) } diff --git a/src/dbnode/client/connection_pool.go b/src/dbnode/client/connection_pool.go index a7c2d90db2..24e70c4053 100644 --- a/src/dbnode/client/connection_pool.go +++ b/src/dbnode/client/connection_pool.go @@ -65,6 +65,7 @@ type connPool struct { sleepHealthRetry sleepFn status status healthStatus tally.Gauge + zone string } type conn struct { @@ -103,6 +104,7 @@ func newConnectionPool(host topology.Host, opts Options) connectionPool { sleepHealth: time.Sleep, sleepHealthRetry: time.Sleep, healthStatus: scope.Gauge("health-status"), + zone: opts.FetchTopologyInitializerZone(), } return p @@ -164,6 +166,10 @@ func (p *connPool) Close() { } } +func (p *connPool) GetZone() string { + return p.zone +} + func (p *connPool) connectEvery(interval time.Duration, stutter time.Duration) { log := p.opts.InstrumentOptions().Logger() target := p.opts.MaxConnectionCount() diff --git a/src/dbnode/client/host_queue.go b/src/dbnode/client/host_queue.go index c600dd6389..dceb6b3ec0 100644 --- a/src/dbnode/client/host_queue.go +++ b/src/dbnode/client/host_queue.go @@ -25,6 +25,7 @@ import ( "context" "errors" "fmt" + "github.com/m3db/m3/src/dbnode/auth" "math" "sync" "time" @@ -549,7 +550,8 @@ func (q *queue) asyncTaggedWrite( } ctx, _ := thrift.NewContext(q.opts.WriteRequestTimeout()) - err = client.WriteTaggedBatchRaw(ctx, req) + authWrappedCtx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(ctx, q.opts.FetchTopologyInitializerZone()) + err = client.WriteTaggedBatchRaw(authWrappedCtx, req) if err == nil { // All succeeded callAllCompletionFns(ops, q.host, nil) @@ -609,7 +611,8 @@ func (q *queue) asyncTaggedWriteV2( } ctx, _ := thrift.NewContext(q.opts.WriteRequestTimeout()) - err = client.WriteTaggedBatchRawV2(ctx, req) + authWrappedCtx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(ctx, q.opts.FetchTopologyInitializerZone()) + err = client.WriteTaggedBatchRawV2(authWrappedCtx, req) if err == nil { // All succeeded callAllCompletionFns(ops, q.host, nil) @@ -674,7 +677,8 @@ func (q *queue) asyncWrite( } ctx, _ := thrift.NewContext(q.opts.WriteRequestTimeout()) - err = client.WriteBatchRaw(ctx, req) + authWrappedCtx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(ctx, q.opts.FetchTopologyInitializerZone()) + err = client.WriteBatchRaw(authWrappedCtx, req) if err == nil { // All succeeded callAllCompletionFns(ops, q.host, nil) @@ -733,7 +737,8 @@ func (q *queue) asyncWriteV2( } ctx, _ := thrift.NewContext(q.opts.WriteRequestTimeout()) - err = client.WriteBatchRawV2(ctx, req) + authWrappedCtx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(ctx, q.opts.FetchTopologyInitializerZone()) + err = client.WriteBatchRawV2(authWrappedCtx, req) if err == nil { // All succeeded. callAllCompletionFns(ops, q.host, nil) @@ -786,7 +791,8 @@ func (q *queue) asyncFetch(op *fetchBatchOp) { } ctx, _ := thrift.NewContext(q.opts.FetchRequestTimeout()) - result, err := client.FetchBatchRaw(ctx, &op.request) + authWrappedCtx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(ctx, q.opts.FetchTopologyInitializerZone()) + result, err := client.FetchBatchRaw(authWrappedCtx, &op.request) if err != nil { op.completeAll(nil, err) cleanup() @@ -837,9 +843,9 @@ func (q *queue) asyncFetchV2( cleanup() return } - ctx, _ := thrift.NewContext(q.opts.FetchRequestTimeout()) - result, err := client.FetchBatchRawV2(ctx, currV2FetchBatchRawReq) + authWrappedCtx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(ctx, q.opts.FetchTopologyInitializerZone()) + result, err := client.FetchBatchRawV2(authWrappedCtx, currV2FetchBatchRawReq) if err != nil { callAllCompletionFns(ops, nil, err) cleanup() @@ -900,7 +906,8 @@ func (q *queue) asyncFetchTagged(op *fetchTaggedOp) { return } - result, err := client.FetchTagged(ctx, &op.request) + authWrappedCtx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(ctx, q.opts.FetchTopologyInitializerZone()) + result, err := client.FetchTagged(authWrappedCtx, &op.request) if err != nil { op.CompletionFn()(fetchTaggedResultAccumulatorOpts{host: q.host}, err) return @@ -945,7 +952,8 @@ func (q *queue) asyncAggregate(op *aggregateOp) { return } - result, err := client.AggregateRaw(ctx, &op.request) + authWrappedCtx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(ctx, q.opts.FetchTopologyInitializerZone()) + result, err := client.AggregateRaw(authWrappedCtx, &op.request) if err != nil { op.CompletionFn()(aggregateResultAccumulatorOpts{host: q.host}, err) return diff --git a/src/dbnode/client/options.go b/src/dbnode/client/options.go index 402336d278..3da3c9da71 100644 --- a/src/dbnode/client/options.go +++ b/src/dbnode/client/options.go @@ -296,6 +296,7 @@ type options struct { writeTimestampOffset time.Duration namespaceInitializer namespace.Initializer thriftContextFn ThriftContextFn + zone string } // NewOptions creates a new set of client options with defaults @@ -320,6 +321,9 @@ func NewOptionsForAsyncClusters(opts Options, topoInits []topology.Initializer, if overrides[i].TargetHostQueueFlushSize != nil { options = options.SetHostQueueOpsFlushSize(*overrides[i].TargetHostQueueFlushSize) } + if topoInit.FetchZone() != "" { + options = options.SetTopologyInitializerZone(topoInit.FetchZone()) + } result = append(result, options) } return result @@ -1171,3 +1175,13 @@ func (o *options) SetThriftContextFn(value ThriftContextFn) Options { func (o *options) ThriftContextFn() ThriftContextFn { return o.thriftContextFn } + +func (o *options) SetTopologyInitializerZone(value string) Options { + opts := *o + opts.zone = value + return &opts +} + +func (o *options) FetchTopologyInitializerZone() string { + return o.zone +} diff --git a/src/dbnode/client/session.go b/src/dbnode/client/session.go index 7251b2e706..5ef749d6ef 100644 --- a/src/dbnode/client/session.go +++ b/src/dbnode/client/session.go @@ -25,6 +25,7 @@ import ( gocontext "context" "errors" "fmt" + "github.com/m3db/m3/src/dbnode/auth" "math" "sort" "strings" @@ -2770,7 +2771,8 @@ func (s *session) streamBlocksMetadataFromPeer( req.IncludeLastRead = &optionIncludeLastRead progress.metadataFetchBatchCall.Inc(1) - result, err := client.FetchBlocksMetadataRawV2(tctx, req) + authWrappedCtx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(tctx, s.opts.FetchTopologyInitializerZone()) + result, err := client.FetchBlocksMetadataRawV2(authWrappedCtx, req) if err != nil { progress.metadataFetchBatchError.Inc(1) return err @@ -3386,7 +3388,8 @@ func (s *session) streamBlocksBatchFromPeer( var attemptErr error borrowErr := peer.BorrowConnection(func(client rpc.TChanNode, _ Channel) { tctx, _ := thrift.NewContext(s.streamBlocksBatchTimeout) - result, attemptErr = client.FetchBlocksRaw(tctx, req) + authWrappedCtx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(tctx, s.opts.FetchTopologyInitializerZone()) + result, attemptErr = client.FetchBlocksRaw(authWrappedCtx, req) }) err := xerrors.FirstError(borrowErr, attemptErr) return err diff --git a/src/dbnode/client/types.go b/src/dbnode/client/types.go index 78206bd158..ba8f918c58 100644 --- a/src/dbnode/client/types.go +++ b/src/dbnode/client/types.go @@ -731,6 +731,10 @@ type Options interface { // ThriftContextFn returns the retrier for streaming blocks. ThriftContextFn() ThriftContextFn + + SetTopologyInitializerZone(value string) Options + + FetchTopologyInitializerZone() string } // ThriftContextFn turns a context into a thrift context for a thrift call. @@ -848,6 +852,8 @@ type connectionPool interface { // Close the connection pool. Close() + + GetZone() string } type peerSource interface { diff --git a/src/dbnode/environment/config.go b/src/dbnode/environment/config.go index bf45a8527a..c223d92d39 100644 --- a/src/dbnode/environment/config.go +++ b/src/dbnode/environment/config.go @@ -178,6 +178,7 @@ type ConfigureResult struct { KVStore kv.Store Async bool ClientOverrides ClientOverrides + Zone string } // ConfigureResults stores initializers and kv store for dynamic and static configs @@ -335,6 +336,7 @@ func (c Configuration) configureDynamic(cfgParams ConfigurationParameters) (Conf KVStore: kv, Async: cluster.Async, ClientOverrides: cluster.ClientOverrides, + Zone: cluster.Service.Zone, } cfgResults = append(cfgResults, result) } diff --git a/src/dbnode/network/server/tchannelthrift/node/service.go b/src/dbnode/network/server/tchannelthrift/node/service.go index 86659db164..bc08a0aeaa 100644 --- a/src/dbnode/network/server/tchannelthrift/node/service.go +++ b/src/dbnode/network/server/tchannelthrift/node/service.go @@ -24,6 +24,7 @@ import ( goctx "context" "errors" "fmt" + "github.com/m3db/m3/src/dbnode/auth" "runtime" "sort" "sync" @@ -440,9 +441,10 @@ func (s *service) Bootstrapped(ctx thrift.Context) (*rpc.NodeBootstrappedResult_ // BootstrappedInPlacementOrNoPlacement is designed to be used with cluster // management tools like k8s that expected an endpoint that will return // success if the node either: -// 1) Has no cluster placement set yet. -// 2) Is bootstrapped and durable, meaning it is bootstrapped and is able -// to bootstrap the shards it owns from it's own local disk. +// 1. Has no cluster placement set yet. +// 2. Is bootstrapped and durable, meaning it is bootstrapped and is able +// to bootstrap the shards it owns from it's own local disk. +// // This is useful in addition to the Bootstrapped RPC method as it helps // progress node addition/removal/modifications when no placement is set // at all and therefore the node has not been able to bootstrap yet. @@ -1159,6 +1161,9 @@ func (i *idResult) WriteSegments(ctx context.Context, dst []*rpc.Segments) ([]*r } func (s *service) Aggregate(tctx thrift.Context, req *rpc.AggregateQueryRequest) (*rpc.AggregateQueryResult_, error) { + if authErr := auth.InboundAuth.ValidateCredentialsFromThriftContext(tctx, auth.ClientCredential); authErr != nil { + return nil, authErr + } db, err := s.startReadRPCWithDB() if err != nil { return nil, err @@ -1203,6 +1208,9 @@ func (s *service) Aggregate(tctx thrift.Context, req *rpc.AggregateQueryRequest) } func (s *service) AggregateRaw(tctx thrift.Context, req *rpc.AggregateQueryRawRequest) (*rpc.AggregateQueryRawResult_, error) { + if authErr := auth.InboundAuth.ValidateCredentialsFromThriftContext(tctx, auth.ClientCredential); authErr != nil { + return nil, authErr + } db, err := s.startReadRPCWithDB() if err != nil { return nil, err diff --git a/src/dbnode/server/server.go b/src/dbnode/server/server.go index b1b856c4b4..5101a810df 100644 --- a/src/dbnode/server/server.go +++ b/src/dbnode/server/server.go @@ -45,6 +45,7 @@ import ( "github.com/m3db/m3/src/cluster/placementhandler" "github.com/m3db/m3/src/cluster/placementhandler/handleroptions" "github.com/m3db/m3/src/cmd/services/m3dbnode/config" + "github.com/m3db/m3/src/dbnode/auth" "github.com/m3db/m3/src/dbnode/client" "github.com/m3db/m3/src/dbnode/encoding" "github.com/m3db/m3/src/dbnode/encoding/m3tsz" @@ -129,6 +130,9 @@ type RunOptions struct { // instead of parsing ConfigFile if ConfigFile is not specified. Config config.DBConfiguration + // Secrets encapsulates auth config for dbnode inbounds and outbounds. + Secrets config.AuthConfig + // BootstrapCh is a channel to listen on to be notified of bootstrap. BootstrapCh chan<- struct{} @@ -192,6 +196,15 @@ func Run(runOpts RunOptions) { os.Exit(1) } + secretsValidateErr := runOpts.Secrets.Validate() + if secretsValidateErr == nil { + // Passing valid parsed AuthConfig to populate inbound and outbound credentials. + PopulateOutboundAuthConfig(runOpts.Secrets) + PopulateInboundAuthConfig(runOpts.Secrets) + } else { + auth.PopulateDefaultAuthConfig() + } + logger, err := cfg.LoggingOrDefault().BuildLogger() if err != nil { // NB(r): Use fmt.Fprintf(os.Stderr, ...) to avoid etcd.SetGlobals() diff --git a/src/dbnode/server/server_creds_provider.go b/src/dbnode/server/server_creds_provider.go new file mode 100644 index 0000000000..04fc24bdc6 --- /dev/null +++ b/src/dbnode/server/server_creds_provider.go @@ -0,0 +1,92 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package server + +import ( + "strings" + + "github.com/m3db/m3/src/cmd/services/m3dbnode/config" + "github.com/m3db/m3/src/dbnode/auth" +) + +var ( + authModeMap = map[string]auth.Mode{ + "none": auth.AuthModeNoAuth, + "shadow": auth.AuthModeShadow, + "enabled": auth.AuthModeEnforced, + } +) + +// PopulateInboundAuthConfig populates inbound auth modules with the provided auth config. +func PopulateInboundAuthConfig(cfg config.AuthConfig) { + nodeInbound := cfg.Inbound.M3DB + inboundAuth := make([]auth.InboundCredentials, 0, len(nodeInbound.Credentials)) + for _, nodeCfg := range nodeInbound.Credentials { + inboundAuth = append(inboundAuth, auth.InboundCredentials{ + Username: *nodeCfg.Username, + Digest: *nodeCfg.Digest, + Type: auth.ClientCredential, + }) + } + authMode := parseAuthMode(*cfg.Inbound.M3DB.Mode) + auth.PopulateInbound(inboundAuth, authMode) +} + +// RefreshInboundAuthConfig take AuthConfig as input param and populates inbound credentials global module. +func RefreshInboundAuthConfig(credentialsConfig config.AuthConfig) { + auth.InboundLock.Lock() + defer auth.InboundLock.Unlock() + PopulateInboundAuthConfig(credentialsConfig) +} + +// PopulateOutboundAuthConfig populates outbound auth modules with the provided auth config. +func PopulateOutboundAuthConfig(cfg config.AuthConfig) { + nodeOutboundPeer := cfg.Outbound.M3DB + outboundPeerAuth := make([]auth.OutboundCredentials, 0, len(nodeOutboundPeer.NodeConfig)) + for _, nodeCfg := range nodeOutboundPeer.NodeConfig { + outboundPeerAuth = append(outboundPeerAuth, auth.OutboundCredentials{ + Username: *nodeCfg.Service.Username, + Password: *nodeCfg.Service.Password, + Zone: nodeCfg.Service.Zone, + Type: auth.PeerCredential, + }) + } + + nodeOutboundEtcd := cfg.Outbound.Etcd + outboundEtcdAuth := make([]auth.OutboundCredentials, 0, len(nodeOutboundEtcd.NodeConfig)) + for _, nodeCfg := range nodeOutboundEtcd.NodeConfig { + outboundEtcdAuth = append(outboundEtcdAuth, auth.OutboundCredentials{ + Username: *nodeCfg.Service.Username, + Password: *nodeCfg.Service.Password, + Zone: nodeCfg.Service.Zone, + Type: auth.EtcdCredential, + }) + } + auth.PopulateOutbound(outboundPeerAuth, outboundEtcdAuth) +} + +func parseAuthMode(str string) auth.Mode { + c, ok := authModeMap[strings.ToLower(str)] + if !ok { + return auth.AuthModeUnknown + } + return c +} diff --git a/src/dbnode/server/server_creds_provider_test.go b/src/dbnode/server/server_creds_provider_test.go new file mode 100644 index 0000000000..002dfd1184 --- /dev/null +++ b/src/dbnode/server/server_creds_provider_test.go @@ -0,0 +1,166 @@ +// Copyright (c) 2023 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package server + +import ( + "testing" + "time" + + "github.com/m3db/m3/src/dbnode/auth" + "github.com/m3db/m3/src/dbnode/auth/integration" + + "github.com/stretchr/testify/assert" + "github.com/uber/tchannel-go/thrift" +) + +func TestInboundCfgPopulate(t *testing.T) { + defer inboundAuthCleanup() + authCfg := integration.CreateTestConfigYaml(integration.BaseConfigWithAuthEnabled) + + PopulateInboundAuthConfig(*authCfg) + + for _, nodeCfg := range authCfg.Inbound.M3DB.Credentials { + err := auth.InboundAuth.ValidateCredentials(auth.InboundCredentials{ + Username: *nodeCfg.Username, + Digest: *nodeCfg.Digest, + Type: auth.ClientCredential, + }) + assert.NoError(t, err) + } +} + +func TestInboundCfgPopulateWithRefresh(t *testing.T) { + defer inboundAuthCleanup() + authCfg := integration.CreateTestConfigYaml(integration.BaseConfigWithAuthEnabled) + + PopulateInboundAuthConfig(*authCfg) + + for _, nodeCfg := range authCfg.Inbound.M3DB.Credentials { + err := auth.InboundAuth.ValidateCredentials(auth.InboundCredentials{ + Username: *nodeCfg.Username, + Digest: *nodeCfg.Digest, + Type: auth.ClientCredential, + }) + assert.NoError(t, err) + } + + refreshAuthCfg := integration.CreateTestConfigYaml(integration.BaseConfigWithAuthEnabledRefresh) + RefreshInboundAuthConfig(*refreshAuthCfg) + + for _, nodeCfg := range refreshAuthCfg.Inbound.M3DB.Credentials { + err := auth.InboundAuth.ValidateCredentials(auth.InboundCredentials{ + Username: *nodeCfg.Username, + Digest: *nodeCfg.Digest, + Type: auth.ClientCredential, + }) + assert.NoError(t, err) + } +} + +func TestInboundCfgPopulateWithIncorrectCredsAuthDisabled(t *testing.T) { + defer inboundAuthCleanup() + authCfg := integration.CreateTestConfigYaml(integration.BaseConfigWithAuthDisabled) + PopulateInboundAuthConfig(*authCfg) + + for _, nodeCfg := range authCfg.Inbound.M3DB.Credentials { + err := auth.InboundAuth.ValidateCredentials(auth.InboundCredentials{ + Username: "some_random_u", + Digest: *nodeCfg.Digest, + Type: auth.ClientCredential, + }) + assert.NoError(t, err) + } + + refreshAuthCfg := integration.CreateTestConfigYaml(integration.BaseConfigWithAuthEnabledRefresh) + RefreshInboundAuthConfig(*refreshAuthCfg) + + for _, nodeCfg := range authCfg.Inbound.M3DB.Credentials { + err := auth.InboundAuth.ValidateCredentials(auth.InboundCredentials{ + Username: *nodeCfg.Username, + Digest: "some_random_p", + Type: auth.ClientCredential, + }) + assert.Error(t, err) + } +} + +func TestInboundCfgPopulateWithCorrectCredsAuthDisabledRefreshAuthEnabled(t *testing.T) { + defer inboundAuthCleanup() + authCfg := integration.CreateTestConfigYaml(integration.BaseConfigWithAuthDisabled) + PopulateInboundAuthConfig(*authCfg) + + for _, nodeCfg := range authCfg.Inbound.M3DB.Credentials { + err := auth.InboundAuth.ValidateCredentials(auth.InboundCredentials{ + Username: "some_random_u", + Digest: *nodeCfg.Digest, + Type: auth.ClientCredential, + }) + assert.NoError(t, err) + } + + refreshAuthCfg := integration.CreateTestConfigYaml(integration.BaseConfigWithAuthEnabledRefresh) + RefreshInboundAuthConfig(*refreshAuthCfg) + + for _, nodeCfg := range refreshAuthCfg.Inbound.M3DB.Credentials { + err := auth.InboundAuth.ValidateCredentials(auth.InboundCredentials{ + Username: *nodeCfg.Username, + Digest: *nodeCfg.Digest, + Type: auth.ClientCredential, + }) + assert.NoError(t, err) + } +} + +func TestPeerOutboundCfgPopulate(t *testing.T) { + defer outboundAuthCleanup() + authCfg := integration.CreateTestConfigYaml(integration.BaseConfigWithOutbounds) + + PopulateOutboundAuthConfig(*authCfg) + tCtx, _ := thrift.NewContext(time.Minute) + + for _, nodeCfg := range authCfg.Outbound.M3DB.NodeConfig { + newTctx := auth.OutboundAuth.WrapThriftContextWithPeerCreds(tCtx, nodeCfg.Service.Zone) + assert.Equal(t, *nodeCfg.Service.Username, newTctx.Headers()["username"]) + assert.Equal(t, *nodeCfg.Service.Password, newTctx.Headers()["password"]) + } +} + +func TestEtcdOutboundCfgPopulate(t *testing.T) { + defer outboundAuthCleanup() + authCfg := integration.CreateTestConfigYaml(integration.BaseConfigWithOutbounds) + + PopulateOutboundAuthConfig(*authCfg) + + for _, nodeCfg := range authCfg.Outbound.Etcd.NodeConfig { + newTctx := auth.OutboundAuth.FetchOutboundEtcdCredentials(nodeCfg.Service.Zone) + assert.Equal(t, *nodeCfg.Service.Username, newTctx.Username) + assert.Equal(t, *nodeCfg.Service.Password, newTctx.Password) + assert.Equal(t, nodeCfg.Service.Zone, newTctx.Zone) + } +} + +func inboundAuthCleanup() { + auth.InboundAuth = &auth.Inbound{} +} + +func outboundAuthCleanup() { + auth.OutboundAuth = &auth.Outbound{} +} diff --git a/src/dbnode/topology/dynamic.go b/src/dbnode/topology/dynamic.go index 2c9d06251b..6cc2038b54 100644 --- a/src/dbnode/topology/dynamic.go +++ b/src/dbnode/topology/dynamic.go @@ -46,6 +46,7 @@ type dynamicInitializer struct { sync.Mutex opts DynamicOptions topo Topology + zone string } // NewDynamicInitializer returns a dynamic topology initializer @@ -93,6 +94,10 @@ func (i *dynamicInitializer) TopologyIsSet() (bool, error) { return true, nil } +func (i *dynamicInitializer) FetchZone() string { + return i.zone +} + type dynamicTopology struct { sync.RWMutex opts DynamicOptions diff --git a/src/dbnode/topology/static.go b/src/dbnode/topology/static.go index c3bd9515a4..bbe1f9f1ad 100644 --- a/src/dbnode/topology/static.go +++ b/src/dbnode/topology/static.go @@ -51,6 +51,10 @@ func (i staticInitializer) TopologyIsSet() (bool, error) { return true, nil } +func (i staticInitializer) FetchZone() string { + return "" +} + type staticTopology struct { w xwatch.Watchable } diff --git a/src/dbnode/topology/topology_mock.go b/src/dbnode/topology/topology_mock.go index 29a8501abe..4c82a0c8e7 100644 --- a/src/dbnode/topology/topology_mock.go +++ b/src/dbnode/topology/topology_mock.go @@ -1,7 +1,7 @@ // Code generated by MockGen. DO NOT EDIT. // Source: ../../topology/types.go -// Copyright (c) 2022 Uber Technologies, Inc. +// Copyright (c) 2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -175,6 +175,20 @@ func (m *MockInitializer) EXPECT() *MockInitializerMockRecorder { return m.recorder } +// FetchZone mocks base method. +func (m *MockInitializer) FetchZone() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchZone") + ret0, _ := ret[0].(string) + return ret0 +} + +// FetchZone indicates an expected call of FetchZone. +func (mr *MockInitializerMockRecorder) FetchZone() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchZone", reflect.TypeOf((*MockInitializer)(nil).FetchZone)) +} + // Init mocks base method. func (m *MockInitializer) Init() (Topology, error) { m.ctrl.T.Helper() diff --git a/src/dbnode/topology/types.go b/src/dbnode/topology/types.go index 1756b93bbd..1c80f0304b 100644 --- a/src/dbnode/topology/types.go +++ b/src/dbnode/topology/types.go @@ -59,6 +59,8 @@ type Initializer interface { // initialized immediately or if instead it will blockingly // wait to be set on initialization TopologyIsSet() (bool, error) + + FetchZone() string } // Topology is a container of a topology map and disseminates topology map changes diff --git a/src/x/config/configflag/flag.go b/src/x/config/configflag/flag.go index 2e1f5bd873..9b912f0a66 100644 --- a/src/x/config/configflag/flag.go +++ b/src/x/config/configflag/flag.go @@ -38,6 +38,9 @@ type Options struct { // ConfigFiles (-f) is a list of config files to load ConfigFiles FlagStringSlice + // CredentialsFile (-c) loads secrets yaml files. + CredentialsFile FlagStringSlice + // ShouldDumpConfigAndExit (-d) causes MainLoad to print config to stdout, // and then exit. ShouldDumpConfigAndExit bool @@ -59,13 +62,15 @@ func (opts *Options) RegisterFlagSet(cmd *flag.FlagSet) { opts.cmd = cmd cmd.Var(&opts.ConfigFiles, "f", "Configuration files to load") + cmd.Var(&opts.CredentialsFile, "c", "Credential files") cmd.BoolVar(&opts.ShouldDumpConfigAndExit, "d", false, "Dump configuration and exit") } // MainLoad is a convenience method, intended for use in main(), which handles all // config commandline options. It: -// - Dumps config and exits if -d was passed. -// - Loads configuration otherwise. +// - Dumps config and exits if -d was passed. +// - Loads configuration otherwise. +// // Users who want a subset of this behavior should call individual methods. func (opts *Options) MainLoad(target interface{}, loadOpts config.Options) error { osFns := opts.osFns @@ -92,12 +97,30 @@ func (opts *Options) MainLoad(target interface{}, loadOpts config.Options) error return nil } +// CredentialLoad is a method used in main(), which handles secrets commandline options. It loads the secrets yaml +// file if present with -c flag. +func (opts *Options) CredentialLoad(target interface{}, loadOpts config.Options) (bool, error) { + if len(opts.CredentialsFile.Value) == 0 { + opts.cmd.Usage() + return false, nil + } + + if err := config.LoadFiles(target, opts.CredentialsFile.Value, loadOpts); err != nil { + return false, fmt.Errorf("unable to load secrets from %s: %w", opts.CredentialsFile.Value, err) + } + return true, nil +} + // FlagStringSlice represents a slice of strings. When used as a flag variable, // it allows for multiple string values. For example, it can be used like this: -// var configFiles FlagStringSlice -// flag.Var(&configFiles, "f", "configuration file(s)") +// +// var configFiles FlagStringSlice +// flag.Var(&configFiles, "f", "configuration file(s)") +// // Then it can be invoked like this: -// ./app -f file1.yaml -f file2.yaml -f valueN.yaml +// +// ./app -f file1.yaml -f file2.yaml -f valueN.yaml +// // Finally, when the flags are parsed, the variable contains all the values. type FlagStringSlice struct { Value []string diff --git a/src/x/config/configflag/flag_test.go b/src/x/config/configflag/flag_test.go index 25cae70922..41c362211b 100644 --- a/src/x/config/configflag/flag_test.go +++ b/src/x/config/configflag/flag_test.go @@ -67,6 +67,8 @@ func TestCommandLineOptions(t *testing.T) { expectedConfig := testConfig{Foo: 1, Bar: "bar"} configFileOpts := []string{"-f", "./testdata/config1.yaml", "-f", "./testdata/config2.yaml"} + configSecretFileOpts := []string{"-c", "./testdata/config1.yaml", "-c", "./testdata/config2.yaml"} + t.Run("loads config from files", func(t *testing.T) { tctx, teardown := setup(t) defer teardown() @@ -109,6 +111,29 @@ func TestCommandLineOptions(t *testing.T) { "-f is required (no config files provided)") assert.True(t, tctx.UsageCalled) }) + t.Run("loads yaml files from -c flag", func(t *testing.T) { + tctx, teardown := setup(t) + defer teardown() + + require.NoError(t, tctx.Flags.Parse(configSecretFileOpts)) + + var cfg testConfig + _, err := tctx.Opts.CredentialLoad(&cfg, config.Options{}) + require.NoError(t, err) + assert.Equal(t, expectedConfig, cfg) + }) + + t.Run("loads yaml files from -c flag", func(t *testing.T) { + tctx, teardown := setup(t) + defer teardown() + + require.NoError(t, tctx.Flags.Parse([]string{})) + + var cfg testConfig + isPresent, err := tctx.Opts.CredentialLoad(&cfg, config.Options{}) + require.NoError(t, err) + require.False(t, isPresent) + }) } func TestFlagArray(t *testing.T) {