Skip to content

Commit

Permalink
set apikey auth for namespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
kwadhwa-openai committed Sep 27, 2024
1 parent cab53e3 commit 9926fd1
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 41 deletions.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ provider "temporalcloud" {

- `allow_insecure` (Boolean) If set to True, it allows for an insecure connection to the Temporal Cloud API. This should never be set to 'true' in production and defaults to false.
- `api_key` (String, Sensitive) The API key for Temporal Cloud. See [this documentation](https://docs.temporal.io/cloud/api-keys) for information on how to obtain an API key.
- `client_version` (String) The version of the Temporal Cloud API client to use. Defaults to `2023-10-01-00`. To create namespaces with API key authentication enabled, use `2024-05-13-00`.
- `endpoint` (String) The endpoint for the Temporal Cloud API. Defaults to `saas-api.tmprl.cloud:443`.
3 changes: 2 additions & 1 deletion docs/resources/namespace.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,14 @@ resource "temporalcloud_namespace" "terraform2" {

### 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
8 changes: 2 additions & 6 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ import (
"go.temporal.io/sdk/client"
)

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

var TemporalCloudAPIVersion = "2023-10-01-00"

// Client is a client for the Temporal Cloud API.
type Client struct {
client.CloudOperationsClient
Expand All @@ -49,12 +45,12 @@ var (
_ client.CloudOperationsClient = &Client{}
)

func NewConnectionWithAPIKey(addrStr string, allowInsecure bool, apiKey string) (*Client, error) {
func NewConnectionWithAPIKey(addrStr string, allowInsecure bool, apiKey string, clientVersion string) (*Client, error) {

var cClient client.CloudOperationsClient
var err error
cClient, err = client.DialCloudOperationsClient(context.Background(), client.CloudOperationsClientOptions{
Version: TemporalCloudAPIVersion,
Version: clientVersion,
Credentials: client.NewAPIKeyStaticCredentials(apiKey),
DisableTLS: allowInsecure,
HostPort: addrStr,
Expand Down
92 changes: 68 additions & 24 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,10 @@ 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,
},
"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,18 +283,34 @@ func (r *namespaceResource) Create(ctx context.Context, req resource.CreateReque
return
}
}

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("accepted_client_ca is required when API key authentication is disabled", "")
return
}
var mtlAuth *namespacev1.MtlsAuthSpec
mtlAuth = &namespacev1.MtlsAuthSpec{
AcceptedClientCa: plan.AcceptedClientCA.ValueString(),
CertificateFilters: certFilters,
}

spec.MtlsAuth = mtlAuth
}

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

if err != nil {
resp.Diagnostics.AddError("Failed to create namespace", err.Error())
return
Expand Down Expand Up @@ -360,19 +381,34 @@ func (r *namespaceResource) Update(ctx context.Context, req resource.UpdateReque
if resp.Diagnostics.HasError() {
return
}

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("accepted_client_ca is required when API key authentication is disabled", "")
return
}
var mtlAuth *namespacev1.MtlsAuthSpec
mtlAuth = &namespacev1.MtlsAuthSpec{
AcceptedClientCa: plan.AcceptedClientCA.ValueString(),
CertificateFilters: certFilters,
}

spec.MtlsAuth = mtlAuth
}

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: &namespacev1.MtlsAuthSpec{
AcceptedClientCa: plan.AcceptedClientCA.ValueString(),
CertificateFilters: certFilters,
},
CodecServer: codecServer,
CustomSearchAttributes: currentNs.GetNamespace().GetSpec().GetCustomSearchAttributes(),
},
Namespace: plan.ID.ValueString(),
Spec: spec,
ResourceVersion: currentNs.GetNamespace().GetResourceVersion(),
})
if err != nil {
Expand Down Expand Up @@ -489,6 +525,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 @@ -521,7 +565,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
121 changes: 115 additions & 6 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" {
client_version = "2024-05-13-00"
}
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,18 +167,27 @@ func TestAccNamespaceWithCodecServer(t *testing.T) {
Name string
RetentionDays int
CodecServer *codecServer
ApiKeyAuth bool
}
)

name := fmt.Sprintf("%s-%s", "tf-codec-server", randomString())
tmpl := template.Must(template.New("config").Parse(`
provider "temporalcloud" {
{{ if .ApiKeyAuth }}
client_version = "2024-05-13-00"
{{ end }}
}
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 +203,7 @@ US8pEmNuIiCguEGwi+pb5CWfabETEHApxmo=
-----END CERTIFICATE-----
PEM
)
{{ end }}
retention_days = {{ .RetentionDays }}
Expand Down Expand Up @@ -207,7 +250,65 @@ PEM
}),
Check: func(s *terraform.State) error {
id := s.RootModule().Resources["temporalcloud_namespace.test"].Primary.Attributes["id"]
conn := newConnection(t)
conn := newConnection(t, "2023-10-01-00")
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,
}),
Check: func(s *terraform.State) error {
id := s.RootModule().Resources["temporalcloud_namespace.test"].Primary.Attributes["id"]
conn := newConnection(t, "2023-10-01-00")
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
},
},
// 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, "2024-05-13-00")
ns, err := conn.GetNamespace(context.Background(), &cloudservicev1.GetNamespaceRequest{
Namespace: id,
})
Expand All @@ -229,13 +330,15 @@ PEM
},
},
{
// 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)
conn := newConnection(t, "2023-10-01-00")
ns, err := conn.GetNamespace(context.Background(), &cloudservicev1.GetNamespaceRequest{
Namespace: id,
})
Expand Down Expand Up @@ -405,15 +508,21 @@ PEM
})
}

func newConnection(t *testing.T) cloudservicev1.CloudServiceClient {
func newConnection(t *testing.T, clientVersion string) cloudservicev1.CloudServiceClient {
apiKey := os.Getenv("TEMPORAL_CLOUD_API_KEY")
endpoint := os.Getenv("TEMPORAL_CLOUD_ENDPOINT")
if endpoint == "" {
endpoint = "saas-api.tmprl.cloud:443"
}
allowInsecure := os.Getenv("TEMPORAL_CLOUD_ALLOW_INSECURE") == "true"
if clientVersion == "" {
clientVersion = os.Getenv("TEMPORAL_CLOUD_CLIENT_VERSION")
if clientVersion == "" {
clientVersion = "2023-10-01-00"
}
}

client, err := client.NewConnectionWithAPIKey(endpoint, allowInsecure, apiKey)
client, err := client.NewConnectionWithAPIKey(endpoint, allowInsecure, apiKey, clientVersion)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
Expand Down
2 changes: 2 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 @@ -303,6 +304,7 @@ func (d *namespacesDataSource) Read(ctx context.Context, req datasource.ReadRequ
State: types.StringValue(ns.State),
ActiveRegion: types.StringValue(ns.ActiveRegion),
AcceptedClientCA: types.StringValue(ns.GetSpec().GetMtlsAuth().GetAcceptedClientCa()),
ApiKeyAuth: types.BoolValue(ns.GetSpec().GetApiKeyAuth().GetEnabled()),
RetentionDays: types.Int64Value(int64(ns.GetSpec().GetRetentionDays())),
CreatedTime: types.StringValue(ns.GetCreatedTime().AsTime().Format(time.RFC3339)),
}
Expand Down
Loading

0 comments on commit 9926fd1

Please sign in to comment.