From f67043a76e579fb9d217c82d924857f0d807cb05 Mon Sep 17 00:00:00 2001 From: Brendan Myers Date: Wed, 9 Jul 2025 22:03:26 +1000 Subject: [PATCH 1/2] feat: api key auth + config reorg --- cmd/main.go | 13 +- config.yaml.sample | 37 +++-- proxy/proxy.go | 87 +++++++---- proxy/proxy_test.go | 163 ++++++++++++++------ utils/config.go | 21 ++- utils/config_test.go | 360 +++++++++++++++++++++++++++++++++++-------- 6 files changed, 512 insertions(+), 169 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index dce4589..3205819 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -149,9 +149,9 @@ func configureProxy(proxyConns *proxy.Conn, cfg *utils.Config) error { metricsHandler := metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{ // Todo: do we need these many attributes? InitialAttributes: attribute.NewSet( - attribute.String("proxy-id", t.ProxyId), - attribute.String("target", t.Target), - attribute.String("namespace", t.Namespace), + attribute.String("proxy_id", t.ProxyId), + attribute.String("namespace", t.TemporalCloud.Namespace), + attribute.String("host_port", t.TemporalCloud.HostPort), attribute.String("auth_type", authType), attribute.String("encryption_key", t.EncryptionKey), ), @@ -172,12 +172,7 @@ func configureProxy(proxyConns *proxy.Conn, cfg *utils.Config) error { } err := proxyConns.AddConn(proxy.AddConnInput{ - ProxyId: t.ProxyId, - Target: t.Target, - TLSCertPath: t.TLS.CertFile, - TLSKeyPath: t.TLS.KeyFile, - EncryptionKeyID: t.EncryptionKey, - Namespace: t.Namespace, + Target: &t, AuthManager: authManager, AuthType: authType, MetricsHandler: metricsHandler, diff --git a/config.yaml.sample b/config.yaml.sample index 542a687..6c60903 100644 --- a/config.yaml.sample +++ b/config.yaml.sample @@ -1,21 +1,29 @@ server: port: 7233 host: "0.0.0.0" + metrics: port: 9090 + encryption: caching: max_cache: 100 max_age: "10m" max_usage: 100 + targets: - - source: "..internal" - target: "..tmprl.cloud:7233" - tls: - cert_file: "/path/to/./tls.crt" - key_file: "/path/to/./tls.key" + - proxy_id: "..internal" + temporal_cloud: + namespace: "." + host_port: "..tmprl.cloud:7233" # endpoint when using mTLS + # host_port: "..api.temporal.io:7233" # endpoint when using API keys + authentication: + # only set either tls or api_key + tls: + cert_file: "/path/to/./tls.crt" + key_file: "/path/to/./tls.key" + api_key: "" encryption_key: "" - namespace: "." authentication: type: "spiffe" config: @@ -24,13 +32,18 @@ targets: audiences: - "temporal_cloud_proxy" - - source: "..internal" - target: "..tmprl.cloud:7233" - tls: - cert_file: "/path/to/./tls.crt" - key_file: "/path/to/./tls.key" + - proxy_id: "..internal" + temporal_cloud: + namespace: "." + host_port: "..tmprl.cloud:7233" # endpoint when using mTLS + # host_port: "..api.temporal.io:7233" # endpoint when using API keys + authentication: + # only set either tls or api_key + tls: + cert_file: "/path/to/./tls.crt" + key_file: "/path/to/./tls.key" + api_key: "ey..." encryption_key: "" - namespace: "." authentication: type: "spiffe" config: diff --git a/proxy/proxy.go b/proxy/proxy.go index 3b0fd13..add6df0 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -5,24 +5,22 @@ import ( "crypto/tls" "errors" "fmt" - "go.temporal.io/sdk/converter" - "os" - "sync" - "temporal-sa/temporal-cloud-proxy/codec" - "temporal-sa/temporal-cloud-proxy/crypto" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/kms" - - "temporal-sa/temporal-cloud-proxy/auth" - "temporal-sa/temporal-cloud-proxy/metrics" - + "go.temporal.io/sdk/converter" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "os" + "sync" + "temporal-sa/temporal-cloud-proxy/auth" + "temporal-sa/temporal-cloud-proxy/codec" + "temporal-sa/temporal-cloud-proxy/crypto" + "temporal-sa/temporal-cloud-proxy/metrics" + "temporal-sa/temporal-cloud-proxy/utils" ) type Conn struct { @@ -59,12 +57,7 @@ func createKMSClient() *kms.KMS { // AddConnInput contains parameters for adding a new connection type AddConnInput struct { - ProxyId string - Target string - TLSCertPath string - TLSKeyPath string - EncryptionKeyID string - Namespace string + Target *utils.TargetConfig AuthManager *auth.AuthManager AuthType string MetricsHandler metrics.MetricsHandler @@ -73,18 +66,19 @@ type AddConnInput struct { // AddConn adds a new connection to the proxy func (mc *Conn) AddConn(input AddConnInput) error { - fmt.Printf("Adding connection id: %s target: %s\n", input.ProxyId, input.Target) + fmt.Printf("Adding connection id: %s namespace: %s hostport: %s\n", + input.Target.ProxyId, input.Target.TemporalCloud.Namespace, input.Target.TemporalCloud.HostPort) - cert, err := tls.LoadX509KeyPair(input.TLSCertPath, input.TLSKeyPath) - if err != nil { - return err + if input.Target.TemporalCloud.Authentication.ApiKey != "" && input.Target.TemporalCloud.Authentication.TLS != nil { + return fmt.Errorf("%s: cannot have both api key and mtls authentication configured on a single target", + input.Target.ProxyId) } //Initialize AWS KMS client kmsClient := createKMSClient() codecContext := map[string]string{ - "namespace": input.Namespace, + "namespace": input.Target.TemporalCloud.Namespace, } clientInterceptor, err := converter.NewPayloadCodecGRPCClientInterceptor( @@ -92,7 +86,7 @@ func (mc *Conn) AddConn(input AddConnInput) error { Codecs: []converter.PayloadCodec{codec.NewEncryptionCodecWithCaching( kmsClient, codecContext, - input.EncryptionKeyID, + input.Target.EncryptionKey, input.MetricsHandler, input.CryptoCachingConfig, )}, @@ -102,21 +96,52 @@ func (mc *Conn) AddConn(input AddConnInput) error { return err } + tlsConfig := tls.Config{} + + grpcInterceptors := []grpc.UnaryClientInterceptor{ + clientInterceptor, + } + + if input.Target.TemporalCloud.Authentication.ApiKey != "" { + grpcInterceptors = append(grpcInterceptors, + func(ctx context.Context, method string, req any, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + md, ok := metadata.FromIncomingContext(ctx) + + if ok { + md = md.Copy() + md.Delete("authorization") + md.Delete("temporal-namespace") + + ctx = metadata.NewOutgoingContext(ctx, md) + ctx = metadata.AppendToOutgoingContext(ctx, "temporal-namespace", input.Target.TemporalCloud.Namespace) + ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+input.Target.TemporalCloud.Authentication.ApiKey) + } + + return invoker(ctx, method, req, reply, cc, opts...) + }) + } else { + cert, err := tls.LoadX509KeyPair(input.Target.TemporalCloud.Authentication.TLS.CertFile, + input.Target.TemporalCloud.Authentication.TLS.KeyFile) + if err != nil { + return err + } + + tlsConfig.Certificates = []tls.Certificate{cert} + } + conn, err := grpc.NewClient( - input.Target, + input.Target.TemporalCloud.HostPort, grpc.WithTransportCredentials(credentials.NewTLS( - &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, + &tlsConfig, )), - grpc.WithUnaryInterceptor(clientInterceptor), + grpc.WithChainUnaryInterceptor(grpcInterceptors...), ) if err != nil { return err } mc.mu.Lock() - mc.namespace[input.ProxyId] = NamespaceConn{ + mc.namespace[input.Target.ProxyId] = NamespaceConn{ conn: conn, authManager: input.AuthManager, authType: input.AuthType, @@ -137,8 +162,10 @@ func (mc *Conn) CloseAll() error { if err := namespace.conn.Close(); err != nil { errs = append(errs, err) } - if err := namespace.authManager.Close(); err != nil { - errs = append(errs, err) + if namespace.authManager != nil { + if err := namespace.authManager.Close(); err != nil { + errs = append(errs, err) + } } } diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 5f96621..1b89a20 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -17,6 +17,8 @@ import ( "time" "temporal-sa/temporal-cloud-proxy/auth" + "temporal-sa/temporal-cloud-proxy/metrics" + "temporal-sa/temporal-cloud-proxy/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -116,60 +118,123 @@ func TestConn_AddConn(t *testing.T) { errorMsg string }{ { - name: "successful connection addition", + name: "successful connection addition with TLS", input: AddConnInput{ - ProxyId: "test-proxy-id", - Target: "localhost:7233", - TLSCertPath: certPath, - TLSKeyPath: keyPath, - EncryptionKeyID: "test-key-id", - Namespace: "test-namespace", - AuthManager: nil, // Use nil for simplicity in tests - AuthType: "jwt", + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + TLS: &utils.TLSConfig{ + CertFile: certPath, + KeyFile: keyPath, + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, // Use nil for simplicity in tests + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, + }, + expectError: false, + }, + { + name: "successful connection addition with API key", + input: AddConnInput{ + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id-api", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + ApiKey: "test-api-key", + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, }, expectError: false, }, { name: "invalid certificate path", input: AddConnInput{ - ProxyId: "test-proxy-id", - Target: "localhost:7233", - TLSCertPath: "/nonexistent/cert.pem", - TLSKeyPath: keyPath, - EncryptionKeyID: "test-key-id", - Namespace: "test-namespace", - AuthManager: nil, - AuthType: "jwt", + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + TLS: &utils.TLSConfig{ + CertFile: "/nonexistent/cert.pem", + KeyFile: keyPath, + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, }, expectError: true, }, { name: "invalid key path", input: AddConnInput{ - ProxyId: "test-proxy-id", - Target: "localhost:7233", - TLSCertPath: certPath, - TLSKeyPath: "/nonexistent/key.pem", - EncryptionKeyID: "test-key-id", - Namespace: "test-namespace", - AuthManager: nil, - AuthType: "jwt", + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + TLS: &utils.TLSConfig{ + CertFile: certPath, + KeyFile: "/nonexistent/key.pem", + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, }, expectError: true, }, { - name: "connection without auth manager", + name: "both API key and TLS configured - should error", input: AddConnInput{ - ProxyId: "test-proxy-id-no-auth", - Target: "localhost:7233", - TLSCertPath: certPath, - TLSKeyPath: keyPath, - EncryptionKeyID: "test-key-id", - Namespace: "test-namespace", - AuthManager: nil, - AuthType: "", + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + ApiKey: "test-api-key", + TLS: &utils.TLSConfig{ + CertFile: certPath, + KeyFile: keyPath, + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, }, - expectError: false, + expectError: true, + errorMsg: "cannot have both api key and mtls authentication", }, } @@ -188,7 +253,7 @@ func TestConn_AddConn(t *testing.T) { assert.Equal(t, 1, len(conn.namespace)) // Verify the connection was stored correctly - nsConn, exists := conn.namespace[tt.input.ProxyId] + nsConn, exists := conn.namespace[tt.input.Target.ProxyId] assert.True(t, exists) assert.NotNil(t, nsConn.conn) assert.Equal(t, tt.input.AuthManager, nsConn.authManager) @@ -430,14 +495,24 @@ func TestConn_ConcurrentAccess(t *testing.T) { defer wg.Done() input := AddConnInput{ - ProxyId: fmt.Sprintf("proxy-id-%d", id), - Target: "localhost:7233", - TLSCertPath: certPath, - TLSKeyPath: keyPath, - EncryptionKeyID: "test-key-id", - Namespace: fmt.Sprintf("namespace-%d", id), - AuthManager: nil, - AuthType: "jwt", + Target: &utils.TargetConfig{ + ProxyId: fmt.Sprintf("proxy-id-%d", id), + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: fmt.Sprintf("namespace-%d", id), + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + TLS: &utils.TLSConfig{ + CertFile: certPath, + KeyFile: keyPath, + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, } err := conn.AddConn(input) diff --git a/utils/config.go b/utils/config.go index 97c363d..39fbe4c 100644 --- a/utils/config.go +++ b/utils/config.go @@ -27,12 +27,21 @@ type CachingConfig struct { } type TargetConfig struct { - ProxyId string `yaml:"proxy_id"` - Target string `yaml:"target"` - TLS TLSConfig `yaml:"tls"` - EncryptionKey string `yaml:"encryption_key"` - Namespace string `yaml:"namespace"` - Authentication *AuthConfig `yaml:"authentication,omitempty"` + ProxyId string `yaml:"proxy_id"` + TemporalCloud TemporalCloudConfig `yaml:"temporal_cloud"` + EncryptionKey string `yaml:"encryption_key"` + Authentication *AuthConfig `yaml:"authentication,omitempty"` +} + +type TemporalCloudConfig struct { + Namespace string `yaml:"namespace"` + HostPort string `yaml:"host_port"` + Authentication TemporalAuthConfig `yaml:"authentication"` +} + +type TemporalAuthConfig struct { + TLS *TLSConfig `yaml:"tls,omitempty"` + ApiKey string `yaml:"api_key,omitempty"` } type TLSConfig struct { diff --git a/utils/config_test.go b/utils/config_test.go index 53e1f30..463d599 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -14,19 +14,28 @@ func TestConfig_UnmarshalYAML(t *testing.T) { wantErr bool }{ { - name: "valid complete config", + name: "valid complete config with TLS authentication", yamlData: ` server: port: 7233 host: "0.0.0.0" +metrics: + port: 8080 +encryption: + caching: + max_cache: 100 + max_age: "1h" + max_usage: 1000 targets: - proxy_id: "test.namespace.internal" - target: "test.namespace.tmprl.cloud:7233" - tls: - cert_file: "/path/to/cert.crt" - key_file: "/path/to/key.key" + temporal_cloud: + namespace: "test.namespace" + host_port: "test.namespace.tmprl.cloud:7233" + authentication: + tls: + cert_file: "/path/to/cert.crt" + key_file: "/path/to/key.key" encryption_key: "test-key" - namespace: "test.namespace" authentication: type: "spiffe" config: @@ -40,16 +49,30 @@ targets: Port: 7233, Host: "0.0.0.0", }, + Metrics: MetricsConfig{ + Port: 8080, + }, + Encryption: EncryptionConfig{ + Caching: CachingConfig{ + MaxCache: 100, + MaxAge: "1h", + MaxUsage: 1000, + }, + }, Targets: []TargetConfig{ { - ProxyId: "test.namespace.internal", - Target: "test.namespace.tmprl.cloud:7233", - EncryptionKey: "test-key", - Namespace: "test.namespace", - TLS: TLSConfig{ - CertFile: "/path/to/cert.crt", - KeyFile: "/path/to/key.key", + ProxyId: "test.namespace.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "test.namespace", + HostPort: "test.namespace.tmprl.cloud:7233", + Authentication: TemporalAuthConfig{ + TLS: &TLSConfig{ + CertFile: "/path/to/cert.crt", + KeyFile: "/path/to/key.key", + }, + }, }, + EncryptionKey: "test-key", Authentication: &AuthConfig{ Type: "spiffe", Config: map[string]interface{}{ @@ -64,35 +87,44 @@ targets: wantErr: false, }, { - name: "minimal config without authentication", + name: "valid config with API key authentication", yamlData: ` server: port: 8080 host: "localhost" +metrics: + port: 9090 targets: - proxy_id: "simple.internal" - target: "simple.external:8080" - tls: - cert_file: "/cert.crt" - key_file: "/key.key" + temporal_cloud: + namespace: "simple" + host_port: "simple.external:8080" + authentication: + api_key: "your-api-key-here" encryption_key: "simple-key" - namespace: "simple" `, want: Config{ Server: ServerConfig{ Port: 8080, Host: "localhost", }, + Metrics: MetricsConfig{ + Port: 9090, + }, + Encryption: EncryptionConfig{ + Caching: CachingConfig{}, + }, Targets: []TargetConfig{ { - ProxyId: "simple.internal", - Target: "simple.external:8080", - EncryptionKey: "simple-key", - Namespace: "simple", - TLS: TLSConfig{ - CertFile: "/cert.crt", - KeyFile: "/key.key", + ProxyId: "simple.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "simple", + HostPort: "simple.external:8080", + Authentication: TemporalAuthConfig{ + ApiKey: "your-api-key-here", + }, }, + EncryptionKey: "simple-key", Authentication: nil, }, }, @@ -100,26 +132,33 @@ targets: wantErr: false, }, { - name: "multiple targets", + name: "multiple targets with mixed authentication", yamlData: ` server: port: 9090 host: "127.0.0.1" +metrics: + port: 8081 +encryption: + caching: + max_cache: 50 targets: - proxy_id: "target1.internal" - target: "target1.external:9090" - tls: - cert_file: "/target1.crt" - key_file: "/target1.key" + temporal_cloud: + namespace: "namespace1" + host_port: "target1.external:9090" + authentication: + tls: + cert_file: "/target1.crt" + key_file: "/target1.key" encryption_key: "key1" - namespace: "namespace1" - proxy_id: "target2.internal" - target: "target2.external:9091" - tls: - cert_file: "/target2.crt" - key_file: "/target2.key" + temporal_cloud: + namespace: "namespace2" + host_port: "target2.external:9091" + authentication: + api_key: "target2-api-key" encryption_key: "key2" - namespace: "namespace2" authentication: type: "oauth" config: @@ -131,27 +170,40 @@ targets: Port: 9090, Host: "127.0.0.1", }, + Metrics: MetricsConfig{ + Port: 8081, + }, + Encryption: EncryptionConfig{ + Caching: CachingConfig{ + MaxCache: 50, + }, + }, Targets: []TargetConfig{ { - ProxyId: "target1.internal", - Target: "target1.external:9090", - EncryptionKey: "key1", - Namespace: "namespace1", - TLS: TLSConfig{ - CertFile: "/target1.crt", - KeyFile: "/target1.key", + ProxyId: "target1.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "namespace1", + HostPort: "target1.external:9090", + Authentication: TemporalAuthConfig{ + TLS: &TLSConfig{ + CertFile: "/target1.crt", + KeyFile: "/target1.key", + }, + }, }, + EncryptionKey: "key1", Authentication: nil, }, { - ProxyId: "target2.internal", - Target: "target2.external:9091", - EncryptionKey: "key2", - Namespace: "namespace2", - TLS: TLSConfig{ - CertFile: "/target2.crt", - KeyFile: "/target2.key", + ProxyId: "target2.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "namespace2", + HostPort: "target2.external:9091", + Authentication: TemporalAuthConfig{ + ApiKey: "target2-api-key", + }, }, + EncryptionKey: "key2", Authentication: &AuthConfig{ Type: "oauth", Config: map[string]interface{}{ @@ -244,14 +296,18 @@ func TestServerConfig_Validation(t *testing.T) { func TestTargetConfig_Structure(t *testing.T) { target := TargetConfig{ - ProxyId: "test.internal", - Target: "test.external:7233", - EncryptionKey: "test-key", - Namespace: "test-namespace", - TLS: TLSConfig{ - CertFile: "/path/to/cert.crt", - KeyFile: "/path/to/key.key", + ProxyId: "test.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "test.external:7233", + Authentication: TemporalAuthConfig{ + TLS: &TLSConfig{ + CertFile: "/path/to/cert.crt", + KeyFile: "/path/to/key.key", + }, + }, }, + EncryptionKey: "test-key", Authentication: &AuthConfig{ Type: "spiffe", Config: map[string]interface{}{ @@ -263,20 +319,20 @@ func TestTargetConfig_Structure(t *testing.T) { if target.ProxyId != "test.internal" { t.Errorf("Expected ProxyId to be 'test.internal', got %s", target.ProxyId) } - if target.Target != "test.external:7233" { - t.Errorf("Expected Target to be 'test.external:7233', got %s", target.Target) + if target.TemporalCloud.HostPort != "test.external:7233" { + t.Errorf("Expected TemporalCloud.HostPort to be 'test.external:7233', got %s", target.TemporalCloud.HostPort) } if target.EncryptionKey != "test-key" { t.Errorf("Expected EncryptionKey to be 'test-key', got %s", target.EncryptionKey) } - if target.Namespace != "test-namespace" { - t.Errorf("Expected Namespace to be 'test-namespace', got %s", target.Namespace) + if target.TemporalCloud.Namespace != "test-namespace" { + t.Errorf("Expected TemporalCloud.Namespace to be 'test-namespace', got %s", target.TemporalCloud.Namespace) } - if target.TLS.CertFile != "/path/to/cert.crt" { - t.Errorf("Expected TLS.CertFile to be '/path/to/cert.crt', got %s", target.TLS.CertFile) + if target.TemporalCloud.Authentication.TLS.CertFile != "/path/to/cert.crt" { + t.Errorf("Expected TemporalCloud.Authentication.TLS.CertFile to be '/path/to/cert.crt', got %s", target.TemporalCloud.Authentication.TLS.CertFile) } - if target.TLS.KeyFile != "/path/to/key.key" { - t.Errorf("Expected TLS.KeyFile to be '/path/to/key.key', got %s", target.TLS.KeyFile) + if target.TemporalCloud.Authentication.TLS.KeyFile != "/path/to/key.key" { + t.Errorf("Expected TemporalCloud.Authentication.TLS.KeyFile to be '/path/to/key.key', got %s", target.TemporalCloud.Authentication.TLS.KeyFile) } if target.Authentication == nil { t.Error("Expected Authentication to not be nil") @@ -342,6 +398,155 @@ func TestAuthConfig_Types(t *testing.T) { } } +func TestMetricsConfig_Structure(t *testing.T) { + tests := []struct { + name string + config MetricsConfig + want int + }{ + { + name: "default metrics port", + config: MetricsConfig{Port: 8080}, + want: 8080, + }, + { + name: "custom metrics port", + config: MetricsConfig{Port: 9090}, + want: 9090, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.config.Port != tt.want { + t.Errorf("Expected Port to be %d, got %d", tt.want, tt.config.Port) + } + }) + } +} + +func TestEncryptionConfig_Structure(t *testing.T) { + tests := []struct { + name string + config EncryptionConfig + want CachingConfig + }{ + { + name: "full caching config", + config: EncryptionConfig{ + Caching: CachingConfig{ + MaxCache: 100, + MaxAge: "1h", + MaxUsage: 1000, + }, + }, + want: CachingConfig{ + MaxCache: 100, + MaxAge: "1h", + MaxUsage: 1000, + }, + }, + { + name: "partial caching config", + config: EncryptionConfig{ + Caching: CachingConfig{ + MaxCache: 50, + }, + }, + want: CachingConfig{ + MaxCache: 50, + }, + }, + { + name: "empty caching config", + config: EncryptionConfig{ + Caching: CachingConfig{}, + }, + want: CachingConfig{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.config.Caching.MaxCache != tt.want.MaxCache { + t.Errorf("Expected MaxCache to be %d, got %d", tt.want.MaxCache, tt.config.Caching.MaxCache) + } + if tt.config.Caching.MaxAge != tt.want.MaxAge { + t.Errorf("Expected MaxAge to be %s, got %s", tt.want.MaxAge, tt.config.Caching.MaxAge) + } + if tt.config.Caching.MaxUsage != tt.want.MaxUsage { + t.Errorf("Expected MaxUsage to be %d, got %d", tt.want.MaxUsage, tt.config.Caching.MaxUsage) + } + }) + } +} + +func TestTemporalAuthConfig_Structure(t *testing.T) { + tests := []struct { + name string + config TemporalAuthConfig + desc string + }{ + { + name: "TLS authentication", + config: TemporalAuthConfig{ + TLS: &TLSConfig{ + CertFile: "/path/to/cert.crt", + KeyFile: "/path/to/key.key", + }, + }, + desc: "should have TLS config and no API key", + }, + { + name: "API key authentication", + config: TemporalAuthConfig{ + ApiKey: "test-api-key", + }, + desc: "should have API key and no TLS config", + }, + { + name: "empty authentication", + config: TemporalAuthConfig{}, + desc: "should have neither TLS nor API key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + switch tt.name { + case "TLS authentication": + if tt.config.TLS == nil { + t.Error("Expected TLS to not be nil") + } else { + if tt.config.TLS.CertFile != "/path/to/cert.crt" { + t.Errorf("Expected CertFile to be '/path/to/cert.crt', got %s", tt.config.TLS.CertFile) + } + if tt.config.TLS.KeyFile != "/path/to/key.key" { + t.Errorf("Expected KeyFile to be '/path/to/key.key', got %s", tt.config.TLS.KeyFile) + } + } + if tt.config.ApiKey != "" { + t.Errorf("Expected ApiKey to be empty, got %s", tt.config.ApiKey) + } + case "API key authentication": + if tt.config.ApiKey != "test-api-key" { + t.Errorf("Expected ApiKey to be 'test-api-key', got %s", tt.config.ApiKey) + } + if tt.config.TLS != nil { + t.Error("Expected TLS to be nil") + } + case "empty authentication": + if tt.config.TLS != nil { + t.Error("Expected TLS to be nil") + } + if tt.config.ApiKey != "" { + t.Errorf("Expected ApiKey to be empty, got %s", tt.config.ApiKey) + } + } + }) + } +} + // Helper function to compare Config structs func configEqual(a, b Config) bool { if a.Server.Port != b.Server.Port || a.Server.Host != b.Server.Host { @@ -363,14 +568,33 @@ func configEqual(a, b Config) bool { } func targetConfigEqual(a, b TargetConfig) bool { - if a.ProxyId != b.ProxyId || a.Target != b.Target || a.EncryptionKey != b.EncryptionKey || a.Namespace != b.Namespace { + if a.ProxyId != b.ProxyId || a.EncryptionKey != b.EncryptionKey { return false } - if a.TLS.CertFile != b.TLS.CertFile || a.TLS.KeyFile != b.TLS.KeyFile { + // Compare TemporalCloud configuration + if a.TemporalCloud.Namespace != b.TemporalCloud.Namespace || a.TemporalCloud.HostPort != b.TemporalCloud.HostPort { return false } + // Compare TemporalCloud Authentication + if a.TemporalCloud.Authentication.ApiKey != b.TemporalCloud.Authentication.ApiKey { + return false + } + + // Compare TLS configuration + if (a.TemporalCloud.Authentication.TLS == nil) != (b.TemporalCloud.Authentication.TLS == nil) { + return false + } + + if a.TemporalCloud.Authentication.TLS != nil && b.TemporalCloud.Authentication.TLS != nil { + if a.TemporalCloud.Authentication.TLS.CertFile != b.TemporalCloud.Authentication.TLS.CertFile || + a.TemporalCloud.Authentication.TLS.KeyFile != b.TemporalCloud.Authentication.TLS.KeyFile { + return false + } + } + + // Compare proxy Authentication (spiffe, oauth, etc.) if (a.Authentication == nil) != (b.Authentication == nil) { return false } From ae15c34fb40d11e224557a5a8d5b66f64b1acfe3 Mon Sep 17 00:00:00 2001 From: Brendan Myers Date: Thu, 10 Jul 2025 10:05:39 +1000 Subject: [PATCH 2/2] feat: use env var for api key --- Makefile | 2 +- config.yaml.sample | 18 ++++--- proxy/proxy.go | 29 ++++++++-- proxy/proxy_test.go | 33 ++++++++++-- utils/config.go | 9 +++- utils/config_test.go | 125 +++++++++++++++++++++++++++++++++++++------ 6 files changed, 183 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index 5bca199..d3a3ab3 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ OUTPUT_PATH := $(OUTPUT_DIR)/$(BINARY_NAME) .PHONY: all build clean test test-verbose test-coverage test-race test-short test-clean benchmark test-auth test-crypto test-proxy test-utils -all: build +all: test build build: go build -o $(OUTPUT_PATH) $(CMD_DIR) diff --git a/config.yaml.sample b/config.yaml.sample index 6c60903..533ce17 100644 --- a/config.yaml.sample +++ b/config.yaml.sample @@ -12,17 +12,18 @@ encryption: max_usage: 100 targets: - - proxy_id: "..internal" + - proxy_id: "" temporal_cloud: namespace: "." host_port: "..tmprl.cloud:7233" # endpoint when using mTLS # host_port: "..api.temporal.io:7233" # endpoint when using API keys - authentication: - # only set either tls or api_key + authentication: # only set either tls or api_key, not both tls: cert_file: "/path/to/./tls.crt" key_file: "/path/to/./tls.key" - api_key: "" + api_key: # only set either value or env, not both + value: "" + env: encryption_key: "" authentication: type: "spiffe" @@ -32,17 +33,18 @@ targets: audiences: - "temporal_cloud_proxy" - - proxy_id: "..internal" + - proxy_id: "" temporal_cloud: namespace: "." host_port: "..tmprl.cloud:7233" # endpoint when using mTLS # host_port: "..api.temporal.io:7233" # endpoint when using API keys - authentication: - # only set either tls or api_key + authentication: # only set either tls or api_key, not both tls: cert_file: "/path/to/./tls.crt" key_file: "/path/to/./tls.key" - api_key: "ey..." + api_key: # only set either value or env, not both + value: "" + env: encryption_key: "" authentication: type: "spiffe" diff --git a/proxy/proxy.go b/proxy/proxy.go index add6df0..5d7e8ab 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -69,7 +69,14 @@ func (mc *Conn) AddConn(input AddConnInput) error { fmt.Printf("Adding connection id: %s namespace: %s hostport: %s\n", input.Target.ProxyId, input.Target.TemporalCloud.Namespace, input.Target.TemporalCloud.HostPort) - if input.Target.TemporalCloud.Authentication.ApiKey != "" && input.Target.TemporalCloud.Authentication.TLS != nil { + mc.mu.RLock() + _, exists := mc.namespace[input.Target.ProxyId] + mc.mu.RUnlock() + if exists { + return fmt.Errorf("proxy-id %s already exists", input.Target.ProxyId) + } + + if input.Target.TemporalCloud.Authentication.ApiKey != nil && input.Target.TemporalCloud.Authentication.TLS != nil { return fmt.Errorf("%s: cannot have both api key and mtls authentication configured on a single target", input.Target.ProxyId) } @@ -102,7 +109,23 @@ func (mc *Conn) AddConn(input AddConnInput) error { clientInterceptor, } - if input.Target.TemporalCloud.Authentication.ApiKey != "" { + if apiKeyConfig := input.Target.TemporalCloud.Authentication.ApiKey; apiKeyConfig != nil { + if apiKeyConfig.Value != "" && apiKeyConfig.EnvVar != "" { + // TODO proper logging + fmt.Printf("WARN - multiple values provided for api key, using value. proxy_id: %s\n", input.Target.ProxyId) + } + + apiKey := "" + if apiKeyConfig.Value != "" { + apiKey = apiKeyConfig.Value + } else if apiKeyConfig.EnvVar != "" { + apiKey = os.Getenv(apiKeyConfig.EnvVar) + } + + if apiKey == "" { + return fmt.Errorf("%s: no api key provided", input.Target.ProxyId) + } + grpcInterceptors = append(grpcInterceptors, func(ctx context.Context, method string, req any, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { md, ok := metadata.FromIncomingContext(ctx) @@ -114,7 +137,7 @@ func (mc *Conn) AddConn(input AddConnInput) error { ctx = metadata.NewOutgoingContext(ctx, md) ctx = metadata.AppendToOutgoingContext(ctx, "temporal-namespace", input.Target.TemporalCloud.Namespace) - ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+input.Target.TemporalCloud.Authentication.ApiKey) + ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+apiKey) } return invoker(ctx, method, req, reply, cc, opts...) diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 1b89a20..7e6420a 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -142,7 +142,7 @@ func TestConn_AddConn(t *testing.T) { expectError: false, }, { - name: "successful connection addition with API key", + name: "successful connection addition with API key (value)", input: AddConnInput{ Target: &utils.TargetConfig{ ProxyId: "test-proxy-id-api", @@ -150,7 +150,9 @@ func TestConn_AddConn(t *testing.T) { Namespace: "test-namespace", HostPort: "localhost:7233", Authentication: utils.TemporalAuthConfig{ - ApiKey: "test-api-key", + ApiKey: &utils.TemporalApiKeyConfig{ + Value: "test-api-key", + }, }, }, EncryptionKey: "test-key-id", @@ -162,6 +164,29 @@ func TestConn_AddConn(t *testing.T) { }, expectError: false, }, + { + name: "successful connection addition with API key (env var)", + input: AddConnInput{ + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id-api-env", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + ApiKey: &utils.TemporalApiKeyConfig{ + EnvVar: "TEST_TEMPORAL_API_KEY", + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, + }, + expectError: true, // Will fail because env var is not set + }, { name: "invalid certificate path", input: AddConnInput{ @@ -219,7 +244,9 @@ func TestConn_AddConn(t *testing.T) { Namespace: "test-namespace", HostPort: "localhost:7233", Authentication: utils.TemporalAuthConfig{ - ApiKey: "test-api-key", + ApiKey: &utils.TemporalApiKeyConfig{ + Value: "test-api-key", + }, TLS: &utils.TLSConfig{ CertFile: certPath, KeyFile: keyPath, diff --git a/utils/config.go b/utils/config.go index 39fbe4c..cb32c2d 100644 --- a/utils/config.go +++ b/utils/config.go @@ -40,8 +40,13 @@ type TemporalCloudConfig struct { } type TemporalAuthConfig struct { - TLS *TLSConfig `yaml:"tls,omitempty"` - ApiKey string `yaml:"api_key,omitempty"` + TLS *TLSConfig `yaml:"tls,omitempty"` + ApiKey *TemporalApiKeyConfig `yaml:"api_key,omitempty"` +} + +type TemporalApiKeyConfig struct { + Value string `yaml:"value,omitempty"` + EnvVar string `yaml:"env,omitempty"` } type TLSConfig struct { diff --git a/utils/config_test.go b/utils/config_test.go index 463d599..6ef2639 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -87,7 +87,7 @@ targets: wantErr: false, }, { - name: "valid config with API key authentication", + name: "valid config with API key authentication (value)", yamlData: ` server: port: 8080 @@ -100,7 +100,8 @@ targets: namespace: "simple" host_port: "simple.external:8080" authentication: - api_key: "your-api-key-here" + api_key: + value: "your-api-key-here" encryption_key: "simple-key" `, want: Config{ @@ -121,7 +122,57 @@ targets: Namespace: "simple", HostPort: "simple.external:8080", Authentication: TemporalAuthConfig{ - ApiKey: "your-api-key-here", + ApiKey: &TemporalApiKeyConfig{ + Value: "your-api-key-here", + }, + }, + }, + EncryptionKey: "simple-key", + Authentication: nil, + }, + }, + }, + wantErr: false, + }, + { + name: "valid config with API key authentication (env var)", + yamlData: ` +server: + port: 8080 + host: "localhost" +metrics: + port: 9090 +targets: + - proxy_id: "simple.internal" + temporal_cloud: + namespace: "simple" + host_port: "simple.external:8080" + authentication: + api_key: + env: "TEMPORAL_API_KEY" + encryption_key: "simple-key" +`, + want: Config{ + Server: ServerConfig{ + Port: 8080, + Host: "localhost", + }, + Metrics: MetricsConfig{ + Port: 9090, + }, + Encryption: EncryptionConfig{ + Caching: CachingConfig{}, + }, + Targets: []TargetConfig{ + { + ProxyId: "simple.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "simple", + HostPort: "simple.external:8080", + Authentication: TemporalAuthConfig{ + ApiKey: &TemporalApiKeyConfig{ + EnvVar: "TEMPORAL_API_KEY", + }, }, }, EncryptionKey: "simple-key", @@ -157,7 +208,8 @@ targets: namespace: "namespace2" host_port: "target2.external:9091" authentication: - api_key: "target2-api-key" + api_key: + value: "target2-api-key" encryption_key: "key2" authentication: type: "oauth" @@ -200,7 +252,9 @@ targets: Namespace: "namespace2", HostPort: "target2.external:9091", Authentication: TemporalAuthConfig{ - ApiKey: "target2-api-key", + ApiKey: &TemporalApiKeyConfig{ + Value: "target2-api-key", + }, }, }, EncryptionKey: "key2", @@ -498,12 +552,23 @@ func TestTemporalAuthConfig_Structure(t *testing.T) { desc: "should have TLS config and no API key", }, { - name: "API key authentication", + name: "API key authentication with value", config: TemporalAuthConfig{ - ApiKey: "test-api-key", + ApiKey: &TemporalApiKeyConfig{ + Value: "test-api-key", + }, }, desc: "should have API key and no TLS config", }, + { + name: "API key authentication with env var", + config: TemporalAuthConfig{ + ApiKey: &TemporalApiKeyConfig{ + EnvVar: "TEMPORAL_API_KEY", + }, + }, + desc: "should have API key env var and no TLS config", + }, { name: "empty authentication", config: TemporalAuthConfig{}, @@ -525,12 +590,33 @@ func TestTemporalAuthConfig_Structure(t *testing.T) { t.Errorf("Expected KeyFile to be '/path/to/key.key', got %s", tt.config.TLS.KeyFile) } } - if tt.config.ApiKey != "" { - t.Errorf("Expected ApiKey to be empty, got %s", tt.config.ApiKey) + if tt.config.ApiKey != nil { + t.Error("Expected ApiKey to be nil") + } + case "API key authentication with value": + if tt.config.ApiKey == nil { + t.Error("Expected ApiKey to not be nil") + } else { + if tt.config.ApiKey.Value != "test-api-key" { + t.Errorf("Expected ApiKey.Value to be 'test-api-key', got %s", tt.config.ApiKey.Value) + } + if tt.config.ApiKey.EnvVar != "" { + t.Errorf("Expected ApiKey.EnvVar to be empty, got %s", tt.config.ApiKey.EnvVar) + } + } + if tt.config.TLS != nil { + t.Error("Expected TLS to be nil") } - case "API key authentication": - if tt.config.ApiKey != "test-api-key" { - t.Errorf("Expected ApiKey to be 'test-api-key', got %s", tt.config.ApiKey) + case "API key authentication with env var": + if tt.config.ApiKey == nil { + t.Error("Expected ApiKey to not be nil") + } else { + if tt.config.ApiKey.EnvVar != "TEMPORAL_API_KEY" { + t.Errorf("Expected ApiKey.EnvVar to be 'TEMPORAL_API_KEY', got %s", tt.config.ApiKey.EnvVar) + } + if tt.config.ApiKey.Value != "" { + t.Errorf("Expected ApiKey.Value to be empty, got %s", tt.config.ApiKey.Value) + } } if tt.config.TLS != nil { t.Error("Expected TLS to be nil") @@ -539,8 +625,8 @@ func TestTemporalAuthConfig_Structure(t *testing.T) { if tt.config.TLS != nil { t.Error("Expected TLS to be nil") } - if tt.config.ApiKey != "" { - t.Errorf("Expected ApiKey to be empty, got %s", tt.config.ApiKey) + if tt.config.ApiKey != nil { + t.Error("Expected ApiKey to be nil") } } }) @@ -577,11 +663,18 @@ func targetConfigEqual(a, b TargetConfig) bool { return false } - // Compare TemporalCloud Authentication - if a.TemporalCloud.Authentication.ApiKey != b.TemporalCloud.Authentication.ApiKey { + // Compare TemporalCloud Authentication - API Key + if (a.TemporalCloud.Authentication.ApiKey == nil) != (b.TemporalCloud.Authentication.ApiKey == nil) { return false } + if a.TemporalCloud.Authentication.ApiKey != nil && b.TemporalCloud.Authentication.ApiKey != nil { + if a.TemporalCloud.Authentication.ApiKey.Value != b.TemporalCloud.Authentication.ApiKey.Value || + a.TemporalCloud.Authentication.ApiKey.EnvVar != b.TemporalCloud.Authentication.ApiKey.EnvVar { + return false + } + } + // Compare TLS configuration if (a.TemporalCloud.Authentication.TLS == nil) != (b.TemporalCloud.Authentication.TLS == nil) { return false