Skip to content

Commit

Permalink
Add name, description and expiration support for service accounts (#594)
Browse files Browse the repository at this point in the history
  • Loading branch information
ribetm authored Nov 10, 2024
1 parent a3dadb6 commit 5dc95e9
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 3 deletions.
3 changes: 3 additions & 0 deletions docs/resources/iam_service_account.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions examples/serviceaccount/serviceaccount.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
3 changes: 3 additions & 0 deletions minio/check_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down
3 changes: 3 additions & 0 deletions minio/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
136 changes: 133 additions & 3 deletions minio/resource_minio_service_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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,
},
},
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
106 changes: 106 additions & 0 deletions minio/resource_minio_service_account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
}
}

0 comments on commit 5dc95e9

Please sign in to comment.