From 125b7a13571ef6a6490a334f3c3479ee24c5974d Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Wed, 10 Jul 2024 12:21:45 +0300 Subject: [PATCH 01/20] add groups --- selectel/provider.go | 4 + ...source_selectel_iam_group_membership_v1.go | 218 ++++++++++++++++++ ...e_selectel_iam_group_membership_v1_test.go | 136 +++++++++++ selectel/resource_selectel_iam_group_v1.go | 181 +++++++++++++++ .../resource_selectel_iam_group_v1_test.go | 148 ++++++++++++ 5 files changed, 687 insertions(+) create mode 100644 selectel/resource_selectel_iam_group_membership_v1.go create mode 100644 selectel/resource_selectel_iam_group_membership_v1_test.go create mode 100644 selectel/resource_selectel_iam_group_v1.go create mode 100644 selectel/resource_selectel_iam_group_v1_test.go diff --git a/selectel/provider.go b/selectel/provider.go index a8d2c8ba..e141d7dc 100644 --- a/selectel/provider.go +++ b/selectel/provider.go @@ -27,6 +27,8 @@ const ( objectUser = "user" objectServiceUser = "service user" objectS3Credentials = "s3 credentials" + objectGroup = "group" + objectGroupMembership = "group-membership" objectCluster = "cluster" objectKubeConfig = "kubeconfig" objectKubeVersions = "kube-versions" @@ -135,6 +137,8 @@ func Provider() *schema.Provider { "selectel_iam_serviceuser_v1": resourceIAMServiceUserV1(), "selectel_iam_user_v1": resourceIAMUserV1(), "selectel_iam_s3_credentials_v1": resourceIAMS3CredentialsV1(), + "selectel_iam_group_v1": resourceIAMGroupV1(), + "selectel_iam_group_membership_v1": resourceIAMGroupMembershipV1(), "selectel_vpc_vrrp_subnet_v2": resourceVPCVRRPSubnetV2(), // DEPRECATED "selectel_vpc_crossregion_subnet_v2": resourceVPCCrossRegionSubnetV2(), // DEPRECATED "selectel_mks_cluster_v1": resourceMKSClusterV1(), diff --git a/selectel/resource_selectel_iam_group_membership_v1.go b/selectel/resource_selectel_iam_group_membership_v1.go new file mode 100644 index 00000000..920b5995 --- /dev/null +++ b/selectel/resource_selectel_iam_group_membership_v1.go @@ -0,0 +1,218 @@ +package selectel + +import ( + "encoding/base64" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "log" + "slices" + "sort" + "strings" +) + +func resourceIAMGroupMembershipV1() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceIAMGroupMembershipV1Create, + ReadContext: resourceIAMGroupMembershipV1Read, + UpdateContext: resourceIAMGroupMembershipV1Update, + DeleteContext: resourceIAMGroupMembershipV1Delete, + Schema: map[string]*schema.Schema{ + "group_id": { + Type: schema.TypeString, + Required: true, + }, + "user_ids": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceIAMGroupMembershipV1Create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + userIDsInterface := d.Get("user_ids").([]interface{}) + userIDs := make([]string, len(userIDsInterface)) + for i, v := range userIDsInterface { + userIDs[i] = v.(string) + } + + log.Print(msgCreate(objectGroupMembership, d.Id())) + if len(userIDs) == 0 { + createErr := fmt.Errorf("error creating group membership: no user ids specified") + return diag.FromErr(errCreatingObject(objectGroupMembership, createErr)) + } + err := iamClient.Groups.AddUsers(ctx, d.Get("group_id").(string), userIDs) + if err != nil { + return diag.FromErr(errCreatingObject(objectGroupMembership, err)) + } + + d.SetId(generateCompositeID(d.Get("group_id").(string), userIDs)) + + return resourceIAMGroupMembershipV1Read(ctx, d, meta) +} + +func resourceIAMGroupMembershipV1Read(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + groupID, userIDs, err := parseCompositeID(d.Id()) + if err != nil { + return diag.FromErr(errGettingObject(objectGroupMembership, d.Id(), err)) + } + + response, err := iamClient.Groups.Get(ctx, groupID) + if err != nil { + return diag.FromErr(errGettingObject(objectGroupMembership, d.Id(), err)) + } + + responseUserIDs := make([]string, 0) + for _, user := range response.Users { + responseUserIDs = append(responseUserIDs, user.KeystoneID) + } + + responseServiceUserIDs := make([]string, 0) + for _, serviceUser := range response.ServiceUsers { + responseServiceUserIDs = append(responseServiceUserIDs, serviceUser.ID) + } + + if !containsAll(userIDs, responseUserIDs) || !containsAll(userIDs, responseServiceUserIDs) { + readErr := fmt.Errorf("error validating group memberships: Group %s does not contain all users %v", groupID, userIDs) + return diag.FromErr(errGettingObject(objectGroupMembership, d.Id(), readErr)) + } + + d.Set("group_id", groupID) + d.Set("user_ids", append(responseUserIDs, responseServiceUserIDs...)) + + return nil +} + +func resourceIAMGroupMembershipV1Update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + groupID, oldUserIDs, err := parseCompositeID(d.Id()) + if err != nil { + return diag.FromErr(errUpdatingObject(objectGroupMembership, d.Id(), err)) + } + + newUserIDsInterface := d.Get("user_ids").([]interface{}) + newUserIDs := make([]string, len(newUserIDsInterface)) + for i, v := range newUserIDsInterface { + newUserIDs[i] = v.(string) + } + + usersToAdd, usersToRemove := diffUsers(oldUserIDs, newUserIDs) + + if len(usersToAdd) > 0 { + err := iamClient.Groups.AddUsers(ctx, groupID, usersToAdd) + if err != nil { + return diag.FromErr(err) + } + } + + if len(usersToRemove) > 0 { + err := iamClient.Groups.DeleteUsers(ctx, groupID, usersToRemove) + if err != nil { + return diag.FromErr(err) + } + } + + d.SetId(generateCompositeID(groupID, newUserIDs)) + + return resourceIAMGroupMembershipV1Read(ctx, d, meta) +} + +func resourceIAMGroupMembershipV1Delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + groupID, userIDs, err := parseCompositeID(d.Id()) + if err != nil { + return diag.FromErr(errDeletingObject(objectGroupMembership, d.Id(), err)) + } + + err = iamClient.Groups.DeleteUsers(ctx, groupID, userIDs) + if err != nil { + return diag.FromErr(errDeletingObject(objectGroupMembership, d.Id(), err)) + } + + d.SetId("") + + return nil +} + +func generateCompositeID(groupID string, userIDs []string) string { + sort.Strings(userIDs) + concatenated := groupID + ":" + strings.Join(userIDs, ",") + encoded := base64.StdEncoding.EncodeToString([]byte(concatenated)) + return encoded +} + +func parseCompositeID(compositeID string) (string, []string, error) { + decodedBytes, err := base64.StdEncoding.DecodeString(compositeID) + if err != nil { + return "", nil, fmt.Errorf("error decoding composite ID: %s, %v", compositeID, err) + } + decodedString := string(decodedBytes) + + parts := strings.Split(decodedString, ":") + if len(parts) != 2 { + return "", nil, fmt.Errorf("invalid decoded composite ID: %s", decodedString) + } + + groupID := parts[0] + userIDs := strings.Split(parts[1], ",") + + return groupID, userIDs, nil +} + +func diffUsers(oldUsers, newUsers []string) ([]string, []string) { + usersToAdd := make([]string, 0) + usersToRemove := make([]string, 0) + + for _, user := range newUsers { + if !slices.Contains(oldUsers, user) { + usersToAdd = append(usersToAdd, user) + } + } + + for _, user := range oldUsers { + if !slices.Contains(newUsers, user) { + usersToRemove = append(usersToRemove, user) + } + } + + return usersToAdd, usersToRemove +} + +// containsAll checks if sliceB is a subset of sliceA +func containsAll(sliceA, sliceB []string) bool { + for _, b := range sliceB { + found := false + for _, a := range sliceA { + if a == b { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/selectel/resource_selectel_iam_group_membership_v1_test.go b/selectel/resource_selectel_iam_group_membership_v1_test.go new file mode 100644 index 00000000..8c98e79c --- /dev/null +++ b/selectel/resource_selectel_iam_group_membership_v1_test.go @@ -0,0 +1,136 @@ +package selectel + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "testing" +) + +func TestAccIAMV1GroupMembershipBasic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccIAMV1GroupMembershipBasic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "group_id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.0"), + ), + }, + }, + }) +} + +func TestAccIAMV1GroupUpdate(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccIAMV1GroupMembershipBasic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "group_id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.0"), + ), + }, + { + Config: testAccIAMV1GroupMembershipUpdate(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "group_id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.0"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.1"), + ), + }, + { + Config: testAccIAMV1GroupMembershipBasic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "group_id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.0"), + resource.TestCheckNoResourceAttr("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.1"), + ), + }, + }, + }) +} + +func testAccIAMV1GroupMembershipBasic() string { + return fmt.Sprintf(` +resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_1" { + name = "test-service-user-1" + password = "Qazwsxedc123" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_2" { + name = "test-service-user-2" + password = "Qazwsxedc123" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_group_v1" "group_tf_acc_test_1" { + name = "test-group" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_group_membership_v1" "membership_tf_acc_test_1" { + group_id = selectel_iam_group_v1.group_tf_acc_test_1.id + + user_ids = [ + selectel_iam_serviceuser_v1.serviceuser_tf_acc_test_1.id + ] +} +`) +} + +func testAccIAMV1GroupMembershipUpdate() string { + return fmt.Sprintf(` +resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_1" { + name = "test-service-user-1" + password = "Qazwsxedc123" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_2" { + name = "test-service-user-2" + password = "Qazwsxedc123" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_group_v1" "group_tf_acc_test_1" { + name = "test-group" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_group_membership_v1" "membership_tf_acc_test_1" { + group_id = selectel_iam_group_v1.group_tf_acc_test_1.id + + user_ids = [ + selectel_iam_serviceuser_v1.serviceuser_tf_acc_test_1.id, + selectel_iam_serviceuser_v1.serviceuser_tf_acc_test_2.id + ] +} +`) +} diff --git a/selectel/resource_selectel_iam_group_v1.go b/selectel/resource_selectel_iam_group_v1.go new file mode 100644 index 00000000..a74e574f --- /dev/null +++ b/selectel/resource_selectel_iam_group_v1.go @@ -0,0 +1,181 @@ +package selectel + +import () + +func resourceIAMGroupV1() *schema.Resource { + return &schema.Resource{ + Description: "Represents a Group in IAM API", + CreateContext: resourceIAMGroupV1Create, + ReadContext: resourceIAMGroupV1Read, + UpdateContext: resourceIAMGroupV1Update, + DeleteContext: resourceIAMGroupV1Delete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the group.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Description of the group.", + }, + "role": { + Type: schema.TypeSet, + Optional: true, + Description: "Role block of the group.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "role_name": { + Type: schema.TypeString, + Required: true, + }, + "scope": { + Type: schema.TypeString, + Required: true, + }, + "project_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + } +} + +func resourceIAMGroupV1Create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + roles, err := convertIAMSetToRoles(d.Get("role").(*schema.Set)) + if err != nil { + return diag.FromErr(err) + } + + log.Print(msgCreate(objectGroup, d.Id())) + group, err := iamClient.Groups.Create(ctx, groups.CreateRequest{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + }) + if err != nil { + return diag.FromErr(errCreatingObject(objectGroup, err)) + } + d.SetId(group.ID) + + if len(roles) != 0 { + err = iamClient.Groups.AssignRoles(ctx, group.ID, roles) + if err != nil { + return diag.FromErr(errCreatingObject(objectGroup, err)) + } + } + + return resourceIAMGroupV1Read(ctx, d, meta) +} + +func resourceIAMGroupV1Read(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + log.Print(msgGet(objectGroup, d.Id())) + group, err := iamClient.Groups.Get(ctx, d.Id()) + if err != nil { + return diag.FromErr(errGettingObject(objectGroup, d.Id(), err)) + } + + if group.Group.Roles != nil && len(group.Group.Roles) != 0 { + err = d.Set("role", convertIAMRolesToSet(group.Roles)) + if err != nil { + return nil + } + } + + return nil +} + +func resourceIAMGroupV1Update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + description := d.Get("description").(string) + + opts := groups.UpdateRequest{ + Name: d.Get("name").(string), + Description: &description, + } + + log.Print(msgUpdate(objectGroup, d.Id(), fmt.Sprintf("Name: %+v, description: %+v", opts.Name, opts.Description))) + _, err := iamClient.Groups.Update(ctx, d.Id(), opts) + if err != nil { + return diag.FromErr(errUpdatingObject(objectGroup, d.Id(), err)) + } + + if d.HasChange("role") { + currentGroup, err := iamClient.Groups.Get(ctx, d.Id()) + if err != nil { + return diag.FromErr(errGettingObject(objectGroup, d.Id(), err)) + } + oldRoles := currentGroup.Roles + newRoles, err := convertIAMSetToRoles(d.Get("role").(*schema.Set)) + if err != nil { + return diag.FromErr(err) + } + + rolesToUnassign, rolesToAssign := diffRoles(oldRoles, newRoles) + + log.Print(msgUpdate(objectGroup, d.Id(), fmt.Sprintf("Roles to unassign: %+v, roles to assign: %+v", rolesToUnassign, rolesToAssign))) + err = applyGroupRoles(ctx, d, iamClient, rolesToUnassign, rolesToAssign) + if err != nil { + return diag.FromErr(errUpdatingObject(objectGroup, d.Id(), err)) + } + + return nil + } + + return resourceIAMGroupV1Read(ctx, d, meta) +} + +func resourceIAMGroupV1Delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + log.Print(msgDelete(objectGroup, d.Id())) + err := iamClient.Groups.Delete(ctx, d.Id()) + if err != nil && !errors.Is(err, iamerrors.ErrGroupNotFound) { + return diag.FromErr(errDeletingObject(objectGroup, d.Id(), err)) + } + + d.SetId("") + + return nil +} + +func applyGroupRoles(ctx context.Context, d *schema.ResourceData, iamClient *iam.Client, rolesToUnassign, rolesToAssign []roles.Role) error { + if len(rolesToAssign) != 0 { + err := iamClient.Groups.AssignRoles(ctx, d.Id(), rolesToAssign) + if err != nil { + return err + } + } + + if len(rolesToUnassign) != 0 { + err := iamClient.Groups.UnassignRoles(ctx, d.Id(), rolesToUnassign) + if err != nil { + return err + } + } + + return nil +} diff --git a/selectel/resource_selectel_iam_group_v1_test.go b/selectel/resource_selectel_iam_group_v1_test.go new file mode 100644 index 00000000..49ea6a67 --- /dev/null +++ b/selectel/resource_selectel_iam_group_v1_test.go @@ -0,0 +1,148 @@ +package selectel + +import () + +func TestAccIAMV1GroupBasic(t *testing.T) { + var group groups.Group + + testName := "test-name" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckIAMV1GroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccIAMV1GroupBasic(testName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMV1GroupExists("selectel_iam_group_v1.group_tf_acc_test_1", &group), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "id"), + resource.TestCheckResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "name", testName), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.role_name"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.scope"), + ), + }, + }, + }) +} + +func TestAccIAMV1GroupUpdateRoles(t *testing.T) { + var group groups.Group + + testName := "test-name" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckIAMV1GroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccIAMV1GroupBasic(testName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMV1GroupExists("selectel_iam_group_v1.group_tf_acc_test_1", &group), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "id"), + resource.TestCheckResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "name", testName), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.role_name"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.scope"), + ), + }, + { + Config: testAccIAMV1GroupAssignRole(testName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMV1GroupExists("selectel_iam_group_v1.group_tf_acc_test_1", &group), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "id"), + resource.TestCheckResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "name", testName), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.role_name"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.scope"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.1.role_name"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.1.scope"), + ), + }, + { + Config: testAccIAMV1GroupBasic(testName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMV1GroupExists("selectel_iam_group_v1.group_tf_acc_test_1", &group), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "id"), + resource.TestCheckResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "name", testName), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.role_name"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.scope"), + resource.TestCheckNoResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "role.1.role_name"), + resource.TestCheckNoResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "role.1.scope"), + ), + }, + }, + }) +} + +func testAccCheckIAMV1GroupDestroy(s *terraform.State) error { + iamClient, diagErr := getIAMClient(testAccProvider.Meta()) + if diagErr != nil { + return fmt.Errorf("can't get iamclient for test group object") + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "selectel_iam_group_v1" { + continue + } + + _, err := iamClient.Groups.Get(context.Background(), rs.Primary.ID) + if err == nil { + return errors.New("group still exists") + } + } + + return nil +} + +func testAccCheckIAMV1GroupExists(n string, group *groups.Group) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return errors.New("no ID is set") + } + + iamClient, diagErr := getIAMClient(testAccProvider.Meta()) + if diagErr != nil { + return fmt.Errorf("can't get iamclient for test group object") + } + + g, err := iamClient.Groups.Get(context.Background(), rs.Primary.ID) + if err != nil { + return errors.New("group not found") + } + + *group = g.Group + + return nil + } +} + +func testAccIAMV1GroupBasic(name string) string { + return fmt.Sprintf(` +resource "selectel_iam_group_v1" "group_tf_acc_test_1" { + name = "%s" + role { + role_name = "reader" + scope = "account" + } +}`, name) +} + +func testAccIAMV1GroupAssignRole(name string) string { + return fmt.Sprintf(` + resource "selectel_iam_group_v1" "group_tf_acc_test_1" { + name = "%s" + role { + role_name = "reader" + scope = "account" + } + role { + role_name = "billing" + scope = "account" + } + }`, name) +} From dd2cfc98d3820cc921d9511c26e7a7537c0a02c1 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Wed, 10 Jul 2024 12:22:46 +0300 Subject: [PATCH 02/20] fix --- .../resource_selectel_iam_s3_credentials_v1.go | 6 +++--- ...resource_selectel_iam_s3_credentials_v1_test.go | 14 +++++++------- .../resource_selectel_iam_serviceuser_v1_test.go | 2 +- selectel/resource_selectel_iam_user_v1_test.go | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/selectel/resource_selectel_iam_s3_credentials_v1.go b/selectel/resource_selectel_iam_s3_credentials_v1.go index 0ba0d476..61e9dae8 100644 --- a/selectel/resource_selectel_iam_s3_credentials_v1.go +++ b/selectel/resource_selectel_iam_s3_credentials_v1.go @@ -90,13 +90,13 @@ func resourceIAMS3CredentialsV1Read(ctx context.Context, d *schema.ResourceData, } log.Print(msgGet(objectS3Credentials, d.Id())) - credentials, err := iamClient.S3Credentials.List(ctx, d.Get("user_id").(string)) + response, err := iamClient.S3Credentials.List(ctx, d.Get("user_id").(string)) if err != nil { return diag.FromErr(errGettingObject(objectS3Credentials, d.Id(), err)) } - var credential s3credentials.Credentials - for _, c := range credentials { + var credential s3credentials.Credential + for _, c := range response.Credentials { if d.Id() == c.AccessKey { credential = c break diff --git a/selectel/resource_selectel_iam_s3_credentials_v1_test.go b/selectel/resource_selectel_iam_s3_credentials_v1_test.go index 3ed967d2..64a864c4 100644 --- a/selectel/resource_selectel_iam_s3_credentials_v1_test.go +++ b/selectel/resource_selectel_iam_s3_credentials_v1_test.go @@ -27,7 +27,7 @@ func TestAccIAMV1S3CredentialsBasic(t *testing.T) { { Config: testAccIAMV1S3CredentialsBasic(projectName, userName, userPassword, s3CredsName), Check: resource.ComposeTestCheckFunc( - testAccCheckIAMV1S3CredentialsExists("selectel_iam_s3_credentials_v1.s3_creds_tf_acc_test_1", &s3credentials), + testAccCheckIAMV1S3CredentialsExists("selectel_iam_s3_credentials_v1.s3_creds_tf_acc_test_1", &s3credential), resource.TestCheckResourceAttrSet("selectel_iam_s3_credentials_v1.s3_creds_tf_acc_test_1", "user_id"), resource.TestCheckResourceAttrSet("selectel_iam_s3_credentials_v1.s3_creds_tf_acc_test_1", "project_id"), resource.TestCheckResourceAttrSet("selectel_iam_s3_credentials_v1.s3_creds_tf_acc_test_1", "secret_key"), @@ -50,8 +50,8 @@ func testAccCheckIAMV1S3CredentialsDestroy(s *terraform.State) error { continue } - credentialsList, _ := iamClient.S3Credentials.List(context.Background(), rs.Primary.Attributes["user_id"]) - for _, cred := range credentialsList { + response, _ := iamClient.S3Credentials.List(context.Background(), rs.Primary.Attributes["user_id"]) + for _, cred := range response.Credentials { if cred.AccessKey == rs.Primary.ID { return errors.New("s3 credentials still exist") } @@ -61,7 +61,7 @@ func testAccCheckIAMV1S3CredentialsDestroy(s *terraform.State) error { return nil } -func testAccCheckIAMV1S3CredentialsExists(n string, s3Credential *s3credentials.Credentials) resource.TestCheckFunc { +func testAccCheckIAMV1S3CredentialsExists(n string, s3Credential *s3credentials.Credential) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -77,9 +77,9 @@ func testAccCheckIAMV1S3CredentialsExists(n string, s3Credential *s3credentials. return fmt.Errorf("can't get iamclient for test s3 credentials object") } - credentialsList, _ := iamClient.S3Credentials.List(context.Background(), rs.Primary.Attributes["user_id"]) - var neededS3Credentials s3credentials.Credentials - for _, cred := range credentialsList { + response, _ := iamClient.S3Credentials.List(context.Background(), rs.Primary.Attributes["user_id"]) + var neededS3Credentials s3credentials.Credential + for _, cred := range response.Credentials { if cred.Name == rs.Primary.Attributes["name"] { neededS3Credentials = cred break diff --git a/selectel/resource_selectel_iam_serviceuser_v1_test.go b/selectel/resource_selectel_iam_serviceuser_v1_test.go index bec34bf8..5afd0a37 100644 --- a/selectel/resource_selectel_iam_serviceuser_v1_test.go +++ b/selectel/resource_selectel_iam_serviceuser_v1_test.go @@ -171,7 +171,7 @@ func testAccCheckIAMV1ServiceUserExists(n string, serviceUser *serviceusers.Serv return errors.New("serviceUser not found") } - *serviceUser = *su + *serviceUser = su.ServiceUser return nil } diff --git a/selectel/resource_selectel_iam_user_v1_test.go b/selectel/resource_selectel_iam_user_v1_test.go index e4058af1..e5f73592 100644 --- a/selectel/resource_selectel_iam_user_v1_test.go +++ b/selectel/resource_selectel_iam_user_v1_test.go @@ -123,7 +123,7 @@ func testAccCheckIAMV1UserExists(n string, user *users.User) resource.TestCheckF return errors.New("user not found") } - *user = *u + *user = u.User return nil } From 3f54e39b06374a45d247bc3538a34a30485d85d3 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Wed, 10 Jul 2024 12:24:31 +0300 Subject: [PATCH 03/20] fix --- selectel/resource_selectel_iam_group_v1_test.go | 10 +++++++++- .../resource_selectel_iam_s3_credentials_v1_test.go | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/selectel/resource_selectel_iam_group_v1_test.go b/selectel/resource_selectel_iam_group_v1_test.go index 49ea6a67..a72abb1b 100644 --- a/selectel/resource_selectel_iam_group_v1_test.go +++ b/selectel/resource_selectel_iam_group_v1_test.go @@ -1,6 +1,14 @@ package selectel -import () +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/selectel/iam-go/service/groups" + "testing" +) func TestAccIAMV1GroupBasic(t *testing.T) { var group groups.Group diff --git a/selectel/resource_selectel_iam_s3_credentials_v1_test.go b/selectel/resource_selectel_iam_s3_credentials_v1_test.go index 64a864c4..a9be4275 100644 --- a/selectel/resource_selectel_iam_s3_credentials_v1_test.go +++ b/selectel/resource_selectel_iam_s3_credentials_v1_test.go @@ -13,7 +13,7 @@ import ( ) func TestAccIAMV1S3CredentialsBasic(t *testing.T) { - var s3credentials s3credentials.Credentials + var s3credential s3credentials.Credential s3CredsName := acctest.RandomWithPrefix("tf-acc") projectName := acctest.RandomWithPrefix("tf-acc") userName := acctest.RandomWithPrefix("tf-acc") From 39a30de6170658fa7979dfd07125da26613bfc48 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Thu, 11 Jul 2024 11:15:17 +0300 Subject: [PATCH 04/20] fix linter --- .../resource_selectel_iam_group_membership_v1.go | 10 +++++++--- ...source_selectel_iam_group_membership_v1_test.go | 12 ++++++------ selectel/resource_selectel_iam_group_v1.go | 14 +++++++++++++- selectel/resource_selectel_iam_group_v1_test.go | 3 ++- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/selectel/resource_selectel_iam_group_membership_v1.go b/selectel/resource_selectel_iam_group_membership_v1.go index 920b5995..5d4ad57a 100644 --- a/selectel/resource_selectel_iam_group_membership_v1.go +++ b/selectel/resource_selectel_iam_group_membership_v1.go @@ -1,14 +1,16 @@ package selectel import ( + "context" "encoding/base64" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "log" "slices" "sort" "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceIAMGroupMembershipV1() *schema.Resource { @@ -160,6 +162,7 @@ func generateCompositeID(groupID string, userIDs []string) string { sort.Strings(userIDs) concatenated := groupID + ":" + strings.Join(userIDs, ",") encoded := base64.StdEncoding.EncodeToString([]byte(concatenated)) + return encoded } @@ -200,7 +203,7 @@ func diffUsers(oldUsers, newUsers []string) ([]string, []string) { return usersToAdd, usersToRemove } -// containsAll checks if sliceB is a subset of sliceA +// containsAll checks if sliceB is a subset of sliceA. func containsAll(sliceA, sliceB []string) bool { for _, b := range sliceB { found := false @@ -214,5 +217,6 @@ func containsAll(sliceA, sliceB []string) bool { return false } } + return true } diff --git a/selectel/resource_selectel_iam_group_membership_v1_test.go b/selectel/resource_selectel_iam_group_membership_v1_test.go index 8c98e79c..cc2b04a6 100644 --- a/selectel/resource_selectel_iam_group_membership_v1_test.go +++ b/selectel/resource_selectel_iam_group_membership_v1_test.go @@ -1,9 +1,9 @@ package selectel import ( - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccIAMV1GroupMembershipBasic(t *testing.T) { @@ -59,7 +59,7 @@ func TestAccIAMV1GroupUpdate(t *testing.T) { } func testAccIAMV1GroupMembershipBasic() string { - return fmt.Sprintf(` + return ` resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_1" { name = "test-service-user-1" password = "Qazwsxedc123" @@ -93,11 +93,11 @@ resource "selectel_iam_group_membership_v1" "membership_tf_acc_test_1" { selectel_iam_serviceuser_v1.serviceuser_tf_acc_test_1.id ] } -`) +` } func testAccIAMV1GroupMembershipUpdate() string { - return fmt.Sprintf(` + return ` resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_1" { name = "test-service-user-1" password = "Qazwsxedc123" @@ -132,5 +132,5 @@ resource "selectel_iam_group_membership_v1" "membership_tf_acc_test_1" { selectel_iam_serviceuser_v1.serviceuser_tf_acc_test_2.id ] } -`) +` } diff --git a/selectel/resource_selectel_iam_group_v1.go b/selectel/resource_selectel_iam_group_v1.go index a74e574f..7610d4f7 100644 --- a/selectel/resource_selectel_iam_group_v1.go +++ b/selectel/resource_selectel_iam_group_v1.go @@ -1,6 +1,18 @@ package selectel -import () +import ( + "context" + "errors" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/selectel/iam-go" + "github.com/selectel/iam-go/iamerrors" + "github.com/selectel/iam-go/service/groups" + "github.com/selectel/iam-go/service/roles" +) func resourceIAMGroupV1() *schema.Resource { return &schema.Resource{ diff --git a/selectel/resource_selectel_iam_group_v1_test.go b/selectel/resource_selectel_iam_group_v1_test.go index a72abb1b..d735a8fd 100644 --- a/selectel/resource_selectel_iam_group_v1_test.go +++ b/selectel/resource_selectel_iam_group_v1_test.go @@ -4,10 +4,11 @@ import ( "context" "errors" "fmt" + "testing" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/selectel/iam-go/service/groups" - "testing" ) func TestAccIAMV1GroupBasic(t *testing.T) { From 587f700a6eb6d4343319a403a1e241516b4ecbd7 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Thu, 11 Jul 2024 11:18:56 +0300 Subject: [PATCH 05/20] update go.mod --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 6370eb94..432de544 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/selectel/dbaas-go v0.12.1 github.com/selectel/domains-go v1.0.2 github.com/selectel/go-selvpcclient/v3 v3.1.1 - github.com/selectel/iam-go v0.2.0 + github.com/selectel/iam-go v0.3.0 github.com/selectel/mks-go v0.14.0 github.com/selectel/secretsmanager-go v0.2.1 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 3949d19d..e5c7160a 100644 --- a/go.sum +++ b/go.sum @@ -172,6 +172,8 @@ github.com/selectel/go-selvpcclient/v3 v3.1.1 h1:C1q2LqqosiapoLpnGITGmysg0YCSQYD github.com/selectel/go-selvpcclient/v3 v3.1.1/go.mod h1:NM7IXhh1IzqZ88DOw1Qc5Ez3tULLViXo95l5+rKPuyQ= github.com/selectel/iam-go v0.2.0 h1:c6ldpbsa/8R3b29ML5B21FU9oyJ2A2AwBNzCbE+pGN8= github.com/selectel/iam-go v0.2.0/go.mod h1:OIAkW7MZK97YUm+uvUgYbgDhkI9SdzTCxwd4yZoOR1o= +github.com/selectel/iam-go v0.3.0 h1:HRoxSBXwvASE9v/A4WgeEDeMvomARIWyj2essV4LmYc= +github.com/selectel/iam-go v0.3.0/go.mod h1:OIAkW7MZK97YUm+uvUgYbgDhkI9SdzTCxwd4yZoOR1o= github.com/selectel/mks-go v0.14.0 h1:huNq/oTutPc3ezB8HRqlGN9WJubTDETpNKuIVqcZOn0= github.com/selectel/mks-go v0.14.0/go.mod h1:VxtV3dzwgOEzZc+9VMQb9DvxfSlej2ZQ8jnT8kqIGgU= github.com/selectel/secretsmanager-go v0.2.1 h1:OSBrA/07lm/Ecpwg59IJHFAoUHZR29oyfwUgTpr/dos= From 14a608f289e4aa64612914d66362ce8ba5a09b64 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Thu, 11 Jul 2024 13:18:36 +0300 Subject: [PATCH 06/20] add docs --- .../r/iam_group_membership_v1.html.markdown | 31 +++++++ website/docs/r/iam_group_v1.html.markdown | 80 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 website/docs/r/iam_group_membership_v1.html.markdown create mode 100644 website/docs/r/iam_group_v1.html.markdown diff --git a/website/docs/r/iam_group_membership_v1.html.markdown b/website/docs/r/iam_group_membership_v1.html.markdown new file mode 100644 index 00000000..8fba3ed4 --- /dev/null +++ b/website/docs/r/iam_group_membership_v1.html.markdown @@ -0,0 +1,31 @@ +--- +layout: "selectel" +page_title: "Selectel: selectel_iam_group_membership_v1" +sidebar_current: "docs-selectel-resource-iam-group_membership-v1" +description: |- + Creates and manages group membership for Selectel products using public API v1. +--- + +# selectel\_iam\_group_membership\_v1 + +Creates and manages group membership for Selectel products using public API v1. +Selectel products support Identity and Access Management (IAM). + +## Example Usage + +```hcl +resource "selectel_iam_group_membership_v1" "group_membership_1" { + group_id = selectel_iam_group_v1.group_1.id + + user_ids = [ + selectel_iam_user_v1.user_1.keystone_id, + selectel_iam_serviceuser_v1.serviceuser_1.id + ] +} +``` + +## Argument Reference + +* `group_id` - (Required) ID of the group. + +* `user_ids` - (Required) List of users Keystone IDs. diff --git a/website/docs/r/iam_group_v1.html.markdown b/website/docs/r/iam_group_v1.html.markdown new file mode 100644 index 00000000..db5d86a6 --- /dev/null +++ b/website/docs/r/iam_group_v1.html.markdown @@ -0,0 +1,80 @@ +--- +layout: "selectel" +page_title: "Selectel: selectel_iam_group_v1" +sidebar_current: "docs-selectel-resource-iam-group-v1" +description: |- + Creates and manages a user group for Selectel products using public API v1. +--- + +# selectel\_iam\_group\_v1 + +Creates and manages a user group for Selectel products using public API v1. + Selectel products support Identity and Access Management (IAM). + +## Example Usage + +```hcl +resource "selectel_iam_group_v1" "group_1" { + name = "My group" + description = "My test group" + role { + role_name = "member" + scope = "account" + } +} +``` + +## Argument Reference + +* `name` - (Required) Name of the group. + +* `description` - (Optional) Description of the group. + +* `role` - (Optional) Manages group roles. You can add multiple roles – each role in a separate block. For more information about roles, see the [Roles](#roles) section. + + * `role_name` - (Required) Role name. Available role names are `iam_admin`, `member`, `reader`, and `billing`. + + * `scope` - (Required) Scope of the role. Available scopes are `account` and `project`. If `scope` is `project`, the `project_id` argument is required. + + * `project_id` - (Optional) Unique identifier of the associated project. If `scope` is `project`, the `project_id` argument is required. Retrieved from the [selectel_vpc_project_v2](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/vpc_project_v2) resource. Learn more about [Projects](https://docs.selectel.ru/en/control-panel-actions/projects/about-projects/). + +### Roles + +To assign roles, use the following values for `scope` and `role_name`: + +* Account administrator - `scope` is `account`, `role_name` is `member`. + +* Billing administrator - `scope` is `account`, `role_name` is `billing`. + +* User administrator - `scope` is `account`, `role_name` is `iam_admin`. + +* Project administrator - `scope` is `project`, `role_name` is `member`. + +* Account viewer - `scope` is `account`, `role_name` is `reader`. + +* Project viewer - `scope` is `project`, `role_name` is `reader`. + +* Object storage admin - `scope` is `project`, `role_name` is `object_storage:admin`. + +* Object storage user - `scope` is `project`, `role_name` is `object_storage_user`. + +## Import + +You can import a group: + +```shell +export OS_DOMAIN_NAME= +export OS_USERNAME= +export OS_PASSWORD= +terraform import selectel_iam_group_v1.group_1 +``` + +where: + +* `` — Selectel account ID. The account ID is in the top right corner of the [Control panel](https://my.selectel.ru/). Learn more about [Registration](https://docs.selectel.ru/en/control-panel-actions/account/registration/). + +* `` — Name of the service user. To get the name, in the [Control panel](https://my.selectel.ru/iam/users_management/users?type=service), go to **Identity & Access Management** ⟶ **User management** ⟶ the **Service users** tab ⟶ copy the name of the required user. Learn more about [Service Users](https://docs.selectel.ru/en/control-panel-actions/users-and-roles/user-types-and-roles/). + +* `` — Password of the service user. + +* `` — Unique identifier of the group to import, for example, `abc1bb378ac84e1234b869b77aadd2ab`. To get the ID, use either [iam-go](https://github.com/selectel/iam-go) or [IAM API](https://developers.selectel.ru/docs/control-panel/iam/). From 03e0eb13a87b1a334925f38989544bd0fe7201a5 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Mon, 15 Jul 2024 16:52:11 +0300 Subject: [PATCH 07/20] tidy --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index e5c7160a..d2989509 100644 --- a/go.sum +++ b/go.sum @@ -170,8 +170,6 @@ github.com/selectel/domains-go v1.0.2 h1:Si6iGaMnTFJxwiJVI50DOdZnwcxc87kqaWrVQYW github.com/selectel/domains-go v1.0.2/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= github.com/selectel/go-selvpcclient/v3 v3.1.1 h1:C1q2LqqosiapoLpnGITGmysg0YCSQYDo2Gh69CioevM= github.com/selectel/go-selvpcclient/v3 v3.1.1/go.mod h1:NM7IXhh1IzqZ88DOw1Qc5Ez3tULLViXo95l5+rKPuyQ= -github.com/selectel/iam-go v0.2.0 h1:c6ldpbsa/8R3b29ML5B21FU9oyJ2A2AwBNzCbE+pGN8= -github.com/selectel/iam-go v0.2.0/go.mod h1:OIAkW7MZK97YUm+uvUgYbgDhkI9SdzTCxwd4yZoOR1o= github.com/selectel/iam-go v0.3.0 h1:HRoxSBXwvASE9v/A4WgeEDeMvomARIWyj2essV4LmYc= github.com/selectel/iam-go v0.3.0/go.mod h1:OIAkW7MZK97YUm+uvUgYbgDhkI9SdzTCxwd4yZoOR1o= github.com/selectel/mks-go v0.14.0 h1:huNq/oTutPc3ezB8HRqlGN9WJubTDETpNKuIVqcZOn0= From 9cb3311cbcccd5287f6a184708eef8b471338729 Mon Sep 17 00:00:00 2001 From: milkrage Date: Wed, 17 Jul 2024 21:45:15 +0300 Subject: [PATCH 08/20] fix ci --- .github/workflows/secure.yml | 4 +++- .github/workflows/verify.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/secure.yml b/.github/workflows/secure.yml index 8b90e7d9..8570abb6 100644 --- a/.github/workflows/secure.yml +++ b/.github/workflows/secure.yml @@ -1,6 +1,8 @@ name: Secure -on: push +on: + push: + pull_request: jobs: # Sample GitHub Actions: diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 97da0b9d..935c18af 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -1,6 +1,8 @@ name: Verify -on: push +on: + push: + pull_request: jobs: tests: From b06ee28bd23d2325e88a5c24eeab5237e018516c Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Mon, 22 Jul 2024 13:42:30 +0300 Subject: [PATCH 09/20] docs update --- website/docs/r/iam_group_membership_v1.html.markdown | 7 ++++--- website/docs/r/iam_group_v1.html.markdown | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/website/docs/r/iam_group_membership_v1.html.markdown b/website/docs/r/iam_group_membership_v1.html.markdown index 8fba3ed4..a787b0c1 100644 --- a/website/docs/r/iam_group_membership_v1.html.markdown +++ b/website/docs/r/iam_group_membership_v1.html.markdown @@ -8,8 +8,9 @@ description: |- # selectel\_iam\_group_membership\_v1 -Creates and manages group membership for Selectel products using public API v1. +Manages group membership for Selectel products using public API v1. Selectel products support Identity and Access Management (IAM). +For more information about groups, see the [official Selectel documentation](https://docs.selectel.ru/control-panel-actions/users-and-roles/groups/). ## Example Usage @@ -26,6 +27,6 @@ resource "selectel_iam_group_membership_v1" "group_membership_1" { ## Argument Reference -* `group_id` - (Required) ID of the group. +* `group_id` - (Required) Unique identifier of the group. Retrieved from the [selectel_iam_group_v1](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/iam_group_v1) resource. -* `user_ids` - (Required) List of users Keystone IDs. +* `user_ids` - (Required) List of unique Keystone identifiers of users. Retrieved from the [selectel_iam_serviceuser_v1](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/iam_serviceuser_v1) and [selectel_iam_user_v1](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/iam_user_v1) resources. diff --git a/website/docs/r/iam_group_v1.html.markdown b/website/docs/r/iam_group_v1.html.markdown index db5d86a6..db93c275 100644 --- a/website/docs/r/iam_group_v1.html.markdown +++ b/website/docs/r/iam_group_v1.html.markdown @@ -9,7 +9,8 @@ description: |- # selectel\_iam\_group\_v1 Creates and manages a user group for Selectel products using public API v1. - Selectel products support Identity and Access Management (IAM). +Selectel products support Identity and Access Management (IAM). +For more information about user groups, see the [official Selectel documentation](https://docs.selectel.ru/control-panel-actions/users-and-roles/groups/). ## Example Usage @@ -26,13 +27,13 @@ resource "selectel_iam_group_v1" "group_1" { ## Argument Reference -* `name` - (Required) Name of the group. +* `name` - (Required) Group name. -* `description` - (Optional) Description of the group. +* `description` - (Optional) Group description. * `role` - (Optional) Manages group roles. You can add multiple roles – each role in a separate block. For more information about roles, see the [Roles](#roles) section. - * `role_name` - (Required) Role name. Available role names are `iam_admin`, `member`, `reader`, and `billing`. + * `role_name` - (Required) Role name. Available role names are `iam_admin`, `member`, `reader`, `billing`, `object_storage:admin`, and `object_storage_user`. * `scope` - (Required) Scope of the role. Available scopes are `account` and `project`. If `scope` is `project`, the `project_id` argument is required. @@ -77,4 +78,4 @@ where: * `` — Password of the service user. -* `` — Unique identifier of the group to import, for example, `abc1bb378ac84e1234b869b77aadd2ab`. To get the ID, use either [iam-go](https://github.com/selectel/iam-go) or [IAM API](https://developers.selectel.ru/docs/control-panel/iam/). +* `` — Unique identifier of the group, for example, `abc1bb378ac84e1234b869b77aadd2ab`. To get the group ID, use either [iam-go](https://github.com/selectel/iam-go) or [IAM API](https://developers.selectel.ru/docs/control-panel/iam/). From d476a1a43050dc06179c1c1d3e1459146e4f48cc Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Wed, 10 Jul 2024 12:21:45 +0300 Subject: [PATCH 10/20] add groups --- selectel/provider.go | 4 + ...source_selectel_iam_group_membership_v1.go | 218 ++++++++++++++++++ ...e_selectel_iam_group_membership_v1_test.go | 136 +++++++++++ selectel/resource_selectel_iam_group_v1.go | 181 +++++++++++++++ .../resource_selectel_iam_group_v1_test.go | 148 ++++++++++++ 5 files changed, 687 insertions(+) create mode 100644 selectel/resource_selectel_iam_group_membership_v1.go create mode 100644 selectel/resource_selectel_iam_group_membership_v1_test.go create mode 100644 selectel/resource_selectel_iam_group_v1.go create mode 100644 selectel/resource_selectel_iam_group_v1_test.go diff --git a/selectel/provider.go b/selectel/provider.go index a8d2c8ba..e141d7dc 100644 --- a/selectel/provider.go +++ b/selectel/provider.go @@ -27,6 +27,8 @@ const ( objectUser = "user" objectServiceUser = "service user" objectS3Credentials = "s3 credentials" + objectGroup = "group" + objectGroupMembership = "group-membership" objectCluster = "cluster" objectKubeConfig = "kubeconfig" objectKubeVersions = "kube-versions" @@ -135,6 +137,8 @@ func Provider() *schema.Provider { "selectel_iam_serviceuser_v1": resourceIAMServiceUserV1(), "selectel_iam_user_v1": resourceIAMUserV1(), "selectel_iam_s3_credentials_v1": resourceIAMS3CredentialsV1(), + "selectel_iam_group_v1": resourceIAMGroupV1(), + "selectel_iam_group_membership_v1": resourceIAMGroupMembershipV1(), "selectel_vpc_vrrp_subnet_v2": resourceVPCVRRPSubnetV2(), // DEPRECATED "selectel_vpc_crossregion_subnet_v2": resourceVPCCrossRegionSubnetV2(), // DEPRECATED "selectel_mks_cluster_v1": resourceMKSClusterV1(), diff --git a/selectel/resource_selectel_iam_group_membership_v1.go b/selectel/resource_selectel_iam_group_membership_v1.go new file mode 100644 index 00000000..920b5995 --- /dev/null +++ b/selectel/resource_selectel_iam_group_membership_v1.go @@ -0,0 +1,218 @@ +package selectel + +import ( + "encoding/base64" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "log" + "slices" + "sort" + "strings" +) + +func resourceIAMGroupMembershipV1() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceIAMGroupMembershipV1Create, + ReadContext: resourceIAMGroupMembershipV1Read, + UpdateContext: resourceIAMGroupMembershipV1Update, + DeleteContext: resourceIAMGroupMembershipV1Delete, + Schema: map[string]*schema.Schema{ + "group_id": { + Type: schema.TypeString, + Required: true, + }, + "user_ids": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceIAMGroupMembershipV1Create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + userIDsInterface := d.Get("user_ids").([]interface{}) + userIDs := make([]string, len(userIDsInterface)) + for i, v := range userIDsInterface { + userIDs[i] = v.(string) + } + + log.Print(msgCreate(objectGroupMembership, d.Id())) + if len(userIDs) == 0 { + createErr := fmt.Errorf("error creating group membership: no user ids specified") + return diag.FromErr(errCreatingObject(objectGroupMembership, createErr)) + } + err := iamClient.Groups.AddUsers(ctx, d.Get("group_id").(string), userIDs) + if err != nil { + return diag.FromErr(errCreatingObject(objectGroupMembership, err)) + } + + d.SetId(generateCompositeID(d.Get("group_id").(string), userIDs)) + + return resourceIAMGroupMembershipV1Read(ctx, d, meta) +} + +func resourceIAMGroupMembershipV1Read(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + groupID, userIDs, err := parseCompositeID(d.Id()) + if err != nil { + return diag.FromErr(errGettingObject(objectGroupMembership, d.Id(), err)) + } + + response, err := iamClient.Groups.Get(ctx, groupID) + if err != nil { + return diag.FromErr(errGettingObject(objectGroupMembership, d.Id(), err)) + } + + responseUserIDs := make([]string, 0) + for _, user := range response.Users { + responseUserIDs = append(responseUserIDs, user.KeystoneID) + } + + responseServiceUserIDs := make([]string, 0) + for _, serviceUser := range response.ServiceUsers { + responseServiceUserIDs = append(responseServiceUserIDs, serviceUser.ID) + } + + if !containsAll(userIDs, responseUserIDs) || !containsAll(userIDs, responseServiceUserIDs) { + readErr := fmt.Errorf("error validating group memberships: Group %s does not contain all users %v", groupID, userIDs) + return diag.FromErr(errGettingObject(objectGroupMembership, d.Id(), readErr)) + } + + d.Set("group_id", groupID) + d.Set("user_ids", append(responseUserIDs, responseServiceUserIDs...)) + + return nil +} + +func resourceIAMGroupMembershipV1Update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + groupID, oldUserIDs, err := parseCompositeID(d.Id()) + if err != nil { + return diag.FromErr(errUpdatingObject(objectGroupMembership, d.Id(), err)) + } + + newUserIDsInterface := d.Get("user_ids").([]interface{}) + newUserIDs := make([]string, len(newUserIDsInterface)) + for i, v := range newUserIDsInterface { + newUserIDs[i] = v.(string) + } + + usersToAdd, usersToRemove := diffUsers(oldUserIDs, newUserIDs) + + if len(usersToAdd) > 0 { + err := iamClient.Groups.AddUsers(ctx, groupID, usersToAdd) + if err != nil { + return diag.FromErr(err) + } + } + + if len(usersToRemove) > 0 { + err := iamClient.Groups.DeleteUsers(ctx, groupID, usersToRemove) + if err != nil { + return diag.FromErr(err) + } + } + + d.SetId(generateCompositeID(groupID, newUserIDs)) + + return resourceIAMGroupMembershipV1Read(ctx, d, meta) +} + +func resourceIAMGroupMembershipV1Delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + groupID, userIDs, err := parseCompositeID(d.Id()) + if err != nil { + return diag.FromErr(errDeletingObject(objectGroupMembership, d.Id(), err)) + } + + err = iamClient.Groups.DeleteUsers(ctx, groupID, userIDs) + if err != nil { + return diag.FromErr(errDeletingObject(objectGroupMembership, d.Id(), err)) + } + + d.SetId("") + + return nil +} + +func generateCompositeID(groupID string, userIDs []string) string { + sort.Strings(userIDs) + concatenated := groupID + ":" + strings.Join(userIDs, ",") + encoded := base64.StdEncoding.EncodeToString([]byte(concatenated)) + return encoded +} + +func parseCompositeID(compositeID string) (string, []string, error) { + decodedBytes, err := base64.StdEncoding.DecodeString(compositeID) + if err != nil { + return "", nil, fmt.Errorf("error decoding composite ID: %s, %v", compositeID, err) + } + decodedString := string(decodedBytes) + + parts := strings.Split(decodedString, ":") + if len(parts) != 2 { + return "", nil, fmt.Errorf("invalid decoded composite ID: %s", decodedString) + } + + groupID := parts[0] + userIDs := strings.Split(parts[1], ",") + + return groupID, userIDs, nil +} + +func diffUsers(oldUsers, newUsers []string) ([]string, []string) { + usersToAdd := make([]string, 0) + usersToRemove := make([]string, 0) + + for _, user := range newUsers { + if !slices.Contains(oldUsers, user) { + usersToAdd = append(usersToAdd, user) + } + } + + for _, user := range oldUsers { + if !slices.Contains(newUsers, user) { + usersToRemove = append(usersToRemove, user) + } + } + + return usersToAdd, usersToRemove +} + +// containsAll checks if sliceB is a subset of sliceA +func containsAll(sliceA, sliceB []string) bool { + for _, b := range sliceB { + found := false + for _, a := range sliceA { + if a == b { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/selectel/resource_selectel_iam_group_membership_v1_test.go b/selectel/resource_selectel_iam_group_membership_v1_test.go new file mode 100644 index 00000000..8c98e79c --- /dev/null +++ b/selectel/resource_selectel_iam_group_membership_v1_test.go @@ -0,0 +1,136 @@ +package selectel + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "testing" +) + +func TestAccIAMV1GroupMembershipBasic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccIAMV1GroupMembershipBasic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "group_id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.0"), + ), + }, + }, + }) +} + +func TestAccIAMV1GroupUpdate(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccIAMV1GroupMembershipBasic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "group_id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.0"), + ), + }, + { + Config: testAccIAMV1GroupMembershipUpdate(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "group_id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.0"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.1"), + ), + }, + { + Config: testAccIAMV1GroupMembershipBasic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "group_id"), + resource.TestCheckResourceAttrSet("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.0"), + resource.TestCheckNoResourceAttr("selectel_iam_group_membership_v1.membership_tf_acc_test_1", "user_ids.1"), + ), + }, + }, + }) +} + +func testAccIAMV1GroupMembershipBasic() string { + return fmt.Sprintf(` +resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_1" { + name = "test-service-user-1" + password = "Qazwsxedc123" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_2" { + name = "test-service-user-2" + password = "Qazwsxedc123" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_group_v1" "group_tf_acc_test_1" { + name = "test-group" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_group_membership_v1" "membership_tf_acc_test_1" { + group_id = selectel_iam_group_v1.group_tf_acc_test_1.id + + user_ids = [ + selectel_iam_serviceuser_v1.serviceuser_tf_acc_test_1.id + ] +} +`) +} + +func testAccIAMV1GroupMembershipUpdate() string { + return fmt.Sprintf(` +resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_1" { + name = "test-service-user-1" + password = "Qazwsxedc123" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_2" { + name = "test-service-user-2" + password = "Qazwsxedc123" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_group_v1" "group_tf_acc_test_1" { + name = "test-group" + role { + role_name = "reader" + scope = "account" + } +} + +resource "selectel_iam_group_membership_v1" "membership_tf_acc_test_1" { + group_id = selectel_iam_group_v1.group_tf_acc_test_1.id + + user_ids = [ + selectel_iam_serviceuser_v1.serviceuser_tf_acc_test_1.id, + selectel_iam_serviceuser_v1.serviceuser_tf_acc_test_2.id + ] +} +`) +} diff --git a/selectel/resource_selectel_iam_group_v1.go b/selectel/resource_selectel_iam_group_v1.go new file mode 100644 index 00000000..a74e574f --- /dev/null +++ b/selectel/resource_selectel_iam_group_v1.go @@ -0,0 +1,181 @@ +package selectel + +import () + +func resourceIAMGroupV1() *schema.Resource { + return &schema.Resource{ + Description: "Represents a Group in IAM API", + CreateContext: resourceIAMGroupV1Create, + ReadContext: resourceIAMGroupV1Read, + UpdateContext: resourceIAMGroupV1Update, + DeleteContext: resourceIAMGroupV1Delete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the group.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Description of the group.", + }, + "role": { + Type: schema.TypeSet, + Optional: true, + Description: "Role block of the group.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "role_name": { + Type: schema.TypeString, + Required: true, + }, + "scope": { + Type: schema.TypeString, + Required: true, + }, + "project_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + } +} + +func resourceIAMGroupV1Create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + roles, err := convertIAMSetToRoles(d.Get("role").(*schema.Set)) + if err != nil { + return diag.FromErr(err) + } + + log.Print(msgCreate(objectGroup, d.Id())) + group, err := iamClient.Groups.Create(ctx, groups.CreateRequest{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + }) + if err != nil { + return diag.FromErr(errCreatingObject(objectGroup, err)) + } + d.SetId(group.ID) + + if len(roles) != 0 { + err = iamClient.Groups.AssignRoles(ctx, group.ID, roles) + if err != nil { + return diag.FromErr(errCreatingObject(objectGroup, err)) + } + } + + return resourceIAMGroupV1Read(ctx, d, meta) +} + +func resourceIAMGroupV1Read(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + log.Print(msgGet(objectGroup, d.Id())) + group, err := iamClient.Groups.Get(ctx, d.Id()) + if err != nil { + return diag.FromErr(errGettingObject(objectGroup, d.Id(), err)) + } + + if group.Group.Roles != nil && len(group.Group.Roles) != 0 { + err = d.Set("role", convertIAMRolesToSet(group.Roles)) + if err != nil { + return nil + } + } + + return nil +} + +func resourceIAMGroupV1Update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + description := d.Get("description").(string) + + opts := groups.UpdateRequest{ + Name: d.Get("name").(string), + Description: &description, + } + + log.Print(msgUpdate(objectGroup, d.Id(), fmt.Sprintf("Name: %+v, description: %+v", opts.Name, opts.Description))) + _, err := iamClient.Groups.Update(ctx, d.Id(), opts) + if err != nil { + return diag.FromErr(errUpdatingObject(objectGroup, d.Id(), err)) + } + + if d.HasChange("role") { + currentGroup, err := iamClient.Groups.Get(ctx, d.Id()) + if err != nil { + return diag.FromErr(errGettingObject(objectGroup, d.Id(), err)) + } + oldRoles := currentGroup.Roles + newRoles, err := convertIAMSetToRoles(d.Get("role").(*schema.Set)) + if err != nil { + return diag.FromErr(err) + } + + rolesToUnassign, rolesToAssign := diffRoles(oldRoles, newRoles) + + log.Print(msgUpdate(objectGroup, d.Id(), fmt.Sprintf("Roles to unassign: %+v, roles to assign: %+v", rolesToUnassign, rolesToAssign))) + err = applyGroupRoles(ctx, d, iamClient, rolesToUnassign, rolesToAssign) + if err != nil { + return diag.FromErr(errUpdatingObject(objectGroup, d.Id(), err)) + } + + return nil + } + + return resourceIAMGroupV1Read(ctx, d, meta) +} + +func resourceIAMGroupV1Delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + iamClient, diagErr := getIAMClient(meta) + if diagErr != nil { + return diagErr + } + + log.Print(msgDelete(objectGroup, d.Id())) + err := iamClient.Groups.Delete(ctx, d.Id()) + if err != nil && !errors.Is(err, iamerrors.ErrGroupNotFound) { + return diag.FromErr(errDeletingObject(objectGroup, d.Id(), err)) + } + + d.SetId("") + + return nil +} + +func applyGroupRoles(ctx context.Context, d *schema.ResourceData, iamClient *iam.Client, rolesToUnassign, rolesToAssign []roles.Role) error { + if len(rolesToAssign) != 0 { + err := iamClient.Groups.AssignRoles(ctx, d.Id(), rolesToAssign) + if err != nil { + return err + } + } + + if len(rolesToUnassign) != 0 { + err := iamClient.Groups.UnassignRoles(ctx, d.Id(), rolesToUnassign) + if err != nil { + return err + } + } + + return nil +} diff --git a/selectel/resource_selectel_iam_group_v1_test.go b/selectel/resource_selectel_iam_group_v1_test.go new file mode 100644 index 00000000..49ea6a67 --- /dev/null +++ b/selectel/resource_selectel_iam_group_v1_test.go @@ -0,0 +1,148 @@ +package selectel + +import () + +func TestAccIAMV1GroupBasic(t *testing.T) { + var group groups.Group + + testName := "test-name" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckIAMV1GroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccIAMV1GroupBasic(testName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMV1GroupExists("selectel_iam_group_v1.group_tf_acc_test_1", &group), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "id"), + resource.TestCheckResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "name", testName), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.role_name"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.scope"), + ), + }, + }, + }) +} + +func TestAccIAMV1GroupUpdateRoles(t *testing.T) { + var group groups.Group + + testName := "test-name" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckIAMV1GroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccIAMV1GroupBasic(testName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMV1GroupExists("selectel_iam_group_v1.group_tf_acc_test_1", &group), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "id"), + resource.TestCheckResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "name", testName), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.role_name"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.scope"), + ), + }, + { + Config: testAccIAMV1GroupAssignRole(testName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMV1GroupExists("selectel_iam_group_v1.group_tf_acc_test_1", &group), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "id"), + resource.TestCheckResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "name", testName), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.role_name"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.scope"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.1.role_name"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.1.scope"), + ), + }, + { + Config: testAccIAMV1GroupBasic(testName), + Check: resource.ComposeTestCheckFunc( + testAccCheckIAMV1GroupExists("selectel_iam_group_v1.group_tf_acc_test_1", &group), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "id"), + resource.TestCheckResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "name", testName), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.role_name"), + resource.TestCheckResourceAttrSet("selectel_iam_group_v1.group_tf_acc_test_1", "role.0.scope"), + resource.TestCheckNoResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "role.1.role_name"), + resource.TestCheckNoResourceAttr("selectel_iam_group_v1.group_tf_acc_test_1", "role.1.scope"), + ), + }, + }, + }) +} + +func testAccCheckIAMV1GroupDestroy(s *terraform.State) error { + iamClient, diagErr := getIAMClient(testAccProvider.Meta()) + if diagErr != nil { + return fmt.Errorf("can't get iamclient for test group object") + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "selectel_iam_group_v1" { + continue + } + + _, err := iamClient.Groups.Get(context.Background(), rs.Primary.ID) + if err == nil { + return errors.New("group still exists") + } + } + + return nil +} + +func testAccCheckIAMV1GroupExists(n string, group *groups.Group) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return errors.New("no ID is set") + } + + iamClient, diagErr := getIAMClient(testAccProvider.Meta()) + if diagErr != nil { + return fmt.Errorf("can't get iamclient for test group object") + } + + g, err := iamClient.Groups.Get(context.Background(), rs.Primary.ID) + if err != nil { + return errors.New("group not found") + } + + *group = g.Group + + return nil + } +} + +func testAccIAMV1GroupBasic(name string) string { + return fmt.Sprintf(` +resource "selectel_iam_group_v1" "group_tf_acc_test_1" { + name = "%s" + role { + role_name = "reader" + scope = "account" + } +}`, name) +} + +func testAccIAMV1GroupAssignRole(name string) string { + return fmt.Sprintf(` + resource "selectel_iam_group_v1" "group_tf_acc_test_1" { + name = "%s" + role { + role_name = "reader" + scope = "account" + } + role { + role_name = "billing" + scope = "account" + } + }`, name) +} From 6a55a7c1b2a1ab22a7212633c8308b0e95db1762 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Wed, 10 Jul 2024 12:22:46 +0300 Subject: [PATCH 11/20] fix --- .../resource_selectel_iam_s3_credentials_v1.go | 6 +++--- ...resource_selectel_iam_s3_credentials_v1_test.go | 14 +++++++------- .../resource_selectel_iam_serviceuser_v1_test.go | 2 +- selectel/resource_selectel_iam_user_v1_test.go | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/selectel/resource_selectel_iam_s3_credentials_v1.go b/selectel/resource_selectel_iam_s3_credentials_v1.go index 0ba0d476..61e9dae8 100644 --- a/selectel/resource_selectel_iam_s3_credentials_v1.go +++ b/selectel/resource_selectel_iam_s3_credentials_v1.go @@ -90,13 +90,13 @@ func resourceIAMS3CredentialsV1Read(ctx context.Context, d *schema.ResourceData, } log.Print(msgGet(objectS3Credentials, d.Id())) - credentials, err := iamClient.S3Credentials.List(ctx, d.Get("user_id").(string)) + response, err := iamClient.S3Credentials.List(ctx, d.Get("user_id").(string)) if err != nil { return diag.FromErr(errGettingObject(objectS3Credentials, d.Id(), err)) } - var credential s3credentials.Credentials - for _, c := range credentials { + var credential s3credentials.Credential + for _, c := range response.Credentials { if d.Id() == c.AccessKey { credential = c break diff --git a/selectel/resource_selectel_iam_s3_credentials_v1_test.go b/selectel/resource_selectel_iam_s3_credentials_v1_test.go index 3ed967d2..64a864c4 100644 --- a/selectel/resource_selectel_iam_s3_credentials_v1_test.go +++ b/selectel/resource_selectel_iam_s3_credentials_v1_test.go @@ -27,7 +27,7 @@ func TestAccIAMV1S3CredentialsBasic(t *testing.T) { { Config: testAccIAMV1S3CredentialsBasic(projectName, userName, userPassword, s3CredsName), Check: resource.ComposeTestCheckFunc( - testAccCheckIAMV1S3CredentialsExists("selectel_iam_s3_credentials_v1.s3_creds_tf_acc_test_1", &s3credentials), + testAccCheckIAMV1S3CredentialsExists("selectel_iam_s3_credentials_v1.s3_creds_tf_acc_test_1", &s3credential), resource.TestCheckResourceAttrSet("selectel_iam_s3_credentials_v1.s3_creds_tf_acc_test_1", "user_id"), resource.TestCheckResourceAttrSet("selectel_iam_s3_credentials_v1.s3_creds_tf_acc_test_1", "project_id"), resource.TestCheckResourceAttrSet("selectel_iam_s3_credentials_v1.s3_creds_tf_acc_test_1", "secret_key"), @@ -50,8 +50,8 @@ func testAccCheckIAMV1S3CredentialsDestroy(s *terraform.State) error { continue } - credentialsList, _ := iamClient.S3Credentials.List(context.Background(), rs.Primary.Attributes["user_id"]) - for _, cred := range credentialsList { + response, _ := iamClient.S3Credentials.List(context.Background(), rs.Primary.Attributes["user_id"]) + for _, cred := range response.Credentials { if cred.AccessKey == rs.Primary.ID { return errors.New("s3 credentials still exist") } @@ -61,7 +61,7 @@ func testAccCheckIAMV1S3CredentialsDestroy(s *terraform.State) error { return nil } -func testAccCheckIAMV1S3CredentialsExists(n string, s3Credential *s3credentials.Credentials) resource.TestCheckFunc { +func testAccCheckIAMV1S3CredentialsExists(n string, s3Credential *s3credentials.Credential) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -77,9 +77,9 @@ func testAccCheckIAMV1S3CredentialsExists(n string, s3Credential *s3credentials. return fmt.Errorf("can't get iamclient for test s3 credentials object") } - credentialsList, _ := iamClient.S3Credentials.List(context.Background(), rs.Primary.Attributes["user_id"]) - var neededS3Credentials s3credentials.Credentials - for _, cred := range credentialsList { + response, _ := iamClient.S3Credentials.List(context.Background(), rs.Primary.Attributes["user_id"]) + var neededS3Credentials s3credentials.Credential + for _, cred := range response.Credentials { if cred.Name == rs.Primary.Attributes["name"] { neededS3Credentials = cred break diff --git a/selectel/resource_selectel_iam_serviceuser_v1_test.go b/selectel/resource_selectel_iam_serviceuser_v1_test.go index bec34bf8..5afd0a37 100644 --- a/selectel/resource_selectel_iam_serviceuser_v1_test.go +++ b/selectel/resource_selectel_iam_serviceuser_v1_test.go @@ -171,7 +171,7 @@ func testAccCheckIAMV1ServiceUserExists(n string, serviceUser *serviceusers.Serv return errors.New("serviceUser not found") } - *serviceUser = *su + *serviceUser = su.ServiceUser return nil } diff --git a/selectel/resource_selectel_iam_user_v1_test.go b/selectel/resource_selectel_iam_user_v1_test.go index e4058af1..e5f73592 100644 --- a/selectel/resource_selectel_iam_user_v1_test.go +++ b/selectel/resource_selectel_iam_user_v1_test.go @@ -123,7 +123,7 @@ func testAccCheckIAMV1UserExists(n string, user *users.User) resource.TestCheckF return errors.New("user not found") } - *user = *u + *user = u.User return nil } From 17f5fa39998c8499f44607fec78e1ecf1ca71251 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Wed, 10 Jul 2024 12:24:31 +0300 Subject: [PATCH 12/20] fix --- selectel/resource_selectel_iam_group_v1_test.go | 10 +++++++++- .../resource_selectel_iam_s3_credentials_v1_test.go | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/selectel/resource_selectel_iam_group_v1_test.go b/selectel/resource_selectel_iam_group_v1_test.go index 49ea6a67..a72abb1b 100644 --- a/selectel/resource_selectel_iam_group_v1_test.go +++ b/selectel/resource_selectel_iam_group_v1_test.go @@ -1,6 +1,14 @@ package selectel -import () +import ( + "context" + "errors" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/selectel/iam-go/service/groups" + "testing" +) func TestAccIAMV1GroupBasic(t *testing.T) { var group groups.Group diff --git a/selectel/resource_selectel_iam_s3_credentials_v1_test.go b/selectel/resource_selectel_iam_s3_credentials_v1_test.go index 64a864c4..a9be4275 100644 --- a/selectel/resource_selectel_iam_s3_credentials_v1_test.go +++ b/selectel/resource_selectel_iam_s3_credentials_v1_test.go @@ -13,7 +13,7 @@ import ( ) func TestAccIAMV1S3CredentialsBasic(t *testing.T) { - var s3credentials s3credentials.Credentials + var s3credential s3credentials.Credential s3CredsName := acctest.RandomWithPrefix("tf-acc") projectName := acctest.RandomWithPrefix("tf-acc") userName := acctest.RandomWithPrefix("tf-acc") From 6ac57bc0ddec0d43c85b6cd4a56f2014a280e22a Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Thu, 11 Jul 2024 11:15:17 +0300 Subject: [PATCH 13/20] fix linter --- .../resource_selectel_iam_group_membership_v1.go | 10 +++++++--- ...source_selectel_iam_group_membership_v1_test.go | 12 ++++++------ selectel/resource_selectel_iam_group_v1.go | 14 +++++++++++++- selectel/resource_selectel_iam_group_v1_test.go | 3 ++- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/selectel/resource_selectel_iam_group_membership_v1.go b/selectel/resource_selectel_iam_group_membership_v1.go index 920b5995..5d4ad57a 100644 --- a/selectel/resource_selectel_iam_group_membership_v1.go +++ b/selectel/resource_selectel_iam_group_membership_v1.go @@ -1,14 +1,16 @@ package selectel import ( + "context" "encoding/base64" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "log" "slices" "sort" "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceIAMGroupMembershipV1() *schema.Resource { @@ -160,6 +162,7 @@ func generateCompositeID(groupID string, userIDs []string) string { sort.Strings(userIDs) concatenated := groupID + ":" + strings.Join(userIDs, ",") encoded := base64.StdEncoding.EncodeToString([]byte(concatenated)) + return encoded } @@ -200,7 +203,7 @@ func diffUsers(oldUsers, newUsers []string) ([]string, []string) { return usersToAdd, usersToRemove } -// containsAll checks if sliceB is a subset of sliceA +// containsAll checks if sliceB is a subset of sliceA. func containsAll(sliceA, sliceB []string) bool { for _, b := range sliceB { found := false @@ -214,5 +217,6 @@ func containsAll(sliceA, sliceB []string) bool { return false } } + return true } diff --git a/selectel/resource_selectel_iam_group_membership_v1_test.go b/selectel/resource_selectel_iam_group_membership_v1_test.go index 8c98e79c..cc2b04a6 100644 --- a/selectel/resource_selectel_iam_group_membership_v1_test.go +++ b/selectel/resource_selectel_iam_group_membership_v1_test.go @@ -1,9 +1,9 @@ package selectel import ( - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) func TestAccIAMV1GroupMembershipBasic(t *testing.T) { @@ -59,7 +59,7 @@ func TestAccIAMV1GroupUpdate(t *testing.T) { } func testAccIAMV1GroupMembershipBasic() string { - return fmt.Sprintf(` + return ` resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_1" { name = "test-service-user-1" password = "Qazwsxedc123" @@ -93,11 +93,11 @@ resource "selectel_iam_group_membership_v1" "membership_tf_acc_test_1" { selectel_iam_serviceuser_v1.serviceuser_tf_acc_test_1.id ] } -`) +` } func testAccIAMV1GroupMembershipUpdate() string { - return fmt.Sprintf(` + return ` resource "selectel_iam_serviceuser_v1" "serviceuser_tf_acc_test_1" { name = "test-service-user-1" password = "Qazwsxedc123" @@ -132,5 +132,5 @@ resource "selectel_iam_group_membership_v1" "membership_tf_acc_test_1" { selectel_iam_serviceuser_v1.serviceuser_tf_acc_test_2.id ] } -`) +` } diff --git a/selectel/resource_selectel_iam_group_v1.go b/selectel/resource_selectel_iam_group_v1.go index a74e574f..7610d4f7 100644 --- a/selectel/resource_selectel_iam_group_v1.go +++ b/selectel/resource_selectel_iam_group_v1.go @@ -1,6 +1,18 @@ package selectel -import () +import ( + "context" + "errors" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/selectel/iam-go" + "github.com/selectel/iam-go/iamerrors" + "github.com/selectel/iam-go/service/groups" + "github.com/selectel/iam-go/service/roles" +) func resourceIAMGroupV1() *schema.Resource { return &schema.Resource{ diff --git a/selectel/resource_selectel_iam_group_v1_test.go b/selectel/resource_selectel_iam_group_v1_test.go index a72abb1b..d735a8fd 100644 --- a/selectel/resource_selectel_iam_group_v1_test.go +++ b/selectel/resource_selectel_iam_group_v1_test.go @@ -4,10 +4,11 @@ import ( "context" "errors" "fmt" + "testing" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/selectel/iam-go/service/groups" - "testing" ) func TestAccIAMV1GroupBasic(t *testing.T) { From e6abe4ac789aacfbb6c74192eec202a46340f3f5 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Thu, 11 Jul 2024 11:18:56 +0300 Subject: [PATCH 14/20] update go.mod --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 6370eb94..432de544 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/selectel/dbaas-go v0.12.1 github.com/selectel/domains-go v1.0.2 github.com/selectel/go-selvpcclient/v3 v3.1.1 - github.com/selectel/iam-go v0.2.0 + github.com/selectel/iam-go v0.3.0 github.com/selectel/mks-go v0.14.0 github.com/selectel/secretsmanager-go v0.2.1 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 3949d19d..e5c7160a 100644 --- a/go.sum +++ b/go.sum @@ -172,6 +172,8 @@ github.com/selectel/go-selvpcclient/v3 v3.1.1 h1:C1q2LqqosiapoLpnGITGmysg0YCSQYD github.com/selectel/go-selvpcclient/v3 v3.1.1/go.mod h1:NM7IXhh1IzqZ88DOw1Qc5Ez3tULLViXo95l5+rKPuyQ= github.com/selectel/iam-go v0.2.0 h1:c6ldpbsa/8R3b29ML5B21FU9oyJ2A2AwBNzCbE+pGN8= github.com/selectel/iam-go v0.2.0/go.mod h1:OIAkW7MZK97YUm+uvUgYbgDhkI9SdzTCxwd4yZoOR1o= +github.com/selectel/iam-go v0.3.0 h1:HRoxSBXwvASE9v/A4WgeEDeMvomARIWyj2essV4LmYc= +github.com/selectel/iam-go v0.3.0/go.mod h1:OIAkW7MZK97YUm+uvUgYbgDhkI9SdzTCxwd4yZoOR1o= github.com/selectel/mks-go v0.14.0 h1:huNq/oTutPc3ezB8HRqlGN9WJubTDETpNKuIVqcZOn0= github.com/selectel/mks-go v0.14.0/go.mod h1:VxtV3dzwgOEzZc+9VMQb9DvxfSlej2ZQ8jnT8kqIGgU= github.com/selectel/secretsmanager-go v0.2.1 h1:OSBrA/07lm/Ecpwg59IJHFAoUHZR29oyfwUgTpr/dos= From 9e954a2f1afaee189c90fd447bf032feb6504967 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Thu, 11 Jul 2024 13:18:36 +0300 Subject: [PATCH 15/20] add docs --- .../r/iam_group_membership_v1.html.markdown | 31 +++++++ website/docs/r/iam_group_v1.html.markdown | 80 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 website/docs/r/iam_group_membership_v1.html.markdown create mode 100644 website/docs/r/iam_group_v1.html.markdown diff --git a/website/docs/r/iam_group_membership_v1.html.markdown b/website/docs/r/iam_group_membership_v1.html.markdown new file mode 100644 index 00000000..8fba3ed4 --- /dev/null +++ b/website/docs/r/iam_group_membership_v1.html.markdown @@ -0,0 +1,31 @@ +--- +layout: "selectel" +page_title: "Selectel: selectel_iam_group_membership_v1" +sidebar_current: "docs-selectel-resource-iam-group_membership-v1" +description: |- + Creates and manages group membership for Selectel products using public API v1. +--- + +# selectel\_iam\_group_membership\_v1 + +Creates and manages group membership for Selectel products using public API v1. +Selectel products support Identity and Access Management (IAM). + +## Example Usage + +```hcl +resource "selectel_iam_group_membership_v1" "group_membership_1" { + group_id = selectel_iam_group_v1.group_1.id + + user_ids = [ + selectel_iam_user_v1.user_1.keystone_id, + selectel_iam_serviceuser_v1.serviceuser_1.id + ] +} +``` + +## Argument Reference + +* `group_id` - (Required) ID of the group. + +* `user_ids` - (Required) List of users Keystone IDs. diff --git a/website/docs/r/iam_group_v1.html.markdown b/website/docs/r/iam_group_v1.html.markdown new file mode 100644 index 00000000..db5d86a6 --- /dev/null +++ b/website/docs/r/iam_group_v1.html.markdown @@ -0,0 +1,80 @@ +--- +layout: "selectel" +page_title: "Selectel: selectel_iam_group_v1" +sidebar_current: "docs-selectel-resource-iam-group-v1" +description: |- + Creates and manages a user group for Selectel products using public API v1. +--- + +# selectel\_iam\_group\_v1 + +Creates and manages a user group for Selectel products using public API v1. + Selectel products support Identity and Access Management (IAM). + +## Example Usage + +```hcl +resource "selectel_iam_group_v1" "group_1" { + name = "My group" + description = "My test group" + role { + role_name = "member" + scope = "account" + } +} +``` + +## Argument Reference + +* `name` - (Required) Name of the group. + +* `description` - (Optional) Description of the group. + +* `role` - (Optional) Manages group roles. You can add multiple roles – each role in a separate block. For more information about roles, see the [Roles](#roles) section. + + * `role_name` - (Required) Role name. Available role names are `iam_admin`, `member`, `reader`, and `billing`. + + * `scope` - (Required) Scope of the role. Available scopes are `account` and `project`. If `scope` is `project`, the `project_id` argument is required. + + * `project_id` - (Optional) Unique identifier of the associated project. If `scope` is `project`, the `project_id` argument is required. Retrieved from the [selectel_vpc_project_v2](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/vpc_project_v2) resource. Learn more about [Projects](https://docs.selectel.ru/en/control-panel-actions/projects/about-projects/). + +### Roles + +To assign roles, use the following values for `scope` and `role_name`: + +* Account administrator - `scope` is `account`, `role_name` is `member`. + +* Billing administrator - `scope` is `account`, `role_name` is `billing`. + +* User administrator - `scope` is `account`, `role_name` is `iam_admin`. + +* Project administrator - `scope` is `project`, `role_name` is `member`. + +* Account viewer - `scope` is `account`, `role_name` is `reader`. + +* Project viewer - `scope` is `project`, `role_name` is `reader`. + +* Object storage admin - `scope` is `project`, `role_name` is `object_storage:admin`. + +* Object storage user - `scope` is `project`, `role_name` is `object_storage_user`. + +## Import + +You can import a group: + +```shell +export OS_DOMAIN_NAME= +export OS_USERNAME= +export OS_PASSWORD= +terraform import selectel_iam_group_v1.group_1 +``` + +where: + +* `` — Selectel account ID. The account ID is in the top right corner of the [Control panel](https://my.selectel.ru/). Learn more about [Registration](https://docs.selectel.ru/en/control-panel-actions/account/registration/). + +* `` — Name of the service user. To get the name, in the [Control panel](https://my.selectel.ru/iam/users_management/users?type=service), go to **Identity & Access Management** ⟶ **User management** ⟶ the **Service users** tab ⟶ copy the name of the required user. Learn more about [Service Users](https://docs.selectel.ru/en/control-panel-actions/users-and-roles/user-types-and-roles/). + +* `` — Password of the service user. + +* `` — Unique identifier of the group to import, for example, `abc1bb378ac84e1234b869b77aadd2ab`. To get the ID, use either [iam-go](https://github.com/selectel/iam-go) or [IAM API](https://developers.selectel.ru/docs/control-panel/iam/). From 5c10df26233e636356af0bfba51e2fa97a6bc562 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Mon, 15 Jul 2024 16:52:11 +0300 Subject: [PATCH 16/20] tidy --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index e5c7160a..d2989509 100644 --- a/go.sum +++ b/go.sum @@ -170,8 +170,6 @@ github.com/selectel/domains-go v1.0.2 h1:Si6iGaMnTFJxwiJVI50DOdZnwcxc87kqaWrVQYW github.com/selectel/domains-go v1.0.2/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= github.com/selectel/go-selvpcclient/v3 v3.1.1 h1:C1q2LqqosiapoLpnGITGmysg0YCSQYDo2Gh69CioevM= github.com/selectel/go-selvpcclient/v3 v3.1.1/go.mod h1:NM7IXhh1IzqZ88DOw1Qc5Ez3tULLViXo95l5+rKPuyQ= -github.com/selectel/iam-go v0.2.0 h1:c6ldpbsa/8R3b29ML5B21FU9oyJ2A2AwBNzCbE+pGN8= -github.com/selectel/iam-go v0.2.0/go.mod h1:OIAkW7MZK97YUm+uvUgYbgDhkI9SdzTCxwd4yZoOR1o= github.com/selectel/iam-go v0.3.0 h1:HRoxSBXwvASE9v/A4WgeEDeMvomARIWyj2essV4LmYc= github.com/selectel/iam-go v0.3.0/go.mod h1:OIAkW7MZK97YUm+uvUgYbgDhkI9SdzTCxwd4yZoOR1o= github.com/selectel/mks-go v0.14.0 h1:huNq/oTutPc3ezB8HRqlGN9WJubTDETpNKuIVqcZOn0= From 5659f9f825cf677f0c8c35f5eb6b3913507dce58 Mon Sep 17 00:00:00 2001 From: Maksim Kuznetsov Date: Mon, 22 Jul 2024 13:42:30 +0300 Subject: [PATCH 17/20] docs update --- website/docs/r/iam_group_membership_v1.html.markdown | 7 ++++--- website/docs/r/iam_group_v1.html.markdown | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/website/docs/r/iam_group_membership_v1.html.markdown b/website/docs/r/iam_group_membership_v1.html.markdown index 8fba3ed4..a787b0c1 100644 --- a/website/docs/r/iam_group_membership_v1.html.markdown +++ b/website/docs/r/iam_group_membership_v1.html.markdown @@ -8,8 +8,9 @@ description: |- # selectel\_iam\_group_membership\_v1 -Creates and manages group membership for Selectel products using public API v1. +Manages group membership for Selectel products using public API v1. Selectel products support Identity and Access Management (IAM). +For more information about groups, see the [official Selectel documentation](https://docs.selectel.ru/control-panel-actions/users-and-roles/groups/). ## Example Usage @@ -26,6 +27,6 @@ resource "selectel_iam_group_membership_v1" "group_membership_1" { ## Argument Reference -* `group_id` - (Required) ID of the group. +* `group_id` - (Required) Unique identifier of the group. Retrieved from the [selectel_iam_group_v1](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/iam_group_v1) resource. -* `user_ids` - (Required) List of users Keystone IDs. +* `user_ids` - (Required) List of unique Keystone identifiers of users. Retrieved from the [selectel_iam_serviceuser_v1](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/iam_serviceuser_v1) and [selectel_iam_user_v1](https://registry.terraform.io/providers/selectel/selectel/latest/docs/resources/iam_user_v1) resources. diff --git a/website/docs/r/iam_group_v1.html.markdown b/website/docs/r/iam_group_v1.html.markdown index db5d86a6..db93c275 100644 --- a/website/docs/r/iam_group_v1.html.markdown +++ b/website/docs/r/iam_group_v1.html.markdown @@ -9,7 +9,8 @@ description: |- # selectel\_iam\_group\_v1 Creates and manages a user group for Selectel products using public API v1. - Selectel products support Identity and Access Management (IAM). +Selectel products support Identity and Access Management (IAM). +For more information about user groups, see the [official Selectel documentation](https://docs.selectel.ru/control-panel-actions/users-and-roles/groups/). ## Example Usage @@ -26,13 +27,13 @@ resource "selectel_iam_group_v1" "group_1" { ## Argument Reference -* `name` - (Required) Name of the group. +* `name` - (Required) Group name. -* `description` - (Optional) Description of the group. +* `description` - (Optional) Group description. * `role` - (Optional) Manages group roles. You can add multiple roles – each role in a separate block. For more information about roles, see the [Roles](#roles) section. - * `role_name` - (Required) Role name. Available role names are `iam_admin`, `member`, `reader`, and `billing`. + * `role_name` - (Required) Role name. Available role names are `iam_admin`, `member`, `reader`, `billing`, `object_storage:admin`, and `object_storage_user`. * `scope` - (Required) Scope of the role. Available scopes are `account` and `project`. If `scope` is `project`, the `project_id` argument is required. @@ -77,4 +78,4 @@ where: * `` — Password of the service user. -* `` — Unique identifier of the group to import, for example, `abc1bb378ac84e1234b869b77aadd2ab`. To get the ID, use either [iam-go](https://github.com/selectel/iam-go) or [IAM API](https://developers.selectel.ru/docs/control-panel/iam/). +* `` — Unique identifier of the group, for example, `abc1bb378ac84e1234b869b77aadd2ab`. To get the group ID, use either [iam-go](https://github.com/selectel/iam-go) or [IAM API](https://developers.selectel.ru/docs/control-panel/iam/). From 34d0f6d97da208774fda84ce28410be2069867b8 Mon Sep 17 00:00:00 2001 From: Icerzack Date: Wed, 31 Jul 2024 15:50:16 +0300 Subject: [PATCH 18/20] upd --- ...source_selectel_iam_group_membership_v1.go | 73 ++++++------------- website/docs/r/iam_group_v1.html.markdown | 6 +- 2 files changed, 25 insertions(+), 54 deletions(-) diff --git a/selectel/resource_selectel_iam_group_membership_v1.go b/selectel/resource_selectel_iam_group_membership_v1.go index 5d4ad57a..32a91deb 100644 --- a/selectel/resource_selectel_iam_group_membership_v1.go +++ b/selectel/resource_selectel_iam_group_membership_v1.go @@ -2,12 +2,9 @@ package selectel import ( "context" - "encoding/base64" "fmt" "log" "slices" - "sort" - "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -57,7 +54,7 @@ func resourceIAMGroupMembershipV1Create(ctx context.Context, d *schema.ResourceD return diag.FromErr(errCreatingObject(objectGroupMembership, err)) } - d.SetId(generateCompositeID(d.Get("group_id").(string), userIDs)) + d.SetId(d.Get("group_id").(string)) return resourceIAMGroupMembershipV1Read(ctx, d, meta) } @@ -68,9 +65,12 @@ func resourceIAMGroupMembershipV1Read(ctx context.Context, d *schema.ResourceDat return diagErr } - groupID, userIDs, err := parseCompositeID(d.Id()) - if err != nil { - return diag.FromErr(errGettingObject(objectGroupMembership, d.Id(), err)) + groupID := d.Id() + + userIDsInterface := d.Get("user_ids").([]interface{}) + userIDs := make([]string, len(userIDsInterface)) + for i, v := range userIDsInterface { + userIDs[i] = v.(string) } response, err := iamClient.Groups.Get(ctx, groupID) @@ -88,11 +88,6 @@ func resourceIAMGroupMembershipV1Read(ctx context.Context, d *schema.ResourceDat responseServiceUserIDs = append(responseServiceUserIDs, serviceUser.ID) } - if !containsAll(userIDs, responseUserIDs) || !containsAll(userIDs, responseServiceUserIDs) { - readErr := fmt.Errorf("error validating group memberships: Group %s does not contain all users %v", groupID, userIDs) - return diag.FromErr(errGettingObject(objectGroupMembership, d.Id(), readErr)) - } - d.Set("group_id", groupID) d.Set("user_ids", append(responseUserIDs, responseServiceUserIDs...)) @@ -105,14 +100,17 @@ func resourceIAMGroupMembershipV1Update(ctx context.Context, d *schema.ResourceD return diagErr } - groupID, oldUserIDs, err := parseCompositeID(d.Id()) - if err != nil { - return diag.FromErr(errUpdatingObject(objectGroupMembership, d.Id(), err)) + groupID := d.Id() + + oldValue, newValue := d.GetChange("user_ids") + + oldUserIDs := make([]string, len(oldValue.([]interface{}))) + for i, v := range oldValue.([]interface{}) { + oldUserIDs[i] = v.(string) } - newUserIDsInterface := d.Get("user_ids").([]interface{}) - newUserIDs := make([]string, len(newUserIDsInterface)) - for i, v := range newUserIDsInterface { + newUserIDs := make([]string, len(newValue.([]interface{}))) + for i, v := range newValue.([]interface{}) { newUserIDs[i] = v.(string) } @@ -132,7 +130,7 @@ func resourceIAMGroupMembershipV1Update(ctx context.Context, d *schema.ResourceD } } - d.SetId(generateCompositeID(groupID, newUserIDs)) + d.SetId(groupID) return resourceIAMGroupMembershipV1Read(ctx, d, meta) } @@ -143,12 +141,15 @@ func resourceIAMGroupMembershipV1Delete(ctx context.Context, d *schema.ResourceD return diagErr } - groupID, userIDs, err := parseCompositeID(d.Id()) - if err != nil { - return diag.FromErr(errDeletingObject(objectGroupMembership, d.Id(), err)) + groupID := d.Id() + + userIDsInterface := d.Get("user_ids").([]interface{}) + userIDs := make([]string, len(userIDsInterface)) + for i, v := range userIDsInterface { + userIDs[i] = v.(string) } - err = iamClient.Groups.DeleteUsers(ctx, groupID, userIDs) + err := iamClient.Groups.DeleteUsers(ctx, groupID, userIDs) if err != nil { return diag.FromErr(errDeletingObject(objectGroupMembership, d.Id(), err)) } @@ -158,32 +159,6 @@ func resourceIAMGroupMembershipV1Delete(ctx context.Context, d *schema.ResourceD return nil } -func generateCompositeID(groupID string, userIDs []string) string { - sort.Strings(userIDs) - concatenated := groupID + ":" + strings.Join(userIDs, ",") - encoded := base64.StdEncoding.EncodeToString([]byte(concatenated)) - - return encoded -} - -func parseCompositeID(compositeID string) (string, []string, error) { - decodedBytes, err := base64.StdEncoding.DecodeString(compositeID) - if err != nil { - return "", nil, fmt.Errorf("error decoding composite ID: %s, %v", compositeID, err) - } - decodedString := string(decodedBytes) - - parts := strings.Split(decodedString, ":") - if len(parts) != 2 { - return "", nil, fmt.Errorf("invalid decoded composite ID: %s", decodedString) - } - - groupID := parts[0] - userIDs := strings.Split(parts[1], ",") - - return groupID, userIDs, nil -} - func diffUsers(oldUsers, newUsers []string) ([]string, []string) { usersToAdd := make([]string, 0) usersToRemove := make([]string, 0) diff --git a/website/docs/r/iam_group_v1.html.markdown b/website/docs/r/iam_group_v1.html.markdown index db93c275..0c6e5b35 100644 --- a/website/docs/r/iam_group_v1.html.markdown +++ b/website/docs/r/iam_group_v1.html.markdown @@ -33,7 +33,7 @@ resource "selectel_iam_group_v1" "group_1" { * `role` - (Optional) Manages group roles. You can add multiple roles – each role in a separate block. For more information about roles, see the [Roles](#roles) section. - * `role_name` - (Required) Role name. Available role names are `iam_admin`, `member`, `reader`, `billing`, `object_storage:admin`, and `object_storage_user`. + * `role_name` - (Required) Role name. Available role names are `iam_admin`, `member`, `reader`, and `billing`. * `scope` - (Required) Scope of the role. Available scopes are `account` and `project`. If `scope` is `project`, the `project_id` argument is required. @@ -55,10 +55,6 @@ To assign roles, use the following values for `scope` and `role_name`: * Project viewer - `scope` is `project`, `role_name` is `reader`. -* Object storage admin - `scope` is `project`, `role_name` is `object_storage:admin`. - -* Object storage user - `scope` is `project`, `role_name` is `object_storage_user`. - ## Import You can import a group: From 074fb570c2e7082bd4a8f8fb108f86e41084eb40 Mon Sep 17 00:00:00 2001 From: Icerzack Date: Wed, 31 Jul 2024 15:54:46 +0300 Subject: [PATCH 19/20] lint fix --- ...esource_selectel_iam_group_membership_v1.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/selectel/resource_selectel_iam_group_membership_v1.go b/selectel/resource_selectel_iam_group_membership_v1.go index 32a91deb..a2ccafab 100644 --- a/selectel/resource_selectel_iam_group_membership_v1.go +++ b/selectel/resource_selectel_iam_group_membership_v1.go @@ -177,21 +177,3 @@ func diffUsers(oldUsers, newUsers []string) ([]string, []string) { return usersToAdd, usersToRemove } - -// containsAll checks if sliceB is a subset of sliceA. -func containsAll(sliceA, sliceB []string) bool { - for _, b := range sliceB { - found := false - for _, a := range sliceA { - if a == b { - found = true - break - } - } - if !found { - return false - } - } - - return true -} From 3b89ce4e25a4efee215599b761d26bc1286252e2 Mon Sep 17 00:00:00 2001 From: Icerzack Date: Wed, 31 Jul 2024 18:04:24 +0300 Subject: [PATCH 20/20] fix? --- ...source_selectel_iam_group_membership_v1.go | 29 +++++++++---------- selectel/resource_selectel_iam_group_v1.go | 15 ++++------ 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/selectel/resource_selectel_iam_group_membership_v1.go b/selectel/resource_selectel_iam_group_membership_v1.go index a2ccafab..59cab131 100644 --- a/selectel/resource_selectel_iam_group_membership_v1.go +++ b/selectel/resource_selectel_iam_group_membership_v1.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "slices" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -43,8 +42,8 @@ func resourceIAMGroupMembershipV1Create(ctx context.Context, d *schema.ResourceD for i, v := range userIDsInterface { userIDs[i] = v.(string) } + log.Print(msgCreate(objectGroupMembership, userIDs)) - log.Print(msgCreate(objectGroupMembership, d.Id())) if len(userIDs) == 0 { createErr := fmt.Errorf("error creating group membership: no user ids specified") return diag.FromErr(errCreatingObject(objectGroupMembership, createErr)) @@ -104,14 +103,14 @@ func resourceIAMGroupMembershipV1Update(ctx context.Context, d *schema.ResourceD oldValue, newValue := d.GetChange("user_ids") - oldUserIDs := make([]string, len(oldValue.([]interface{}))) - for i, v := range oldValue.([]interface{}) { - oldUserIDs[i] = v.(string) + oldUserIDs := make(map[string]struct{}) + for _, v := range oldValue.([]interface{}) { + oldUserIDs[v.(string)] = struct{}{} } - newUserIDs := make([]string, len(newValue.([]interface{}))) - for i, v := range newValue.([]interface{}) { - newUserIDs[i] = v.(string) + newUserIDs := make(map[string]struct{}) + for _, v := range newValue.([]interface{}) { + newUserIDs[v.(string)] = struct{}{} } usersToAdd, usersToRemove := diffUsers(oldUserIDs, newUserIDs) @@ -159,19 +158,19 @@ func resourceIAMGroupMembershipV1Delete(ctx context.Context, d *schema.ResourceD return nil } -func diffUsers(oldUsers, newUsers []string) ([]string, []string) { +func diffUsers(oldUsers, newUsers map[string]struct{}) ([]string, []string) { usersToAdd := make([]string, 0) usersToRemove := make([]string, 0) - for _, user := range newUsers { - if !slices.Contains(oldUsers, user) { - usersToAdd = append(usersToAdd, user) + for id := range newUsers { + if _, ok := oldUsers[id]; !ok { + usersToAdd = append(usersToAdd, id) } } - for _, user := range oldUsers { - if !slices.Contains(newUsers, user) { - usersToRemove = append(usersToRemove, user) + for id := range oldUsers { + if _, ok := newUsers[id]; !ok { + usersToRemove = append(usersToRemove, id) } } diff --git a/selectel/resource_selectel_iam_group_v1.go b/selectel/resource_selectel_iam_group_v1.go index 7610d4f7..656ca117 100644 --- a/selectel/resource_selectel_iam_group_v1.go +++ b/selectel/resource_selectel_iam_group_v1.go @@ -71,11 +71,13 @@ func resourceIAMGroupV1Create(ctx context.Context, d *schema.ResourceData, meta return diag.FromErr(err) } - log.Print(msgCreate(objectGroup, d.Id())) - group, err := iamClient.Groups.Create(ctx, groups.CreateRequest{ + opts := groups.CreateRequest{ Name: d.Get("name").(string), Description: d.Get("description").(string), - }) + } + log.Print(msgCreate(objectGroup, opts)) + + group, err := iamClient.Groups.Create(ctx, opts) if err != nil { return diag.FromErr(errCreatingObject(objectGroup, err)) } @@ -103,12 +105,7 @@ func resourceIAMGroupV1Read(ctx context.Context, d *schema.ResourceData, meta in return diag.FromErr(errGettingObject(objectGroup, d.Id(), err)) } - if group.Group.Roles != nil && len(group.Group.Roles) != 0 { - err = d.Set("role", convertIAMRolesToSet(group.Roles)) - if err != nil { - return nil - } - } + d.Set("role", convertIAMRolesToSet(group.Roles)) return nil }