From dba4dbdb2a83bc6dc8883c22952e9f4afd6fd49f Mon Sep 17 00:00:00 2001 From: seanyyan Date: Tue, 22 Oct 2024 19:23:18 +0800 Subject: [PATCH] add new auth for tencentcloud cos backend --- internal/backend/remote-state/cos/backend.go | 283 ++++++++++++++++++- internal/backend/remote-state/cos/go.mod | 1 + website/docs/language/backend/cos.mdx | 102 ++++++- 3 files changed, 373 insertions(+), 13 deletions(-) diff --git a/internal/backend/remote-state/cos/backend.go b/internal/backend/remote-state/cos/backend.go index 5e4716dbe5c9..7d3dfad1827e 100644 --- a/internal/backend/remote-state/cos/backend.go +++ b/internal/backend/remote-state/cos/backend.go @@ -5,18 +5,23 @@ package cos import ( "context" + "encoding/json" "fmt" + "io" + "io/ioutil" "log" "net/http" "net/url" "os" "regexp" + "runtime" "strconv" "strings" "time" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/legacy/helper/schema" + "github.com/mitchellh/go-homedir" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" sts "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts/v20180813" @@ -35,6 +40,14 @@ const ( PROVIDER_ASSUME_ROLE_ARN = "TENCENTCLOUD_ASSUME_ROLE_ARN" PROVIDER_ASSUME_ROLE_SESSION_NAME = "TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME" PROVIDER_ASSUME_ROLE_SESSION_DURATION = "TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION" + PROVIDER_ASSUME_ROLE_EXTERNAL_ID = "TENCENTCLOUD_ASSUME_ROLE_EXTERNAL_ID" + PROVIDER_SHARED_CREDENTIALS_DIR = "TENCENTCLOUD_SHARED_CREDENTIALS_DIR" + PROVIDER_PROFILE = "TENCENTCLOUD_PROFILE" + PROVIDER_CAM_ROLE_NAME = "TENCENTCLOUD_CAM_ROLE_NAME" +) + +const ( + DEFAULT_PROFILE = "default" ) // Backend implements "backend".Backend for tencentCloud cos @@ -56,6 +69,15 @@ type Backend struct { domain string } +type CAMResponse struct { + TmpSecretId string `json:"TmpSecretId"` + TmpSecretKey string `json:"TmpSecretKey"` + ExpiredTime int64 `json:"ExpiredTime"` + Expiration string `json:"Expiration"` + Token string `json:"Token"` + Code string `json:"Code"` +} + // New creates a new backend for TencentCloud cos remote state. func New() backend.Backend { s := &schema.Backend{ @@ -191,9 +213,33 @@ func New() backend.Backend { Optional: true, Description: "A more restrictive policy when making the AssumeRole call. Its content must not contains `principal` elements. Notice: more syntax references, please refer to: [policies syntax logic](https://intl.cloud.tencent.com/document/product/598/10603).", }, + "external_id": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ASSUME_ROLE_EXTERNAL_ID, nil), + Description: "External role ID, which can be obtained by clicking the role name in the CAM console. It can contain 2-128 letters, digits, and symbols (=,.@:/-). Regex: [\\w+=,.@:/-]*. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_EXTERNAL_ID`.", + }, }, }, }, + "shared_credentials_dir": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SHARED_CREDENTIALS_DIR, nil), + Description: "The directory of the shared credentials. It can also be sourced from the `TENCENTCLOUD_SHARED_CREDENTIALS_DIR` environment variable. If not set this defaults to ~/.tccli.", + }, + "profile": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_PROFILE, nil), + Description: "The profile name as set in the shared credentials. It can also be sourced from the `TENCENTCLOUD_PROFILE` environment variable. If not set, the default profile created with `tccli configure` will be used.", + }, + "cam_role_name": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_CAM_ROLE_NAME, nil), + Description: "The name of the CVM instance CAM role. It can be sourced from the `TENCENTCLOUD_CAM_ROLE_NAME` environment variable.", + }, }, } @@ -275,9 +321,56 @@ func (b *Backend) configure(ctx context.Context) error { return err } - secretId := data.Get("secret_id").(string) - secretKey := data.Get("secret_key").(string) - securityToken := data.Get("security_token").(string) + var getProviderConfig = func(key string) string { + var str string + value, err := getConfigFromProfile(data, key) + if err == nil && value != nil { + str = value.(string) + } + + return str + } + + var ( + secretId string + secretKey string + securityToken string + ) + + // get auth from tf/env + if v, ok := data.GetOk("secret_id"); ok { + secretId = v.(string) + } + + if v, ok := data.GetOk("secret_key"); ok { + secretKey = v.(string) + } + + if v, ok := data.GetOk("security_token"); ok { + securityToken = v.(string) + } + + // get auth from tccli + if secretId == "" && secretKey == "" && securityToken == "" { + secretId = getProviderConfig("secretId") + secretKey = getProviderConfig("secretKey") + securityToken = getProviderConfig("token") + } + + // get auth from CAM role name + if v, ok := data.GetOk("cam_role_name"); ok { + camRoleName := v.(string) + if camRoleName != "" { + camResp, err := getAuthFromCAM(camRoleName) + if err != nil { + return err + } + + secretId = camResp.TmpSecretId + secretKey = camResp.TmpSecretKey + securityToken = camResp.Token + } + } // init credential by AKSK & TOKEN b.credential = common.NewTokenCredential(secretId, secretKey, securityToken) @@ -304,23 +397,70 @@ func (b *Backend) configure(ctx context.Context) error { } func handleAssumeRole(data *schema.ResourceData, b *Backend) error { + var ( + assumeRoleArn string + assumeRoleSessionName string + assumeRoleSessionDuration int + assumeRolePolicy string + assumeRoleExternalId string + ) + + // get assume role from credential + if providerConfig["role-arn"] != nil { + assumeRoleArn = providerConfig["role-arn"].(string) + } + + if providerConfig["role-session-name"] != nil { + assumeRoleSessionName = providerConfig["role-session-name"].(string) + } + + if assumeRoleArn != "" && assumeRoleSessionName != "" { + assumeRoleSessionDuration = 7200 + } + + // get assume role from env + envRoleArn := os.Getenv(PROVIDER_ASSUME_ROLE_ARN) + envSessionName := os.Getenv(PROVIDER_ASSUME_ROLE_SESSION_NAME) + if envRoleArn != "" && envSessionName != "" { + assumeRoleArn = envRoleArn + assumeRoleSessionName = envSessionName + if envSessionDuration := os.Getenv(PROVIDER_ASSUME_ROLE_SESSION_DURATION); envSessionDuration != "" { + var err error + assumeRoleSessionDuration, err = strconv.Atoi(envSessionDuration) + if err != nil { + return err + } + } + + if assumeRoleSessionDuration == 0 { + assumeRoleSessionDuration = 7200 + } + + assumeRoleExternalId = os.Getenv(PROVIDER_ASSUME_ROLE_EXTERNAL_ID) + } + + // get assume role from tf assumeRoleList := data.Get("assume_role").(*schema.Set).List() if len(assumeRoleList) == 1 { assumeRole := assumeRoleList[0].(map[string]interface{}) - assumeRoleArn := assumeRole["role_arn"].(string) - assumeRoleSessionName := assumeRole["session_name"].(string) - assumeRoleSessionDuration := assumeRole["session_duration"].(int) - assumeRolePolicy := assumeRole["policy"].(string) + assumeRoleArn = assumeRole["role_arn"].(string) + assumeRoleSessionName = assumeRole["session_name"].(string) + assumeRoleSessionDuration = assumeRole["session_duration"].(int) + assumeRolePolicy = assumeRole["policy"].(string) + assumeRoleExternalId = assumeRole["external_id"].(string) + } - err := b.updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName, assumeRoleSessionDuration, assumeRolePolicy) + if assumeRoleArn != "" && assumeRoleSessionName != "" { + err := b.updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName, assumeRoleSessionDuration, assumeRolePolicy, assumeRoleExternalId) if err != nil { return err } } + return nil } -func (b *Backend) updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName string, assumeRoleSessionDuration int, assumeRolePolicy string) error { +func (b *Backend) updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName string, assumeRoleSessionDuration int, assumeRolePolicy string, assumeRoleExternalId string) error { // assume role by STS request := sts.NewAssumeRoleRequest() request.RoleArn = &assumeRoleArn @@ -332,6 +472,10 @@ func (b *Backend) updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName s request.Policy = &policy } + if assumeRoleExternalId != "" { + request.ExternalId = &assumeRoleExternalId + } + response, err := b.UseStsClient().AssumeRole(request) if err != nil { return err @@ -382,3 +526,124 @@ func (b *Backend) NewClientProfile(timeout int) *profile.ClientProfile { return cpf } + +func getAuthFromCAM(roleName string) (camResp *CAMResponse, err error) { + url := fmt.Sprintf("http://metadata.tencentyun.com/latest/meta-data/cam/security-credentials/%s", roleName) + log.Printf("[CRITAL] Request CAM security credentials url: %s\n", url) + // maximum waiting time + client := &http.Client{Timeout: 2 * time.Second} + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return + } + + resp, err := client.Do(req) + if err != nil { + log.Printf("[CRITAL] Request CAM security credentials resp err: %s", err.Error()) + return + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("[CRITAL] Request CAM security credentials body read err: %s", err.Error()) + return + } + + err = json.Unmarshal(body, &camResp) + if err != nil { + log.Printf("[CRITAL] Request CAM security credentials resp json err: %s", err.Error()) + return + } + + return +} + +var providerConfig map[string]interface{} + +func getConfigFromProfile(d *schema.ResourceData, ProfileKey string) (interface{}, error) { + if providerConfig == nil { + var ( + profile string + sharedCredentialsDir string + credentialPath string + configurePath string + ) + + if v, ok := d.GetOk("profile"); ok { + profile = v.(string) + } else { + profile = DEFAULT_PROFILE + } + + if v, ok := d.GetOk("shared_credentials_dir"); ok { + sharedCredentialsDir = v.(string) + } + + tmpSharedCredentialsDir, err := homedir.Expand(sharedCredentialsDir) + if err != nil { + return nil, err + } + + if tmpSharedCredentialsDir == "" { + credentialPath = fmt.Sprintf("%s/.tccli/%s.credential", os.Getenv("HOME"), profile) + configurePath = fmt.Sprintf("%s/.tccli/%s.configure", os.Getenv("HOME"), profile) + if runtime.GOOS == "windows" { + credentialPath = fmt.Sprintf("%s/.tccli/%s.credential", os.Getenv("USERPROFILE"), profile) + configurePath = fmt.Sprintf("%s/.tccli/%s.configure", os.Getenv("USERPROFILE"), profile) + } + } else { + credentialPath = fmt.Sprintf("%s/%s.credential", tmpSharedCredentialsDir, profile) + configurePath = fmt.Sprintf("%s/%s.configure", tmpSharedCredentialsDir, profile) + } + + providerConfig = make(map[string]interface{}) + _, err = os.Stat(credentialPath) + if !os.IsNotExist(err) { + data, err := ioutil.ReadFile(credentialPath) + if err != nil { + return nil, err + } + + config := map[string]interface{}{} + err = json.Unmarshal(data, &config) + if err != nil { + return nil, err + } + + for k, v := range config { + providerConfig[k] = strings.TrimSpace(v.(string)) + } + } + + _, err = os.Stat(configurePath) + if !os.IsNotExist(err) { + data, err := ioutil.ReadFile(configurePath) + if err != nil { + return nil, err + } + + config := map[string]interface{}{} + err = json.Unmarshal(data, &config) + if err != nil { + return nil, err + } + + outerLoop: + for k, v := range config { + if k == "_sys_param" { + tmpMap := v.(map[string]interface{}) + for tmpK, tmpV := range tmpMap { + if tmpK == "region" { + providerConfig[tmpK] = strings.TrimSpace(tmpV.(string)) + break outerLoop + } + } + } + } + } + } + + return providerConfig[ProfileKey], nil +} diff --git a/internal/backend/remote-state/cos/go.mod b/internal/backend/remote-state/cos/go.mod index c4101374beab..b10f65c2c5ce 100644 --- a/internal/backend/remote-state/cos/go.mod +++ b/internal/backend/remote-state/cos/go.mod @@ -5,6 +5,7 @@ go 1.23.1 require ( github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 github.com/hashicorp/terraform/internal/legacy v0.0.0-00010101000000-000000000000 + github.com/mitchellh/go-homedir v1.1.0 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233 diff --git a/website/docs/language/backend/cos.mdx b/website/docs/language/backend/cos.mdx index feaf2361362a..80551ad7ab14 100644 --- a/website/docs/language/backend/cos.mdx +++ b/website/docs/language/backend/cos.mdx @@ -88,9 +88,9 @@ terraform { bucket = "bucket-for-terraform-state-{appid}" prefix = "terraform/state" assume_role { - role_arn = "qcs::cam::uin/xxx:roleName/yyy" - session_name = "my-session-name" - session_duration = 3600 + role_arn = "qcs::cam::uin/xxx:roleName/yyy" + session_name = "my-session-name" + session_duration = 7200 } } } @@ -106,7 +106,101 @@ $ export TENCENTCLOUD_SECRET_KEY="my-secret-key" $ export TENCENTCLOUD_REGION="ap-guangzhou" $ export TENCENTCLOUD_ASSUME_ROLE_ARN="qcs::cam::uin/xxx:roleName/yyy" $ export TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME="my-session-name" -$ export TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION=3600 +$ export TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION=7200 +$ terraform plan +``` + +### Shared credentials +You can use [Tencent Cloud credentials](https://www.tencentcloud.com/document/product/1013/33464) to specify your credentials. The default location is `$HOME/.tccli` on Linux and macOS, And `"%USERPROFILE%\.tccli"` on Windows. You can optionally specify a different location in the Terraform configuration by providing the `shared_credentials_dir` argument or using the `TENCENTCLOUD_SHARED_CREDENTIALS_DIR` environment variable. This method also supports a `profile` configuration and matching `TENCENTCLOUD_PROFILE` environment variable: + +- `shared_credentials_dir` - (Optional) The directory of the shared credentials. It can also be sourced from the `TENCENTCLOUD_SHARED_CREDENTIALS_DIR` environment variable. If not set this defaults to ~/.tccli. +- `profile` - (Optional) The profile name as set in the shared credentials. It can also be sourced from the `TENCENTCLOUD_PROFILE` environment variable. If not set, the default profile created with `tccli configure` will be used. + +Usage: + +On Linux/MacOS + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-{appid}" + prefix = "terraform/state" + shared_credentials_dir = "/Users/tf_user/.tccli" + profile = "default" + } +} +``` + +On Windows + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-{appid}" + prefix = "terraform/state" + shared_credentials_dir = "C:\\Users\\tf_user\\.tccli" + profile = "default" + } +} +``` + +In addition, these `shared_credentials_dir`, `profile` configurations can also be provided by environment variables. + +Usage: + +```shell +$ export PROVIDER_SHARED_CREDENTIALS_DIR="/Users/tf_user/.tccli" +$ export PROVIDER_PROFILE="default" +$ terraform plan +``` + +### Cam role name +If provided with a Cam role name, Terraform will just access the metadata URL: `http://metadata.tencentyun.com/latest/meta-data/cam/security-credentials/` to obtain the STS credential. The CVM Instance Role also can be set using the `TENCENTCLOUD_CAM_ROLE_NAME` environment variables. + +- `cam_role_name` - (Optional) The name of the CVM instance CAM role. It can be sourced from the `TENCENTCLOUD_CAM_ROLE_NAME` environment variable. + +Usage: + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-{appid}" + prefix = "terraform/state" + cam_role_name = "my-cam-role-name" + } +} +``` + +It can also be authenticated together with method Assume role. Authentication process: Perform CAM authentication first, then proceed with Assume role authentication. + +Usage: + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-{appid}" + prefix = "terraform/state" + cam_role_name = "my-cam-role-name" + assume_role { + role_arn = "qcs::cam::uin/xxx:roleName/yyy" + session_name = "my-session-name" + session_duration = 7200 + external_id = "my-external-id" + } + } +} +``` + +In addition, these `cam_role_name` configurations can also be provided by environment variables. + +Usage: + +```shell +$ export PROVIDER_CAM_ROLE_NAME="my-cam-role-name" $ terraform plan ```