Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API Key auth for namespaces #130

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/data-sources/namespaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ output "namespaces" {

Optional:

- `api_key_auth` (Boolean) If true, Temporal Cloud will use API key authentication for this namespace. If false, mutual TLS (mTLS) authentication will be used.
- `certificate_filters` (Attributes List) A list of filters to apply to client certificates when initiating a connection Temporal Cloud. If present, connections will only be allowed from client certificates whose distinguished name properties match at least one of the filters. (see [below for nested schema](#nestedatt--namespaces--certificate_filters))
- `codec_server` (Attributes) A codec server is used by the Temporal Cloud UI to decode payloads for all users interacting with this namespace, even if the workflow history itself is encrypted. (see [below for nested schema](#nestedatt--namespaces--codec_server))
- `custom_search_attributes` (Map of String) The custom search attributes to use for the namespace.
Expand Down
11 changes: 10 additions & 1 deletion docs/resources/namespace.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,29 @@ resource "temporalcloud_namespace" "terraform2" {
accepted_client_ca = base64encode(tls_self_signed_cert.ca.cert_pem)
retention_days = 14
}

// example namespace that uses API Key for authentication
resource "temporalcloud_namespace" "terraform3" {
name = "terraform3"
regions = ["aws-us-east-1"]
api_key_auth = true
retention_days = 14
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `accepted_client_ca` (String) The Base64-encoded CA cert in PEM format that clients use when authenticating with Temporal Cloud.
- `name` (String) The name of the namespace.
- `regions` (List of String) The list of regions that this namespace is available in. If more than one region is specified, this namespace is a "Multi-region Namespace", which is currently unsupported by the Terraform provider.
- `retention_days` (Number) The number of days to retain workflow history. Any changes to the retention period will be applied to all new running workflows.

### Optional

- `accepted_client_ca` (String) The Base64-encoded CA cert in PEM format that clients use when authenticating with Temporal Cloud.
- `api_key_auth` (Boolean) If true, Temporal Cloud will use API key authentication for this namespace. If false, mutual TLS (mTLS) authentication will be used.
- `certificate_filters` (Attributes List) A list of filters to apply to client certificates when initiating a connection Temporal Cloud. If present, connections will only be allowed from client certificates whose distinguished name properties match at least one of the filters. (see [below for nested schema](#nestedatt--certificate_filters))
- `codec_server` (Attributes) A codec server is used by the Temporal Cloud UI to decode payloads for all users interacting with this namespace, even if the workflow history itself is encrypted. (see [below for nested schema](#nestedatt--codec_server))
- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))
Expand Down
10 changes: 9 additions & 1 deletion examples/resources/temporalcloud_namespace/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,12 @@ resource "temporalcloud_namespace" "terraform2" {
regions = ["aws-us-east-1"]
accepted_client_ca = base64encode(tls_self_signed_cert.ca.cert_pem)
retention_days = 14
}
}

// example namespace that uses API Key for authentication
resource "temporalcloud_namespace" "terraform3" {
name = "terraform3"
regions = ["aws-us-east-1"]
api_key_auth = true
retention_days = 14
}
2 changes: 0 additions & 2 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ import (
"go.temporal.io/sdk/client"
)

const TemporalCloudAPIVersionHeader = "temporal-cloud-api-version"

var TemporalCloudAPIVersion = "2024-05-13-00"

// Client is a client for the Temporal Cloud API.
Expand Down
98 changes: 70 additions & 28 deletions internal/provider/namespace_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type (
AcceptedClientCA internaltypes.EncodedCAValue `tfsdk:"accepted_client_ca"`
RetentionDays types.Int64 `tfsdk:"retention_days"`
CertificateFilters types.List `tfsdk:"certificate_filters"`
ApiKeyAuth types.Bool `tfsdk:"api_key_auth"`
CodecServer types.Object `tfsdk:"codec_server"`
Endpoints types.Object `tfsdk:"endpoints"`

Expand Down Expand Up @@ -170,7 +171,7 @@ func (r *namespaceResource) Schema(ctx context.Context, _ resource.SchemaRequest
"accepted_client_ca": schema.StringAttribute{
CustomType: internaltypes.EncodedCAType{},
Description: "The Base64-encoded CA cert in PEM format that clients use when authenticating with Temporal Cloud.",
Required: true,
Optional: true,
},
"retention_days": schema.Int64Attribute{
Description: "The number of days to retain workflow history. Any changes to the retention period will be applied to all new running workflows.",
Expand Down Expand Up @@ -200,6 +201,11 @@ func (r *namespaceResource) Schema(ctx context.Context, _ resource.SchemaRequest
},
},
},
"api_key_auth": schema.BoolAttribute{
Description: "If true, Temporal Cloud will use API key authentication for this namespace. If false, mutual TLS (mTLS) authentication will be used.",
Optional: true,
Computed: true,
},
"codec_server": schema.SingleNestedAttribute{
Description: "A codec server is used by the Temporal Cloud UI to decode payloads for all users interacting with this namespace, even if the workflow history itself is encrypted.",
Attributes: map[string]schema.Attribute{
Expand Down Expand Up @@ -278,21 +284,35 @@ func (r *namespaceResource) Create(ctx context.Context, req resource.CreateReque
return
}
}
mtls := &namespacev1.MtlsAuthSpec{}
if plan.AcceptedClientCA.ValueString() != "" {
mtls.Enabled = true
mtls.AcceptedClientCa = plan.AcceptedClientCA.ValueString()
mtls.CertificateFilters = certFilters

var spec = &namespacev1.NamespaceSpec{
Name: plan.Name.ValueString(),
Regions: regions,
RetentionDays: int32(plan.RetentionDays.ValueInt64()),
CodecServer: codecServer,
}

if plan.ApiKeyAuth.ValueBool() {
spec.ApiKeyAuth = &namespacev1.ApiKeyAuthSpec{Enabled: true}
} else {
if plan.AcceptedClientCA.IsNull() {
resp.Diagnostics.AddError("Namespace not configured with authentication. accepted_client_ca is required when API key authentication is not enabled (api_key_auth is not set to true).", "")
return
}
mtls := &namespacev1.MtlsAuthSpec{}
if plan.AcceptedClientCA.ValueString() != "" {
mtls.Enabled = true
mtls.AcceptedClientCa = plan.AcceptedClientCA.ValueString()
mtls.CertificateFilters = certFilters
}

spec.MtlsAuth = mtls
}

svcResp, err := r.client.CloudService().CreateNamespace(ctx, &cloudservicev1.CreateNamespaceRequest{
Spec: &namespacev1.NamespaceSpec{
Name: plan.Name.ValueString(),
Regions: regions,
RetentionDays: int32(plan.RetentionDays.ValueInt64()),
MtlsAuth: mtls,
CodecServer: codecServer,
},
Spec: spec,
})

if err != nil {
resp.Diagnostics.AddError("Failed to create namespace", err.Error())
return
Expand Down Expand Up @@ -363,22 +383,36 @@ func (r *namespaceResource) Update(ctx context.Context, req resource.UpdateReque
if resp.Diagnostics.HasError() {
return
}
mtls := &namespacev1.MtlsAuthSpec{}
if plan.AcceptedClientCA.ValueString() != "" {
mtls.Enabled = true
mtls.AcceptedClientCa = plan.AcceptedClientCA.ValueString()
mtls.CertificateFilters = certFilters

var spec = &namespacev1.NamespaceSpec{
Name: plan.Name.ValueString(),
Regions: regions,
RetentionDays: int32(plan.RetentionDays.ValueInt64()),
CodecServer: codecServer,
CustomSearchAttributes: currentNs.GetNamespace().GetSpec().GetCustomSearchAttributes(),
}

if plan.ApiKeyAuth.ValueBool() {
spec.ApiKeyAuth = &namespacev1.ApiKeyAuthSpec{Enabled: true}
} else {
if plan.AcceptedClientCA.IsNull() {
resp.Diagnostics.AddError("Namespace not configured with authentication. accepted_client_ca is required when API key authentication is not enabled (api_key_auth is not set to true).", "")
return
}

mtls := &namespacev1.MtlsAuthSpec{}
if plan.AcceptedClientCA.ValueString() != "" {
mtls.Enabled = true
mtls.AcceptedClientCa = plan.AcceptedClientCA.ValueString()
mtls.CertificateFilters = certFilters
}

spec.MtlsAuth = mtls
}

svcResp, err := r.client.CloudService().UpdateNamespace(ctx, &cloudservicev1.UpdateNamespaceRequest{
Namespace: plan.ID.ValueString(),
Spec: &namespacev1.NamespaceSpec{
Name: plan.Name.ValueString(),
Regions: regions,
RetentionDays: int32(plan.RetentionDays.ValueInt64()),
MtlsAuth: mtls,
CodecServer: codecServer,
CustomSearchAttributes: currentNs.GetNamespace().GetSpec().GetCustomSearchAttributes(),
},
Namespace: plan.ID.ValueString(),
Spec: spec,
ResourceVersion: currentNs.GetNamespace().GetResourceVersion(),
})
if err != nil {
Expand Down Expand Up @@ -495,6 +529,14 @@ func updateModelFromSpec(ctx context.Context, diags diag.Diagnostics, state *nam
certificateFilter = filters
}

if ns.GetSpec().GetMtlsAuth().GetAcceptedClientCa() != "" {
state.AcceptedClientCA = internaltypes.EncodedCA(ns.GetSpec().GetMtlsAuth().GetAcceptedClientCa())
}

if ns.GetSpec().GetApiKeyAuth() != nil {
state.ApiKeyAuth = types.BoolValue(ns.GetSpec().GetApiKeyAuth().GetEnabled())
}

var codecServerState basetypes.ObjectValue
// The API always returns a non-empty CodecServerSpec, even if it wasn't specified on object creation. We explicitly
// map an endpoint whose value is the empty string to `null`, since an empty endpoint implies that the codec server
Expand Down Expand Up @@ -527,7 +569,7 @@ func updateModelFromSpec(ctx context.Context, diags diag.Diagnostics, state *nam
state.Endpoints = endpointsState
state.Regions = planRegions
state.CertificateFilters = certificateFilter
state.AcceptedClientCA = internaltypes.EncodedCA(ns.GetSpec().GetMtlsAuth().GetAcceptedClientCa())

state.RetentionDays = types.Int64Value(int64(ns.GetSpec().GetRetentionDays()))
}

Expand Down
104 changes: 102 additions & 2 deletions internal/provider/namespace_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,39 @@ PEM

}

func TestAccBasicNamespaceWithApiKeyAuth(t *testing.T) {
name := fmt.Sprintf("%s-%s", "tf-basic-namespace", randomString())
config := func(name string, retention int) string {
return fmt.Sprintf(`
provider "temporalcloud" {

}

resource "temporalcloud_namespace" "terraform" {
name = "%s"
regions = ["aws-us-east-1"]
api_key_auth = true
retention_days = %d
}`, name, retention)
}

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
// New namespace with retention of 7
Config: config(name, 7),
},
{
Config: config(name, 14),
},
// Delete testing automatically occurs in TestCase
},
})

}

func TestAccBasicNamespaceWithCertFilters(t *testing.T) {
name := fmt.Sprintf("%s-%s", "tf-cert-filters", randomString())
config := func(name string, retention int) string {
Expand Down Expand Up @@ -134,6 +167,7 @@ func TestAccNamespaceWithCodecServer(t *testing.T) {
Name string
RetentionDays int
CodecServer *codecServer
ApiKeyAuth bool
}
)

Expand All @@ -144,8 +178,14 @@ provider "temporalcloud" {
}

resource "temporalcloud_namespace" "test" {
name = "{{ .Name }}"
name = "{{ .Name }}-{{ .ApiKeyAuth }}"
regions = ["aws-us-east-1"]

{{ if .ApiKeyAuth }}
api_key_auth = true
{{ end }}

{{ if not .ApiKeyAuth }}
accepted_client_ca = base64encode(<<PEM
-----BEGIN CERTIFICATE-----
MIIBxjCCAU2gAwIBAgIRAlyZ5KUmunPLeFAupDwGL8AwCgYIKoZIzj0EAwMwEjEQ
Expand All @@ -161,6 +201,7 @@ US8pEmNuIiCguEGwi+pb5CWfabETEHApxmo=
-----END CERTIFICATE-----
PEM
)
{{ end }}

retention_days = {{ .RetentionDays }}

Expand Down Expand Up @@ -229,6 +270,7 @@ PEM
},
},
{
// remove codec server
Config: config(configArgs{
Name: name,
RetentionDays: 7,
Expand All @@ -250,6 +292,65 @@ PEM
return nil
},
},
// use API key auth
{
Config: config(configArgs{
Name: name,
RetentionDays: 7,
CodecServer: &codecServer{
Endpoint: "https://example.com",
PassAccessToken: true,
IncludeCrossOriginCredentials: true,
},
ApiKeyAuth: true,
}),
Check: func(s *terraform.State) error {
id := s.RootModule().Resources["temporalcloud_namespace.test"].Primary.Attributes["id"]
conn := newConnection(t)
ns, err := conn.GetNamespace(context.Background(), &cloudservicev1.GetNamespaceRequest{
Namespace: id,
})
if err != nil {
return fmt.Errorf("failed to get namespace: %v", err)
}

spec := ns.Namespace.GetSpec()
if spec.GetCodecServer().GetEndpoint() != "https://example.com" {
return fmt.Errorf("unexpected endpoint: %s", spec.GetCodecServer().GetEndpoint())
}
if !spec.GetCodecServer().GetPassAccessToken() {
return errors.New("expected pass_access_token to be true")
}
if !spec.GetCodecServer().GetIncludeCrossOriginCredentials() {
return errors.New("expected include_cross_origin_credentials to be true")
}
return nil
},
},
{
// remove codec server
Config: config(configArgs{
Name: name,
RetentionDays: 7,
ApiKeyAuth: true,
}),
Check: func(s *terraform.State) error {
id := s.RootModule().Resources["temporalcloud_namespace.test"].Primary.Attributes["id"]
conn := newConnection(t)
ns, err := conn.GetNamespace(context.Background(), &cloudservicev1.GetNamespaceRequest{
Namespace: id,
})
if err != nil {
return fmt.Errorf("failed to get namespace: %v", err)
}

spec := ns.Namespace.GetSpec()
if spec.GetCodecServer().GetEndpoint() != "" {
return fmt.Errorf("unexpected endpoint: %s", spec.GetCodecServer().GetEndpoint())
}
return nil
},
},
// Delete testing automatically occurs in TestCase
},
})
Expand Down Expand Up @@ -412,7 +513,6 @@ func newConnection(t *testing.T) cloudservicev1.CloudServiceClient {
endpoint = "saas-api.tmprl.cloud:443"
}
allowInsecure := os.Getenv("TEMPORAL_CLOUD_ALLOW_INSECURE") == "true"

client, err := client.NewConnectionWithAPIKey(endpoint, allowInsecure, apiKey)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
Expand Down
10 changes: 10 additions & 0 deletions internal/provider/namespaces_datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type (
AcceptedClientCA types.String `tfsdk:"accepted_client_ca"`
RetentionDays types.Int64 `tfsdk:"retention_days"`
CertificateFilters types.List `tfsdk:"certificate_filters"`
ApiKeyAuth types.Bool `tfsdk:"api_key_auth"`
CodecServer types.Object `tfsdk:"codec_server"`
Endpoints types.Object `tfsdk:"endpoints"`
PrivateConnectivities types.List `tfsdk:"private_connectivities"`
Expand Down Expand Up @@ -180,6 +181,10 @@ func (d *namespacesDataSource) Schema(_ context.Context, _ datasource.SchemaRequ
},
},
},
"api_key_auth": schema.BoolAttribute{
Description: "If true, Temporal Cloud will use API key authentication for this namespace. If false, mutual TLS (mTLS) authentication will be used.",
Optional: true,
},
"codec_server": schema.SingleNestedAttribute{
Optional: true,
Computed: true,
Expand Down Expand Up @@ -306,6 +311,11 @@ func (d *namespacesDataSource) Read(ctx context.Context, req datasource.ReadRequ
RetentionDays: types.Int64Value(int64(ns.GetSpec().GetRetentionDays())),
CreatedTime: types.StringValue(ns.GetCreatedTime().AsTime().Format(time.RFC3339)),
}

if ns.GetSpec().GetApiKeyAuth().GetEnabled() {
namespaceModel.ApiKeyAuth = types.BoolValue(true)
}

if ns.GetLastModifiedTime().String() != "" {
namespaceModel.LastModifiedTime = types.StringValue(ns.GetLastModifiedTime().AsTime().Format(time.RFC3339))
} else {
Expand Down
Loading