diff --git a/docs/resources/iam_service_account.md b/docs/resources/iam_service_account.md index 619cc394..bba1a18c 100644 --- a/docs/resources/iam_service_account.md +++ b/docs/resources/iam_service_account.md @@ -44,7 +44,10 @@ output "minio_password" { ### Optional +- `description` (String) Description of service account (256 bytes max), can't be cleared once set - `disable_user` (Boolean) Disable service account +- `expiration` (String) Expiration of service account. Must be between NOW+15min & NOW+365d +- `name` (String) Name of service account (32 bytes max), can't be cleared once set - `policy` (String) policy of service account as encoded JSON string - `update_secret` (Boolean) rotate secret key diff --git a/examples/serviceaccount/serviceaccount.tf b/examples/serviceaccount/serviceaccount.tf index 711181cc..6f2c709b 100644 --- a/examples/serviceaccount/serviceaccount.tf +++ b/examples/serviceaccount/serviceaccount.tf @@ -3,6 +3,7 @@ resource "minio_iam_user" "test_user" { } resource "minio_iam_service_account" "test_service_account" { + name = "test-svc" # optional target_user = "test-user" policy = <<-EOF { diff --git a/minio/check_config.go b/minio/check_config.go index 9ee3bf53..9414abc5 100644 --- a/minio/check_config.go +++ b/minio/check_config.go @@ -123,6 +123,9 @@ func ServiceAccountConfig(d *schema.ResourceData, meta interface{}) *S3MinioServ MinioDisableUser: d.Get("disable_user").(bool), MinioUpdateKey: d.Get("update_secret").(bool), MinioSAPolicy: d.Get("policy").(string), + MinioName: d.Get("name").(string), + MinioDescription: d.Get("description").(string), + MinioExpiration: d.Get("expiration").(string), } } diff --git a/minio/payload.go b/minio/payload.go index 5df016bb..05f4e3bf 100644 --- a/minio/payload.go +++ b/minio/payload.go @@ -156,6 +156,9 @@ type S3MinioServiceAccountConfig struct { MinioForceDestroy bool MinioUpdateKey bool MinioIAMTags map[string]string + MinioDescription string + MinioName string + MinioExpiration string } // S3MinioIAMUserConfig defines IAM config diff --git a/minio/resource_minio_service_account.go b/minio/resource_minio_service_account.go index 7a5e812b..74a44392 100644 --- a/minio/resource_minio_service_account.go +++ b/minio/resource_minio_service_account.go @@ -5,10 +5,13 @@ import ( "fmt" "log" "strings" + "time" "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/minio/madmin-go/v3" ) @@ -63,6 +66,28 @@ func resourceMinioServiceAccount() *schema.Resource { ValidateFunc: validateIAMPolicyJSON, DiffSuppressFunc: suppressEquivalentAwsPolicyDiffs, }, + "name": { + Type: schema.TypeString, + Description: "Name of service account (32 bytes max), can't be cleared once set", + Optional: true, + DiffSuppressFunc: stringChangedToEmpty, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 32)), + }, + "description": { + Type: schema.TypeString, + Description: "Description of service account (256 bytes max), can't be cleared once set", + Optional: true, + DiffSuppressFunc: stringChangedToEmpty, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 256)), + }, + "expiration": { + Type: schema.TypeString, + Description: "Expiration of service account. Must be between NOW+15min & NOW+365d", + Optional: true, + Default: "1970-01-01T00:00:00Z", + ValidateDiagFunc: validateExpiration, + DiffSuppressFunc: suppressTimeDiffs, + }, }, } } @@ -74,10 +99,17 @@ func minioCreateServiceAccount(ctx context.Context, d *schema.ResourceData, meta var err error targetUser := serviceAccountConfig.MinioTargetUser policy := serviceAccountConfig.MinioSAPolicy + expiration, err := time.Parse(time.RFC3339, serviceAccountConfig.MinioExpiration) + if err != nil { + return NewResourceError("Failed to parse expiration", serviceAccountConfig.MinioExpiration, err) + } serviceAccount, err := serviceAccountConfig.MinioAdmin.AddServiceAccount(ctx, madmin.AddServiceAccountReq{ - Policy: processServiceAccountPolicy(policy), - TargetUser: targetUser, + Policy: processServiceAccountPolicy(policy), + TargetUser: targetUser, + Name: serviceAccountConfig.MinioName, + Description: serviceAccountConfig.MinioDescription, + Expiration: &expiration, }) if err != nil { return NewResourceError("error creating service account", targetUser, err) @@ -157,14 +189,54 @@ func minioUpdateServiceAccount(ctx context.Context, d *schema.ResourceData, meta _ = d.Set("policy", policy) } + if d.HasChange("name") { + if serviceAccountConfig.MinioName == "" { + return NewResourceError("Minio does not support removing service account names", d.Id(), serviceAccountConfig.MinioName) + } + err := serviceAccountConfig.MinioAdmin.UpdateServiceAccount(ctx, d.Id(), madmin.UpdateServiceAccountReq{ + NewName: serviceAccountConfig.MinioName, + }) + if err != nil { + return NewResourceError("error updating service account name %s: %s", d.Id(), err) + } + } + + if d.HasChange("description") { + if serviceAccountConfig.MinioDescription == "" { + return NewResourceError("Minio does not support removing service account descriptions", d.Id(), serviceAccountConfig.MinioDescription) + } + err := serviceAccountConfig.MinioAdmin.UpdateServiceAccount(ctx, d.Id(), madmin.UpdateServiceAccountReq{ + NewDescription: serviceAccountConfig.MinioDescription, + }) + if err != nil { + return NewResourceError("error updating service account description %s: %s", d.Id(), err) + } + } + + if d.HasChange("expiration") { + expiration, err := time.Parse(time.RFC3339, serviceAccountConfig.MinioExpiration) + if err != nil { + return NewResourceError("error parsing service account expiration %s: %s", d.Id(), err) + } + err = serviceAccountConfig.MinioAdmin.UpdateServiceAccount(ctx, d.Id(), madmin.UpdateServiceAccountReq{ + NewExpiration: &expiration, + }) + if err != nil { + return NewResourceError("error updating service account expiration %s: %s", d.Id(), err) + } + } + return minioReadServiceAccount(ctx, d, meta) } func minioReadServiceAccount(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - serviceAccountConfig := ServiceAccountConfig(d, meta) output, err := serviceAccountConfig.MinioAdmin.InfoServiceAccount(ctx, d.Id()) + if err != nil && err.Error() == "The specified service account is not found (Specified service account does not exist)" { + d.SetId("") + return nil + } if err != nil { return NewResourceError("error reading service account %s: %s", d.Id(), err) } @@ -189,6 +261,22 @@ func minioReadServiceAccount(ctx context.Context, d *schema.ResourceData, meta i if !output.ImpliedPolicy { _ = d.Set("policy", output.Policy) } + + if err := d.Set("name", output.Name); err != nil { + return NewResourceError("reading service account failed", d.Id(), err) + } + if err := d.Set("description", output.Description); err != nil { + return NewResourceError("reading service account failed", d.Id(), err) + } + + expiration := "1970-01-01T00:00:00Z" + if output.Expiration != nil { + expiration = output.Expiration.Format(time.RFC3339) + } + if err := d.Set("expiration", expiration); err != nil { + return NewResourceError("reading service account failed", d.Id(), err) + } + return nil } @@ -251,3 +339,45 @@ func parseUserFromParentUser(parentUser string) string { return user } + +func stringChangedToEmpty(k, oldValue, newValue string, d *schema.ResourceData) bool { + return oldValue != "" && newValue == "" +} + +func suppressTimeDiffs(k, old, new string, d *schema.ResourceData) bool { + old_exp, err := time.Parse(time.RFC3339, old) + if err != nil { + return false + } + new_exp, err := time.Parse(time.RFC3339, new) + if err != nil { + return false + } + + return old_exp.Compare(new_exp) == 0 +} + +func validateExpiration(val any, p cty.Path) diag.Diagnostics { + var diags diag.Diagnostics + + value := val.(string) + expiration, err := time.Parse(time.RFC3339, value) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid expiration", + Detail: fmt.Sprintf("%q cannot be parsed as RFC3339 Timestamp Format", value), + }) + } + + key_duration := time.Until(expiration) + if key_duration < 15*time.Minute || key_duration > 365*24*time.Minute { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid expiration", + Detail: "Expiration must between 15 minutes and 365 days in the future", + }) + } + + return diags +} diff --git a/minio/resource_minio_service_account_test.go b/minio/resource_minio_service_account_test.go index 6c087567..2424ff5c 100644 --- a/minio/resource_minio_service_account_test.go +++ b/minio/resource_minio_service_account_test.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -154,6 +155,67 @@ func TestParseUserFromParentUser(t *testing.T) { assert.Equal(t, "minio-user", parseUserFromParentUser("cn=minio-user, DC=example")) } +func TestServiceAccount_NameDesc(t *testing.T) { + var serviceAccount madmin.InfoServiceAccountResp + + targetUser := "minio" + resourceName := "minio_iam_service_account.test" + name := "svc-account" + description := "A service account" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckMinioServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMinioServiceAccountConfig(targetUser), + Check: resource.ComposeTestCheckFunc( + testAccCheckMinioServiceAccountExists(resourceName, &serviceAccount), + ), + }, + { + Config: testAccMinioServiceAccountConfigUpdateNameDesc(targetUser, name, description), + Check: resource.ComposeTestCheckFunc( + testAccCheckMinioServiceAccountExists(resourceName, &serviceAccount), + testAccCheckMinioServiceAccountNameDesc(resourceName, name, description), + ), + }, + }, + }) +} + +func TestServiceAccount_Expiration(t *testing.T) { + var serviceAccount madmin.InfoServiceAccountResp + + targetUser := "minio" + resourceName := "minio_iam_service_account.test" + expiration := time.Now().Add(time.Hour * 1).UTC().Format(time.RFC3339) + epoch := time.UnixMicro(0).UTC().Format(time.RFC3339) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckMinioServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMinioServiceAccountConfig(targetUser), + Check: resource.ComposeTestCheckFunc( + testAccCheckMinioServiceAccountExists(resourceName, &serviceAccount), + testAccCheckMinioServiceAccountExpiration(resourceName, epoch), + ), + }, + { + Config: testAccMinioServiceAccountConfigUpdateExpiration(targetUser, expiration), + Check: resource.ComposeTestCheckFunc( + testAccCheckMinioServiceAccountExists(resourceName, &serviceAccount), + testAccCheckMinioServiceAccountExpiration(resourceName, expiration), + ), + }, + }, + }) +} + func testAccMinioServiceAccountConfig(rName string) string { return fmt.Sprintf(` resource "minio_iam_service_account" "test" { @@ -237,6 +299,23 @@ resource "minio_iam_service_account" "test_service_account" { `, rName) } +func testAccMinioServiceAccountConfigUpdateNameDesc(rName string, name string, description string) string { + return fmt.Sprintf(` + resource "minio_iam_service_account" "test" { + target_user = %q + name = %q + description = %q + }`, rName, name, description) +} + +func testAccMinioServiceAccountConfigUpdateExpiration(rName string, expiration string) string { + return fmt.Sprintf(` + resource "minio_iam_service_account" "test" { + target_user = %q + expiration = %q + }`, rName, expiration) +} + func testAccCheckMinioServiceAccountExists(n string, res *madmin.InfoServiceAccountResp) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -380,3 +459,30 @@ func testAccCheckMinioServiceAccountPolicy(n string, expectedPolicy string) reso return nil } } + +func testAccCheckMinioServiceAccountNameDesc(n string, name string, description string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs := s.RootModule().Resources[n] + + if rs.Primary.Attributes["name"] != name { + return fmt.Errorf("bad name: %s", name) + } + if rs.Primary.Attributes["description"] != description { + return fmt.Errorf("bad description: %s", description) + } + + return nil + } +} + +func testAccCheckMinioServiceAccountExpiration(n string, expiration string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs := s.RootModule().Resources[n] + + if rs.Primary.Attributes["expiration"] != expiration { + return fmt.Errorf("bad expiration: %s", expiration) + } + + return nil + } +}