diff --git a/vault/data_source_tencentcloud_access_credentials.go b/vault/data_source_tencentcloud_access_credentials.go new file mode 100644 index 000000000..0211e39a3 --- /dev/null +++ b/vault/data_source_tencentcloud_access_credentials.go @@ -0,0 +1,111 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "log" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +func tencentCloudAccessCredentialsDataSource() *schema.Resource { + return &schema.Resource{ + Read: provider.ReadWrapper(tencentCloudAccessCredentialsDataSourceRead), + + Schema: map[string]*schema.Schema{ + "backend": { + Type: schema.TypeString, + Required: true, + Description: "Tencent cloud Secret Backend to read credentials from.", + }, + + "role": { + Type: schema.TypeString, + Required: true, + Description: "Tencent cloud Secret Role to read credentials from.", + }, + + "secret_id": { + Type: schema.TypeString, + Computed: true, + Description: "Tencent cloud secret ID read from Vault.", + }, + + "secret_key": { + Type: schema.TypeString, + Computed: true, + Description: "Tencent cloud secret key read from Vault.", + }, + + "token": { + Type: schema.TypeString, + Computed: true, + Description: "Tencent cloud security token read from Vault. (Only returned if type is 'sts').", + }, + + consts.FieldLeaseID: { + Type: schema.TypeString, + Computed: true, + Description: "Lease identifier assigned by vault.", + }, + + consts.FieldLeaseDuration: { + Type: schema.TypeInt, + Computed: true, + Description: "Lease duration in seconds relative to the time in lease_start_time.", + }, + + consts.FieldLeaseRenewable: { + Type: schema.TypeBool, + Computed: true, + Description: "True if the duration of this lease can be extended through renewal.", + }, + }, + } +} + +func tencentCloudAccessCredentialsDataSourceRead(d *schema.ResourceData, meta interface{}) error { + client, e := provider.GetClient(d, meta) + if e != nil { + return e + } + + backend := d.Get("backend").(string) + credType := "creds" + role := d.Get("role").(string) + path := backend + "/" + credType + "/" + role + + data := map[string][]string{} + + log.Printf("[DEBUG] Reading %q from Vault with data %#v", path, data) + secret, err := client.Logical().ReadWithData(path, data) + if err != nil { + return fmt.Errorf("error reading from Vault: %s", err) + } + log.Printf("[DEBUG] Read %q from Vault", path) + + if secret == nil { + return fmt.Errorf("no role found at path %q", path) + } + + secretId := secret.Data["secret_id"].(string) + secretKey := secret.Data["secret_key"].(string) + var token string + if secret.Data["token"] != nil { + token = secret.Data["token"].(string) + } + + d.SetId(secret.LeaseID) + _ = d.Set("secret_id", secretId) + _ = d.Set("secret_key", secretKey) + _ = d.Set("token", token) + _ = d.Set(consts.FieldLeaseID, secret.LeaseID) + _ = d.Set(consts.FieldLeaseDuration, secret.LeaseDuration) + _ = d.Set(consts.FieldLeaseRenewable, secret.Renewable) + + return nil +} diff --git a/vault/provider.go b/vault/provider.go index f64faf555..d6e7d3adc 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -87,6 +87,10 @@ var ( Resource: UpdateSchemaResource(nomadAccessCredentialsDataSource()), PathInventory: []string{"/nomad/creds/{role}"}, }, + "vault_tencentcloud_access_credentials": { + Resource: UpdateSchemaResource(tencentCloudAccessCredentialsDataSource()), + PathInventory: []string{"/tencentcloud/creds/{role}"}, + }, "vault_aws_access_credentials": { Resource: UpdateSchemaResource(awsAccessCredentialsDataSource()), PathInventory: []string{"/aws/creds"}, @@ -285,6 +289,22 @@ var ( Resource: UpdateSchemaResource(awsSecretBackendResource()), PathInventory: []string{"/aws/config/root"}, }, + "vault_tencentcloud_auth_backend_client": { + Resource: UpdateSchemaResource(tencentCloudAuthBackendClientResource()), + PathInventory: []string{"/auth/tencentcloud/config/client"}, + }, + "vault_tencentcloud_auth_backend_role": { + Resource: UpdateSchemaResource(tencentCloudAuthBackendRoleResource()), + PathInventory: []string{"/auth/tencentcloud/role/{role}"}, + }, + "vault_tencentcloud_secret_backend": { + Resource: UpdateSchemaResource(tencentCloudSecretBackendResource()), + PathInventory: []string{"/tencentcloud/config"}, + }, + "vault_tencentcloud_secret_backend_role": { + Resource: UpdateSchemaResource(tencentCloudSecretBackendRoleResource("vault_tencentcloud_secret_backend_role")), + PathInventory: []string{"/tencentcloud/role/{name}"}, + }, "vault_aws_secret_backend_role": { Resource: UpdateSchemaResource(awsSecretBackendRoleResource("vault_aws_secret_backend_role")), PathInventory: []string{"/aws/roles/{name}"}, diff --git a/vault/resource_tencentcloud_auth_backend_client.go b/vault/resource_tencentcloud_auth_backend_client.go new file mode 100644 index 000000000..14b0ca8fe --- /dev/null +++ b/vault/resource_tencentcloud_auth_backend_client.go @@ -0,0 +1,146 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "log" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +func tencentCloudAuthBackendClientResource() *schema.Resource { + return &schema.Resource{ + CreateContext: tencentCloudAuthBackendWrite, + ReadContext: provider.ReadContextWrapper(tencentCloudAuthBackendRead), + UpdateContext: tencentCloudAuthBackendWrite, + DeleteContext: tencentCloudAuthBackendDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + consts.FieldBackend: { + Type: schema.TypeString, + Optional: true, + Description: "Unique name of the auth backend to configure.", + ForceNew: true, + Default: "tencentcloud", + // standardise on no beginning or trailing slashes + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + consts.FieldSecretID: { + Type: schema.TypeString, + Optional: true, + Description: "Tencent Cloud Secret ID with permissions to query tencent cloud APIs.", + Sensitive: true, + }, + consts.FieldSecretKey: { + Type: schema.TypeString, + Optional: true, + Description: "Tencent Cloud Secret key with permissions to query tencent cloud APIs.", + Sensitive: true, + }, + }, + } +} + +func tencentCloudAuthBackendWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + // if backend comes from the config, it won't have the StateFunc + // applied yet, so we need to apply it again. + backend := d.Get(consts.FieldBackend).(string) + path := tencentCloudAuthBackendClientPath(backend) + + data := map[string]interface{}{} + + if d.HasChange(consts.FieldSecretID) || d.HasChange(consts.FieldSecretKey) { + log.Printf("[DEBUG] Updating Tencent Cloud credentials at %q", path) + data[consts.FieldSecretID] = d.Get(consts.FieldSecretID).(string) + data[consts.FieldSecretKey] = d.Get(consts.FieldSecretKey).(string) + } + + log.Printf("[DEBUG] Writing Tencent Cloud auth backend client config to %q", path) + _, err := client.Logical().WriteWithContext(ctx, path, data) + if err != nil { + return diag.Errorf("error writing to %q: %s", path, err) + } + log.Printf("[DEBUG] Wrote tencent cloud auth backend client config to %q", path) + + d.SetId(path) + + return tencentCloudAuthBackendRead(ctx, d, meta) +} + +func tencentCloudAuthBackendRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + log.Printf("[DEBUG] Reading Tencent Cloud auth backend client config") + secret, err := client.Logical().ReadWithContext(ctx, d.Id()) + if err != nil { + return diag.Errorf("error reading Tencent Cloud auth backend client config from %q: %s", d.Id(), err) + } + log.Printf("[DEBUG] Read Tencent Cloud auth backend client config") + + if secret == nil { + log.Printf("[WARN] No info found at %q; removing from state.", d.Id()) + d.SetId("") + return nil + } + + // set the backend to the original passed path (without config/client at the end) + re := regexp.MustCompile(`^auth/(.*)/config/client$`) + if !re.MatchString(d.Id()) { + return diag.Errorf("`config/client` has not been appended to the ID (%s)", d.Id()) + } + _ = d.Set("backend", re.FindStringSubmatch(d.Id())[1]) + + fields := []string{ + consts.FieldSecretID, + consts.FieldSecretKey, + } + for _, k := range fields { + if v, ok := secret.Data[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.FromErr(err) + } + } + } + return nil +} + +func tencentCloudAuthBackendDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + log.Printf("[DEBUG] Deleting Tencent Cloud auth backend client config from %q", d.Id()) + _, err := client.Logical().DeleteWithContext(ctx, d.Id()) + if err != nil { + return diag.Errorf("error deleting Tencent Cloud auth backend client config from %q: %s", d.Id(), err) + } + log.Printf("[DEBUG] Deleted Tencent Cloud auth backend client config from %q", d.Id()) + + return nil +} + +func tencentCloudAuthBackendClientPath(path string) string { + return "auth/" + strings.Trim(path, "/") + "/config/client" +} diff --git a/vault/resource_tencentcloud_auth_backend_role.go b/vault/resource_tencentcloud_auth_backend_role.go new file mode 100644 index 000000000..5e3490fe4 --- /dev/null +++ b/vault/resource_tencentcloud_auth_backend_role.go @@ -0,0 +1,339 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "fmt" + "log" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +var ( + tencentCloudAuthBackendRoleBackendFromPathRegex = regexp.MustCompile("^auth/(.+)/role/.+$") + tencentCloudAuthBackendRoleNameFromPathRegex = regexp.MustCompile("^auth/.+/role/(.+)$") +) + +func tencentCloudAuthBackendRoleResource() *schema.Resource { + fields := map[string]*schema.Schema{ + "role": { + Type: schema.TypeString, + Required: true, + Description: "Name of the role. Must correspond with the name of the role reflected in the arn.", + ForceNew: true, + }, + "arn": { + Type: schema.TypeString, + Required: true, + Description: "Arn of the above role.", + ForceNew: true, + }, + "backend": { + Type: schema.TypeString, + Optional: true, + Description: "Unique name of the auth backend to configure.", + ForceNew: true, + Default: "tencentcloud", + // standardise on no beginning or trailing slashes + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + "token_ttl": { + Type: schema.TypeInt, + Optional: true, + Description: "The incremental lifetime for generated tokens. This current value of this will be referenced at renewal time.", + }, + "token_max_ttl": { + Type: schema.TypeInt, + Optional: true, + Description: "The maximum lifetime for generated tokens. This current value of this will be referenced at renewal time.", + }, + "token_policies": { + Type: schema.TypeSet, + Optional: true, + Description: "List of policies to encode onto generated tokens. Depending on the auth method, this list may be supplemented by user/group/other values.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "token_bound_cidrs": { + Type: schema.TypeSet, + Optional: true, + Description: "List of CIDR blocks; if set, specifies blocks of IP addresses which can authenticate successfully, and ties the resulting token to these blocks as well.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "token_explicit_max_ttl": { + Type: schema.TypeInt, + Optional: true, + Description: "If set, will encode an explicit max TTL onto the token. This is a hard cap even if token_ttl and token_max_ttl would otherwise allow a renewal.", + }, + "token_no_default_policy": { + Type: schema.TypeBool, + Optional: true, + Description: "If set, the default policy will not be set on generated tokens; otherwise it will be added to the policies set in token_policies.", + }, + "token_num_uses": { + Type: schema.TypeInt, + Optional: true, + Description: "The maximum number of times a generated token may be used (within its lifetime); 0 means unlimited. If you require the token to have the ability to create child tokens, you will need to set this value to 0.", + }, + "token_period": { + Type: schema.TypeInt, + Optional: true, + Description: "The period, if any, to set on the token.", + }, + "token_type": { + Type: schema.TypeString, + Optional: true, + Description: "The type of token that should be generated. Can be service, batch, or default to use the mount's tuned default (which unless changed will be service tokens). For token store roles, there are two additional possibilities: default-service and default-batch which specify the type to return unless the client requests a different type at generation time.", + }, + } + + return &schema.Resource{ + CreateContext: tencentCloudAuthBackendRoleCreate, + ReadContext: provider.ReadContextWrapper(tencentCloudAuthBackendRoleRead), + UpdateContext: tencentCloudAuthBackendRoleUpdate, + DeleteContext: tencentCloudAuthBackendRoleDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: fields, + } +} + +func tencentCloudAuthBackendRoleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + backend := d.Get("backend").(string) + role := d.Get("role").(string) + + path := tencentCloudAuthBackendRolePath(backend, role) + + log.Printf("[DEBUG] Writing Tencent Cloud auth backend role %q", path) + + arn := d.Get("arn").(string) + data := map[string]interface{}{ + "arn": arn, + } + + if v, ok := d.GetOk("token_ttl"); ok { + data["token_ttl"] = v.(int) + } + if v, ok := d.GetOk("token_max_ttl"); ok { + data["token_max_ttl"] = v.(int) + } + if v, ok := d.GetOk("token_policies"); ok { + data["token_policies"] = v.(*schema.Set).List() + } + if v, ok := d.GetOk("token_bound_cidrs"); ok { + data["token_bound_cidrs"] = v.(*schema.Set).List() + } + if v, ok := d.GetOkExists("token_explicit_max_ttl"); ok { + data["token_explicit_max_ttl"] = v.(int) + } + if v, ok := d.GetOkExists("token_no_default_policy"); ok { + data["token_no_default_policy"] = v.(bool) + } + if v, ok := d.GetOkExists("token_num_uses"); ok { + data["token_num_uses"] = v.(int) + } + if v, ok := d.GetOkExists("token_period"); ok { + data["token_period"] = v.(int) + } + if v, ok := d.GetOk("token_type"); ok { + data["token_type"] = v.(string) + } + + d.SetId(path) + if _, err := client.Logical().Write(path, data); err != nil { + d.SetId("") + return diag.Errorf("error writing Tencent Cloud auth backend role %q: %s", path, err) + } + log.Printf("[DEBUG] Wrote Tencent Cloud auth backend role %q", path) + + return tencentCloudAuthBackendRoleRead(ctx, d, meta) +} + +func tencentCloudAuthBackendRoleRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + path := d.Id() + + backend, err := tencentCloudAuthBackendRoleBackendFromPath(path) + if err != nil { + return diag.Errorf("invalid path %q for Tencent Cloud auth backend role: %s", path, err) + } + + role, err := tencentCloudAuthBackendRoleNameFromPath(path) + if err != nil { + return diag.Errorf("invalid path %q for Tencent Cloud auth backend role: %s", path, err) + } + + log.Printf("[DEBUG] Reading Tencent Cloud auth backend role %q", path) + resp, err := client.Logical().Read(path) + if err != nil { + return diag.Errorf("error reading Tencent Cloud auth backend role %q: %s", path, err) + } + log.Printf("[DEBUG] Read Tencent Cloud auth backend role %q", path) + if resp == nil { + log.Printf("[WARN] Tencent Cloud auth backend role %q not found, removing from state", path) + d.SetId("") + return nil + } + + if err := readTokenFields(d, resp); err != nil { + return diag.FromErr(err) + } + + _ = d.Set("backend", backend) + _ = d.Set("role", role) + _ = d.Set("arn", resp.Data["arn"]) + + if v, ok := resp.Data["token_ttl"]; ok { + _ = d.Set("token_ttl", v) + } + + if v, ok := resp.Data["token_max_ttl"]; ok { + _ = d.Set("token_max_ttl", v) + } + + if v, ok := resp.Data["token_policies"]; ok { + _ = d.Set("token_policies", v) + } + + if v, ok := resp.Data["token_bound_cidrs"]; ok { + _ = d.Set("token_bound_cidrs", v) + } + + if v, ok := resp.Data["token_explicit_max_ttl"]; ok { + _ = d.Set("token_explicit_max_ttl", v) + } + + if v, ok := resp.Data["token_no_default_policy"]; ok { + _ = d.Set("token_no_default_policy", v) + } + + if v, ok := resp.Data["token_num_uses"]; ok { + _ = d.Set("token_num_uses", v) + } + + if v, ok := resp.Data["token_period"]; ok { + _ = d.Set("token_period", v) + } + + if v, ok := resp.Data["token_type"]; ok { + _ = d.Set("token_type", v) + } + + return nil +} + +func tencentCloudAuthBackendRoleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := d.Id() + + log.Printf("[DEBUG] Updating Tencent Cloud auth backend role %q", path) + + arn := d.Get("arn").(string) + data := map[string]interface{}{ + "arn": arn, + } + + if v, ok := d.GetOk("token_ttl"); ok { + data["token_ttl"] = v.(int) + } + if v, ok := d.GetOk("token_max_ttl"); ok { + data["token_max_ttl"] = v.(int) + } + if v, ok := d.GetOk("token_policies"); ok { + data["token_policies"] = v.(*schema.Set).List() + } + if v, ok := d.GetOk("token_bound_cidrs"); ok { + data["token_bound_cidrs"] = v.(*schema.Set).List() + } + if v, ok := d.GetOkExists("token_explicit_max_ttl"); ok { + data["token_explicit_max_ttl"] = v.(int) + } + if v, ok := d.GetOkExists("token_no_default_policy"); ok { + data["token_no_default_policy"] = v.(bool) + } + if v, ok := d.GetOkExists("token_num_uses"); ok { + data["token_num_uses"] = v.(int) + } + if v, ok := d.GetOkExists("token_period"); ok { + data["token_period"] = v.(int) + } + if v, ok := d.GetOk("token_type"); ok { + data["token_type"] = v.(string) + } + + _, err := client.Logical().Write(path, data) + if err != nil { + return diag.Errorf("error updating Tencent Cloud auth backend role %q: %s", path, err) + } + log.Printf("[DEBUG] Updated Tencent Cloud auth backend role %q", path) + + return tencentCloudAuthBackendRoleRead(ctx, d, meta) +} + +func tencentCloudAuthBackendRoleDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + path := d.Id() + + log.Printf("[DEBUG] Deleting Tencent Cloud auth backend role %q", path) + _, err := client.Logical().Delete(path) + if err != nil { + return diag.Errorf("error deleting Tencent Cloud auth backend role %q", path) + } + log.Printf("[DEBUG] Deleted Tencent Cloud auth backend role %q", path) + + return nil +} + +func tencentCloudAuthBackendRolePath(backend, role string) string { + return "auth/" + strings.Trim(backend, "/") + "/role/" + strings.Trim(role, "/") +} + +func tencentCloudAuthBackendRoleNameFromPath(path string) (string, error) { + if !tencentCloudAuthBackendRoleNameFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no role found") + } + res := tencentCloudAuthBackendRoleNameFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for role", len(res)) + } + return res[1], nil +} + +func tencentCloudAuthBackendRoleBackendFromPath(path string) (string, error) { + if !tencentCloudAuthBackendRoleBackendFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no backend found") + } + res := tencentCloudAuthBackendRoleBackendFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for backend", len(res)) + } + return res[1], nil +} diff --git a/vault/resource_tencentcloud_secret_backend.go b/vault/resource_tencentcloud_secret_backend.go new file mode 100644 index 000000000..86caa6a04 --- /dev/null +++ b/vault/resource_tencentcloud_secret_backend.go @@ -0,0 +1,275 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/vault/api" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/util" + "github.com/hashicorp/terraform-provider-vault/util/mountutil" +) + +func tencentCloudSecretBackendResource() *schema.Resource { + return provider.MustAddMountMigrationSchema(&schema.Resource{ + CreateContext: tencentCloudSecretBackendCreate, + ReadContext: provider.ReadContextWrapper(tencentCloudSecretBackendRead), + UpdateContext: tencentCloudSecretBackendUpdate, + DeleteContext: tencentCloudSecretBackendDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + CustomizeDiff: getMountCustomizeDiffFunc(consts.FieldPath), + + Schema: map[string]*schema.Schema{ + consts.FieldPath: { + Type: schema.TypeString, + Optional: true, + Default: "tencentcloud", + Description: "Path to mount the backend at.", + ValidateFunc: func(v interface{}, k string) (ws []string, errs []error) { + value := v.(string) + if strings.HasSuffix(value, "/") { + errs = append(errs, fmt.Errorf("path cannot end in '/'")) + } + return + }, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return old+"/" == new || new+"/" == old + }, + }, + consts.FieldDescription: { + Type: schema.TypeString, + Optional: true, + Description: "Human-friendly description of the mount for the backend.", + }, + consts.FieldLocal: { + Type: schema.TypeBool, + ForceNew: true, + Optional: true, + Default: false, + Description: "Specifies if the secret backend is local only", + }, + consts.FieldDefaultLeaseTTL: { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Default lease duration for secrets in seconds", + }, + consts.FieldMaxLeaseTTL: { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Maximum possible lease duration for secrets in seconds", + }, + consts.FieldSecretID: { + Type: schema.TypeString, + Optional: true, + Description: "The Tencent Cloud Access Key ID to use when generating new credentials.", + Sensitive: true, + }, + consts.FieldSecretKey: { + Type: schema.TypeString, + Optional: true, + Description: "The Tencent Cloud Secret Access Key to use when generating new credentials.", + Sensitive: true, + }, + }, + }, false) +} + +func tencentCloudSecretBackendCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := d.Get(consts.FieldPath).(string) + description := d.Get(consts.FieldDescription).(string) + defaultTTL := d.Get(consts.FieldDefaultLeaseTTL).(int) + maxTTL := d.Get(consts.FieldMaxLeaseTTL).(int) + secretId := d.Get(consts.FieldSecretID).(string) + secretKey := d.Get(consts.FieldSecretKey).(string) + local := d.Get(consts.FieldLocal).(bool) + + d.Partial(true) + log.Printf("[DEBUG] Mounting TencentCloud backend at %q", path) + mountConfig := api.MountConfigInput{ + DefaultLeaseTTL: fmt.Sprintf("%ds", defaultTTL), + MaxLeaseTTL: fmt.Sprintf("%ds", maxTTL), + } + + err := client.Sys().MountWithContext(ctx, path, &api.MountInput{ + Type: "vault-plugin-secrets-tencentcloud", + Description: description, + Local: local, + Config: mountConfig, + }) + if err != nil { + return diag.Errorf("error mounting to %q: %s", path, err) + } + log.Printf("[DEBUG] Mounted Tencent CLoud backend at %q", path) + d.SetId(path) + + log.Printf("[DEBUG] Writing root credentials to %q", path+"/config") + data := map[string]interface{}{ + consts.FieldSecretID: secretId, + consts.FieldSecretKey: secretKey, + } + + time.Sleep(3 * time.Second) + + _, err = client.Logical().WriteWithContext(ctx, path+"/config", data) + if err != nil { + return diag.Errorf("error configuring root credentials for %q: %s", path, err) + } + log.Printf("[DEBUG] Wrote root credentials to %q", path+"/config") + d.Partial(false) + + return tencentCloudSecretBackendRead(ctx, d, meta) +} + +func tencentCloudSecretBackendRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := d.Id() + + log.Printf("[DEBUG] Reading Tencent Cloud backend mount %q from Vault", path) + + mount, err := mountutil.GetMount(ctx, client, path) + if err != nil { + if mountutil.IsMountNotFoundError(err) { + log.Printf("[WARN] Mount %q not found, removing from state.", path) + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + log.Printf("[DEBUG] Read Tencent Cloud backend mount %q from Vault", path) + + log.Printf("[DEBUG] Read Tencent Cloud secret backend config/root %s", path) + resp, err := client.Logical().ReadWithContext(ctx, path+"/config") + if err != nil { + // This is here to support backwards compatibility with Vault. Read operations on the config/root + // endpoint were just added and haven't been released yet, and so in currently released versions + // the read operations return a 405 error. We'll gracefully revert back to the old behavior in that + // case to allow for a transition period. + respErr, ok := err.(*api.ResponseError) + if !ok || respErr.StatusCode != 405 { + return diag.Errorf("error reading Tencent Cloud secret backend config/root: %s", err) + } + log.Printf("[DEBUG] Unable to read config/root due to old version detected; skipping reading access_key and region parameters") + resp = nil + } + if resp != nil { + if v, ok := resp.Data[consts.FieldSecretID].(string); ok { + _ = d.Set(consts.FieldSecretID, v) + } + if v, ok := resp.Data[consts.FieldSecretKey].(string); ok { + _ = d.Set(consts.FieldSecretKey, v) + } + // Terrible backwards compatibility hack. Previously, if no region was specified, + // this provider would just write a region of "us-east-1" into its state. Now that + // we're actually reading the region out from the backend, if it hadn't been set, + // it will return an empty string. This could potentially cause unexpected diffs + // for users of the provider, so to avoid it, we're doing something similar here + // and injecting a fake region of us-east-1 into the state. + + } + + if err := d.Set(consts.FieldPath, path); err != nil { + return diag.FromErr(err) + } + if err := d.Set(consts.FieldDescription, mount.Description); err != nil { + return diag.FromErr(err) + } + if err := d.Set(consts.FieldDefaultLeaseTTL, mount.Config.DefaultLeaseTTL); err != nil { + return diag.FromErr(err) + } + if err := d.Set(consts.FieldMaxLeaseTTL, mount.Config.MaxLeaseTTL); err != nil { + return diag.FromErr(err) + } + if err := d.Set(consts.FieldLocal, mount.Local); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func tencentCloudSecretBackendUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := d.Id() + d.Partial(true) + + path, err := util.Remount(d, client, consts.FieldPath, false) + if err != nil { + return diag.FromErr(err) + } + if d.HasChanges(consts.FieldDefaultLeaseTTL, consts.FieldMaxLeaseTTL, consts.FieldDescription) { + description := d.Get(consts.FieldDescription).(string) + config := api.MountConfigInput{ + Description: &description, + DefaultLeaseTTL: fmt.Sprintf("%ds", d.Get(consts.FieldDefaultLeaseTTL)), + MaxLeaseTTL: fmt.Sprintf("%ds", d.Get(consts.FieldMaxLeaseTTL)), + } + + log.Printf("[DEBUG] Updating mount config input for %q", path) + err := client.Sys().TuneMountWithContext(ctx, path, config) + if err != nil { + return diag.Errorf("error updating mount config input for %q: %s", path, err) + } + log.Printf("[DEBUG] Updated mount config input for %q", path) + } + if d.HasChanges(consts.FieldSecretID, consts.FieldSecretKey) { + log.Printf("[DEBUG] Updating root credentials at %q", path+"/config/root") + data := map[string]interface{}{ + consts.FieldSecretID: d.Get(consts.FieldSecretID).(string), + consts.FieldSecretKey: d.Get(consts.FieldSecretKey).(string), + } + + _, err := client.Logical().WriteWithContext(ctx, path+"/config", data) + if err != nil { + return diag.Errorf("error configuring root credentials for %q: %s", path, err) + } + log.Printf("[DEBUG] Updated root credentials at %q", path+"/config") + } + d.Partial(false) + return tencentCloudSecretBackendRead(ctx, d, meta) +} + +func tencentCloudSecretBackendDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := d.Id() + + log.Printf("[DEBUG] Unmounting Tencent Cloud backend %q", path) + err := client.Sys().UnmountWithContext(ctx, path) + if err != nil { + return diag.Errorf("error unmounting Tencent Cloud backend from %q: %s", path, err) + } + log.Printf("[DEBUG] Unmounted Tencent Cloud backend %q", path) + return nil +} diff --git a/vault/resource_tencentcloud_secret_backend_role.go b/vault/resource_tencentcloud_secret_backend_role.go new file mode 100644 index 000000000..d58dcd6b9 --- /dev/null +++ b/vault/resource_tencentcloud_secret_backend_role.go @@ -0,0 +1,203 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +func tencentCloudSecretBackendRoleResource(name string) *schema.Resource { + return &schema.Resource{ + Create: tencentCloudSecretBackendRoleWrite, + Read: provider.ReadWrapper(tencentCloudSecretBackendRoleRead), + Update: tencentCloudSecretBackendRoleWrite, + Delete: tencentCloudSecretBackendRoleDelete, + Exists: tencentCloudSecretBackendRoleExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Specifies the name of the role to generate credentials against. This is part of the request URL.", + }, + "backend": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The path of the Tencent Cloud Secret Backend the role belongs to.", + }, + "role_arn": { + Type: schema.TypeString, + Optional: true, + Description: "The ARN of a role that will be assumed to obtain STS credentials.", + }, + "remote_policies": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "The names and types of a pre-existing policies to be applied to the generate access token. Example: 'name: ReadOnlyAccess,type:-'", + }, + "inline_policies": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "The policy document JSON to be generated and attached to the access token.", + //ValidateFunc: ValidateDataJSONFunc(name), + //DiffSuppressFunc: util.JsonDiffSuppress, + }, + "ttl": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "The duration in seconds after which the issued token should expire. Defaults to 0, in which case the value will fallback to the system/mount defaults.", + }, + "max_ttl": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "The maximum allowed lifetime of tokens issued using this role.", + }, + }, + } +} + +func tencentCloudSecretBackendRoleWrite(d *schema.ResourceData, meta interface{}) error { + client, e := provider.GetClient(d, meta) + if e != nil { + return e + } + + name := d.Get("name").(string) + backend := d.Get("backend").(string) + + data := map[string]interface{}{} + roleArn := d.Get("role_arn").(string) + remotePolicy := d.Get("remote_policies").(*schema.Set).List() + inlinePolicy := d.Get("inline_policies").(*schema.Set).List() + + if roleArn == "" && len(remotePolicy) == 0 && len(inlinePolicy) == 0 { + return fmt.Errorf("at least one of: `role_arn`, `remote_policies` or `inline_policies` must be set") + } + + if d.HasChange("remote_policies") { + data["remote_policies"] = remotePolicy + } + if d.HasChange("inline_policies") { + data["inline_policies"] = inlinePolicy + } + if d.HasChange("role_arn") { + data["role_arn"] = roleArn + } + + if v, ok := d.GetOkExists("ttl"); ok { + data["ttl"] = v.(int) + } + if v, ok := d.GetOkExists("max_ttl"); ok { + data["max_ttl"] = v.(int) + } + + log.Printf("[DEBUG] Creating role %q on AWS backend %q", name, backend) + _, err := client.Logical().Write(backend+"/role/"+name, data) + if err != nil { + return fmt.Errorf("error creating role %q for backend %q: %s", name, backend, err) + } + log.Printf("[DEBUG] Created role %q on AWS backend %q", name, backend) + + d.SetId(backend + "/role/" + name) + return tencentCloudSecretBackendRoleRead(d, meta) +} + +func tencentCloudSecretBackendRoleRead(d *schema.ResourceData, meta interface{}) error { + client, e := provider.GetClient(d, meta) + if e != nil { + return e + } + + path := d.Id() + pathPieces := strings.Split(path, "/") + if len(pathPieces) < 3 || pathPieces[len(pathPieces)-2] != "role" { + return fmt.Errorf("invalid id %q; must be {backend}/role/{name}", path) + } + + log.Printf("[DEBUG] Reading role from %q", path) + secret, err := client.Logical().Read(path) + if err != nil { + return fmt.Errorf("error reading role %q: %s", path, err) + } + log.Printf("[DEBUG] Read role from %q", path) + if secret == nil { + log.Printf("[WARN] Role %q not found, removing from state", path) + d.SetId("") + return nil + } + + if v, ok := secret.Data["remote_policies"]; ok { + _ = d.Set("remote_policies", v) + } + + if v, ok := secret.Data["inline_policies"]; ok { + _ = d.Set("inline_policies", v) + } + + if v, ok := secret.Data["role_arn"]; ok { + _ = d.Set("role_arn", v) + } + + if v, ok := secret.Data["ttl"]; ok { + _ = d.Set("ttl", v) + } + if v, ok := secret.Data["max_ttl"]; ok { + _ = d.Set("max_ttl", v) + } + + _ = d.Set("backend", strings.Join(pathPieces[:len(pathPieces)-2], "/")) + _ = d.Set("name", pathPieces[len(pathPieces)-1]) + return nil +} + +func tencentCloudSecretBackendRoleDelete(d *schema.ResourceData, meta interface{}) error { + client, e := provider.GetClient(d, meta) + if e != nil { + return e + } + + path := d.Id() + log.Printf("[DEBUG] Deleting role %q", path) + _, err := client.Logical().Delete(path) + if err != nil { + return fmt.Errorf("error deleting role %q: %s", path, err) + } + log.Printf("[DEBUG] Deleted role %q", path) + return nil +} + +func tencentCloudSecretBackendRoleExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client, e := provider.GetClient(d, meta) + if e != nil { + return false, e + } + + path := d.Id() + log.Printf("[DEBUG] Checking if %q exists", path) + secret, err := client.Logical().Read(path) + if err != nil { + return true, fmt.Errorf("error checking if %q exists: %s", path, err) + } + log.Printf("[DEBUG] Checked if %q exists", path) + return secret != nil, nil +}