Add new auth for tencentcloud cos backend (#35888)

* add new auth for tencentcloud cos backend

* add new auth for tencentcloud cos backend

* add
pull/35993/head
SevenEarth 2 years ago committed by GitHub
parent f57c3c768a
commit 7921b1317c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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,126 @@ 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 {
if strValue, ok := v.(string); ok {
providerConfig[k] = strings.TrimSpace(strValue)
}
}
}
_, 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
}

@ -5,6 +5,7 @@ go 1.23.3
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

@ -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/<cam_role_name>` 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
```

Loading…
Cancel
Save