Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add name, description and expiration support for service accounts #594

Merged
merged 5 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
Loading