diff --git a/go.mod b/go.mod index 52adb586..82b1af13 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/hashicorp/terraform-plugin-sdk v1.4.0 + github.com/mitchellh/mapstructure v1.1.2 github.com/pkg/errors v0.8.0 github.com/tidwall/gjson v1.16.0 github.com/ucloud/ucloud-sdk-go v0.22.5 @@ -53,7 +54,6 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/mitchellh/reflectwalk v1.0.1 // indirect github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.0.5+incompatible // indirect diff --git a/ucloud/config.go b/ucloud/config.go index b93c9f04..272948a9 100644 --- a/ucloud/config.go +++ b/ucloud/config.go @@ -3,15 +3,16 @@ package ucloud import ( "encoding/json" "fmt" - "github.com/ucloud/ucloud-sdk-go/services/iam" - "github.com/ucloud/ucloud-sdk-go/services/uads" - "github.com/ucloud/ucloud-sdk-go/services/uphost" "io/ioutil" "os" "path/filepath" "runtime" "time" + "github.com/ucloud/ucloud-sdk-go/services/iam" + "github.com/ucloud/ucloud-sdk-go/services/uads" + "github.com/ucloud/ucloud-sdk-go/services/uphost" + "github.com/ucloud/ucloud-sdk-go/services/cube" "github.com/ucloud/ucloud-sdk-go/services/ufile" "github.com/ucloud/ucloud-sdk-go/services/ufs" @@ -23,6 +24,7 @@ import ( "github.com/ucloud/ucloud-sdk-go/private/protocol/http" pumem "github.com/ucloud/ucloud-sdk-go/private/services/umem" "github.com/ucloud/ucloud-sdk-go/services/ipsecvpn" + "github.com/ucloud/ucloud-sdk-go/services/sts" "github.com/ucloud/ucloud-sdk-go/services/uaccount" "github.com/ucloud/ucloud-sdk-go/services/udb" "github.com/ucloud/ucloud-sdk-go/services/udisk" @@ -42,12 +44,20 @@ type Config struct { PrivateKey string Profile string SharedCredentialsFile string + AssumeRole *AssumeRoleConfig + Region string + ProjectId string + Insecure bool + BaseURL string + MaxRetries int +} - Region string - ProjectId string - Insecure bool - BaseURL string - MaxRetries int +// AssumeRoleConfig is the configuration of assume role +type AssumeRoleConfig struct { + Duration time.Duration + RoleURN string + Policy string + SessionName string } type cloudShellCredential struct { @@ -124,7 +134,14 @@ func (c *Config) Client() (*UCloudClient, error) { } else { return nil, fmt.Errorf("must set credential about public_key and private_key") } - + if c.AssumeRole != nil { + // get STS credential + stsCredential, err := getSTSCredential(*c.AssumeRole, cfg, cred) + if err != nil { + return nil, fmt.Errorf("fail to get STS credential, %w", err) + } + cred = *stsCredential + } // initialize client connections client.unetconn = unet.NewClient(&cfg, &cred) client.ulbconn = ulb.NewClient(&cfg, &cred) @@ -209,3 +226,29 @@ func userHomeDir() string { } return os.Getenv("HOME") } + +func getSTSCredential(assumeRole AssumeRoleConfig, config ucloud.Config, credential auth.Credential) (*auth.Credential, error) { + // get STS credential + stsClient := sts.NewClient(&config, &credential) + var assumeRoleRequest sts.AssumeRoleRequest + assumeRoleRequest.Policy = ucloud.String(assumeRole.Policy) + assumeRoleRequest.RoleUrn = ucloud.String(assumeRole.RoleURN) + assumeRoleRequest.RoleSessionName = ucloud.String(assumeRole.SessionName) + assumeRoleRequest.DurationSeconds = ucloud.Int(int(assumeRole.Duration.Seconds())) + assumeRoleResponse, err := stsClient.AssumeRole(&assumeRoleRequest) + if err != nil { + return nil, fmt.Errorf("fail to assume role, %w", err) + } + // set STS credential + var stsCredential auth.Credential + stsCredential.PublicKey = assumeRoleResponse.Credentials.AccessKeyId + stsCredential.PrivateKey = assumeRoleResponse.Credentials.AccessKeySecret + stsCredential.SecurityToken = assumeRoleResponse.Credentials.SecurityToken + stsCredential.CanExpire = true + expireTime, err := time.Parse(time.RFC3339, assumeRoleResponse.Credentials.Expiration) + if err != nil { + return nil, fmt.Errorf("fail to parse expiration time, %w", err) + } + stsCredential.Expires = expireTime + return &stsCredential, nil +} diff --git a/ucloud/provider.go b/ucloud/provider.go index d5d66752..6f3b09da 100644 --- a/ucloud/provider.go +++ b/ucloud/provider.go @@ -1,8 +1,11 @@ package ucloud import ( + "time" + "github.com/hashicorp/terraform-plugin-sdk/helper/mutexkv" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" "github.com/hashicorp/terraform-plugin-sdk/terraform" ) @@ -73,6 +76,8 @@ func Provider() terraform.ResourceProvider { ConflictsWith: []string{"insecure"}, ValidateFunc: validateBaseUrl, }, + + "assume_role": assumeRoleSchema(), }, DataSourcesMap: map[string]*schema.Resource{ @@ -185,10 +190,76 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { config.BaseURL = GetEndpointURL(config.Region) } + if v, ok := d.GetOk("assume_role"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + config.AssumeRole = expandAssumeRole(v.([]interface{})[0].(map[string]interface{})) + } + client, err := config.Client() return client, err } +func assumeRoleSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "duration": { + Type: schema.TypeString, + Optional: true, + Description: "The duration of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", + ValidateFunc: validateAssumeRoleDuration, + Default: "900s", + }, + "policy": { + Type: schema.TypeString, + Optional: true, + Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", + ValidateFunc: validation.ValidateJsonString, + }, + "role_urn": { + Type: schema.TypeString, + Required: true, + Description: "UCloud Resource Name (URN) of an IAM Role to assume prior to making API calls.", + }, + "session_name": { + Type: schema.TypeString, + Required: true, + Description: "An identifier for the assumed role session.", + }, + }, + }, + } +} + +func expandAssumeRole(tfMap map[string]interface{}) *AssumeRoleConfig { + if tfMap == nil { + return nil + } + + assumeRole := AssumeRoleConfig{} + + if v, ok := tfMap["duration"].(string); ok && v != "" { + duration, _ := time.ParseDuration(v) + assumeRole.Duration = duration + } + + if v, ok := tfMap["policy"].(string); ok && v != "" { + assumeRole.Policy = v + } + + if v, ok := tfMap["role_urn"].(string); ok && v != "" { + assumeRole.RoleURN = v + } + + if v, ok := tfMap["session_name"].(string); ok && v != "" { + assumeRole.SessionName = v + } + + return &assumeRole +} + var ucloudMutexKV = mutexkv.NewMutexKV() var descriptions map[string]string diff --git a/ucloud/validators.go b/ucloud/validators.go index f7ae2a84..2334675c 100644 --- a/ucloud/validators.go +++ b/ucloud/validators.go @@ -6,6 +6,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/helper/validation" @@ -340,3 +341,15 @@ func validateBaseUrl(v interface{}, k string) (ws []string, errors []error) { } var validateToaID = validation.IntBetween(0, 254) + +// validateAssumeRoleDuration validates a string can be parsed as a valid time.Duration +func validateAssumeRoleDuration(v interface{}, k string) (ws []string, errors []error) { + _, err := time.ParseDuration(v.(string)) + + if err != nil { + errors = append(errors, fmt.Errorf("%q cannot be parsed as a duration: %w", k, err)) + return + } + + return +} diff --git a/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/apis.go b/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/apis.go new file mode 100644 index 00000000..c698d7a1 --- /dev/null +++ b/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/apis.go @@ -0,0 +1,66 @@ +// Code is generated by ucloud-model, DO NOT EDIT IT. + +package sts + +import ( + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + "github.com/ucloud/ucloud-sdk-go/ucloud/response" +) + +// STS API Schema + +// AssumeRoleRequest is request schema for AssumeRole action +type AssumeRoleRequest struct { + request.CommonBase + + // Token有效期。 + DurationSeconds *int `required:"false"` + + // 为STS Token额外添加的一个权限策略,进一步限制STS Token的权限。 + Policy *string `required:"false"` + + // 角色会话名称。 + RoleSessionName *string `required:"true"` + + // 要扮演的RAM角色URN。 + RoleUrn *string `required:"true"` +} + +// AssumeRoleResponse is response schema for AssumeRole action +type AssumeRoleResponse struct { + response.CommonBase + + // 访问凭证。 + Credentials Credentials +} + +// NewAssumeRoleRequest will create request of AssumeRole action. +func (c *STSClient) NewAssumeRoleRequest() *AssumeRoleRequest { + req := &AssumeRoleRequest{} + + // setup request with client config + c.Client.SetupRequest(req) + + // setup retryable with default retry policy (retry for non-create action and common error) + req.SetRetryable(true) + return req +} + +/* +API: AssumeRole + +获取扮演角色的临时身份凭证 +*/ +func (c *STSClient) AssumeRole(req *AssumeRoleRequest) (*AssumeRoleResponse, error) { + var err error + var res AssumeRoleResponse + + reqCopier := *req + + err = c.Client.InvokeAction("AssumeRole", &reqCopier, &res) + if err != nil { + return &res, err + } + + return &res, nil +} diff --git a/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/client.go b/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/client.go new file mode 100644 index 00000000..eb6714aa --- /dev/null +++ b/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/client.go @@ -0,0 +1,22 @@ +// Code is generated by ucloud-model, DO NOT EDIT IT. + +package sts + +import ( + "github.com/ucloud/ucloud-sdk-go/ucloud" + "github.com/ucloud/ucloud-sdk-go/ucloud/auth" +) + +// STSClient is the client of STS +type STSClient struct { + *ucloud.Client +} + +// NewClient will return a instance of STSClient +func NewClient(config *ucloud.Config, credential *auth.Credential) *STSClient { + meta := ucloud.ClientMeta{Product: "STS"} + client := ucloud.NewClientWithMeta(config, credential, meta) + return &STSClient{ + client, + } +} diff --git a/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/doc.go b/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/doc.go new file mode 100644 index 00000000..2e9bc969 --- /dev/null +++ b/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/doc.go @@ -0,0 +1,13 @@ +// Code is generated by ucloud-model, DO NOT EDIT IT. + +/* +Package sts include resources of ucloud sts product + +See also + + - API: https://docs.ucloud.cn/api/sts-api/index + - Product: https://www.ucloud.cn/site/product/sts.html + +for detail. +*/ +package sts diff --git a/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/models.go b/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/models.go new file mode 100644 index 00000000..0168a10c --- /dev/null +++ b/vendor/github.com/ucloud/ucloud-sdk-go/services/sts/models.go @@ -0,0 +1,21 @@ +// Code is generated by ucloud-model, DO NOT EDIT IT. + +package sts + +/* +Credentials - +*/ +type Credentials struct { + + // 密钥ID。 + AccessKeyId string + + // 密钥Secret。 + AccessKeySecret string + + // Token到期失效时间(UTC时间)。 + Expiration string + + // 安全令牌。 + SecurityToken string +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c1236b5c..803132bf 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -307,6 +307,7 @@ github.com/ucloud/ucloud-sdk-go/private/utils github.com/ucloud/ucloud-sdk-go/services/cube github.com/ucloud/ucloud-sdk-go/services/iam github.com/ucloud/ucloud-sdk-go/services/ipsecvpn +github.com/ucloud/ucloud-sdk-go/services/sts github.com/ucloud/ucloud-sdk-go/services/uaccount github.com/ucloud/ucloud-sdk-go/services/uads github.com/ucloud/ucloud-sdk-go/services/udb diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 0f9bba12..650111d2 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -155,6 +155,17 @@ In addition to [generic `provider` arguments](https://www.terraform.io/docs/conf * `base_url` - (Optional) This is the base url.(Default: `https://api.ucloud.cn`). ~> **Note** this argument conflicts with `insecure`. +* `assume_role` - (Optional) Configuration block for assuming an IAM role. See the [`assume_role` Configuration Block](#assume_role-configuration-block) section below. Only one `assume_role` block may be in the configuration. + +### assume_role Configuration Block + +The `assume_role` configuration block supports the following arguments: + +* `duration` - (Optional) Duration of the assume role session. Represented by a string such as `1h`, `2h45m`, or `30m15s`. Default is `900s`. The maximum value is the maximum session duration of the role +* `policy` - (Optional) IAM Policy JSON describing further restricting permissions for the IAM Role being assumed. +* `role_urn` - (Required) URN of the IAM Role to assume. +* `session_name` - (Required) Session name to use when assuming the role. The value can contain 1 to 64 letters, digits, underscores (_), hyphens (-), and periods (.). + ## Testing Credentials must be provided via the `UCLOUD_PUBLIC_KEY`, `UCLOUD_PRIVATE_KEY`, `UCLOUD_PROJECT_ID` environment variables in order to run acceptance tests.