diff --git a/tencentcloud/common.go b/tencentcloud/common.go index 54801a87d3..973aed8c5b 100644 --- a/tencentcloud/common.go +++ b/tencentcloud/common.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/mitchellh/go-homedir" "github.com/pkg/errors" sdkErrors "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" "github.com/tencentyun/cos-go-sdk-v5" @@ -251,6 +252,34 @@ func isExpectError(err error, expectError []string) bool { return false } +// IsNil Determine whether i is empty +func IsNil(v interface{}) bool { + + valueOf := reflect.ValueOf(v) + + k := valueOf.Kind() + + switch k { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice: + return valueOf.IsNil() + default: + return v == nil + } +} + +// IsString Determine whether data is a string +func IsString(data interface{}) bool { + if IsNil(data) { + return false + } + + if _, ok := data.(string); ok { + return true + } + + return false +} + // writeToFile write data to file func writeToFile(filePath string, data interface{}) error { if strings.HasPrefix(filePath, "~") { @@ -277,6 +306,10 @@ func writeToFile(filePath string, data interface{}) error { } } + if IsString(data) { + return ioutil.WriteFile(filePath, []byte(data.(string)), 0422) + } + jsonStr, err := json.MarshalIndent(data, "", "\t") if err != nil { return fmt.Errorf("json decode error,reason %s", err.Error()) @@ -285,6 +318,21 @@ func writeToFile(filePath string, data interface{}) error { return ioutil.WriteFile(filePath, jsonStr, 0422) } +// ReadFromFile return file content +func ReadFromFile(file string) ([]byte, error) { + fileName, err := homedir.Expand(file) + if err != nil { + log.Printf("[CRITAL] wrong file path, error: %v", err) + return nil, err + } + content, err := ioutil.ReadFile(fileName) + if err != nil { + log.Printf("[CRITAL] file read failed, error: %v", err) + return nil, err + } + return content, nil +} + func CheckNil(object interface{}, fields map[string]string) (nilFields []string) { // if object is a pointer, get value which object points to object = reflect.Indirect(reflect.ValueOf(object)).Interface() diff --git a/tencentcloud/provider.go b/tencentcloud/provider.go index 31761286af..56d800353c 100644 --- a/tencentcloud/provider.go +++ b/tencentcloud/provider.go @@ -182,6 +182,7 @@ Cloud Access Management(CAM) tencentcloud_cam_oidc_sso tencentcloud_cam_role_sso tencentcloud_cam_service_linked_role + tencentcloud_cam_user_saml_config Cloud Block Storage(CBS) Data Source @@ -1380,6 +1381,7 @@ func Provider() terraform.ResourceProvider { "tencentcloud_cam_group_membership": resourceTencentCloudCamGroupMembership(), "tencentcloud_cam_saml_provider": resourceTencentCloudCamSAMLProvider(), "tencentcloud_cam_service_linked_role": resourceTencentCloudCamServiceLinkedRole(), + "tencentcloud_cam_user_saml_config": resourceTencentCloudCamUserSamlConfig(), "tencentcloud_scf_function": resourceTencentCloudScfFunction(), "tencentcloud_scf_namespace": resourceTencentCloudScfNamespace(), "tencentcloud_scf_layer": resourceTencentCloudScfLayer(), diff --git a/tencentcloud/resource_tc_cam_user_saml_config.go b/tencentcloud/resource_tc_cam_user_saml_config.go new file mode 100644 index 0000000000..3ffc8069e3 --- /dev/null +++ b/tencentcloud/resource_tc_cam_user_saml_config.go @@ -0,0 +1,230 @@ +/* +Provides a resource to create a cam user_saml_config + +Example Usage + +```hcl +resource "tencentcloud_cam_user_saml_config" "user_saml_config" { + saml_metadata_document = "./metadataDocument.xml" + # saml_metadata_document = <<-EOT + # + # EOT +} +``` + +Import + +cam user_saml_config can be imported using the id, e.g. + +``` +terraform import tencentcloud_cam_user_saml_config.user_saml_config user_id +``` +*/ +package tencentcloud + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + cam "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cam/v20190116" + "github.com/tencentcloudstack/terraform-provider-tencentcloud/tencentcloud/internal/helper" +) + +func resourceTencentCloudCamUserSamlConfig() *schema.Resource { + return &schema.Resource{ + Create: resourceTencentCloudCamUserSamlConfigCreate, + Read: resourceTencentCloudCamUserSamlConfigRead, + Update: resourceTencentCloudCamUserSamlConfigUpdate, + Delete: resourceTencentCloudCamUserSamlConfigDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "saml_metadata_document": { + Required: true, + Type: schema.TypeString, + Description: "SAML metadata document, xml format, support string content or file path.", + StateFunc: func(v interface{}) string { + saml := v.(string) + if saml != "" { + b := strings.HasSuffix(saml, ".xml") + if b { + metadata, _ := ReadFromFile(saml) + return string(metadata) + } + } + return saml + }, + }, + + "status": { + Computed: true, + Type: schema.TypeInt, + Description: "Status: `0`: not set, `11`: enabled, `2`: disabled.", + }, + + "metadata_document_file": { + Type: schema.TypeString, + Optional: true, + Description: "The path used to save the samlMetadat file.", + }, + }, + } +} + +func resourceTencentCloudCamUserSamlConfigCreate(d *schema.ResourceData, meta interface{}) error { + defer logElapsed("resource.tencentcloud_cam_user_saml_config.create")() + defer inconsistentCheck(d, meta)() + + logId := getLogId(contextNil) + + var ( + request = cam.NewCreateUserSAMLConfigRequest() + response = cam.NewCreateUserSAMLConfigResponse() + ) + if v, ok := d.GetOk("saml_metadata_document"); ok { + saml := v.(string) + b := strings.HasSuffix(saml, ".xml") + if b { + metadata, err := ReadFromFile(v.(string)) + if err != nil { + return err + } + saml = string(metadata) + } + request.SAMLMetadataDocument = helper.String(StringToBase64(saml)) + } + + err := resource.Retry(writeRetryTimeout, func() *resource.RetryError { + result, e := meta.(*TencentCloudClient).apiV3Conn.UseCamClient().CreateUserSAMLConfig(request) + if e != nil { + return retryError(e) + } else { + log.Printf("[DEBUG]%s api[%s] success, request body [%s], response body [%s]\n", logId, request.GetAction(), request.ToJsonString(), result.ToJsonString()) + } + response = result + return nil + }) + if err != nil { + log.Printf("[CRITAL]%s create cam userSamlConfig failed, reason:%+v", logId, err) + return err + } + + d.SetId(*response.Response.RequestId) + return resourceTencentCloudCamUserSamlConfigRead(d, meta) +} + +func resourceTencentCloudCamUserSamlConfigRead(d *schema.ResourceData, meta interface{}) error { + defer logElapsed("resource.tencentcloud_cam_user_saml_config.read")() + defer inconsistentCheck(d, meta)() + + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), logIdKey, logId) + + service := CamService{client: meta.(*TencentCloudClient).apiV3Conn} + + samlConfig, err := service.DescribeCamUserSamlConfigById(ctx) + if err != nil { + return err + } + + if samlConfig == nil || samlConfig.Response == nil || *samlConfig.Response.Status == 2 { + d.SetId("") + log.Printf("[WARN]%s resource `CamUserSamlConfig` status is closed, please check if it has been closed.", logId) + return nil + } + userSamlConfig := samlConfig.Response + + if userSamlConfig.SAMLMetadata != nil { + metadata, err := Base64ToString(*userSamlConfig.SAMLMetadata) + if err != nil { + return fmt.Errorf("`SamlConfig.SAMLMetadata` %s does not be decoded to xml", *userSamlConfig.SAMLMetadata) + } + _ = d.Set("saml_metadata_document", metadata) + } + + if userSamlConfig.Status != nil { + _ = d.Set("status", userSamlConfig.Status) + } + + output, ok := d.GetOk("metadata_document_files") + if ok && output.(string) != "" { + if err = writeToFile(output.(string), d.Get("saml_metadata_document")); err != nil { + return err + } + } + + return nil +} + +func resourceTencentCloudCamUserSamlConfigUpdate(d *schema.ResourceData, meta interface{}) error { + defer logElapsed("resource.tencentcloud_cam_user_saml_config.update")() + defer inconsistentCheck(d, meta)() + + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), logIdKey, logId) + + request := cam.NewUpdateUserSAMLConfigRequest() + + if d.HasChange("saml_metadata_document") { + if v, ok := d.GetOk("saml_metadata_document"); ok { + saml := v.(string) + b := strings.HasSuffix(saml, ".xml") + if b { + metadata, err := ReadFromFile(v.(string)) + if err != nil { + return err + } + saml = string(metadata) + } + request.SAMLMetadataDocument = helper.String(StringToBase64(saml)) + } + } + + service := CamService{client: meta.(*TencentCloudClient).apiV3Conn} + samlConfig, describeErr := service.DescribeCamUserSamlConfigById(ctx) + if describeErr != nil { + return describeErr + } + if *samlConfig.Response.Status == 2 { + request.Operate = helper.String("enable") + } else { + request.Operate = helper.String("updateSAML") + } + + err := resource.Retry(writeRetryTimeout, func() *resource.RetryError { + result, e := meta.(*TencentCloudClient).apiV3Conn.UseCamClient().UpdateUserSAMLConfig(request) + if e != nil { + return retryError(e) + } else { + log.Printf("[DEBUG]%s api[%s] success, request body [%s], response body [%s]\n", logId, request.GetAction(), request.ToJsonString(), result.ToJsonString()) + } + return nil + }) + if err != nil { + log.Printf("[CRITAL]%s update cam userSamlConfig failed, reason:%+v", logId, err) + return err + } + + return resourceTencentCloudCamUserSamlConfigRead(d, meta) +} + +func resourceTencentCloudCamUserSamlConfigDelete(d *schema.ResourceData, meta interface{}) error { + defer logElapsed("resource.tencentcloud_cam_user_saml_config.delete")() + defer inconsistentCheck(d, meta)() + + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), logIdKey, logId) + + service := CamService{client: meta.(*TencentCloudClient).apiV3Conn} + + if err := service.DeleteCamUserSamlConfigById(ctx); err != nil { + return err + } + + return nil +} diff --git a/tencentcloud/resource_tc_cam_user_saml_config_test.go b/tencentcloud/resource_tc_cam_user_saml_config_test.go new file mode 100644 index 0000000000..dd8871e172 --- /dev/null +++ b/tencentcloud/resource_tc_cam_user_saml_config_test.go @@ -0,0 +1,36 @@ +package tencentcloud + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccTencentCloudNeedFixCamUserSamlConfigResource_basic(t *testing.T) { + t.Parallel() + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCamUserSamlConfig, + Check: resource.ComposeTestCheckFunc(resource.TestCheckResourceAttrSet("tencentcloud_cam_user_saml_config.user_saml_config", "id")), + }, + { + ResourceName: "tencentcloud_cam_user_saml_config.user_saml_config", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +const testAccCamUserSamlConfig = ` + +resource "tencentcloud_cam_user_saml_config" "user_saml_config" { + saml_metadata_document = "" +} + +` diff --git a/tencentcloud/service_tencentcloud_cam.go b/tencentcloud/service_tencentcloud_cam.go index ed86686b5e..4695a89926 100644 --- a/tencentcloud/service_tencentcloud_cam.go +++ b/tencentcloud/service_tencentcloud_cam.go @@ -12,6 +12,7 @@ import ( cam "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cam/v20190116" sdkErrors "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" "github.com/tencentcloudstack/terraform-provider-tencentcloud/tencentcloud/connectivity" + "github.com/tencentcloudstack/terraform-provider-tencentcloud/tencentcloud/internal/helper" "github.com/tencentcloudstack/terraform-provider-tencentcloud/tencentcloud/ratelimit" ) @@ -1305,3 +1306,52 @@ func (me *CamService) DescribeCamServiceLinkedRoleDeleteStatus(ctx context.Conte return } + +func (me *CamService) DescribeCamUserSamlConfigById(ctx context.Context) (userSamlConfig *cam.DescribeUserSAMLConfigResponse, errRet error) { + logId := getLogId(ctx) + + request := cam.NewDescribeUserSAMLConfigRequest() + + defer func() { + if errRet != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%s]\n", logId, request.GetAction(), request.ToJsonString(), errRet.Error()) + } + }() + + ratelimit.Check(request.GetAction()) + + response, err := me.client.UseCamClient().DescribeUserSAMLConfig(request) + if err != nil { + errRet = err + return + } + log.Printf("[DEBUG]%s api[%s] success, request body [%s], response body [%s]\n", logId, request.GetAction(), request.ToJsonString(), response.ToJsonString()) + + userSamlConfig = response + return +} + +func (me *CamService) DeleteCamUserSamlConfigById(ctx context.Context) (errRet error) { + logId := getLogId(ctx) + + request := cam.NewUpdateUserSAMLConfigRequest() + + defer func() { + if errRet != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%s]\n", logId, request.GetAction(), request.ToJsonString(), errRet.Error()) + } + }() + + request.Operate = helper.String("disable") + + ratelimit.Check(request.GetAction()) + + response, err := me.client.UseCamClient().UpdateUserSAMLConfig(request) + if err != nil { + errRet = err + return + } + log.Printf("[DEBUG]%s api[%s] success, request body [%s], response body [%s]\n", logId, request.GetAction(), request.ToJsonString(), response.ToJsonString()) + + return +} diff --git a/website/docs/r/cam_user_saml_config.html.markdown b/website/docs/r/cam_user_saml_config.html.markdown new file mode 100644 index 0000000000..b2b6381deb --- /dev/null +++ b/website/docs/r/cam_user_saml_config.html.markdown @@ -0,0 +1,47 @@ +--- +subcategory: "Cloud Access Management(CAM)" +layout: "tencentcloud" +page_title: "TencentCloud: tencentcloud_cam_user_saml_config" +sidebar_current: "docs-tencentcloud-resource-cam_user_saml_config" +description: |- + Provides a resource to create a cam user_saml_config +--- + +# tencentcloud_cam_user_saml_config + +Provides a resource to create a cam user_saml_config + +## Example Usage + +```hcl +resource "tencentcloud_cam_user_saml_config" "user_saml_config" { + saml_metadata_document = "./metadataDocument.xml" + # saml_metadata_document = <<-EOT + # + # EOT +} +``` + +## Argument Reference + +The following arguments are supported: + +* `saml_metadata_document` - (Required, String) SAML metadata document, xml format, support string content or file path. +* `metadata_document_file` - (Optional, String) The path used to save the samlMetadat file. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - ID of the resource. +* `status` - Status: `0`: not set, `11`: enabled, `2`: disabled. + + +## Import + +cam user_saml_config can be imported using the id, e.g. + +``` +terraform import tencentcloud_cam_user_saml_config.user_saml_config user_id +``` + diff --git a/website/tencentcloud.erb b/website/tencentcloud.erb index fdb75d7d8b..897fd487a7 100644 --- a/website/tencentcloud.erb +++ b/website/tencentcloud.erb @@ -356,6 +356,9 @@
  • tencentcloud_cam_user_policy_attachment
  • +
  • + tencentcloud_cam_user_saml_config +