diff --git a/internal/namespace/cert.go b/internal/namespace/cert.go index b4abc42..a7132b9 100644 --- a/internal/namespace/cert.go +++ b/internal/namespace/cert.go @@ -56,7 +56,7 @@ func (c *Client) AddCACerts(ctx context.Context, params AddCACertsParams) (*oper spec := ns.GetSpec() // Ensure MtlsAuth is initialized before accessing its fields if spec.MtlsAuth == nil { - spec.MtlsAuth = &namespacev1.MtlsAuthSpec{} + spec.MtlsAuth = &namespacev1.MtlsAuthSpec{Enabled: true} } spec.MtlsAuth.AcceptedClientCa = bundleBytes @@ -110,7 +110,7 @@ func (c *Client) DeleteCACerts(ctx context.Context, params DeleteCACertsParams) } else { // Ensure MtlsAuth is initialized before accessing its fields if spec.MtlsAuth == nil { - spec.MtlsAuth = &namespacev1.MtlsAuthSpec{} + spec.MtlsAuth = &namespacev1.MtlsAuthSpec{Enabled: true} } spec.MtlsAuth.AcceptedClientCa = bundleBytes } diff --git a/internal/namespace/cert_filter.go b/internal/namespace/cert_filter.go index a2b232c..10f6c23 100644 --- a/internal/namespace/cert_filter.go +++ b/internal/namespace/cert_filter.go @@ -44,7 +44,7 @@ func (c *Client) AddCertFilters(ctx context.Context, params AddCertFiltersParams spec := ns.GetSpec() // Ensure MtlsAuth is initialized if spec.MtlsAuth == nil { - spec.MtlsAuth = &namespacev1.MtlsAuthSpec{} + spec.MtlsAuth = &namespacev1.MtlsAuthSpec{Enabled: true} } existingFilters := spec.MtlsAuth.GetCertificateFilters() @@ -101,7 +101,7 @@ func (c *Client) DeleteCertFilters(ctx context.Context, params DeleteCertFilters // Update the spec with the new filter list if spec.MtlsAuth == nil { - spec.MtlsAuth = &namespacev1.MtlsAuthSpec{} + spec.MtlsAuth = &namespacev1.MtlsAuthSpec{Enabled: true} } spec.MtlsAuth.CertificateFilters = newFilters diff --git a/internal/namespace/cert_test.go b/internal/namespace/cert_test.go index bb8d938..ebf7703 100644 --- a/internal/namespace/cert_test.go +++ b/internal/namespace/cert_test.go @@ -40,6 +40,7 @@ func TestAddCACerts_Success(t *testing.T) { ResourceVersion: "v1", Spec: &namespacev1.NamespaceSpec{ MtlsAuth: &namespacev1.MtlsAuthSpec{ + Enabled: true, AcceptedClientCa: existingCertPEM, }, }, @@ -67,6 +68,7 @@ func TestAddCACerts_Success(t *testing.T) { AsyncOperationId: "test-async-op", Spec: &namespacev1.NamespaceSpec{ MtlsAuth: &namespacev1.MtlsAuthSpec{ + Enabled: true, AcceptedClientCa: expectedCertBundle, }, }, @@ -104,6 +106,7 @@ func TestAddCACerts_DuplicateCertificate(t *testing.T) { Spec: &namespacev1.NamespaceSpec{ MtlsAuth: &namespacev1.MtlsAuthSpec{ AcceptedClientCa: existingCertPEM, + Enabled: true, }, }, } @@ -123,6 +126,7 @@ func TestAddCACerts_DuplicateCertificate(t *testing.T) { ResourceVersion: "v1", Spec: &namespacev1.NamespaceSpec{ MtlsAuth: &namespacev1.MtlsAuthSpec{ + Enabled: true, AcceptedClientCa: existingCertPEM, }, }, @@ -221,6 +225,7 @@ func TestAddCACerts_CustomResourceVersion(t *testing.T) { Spec: &namespacev1.NamespaceSpec{ MtlsAuth: &namespacev1.MtlsAuthSpec{ AcceptedClientCa: existingCertPEM, + Enabled: true, }, }, } @@ -246,6 +251,7 @@ func TestAddCACerts_CustomResourceVersion(t *testing.T) { ResourceVersion: "custom-version", Spec: &namespacev1.NamespaceSpec{ MtlsAuth: &namespacev1.MtlsAuthSpec{ + Enabled: true, AcceptedClientCa: expectedCertBundle, }, }, diff --git a/temporalcloudcli/commands.namespace.create_test.go b/temporalcloudcli/commands.namespace.create_test.go index 48772d0..6b4fdce 100644 --- a/temporalcloudcli/commands.namespace.create_test.go +++ b/temporalcloudcli/commands.namespace.create_test.go @@ -93,6 +93,7 @@ func TestCreateNamespace_BuildsSpec(t *testing.T) { Lifecycle: &namespacev1.LifecycleSpec{EnableDeleteProtection: false}, Fairness: &namespacev1.FairnessSpec{TaskQueueFairnessEnabled: true}, MtlsAuth: &namespacev1.MtlsAuthSpec{ + Enabled: true, CertificateFilters: []*namespacev1.CertificateFilterSpec{ {CommonName: "test.temporal.io"}, {SubjectAlternativeName: "*.temporal.io"}, diff --git a/temporalcloudcli/commands.namespace.go b/temporalcloudcli/commands.namespace.go index 31ee48f..fc58754 100644 --- a/temporalcloudcli/commands.namespace.go +++ b/temporalcloudcli/commands.namespace.go @@ -396,12 +396,13 @@ func CreateNamespace(ctx context.Context, params CreateNamespaceParams) error { } if len(certBytes) > 0 { - spec.MtlsAuth = &namespacev1.MtlsAuthSpec{AcceptedClientCa: certBytes} + spec.MtlsAuth = &namespacev1.MtlsAuthSpec{AcceptedClientCa: certBytes, Enabled: true} } if len(certFilters) > 0 { if spec.MtlsAuth == nil { spec.MtlsAuth = &namespacev1.MtlsAuthSpec{} } + spec.MtlsAuth.Enabled = true spec.MtlsAuth.CertificateFilters = certFilters } if params.CodecEndpoint != "" { diff --git a/temporalcloudcli/commands.namespace_test.go b/temporalcloudcli/commands.namespace_test.go index d9bb6a1..c8c335e 100644 --- a/temporalcloudcli/commands.namespace_test.go +++ b/temporalcloudcli/commands.namespace_test.go @@ -4,10 +4,19 @@ package temporalcloudcli_test import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" "encoding/json" + "encoding/pem" "fmt" "io" + "math/big" "strings" + "testing" + "time" "github.com/temporalio/cloud-cli/temporalcloudcli" "go.temporal.io/api/temporalproto" @@ -31,11 +40,36 @@ func (s *SharedServerSuite) TestBasicNamespaceOperations() { s.cleanupNamespaces() } +// generateTestCACertBase64 generates a self-signed CA certificate and returns it as a base64-encoded PEM string. +func generateTestCACertBase64(t *testing.T) string { + t.Helper() + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("failed to generate key: %v", err) + } + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test.temporal.io"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + } + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + t.Fatalf("failed to create certificate: %v", err) + } + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + return base64.StdEncoding.EncodeToString(certPEM) +} + func (s *SharedServerSuite) TestNamespaceCreate() { s.cleanupNamespaces() defer s.cleanupNamespaces() namespaceName := s.generateRandomNamespaceName() + caCertBase64 := generateTestCACertBase64(s.Suite.T()) res := s.Execute( "namespace", @@ -45,7 +79,7 @@ func (s *SharedServerSuite) TestNamespaceCreate() { "--name", namespaceName, "--region", "aws-ca-central-1", "--retention-days", "30", - "--api-key-auth-enabled", + "--ca-certificate", caCertBase64, "--search-attribute", "MyText=Text", "--search-attribute", "MyKeyword=Keyword", "--certificate-filter", `{"commonName":"test.temporal.io","organization":"Temporal"}`, @@ -78,13 +112,13 @@ func (s *SharedServerSuite) TestNamespaceCreate() { s.Suite.Equal(namespaceName, gotSpec.Name) s.Suite.Equal("aws-ca-central-1", getNsRes.Namespace.ActiveRegion) s.Suite.Equal(int32(30), gotSpec.RetentionDays) - s.Suite.Require().NotNil(gotSpec.ApiKeyAuth) - s.Suite.True(gotSpec.ApiKeyAuth.Enabled) s.Suite.Equal(namespace.NamespaceSpec_SEARCH_ATTRIBUTE_TYPE_TEXT, gotSpec.SearchAttributes["MyText"]) s.Suite.Equal(namespace.NamespaceSpec_SEARCH_ATTRIBUTE_TYPE_KEYWORD, gotSpec.SearchAttributes["MyKeyword"]) s.Suite.Require().NotNil(gotSpec.MtlsAuth) + s.Suite.True(gotSpec.MtlsAuth.Enabled) + s.Suite.NotEmpty(gotSpec.MtlsAuth.AcceptedClientCa) s.Suite.Require().Len(gotSpec.MtlsAuth.CertificateFilters, 2) s.Suite.Equal("test.temporal.io", gotSpec.MtlsAuth.CertificateFilters[0].CommonName) s.Suite.Equal("Temporal", gotSpec.MtlsAuth.CertificateFilters[0].Organization) @@ -159,6 +193,8 @@ func (s *SharedServerSuite) testnamespaceCRUD() { s.Suite.Equal(namespaceSpec.Regions[0], readNamespace.ActiveRegion) s.Suite.Equal(namespaceSpec.SearchAttributes, readNamespace.Spec.SearchAttributes) s.Suite.Equal(namespaceSpec.RetentionDays, readNamespace.Spec.RetentionDays) + s.Suite.Require().NotNil(readNamespace.Spec.ApiKeyAuth) + s.Suite.Equal(namespaceSpec.ApiKeyAuth.Enabled, readNamespace.Spec.ApiKeyAuth.Enabled) // get the namespace via listing res = s.Execute(