diff --git a/docs/resources/authorization_folder_role_assignment.md b/docs/resources/authorization_folder_role_assignment.md new file mode 100644 index 000000000..9b5707efd --- /dev/null +++ b/docs/resources/authorization_folder_role_assignment.md @@ -0,0 +1,50 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_authorization_folder_role_assignment Resource - stackit" +subcategory: "" +description: |- + Folder Role Assignment resource schema. + ~> This resource is part of the iam experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. +--- + +# stackit_authorization_folder_role_assignment (Resource) + +Folder Role Assignment resource schema. + +~> This resource is part of the iam experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. + +## Example Usage + +```terraform +resource "stackit_resourcemanager_folder" "example" { + name = "example_folder" + owner_email = "foo.bar@stackit.cloud" + # in this case a org-id + parent_container_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_authorization_folder_role_assignment" "fra" { + resource_id = stackit_resourcemanager_folder.example.folder_id + role = "reader" + subject = "foo.bar@stackit.cloud" +} + +# Only use the import statement, if you want to import an existing folder role assignment +import { + to = stackit_authorization_folder_role_assignment.import-example + id = "${var.folder_id},${var.folder_role_assignment},${var.folder_role_assignment_subject}" +} +``` + + +## Schema + +### Required + +- `resource_id` (String) folder Resource to assign the role to. +- `role` (String) Role to be assigned. Available roles can be queried using stackit-cli: `stackit curl https://authorization.api.stackit.cloud/v2/permissions` +- `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients + +### Read-Only + +- `id` (String) Terraform's internal resource identifier. It is structured as "`resource_id`,`role`,`subject`". diff --git a/docs/resources/authorization_organization_role_assignment.md b/docs/resources/authorization_organization_role_assignment.md index 3d8e0a273..cf965f7ab 100644 --- a/docs/resources/authorization_organization_role_assignment.md +++ b/docs/resources/authorization_organization_role_assignment.md @@ -3,13 +3,13 @@ page_title: "stackit_authorization_organization_role_assignment Resource - stackit" subcategory: "" description: |- - organization Role Assignment resource schema. + Organization Role Assignment resource schema. ~> This resource is part of the iam experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. --- # stackit_authorization_organization_role_assignment (Resource) -organization Role Assignment resource schema. +Organization Role Assignment resource schema. ~> This resource is part of the iam experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. @@ -35,9 +35,9 @@ import { ### Required - `resource_id` (String) organization Resource to assign the role to. -- `role` (String) Role to be assigned +- `role` (String) Role to be assigned. Available roles can be queried using stackit-cli: `stackit curl https://authorization.api.stackit.cloud/v2/permissions` - `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients ### Read-Only -- `id` (String) Terraform's internal resource identifier. It is structured as "[resource_id],[role],[subject]". +- `id` (String) Terraform's internal resource identifier. It is structured as "`resource_id`,`role`,`subject`". diff --git a/docs/resources/authorization_project_role_assignment.md b/docs/resources/authorization_project_role_assignment.md index 141644219..9317957ed 100644 --- a/docs/resources/authorization_project_role_assignment.md +++ b/docs/resources/authorization_project_role_assignment.md @@ -3,23 +3,30 @@ page_title: "stackit_authorization_project_role_assignment Resource - stackit" subcategory: "" description: |- - project Role Assignment resource schema. + Project Role Assignment resource schema. ~> This resource is part of the iam experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. --- # stackit_authorization_project_role_assignment (Resource) -project Role Assignment resource schema. +Project Role Assignment resource schema. ~> This resource is part of the iam experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. ## Example Usage ```terraform -resource "stackit_authorization_project_role_assignment" "example" { - resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - role = "owner" - subject = "john.doe@stackit.cloud" +resource "stackit_resourcemanager_project" "example" { + name = "example_project" + owner_email = "foo.bar@stackit.cloud" + # in this case a folder or a org-id + parent_container_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_authorization_project_role_assignment" "pra" { + resource_id = stackit_resourcemanager_project.example.folder_id + role = "reader" + subject = "foo.bar@stackit.cloud" } # Only use the import statement, if you want to import an existing project role assignment @@ -35,9 +42,9 @@ import { ### Required - `resource_id` (String) project Resource to assign the role to. -- `role` (String) Role to be assigned +- `role` (String) Role to be assigned. Available roles can be queried using stackit-cli: `stackit curl https://authorization.api.stackit.cloud/v2/permissions` - `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients ### Read-Only -- `id` (String) Terraform's internal resource identifier. It is structured as "[resource_id],[role],[subject]". +- `id` (String) Terraform's internal resource identifier. It is structured as "`resource_id`,`role`,`subject`". diff --git a/examples/resources/stackit_authorization_folder_role_assignment/resource.tf b/examples/resources/stackit_authorization_folder_role_assignment/resource.tf new file mode 100644 index 000000000..27263a476 --- /dev/null +++ b/examples/resources/stackit_authorization_folder_role_assignment/resource.tf @@ -0,0 +1,18 @@ +resource "stackit_resourcemanager_folder" "example" { + name = "example_folder" + owner_email = "foo.bar@stackit.cloud" + # in this case a org-id + parent_container_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_authorization_folder_role_assignment" "fra" { + resource_id = stackit_resourcemanager_folder.example.folder_id + role = "reader" + subject = "foo.bar@stackit.cloud" +} + +# Only use the import statement, if you want to import an existing folder role assignment +import { + to = stackit_authorization_folder_role_assignment.import-example + id = "${var.folder_id},${var.folder_role_assignment},${var.folder_role_assignment_subject}" +} diff --git a/examples/resources/stackit_authorization_project_role_assignment/resource.tf b/examples/resources/stackit_authorization_project_role_assignment/resource.tf index a335c5fde..4c286e626 100644 --- a/examples/resources/stackit_authorization_project_role_assignment/resource.tf +++ b/examples/resources/stackit_authorization_project_role_assignment/resource.tf @@ -1,7 +1,14 @@ -resource "stackit_authorization_project_role_assignment" "example" { - resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - role = "owner" - subject = "john.doe@stackit.cloud" +resource "stackit_resourcemanager_project" "example" { + name = "example_project" + owner_email = "foo.bar@stackit.cloud" + # in this case a folder or a org-id + parent_container_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_authorization_project_role_assignment" "pra" { + resource_id = stackit_resourcemanager_project.example.folder_id + role = "reader" + subject = "foo.bar@stackit.cloud" } # Only use the import statement, if you want to import an existing project role assignment diff --git a/stackit/internal/services/authorization/authorization_acc_test.go b/stackit/internal/services/authorization/authorization_acc_test.go index 7fcede14d..4ba5cf486 100644 --- a/stackit/internal/services/authorization/authorization_acc_test.go +++ b/stackit/internal/services/authorization/authorization_acc_test.go @@ -2,113 +2,452 @@ package authorization_test import ( "context" - "errors" "fmt" - "regexp" - "slices" + "maps" + "strings" "testing" _ "embed" "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -//go:embed testfiles/prerequisites.tf -var prerequisites string +var ( + //go:embed testdata/resource-project-role-assignment.tf + resourceProjectRoleAssignment string -//go:embed testfiles/double-definition.tf -var doubleDefinition string + //go:embed testdata/resource-folder-role-assignment.tf + resourceFolderRoleAssignment string -//go:embed testfiles/project-owner.tf -var projectOwner string + //go:embed testdata/resource-org-role-assignment.tf + resourceOrgRoleAssignment string +) + +var testProjectName = fmt.Sprintf("proj-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) +var testFolderName = fmt.Sprintf("folder-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) + +var testConfigVarsProjectRoleAssignment = config.Variables{ + "name": config.StringVariable(testProjectName), + "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), + "parent_container_id": config.StringVariable(testutil.OrganizationId), + "role": config.StringVariable("reader"), + "subject": config.StringVariable(testutil.TestProjectServiceAccountEmail), +} -//go:embed testfiles/invalid-role.tf -var invalidRole string +var testConfigVarsFolderRoleAssignment = config.Variables{ + "name": config.StringVariable(testFolderName), + "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), + "parent_container_id": config.StringVariable(testutil.OrganizationId), + "role": config.StringVariable("reader"), + "subject": config.StringVariable(testutil.TestProjectServiceAccountEmail), +} + +var testConfigVarsOrgRoleAssignment = config.Variables{ + "parent_container_id": config.StringVariable(testutil.OrganizationId), + "role": config.StringVariable("iaas.admin"), + "subject": config.StringVariable(testutil.TestProjectServiceAccountEmail), +} + +func testConfigVarsProjectRoleAssignmentUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigVarsProjectRoleAssignment)) + maps.Copy(tempConfig, testConfigVarsProjectRoleAssignment) + + tempConfig["role"] = config.StringVariable("editor") + return tempConfig +} + +func testConfigVarsFolderRoleAssignmentUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigVarsFolderRoleAssignment)) + maps.Copy(tempConfig, testConfigVarsFolderRoleAssignment) + + tempConfig["role"] = config.StringVariable("editor") + return tempConfig +} -//go:embed testfiles/organization-role.tf -var organizationRole string +func testConfigVarsOrgRoleAssignmentUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigVarsOrgRoleAssignment)) + maps.Copy(tempConfig, testConfigVarsOrgRoleAssignment) -var testConfigVars = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "test_service_account": config.StringVariable(testutil.TestProjectServiceAccountEmail), - "organization_id": config.StringVariable(testutil.OrganizationId), + tempConfig["role"] = config.StringVariable("iaas.project.admin") + return tempConfig } func TestAccProjectRoleAssignmentResource(t *testing.T) { - t.Log(testutil.AuthorizationProviderConfig()) + t.Log("Testing project role assignment resource") resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + // deleting project will also delete project role assignments + CheckDestroy: testAccCheckResourceManagerProjectsDestroy, Steps: []resource.TestStep{ + // Creation { - ConfigVariables: testConfigVars, - Config: testutil.AuthorizationProviderConfig() + prerequisites, - Check: func(_ *terraform.State) error { - client, err := authApiClient() - if err != nil { - return err + ConfigVariables: testConfigVarsProjectRoleAssignment, + Config: testutil.AuthorizationProviderConfig() + "\n" + resourceProjectRoleAssignment, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_resourcemanager_project.project", "name", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignment["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.project", "owner_email", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignment["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.project", "parent_container_id", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignment["parent_container_id"])), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.project", "project_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.project", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.project", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.project", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.project", "update_time"), + + resource.TestCheckResourceAttrSet("stackit_authorization_project_role_assignment.pra", "resource_id"), + resource.TestCheckResourceAttrSet("stackit_authorization_project_role_assignment.pra", "id"), + resource.TestCheckResourceAttr("stackit_authorization_project_role_assignment.pra", "role", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignment["role"])), + resource.TestCheckResourceAttr("stackit_authorization_project_role_assignment.pra", "subject", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignment["subject"])), + ), + }, + // Import + { + ConfigVariables: testConfigVarsProjectRoleAssignment, + ResourceName: "stackit_authorization_project_role_assignment.pra", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_authorization_project_role_assignment.pra"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_authorization_project_role_assignment.pra") + } + resourceId, ok := r.Primary.Attributes["resource_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute resource_id") + } + role, ok := r.Primary.Attributes["role"] + if !ok { + return "", fmt.Errorf("couldn't find attribute role") + } + subject, ok := r.Primary.Attributes["subject"] + if !ok { + return "", fmt.Errorf("couldn't find attribute subject") } - members, err := client.ListMembers(context.TODO(), "project", testutil.ProjectId).Execute() + return fmt.Sprintf("%s,%s,%s", resourceId, role, subject), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigVarsProjectRoleAssignmentUpdated(), + Config: testutil.AuthorizationProviderConfig() + "\n" + resourceProjectRoleAssignment, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_resourcemanager_project.project", "name", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignmentUpdated()["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.project", "owner_email", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignmentUpdated()["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_project.project", "parent_container_id", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignmentUpdated()["parent_container_id"])), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.project", "project_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.project", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.project", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.project", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.project", "update_time"), - if err != nil { - return err - } + resource.TestCheckResourceAttrSet("stackit_authorization_project_role_assignment.pra", "resource_id"), + resource.TestCheckResourceAttrSet("stackit_authorization_project_role_assignment.pra", "id"), + resource.TestCheckResourceAttr("stackit_authorization_project_role_assignment.pra", "role", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignmentUpdated()["role"])), + resource.TestCheckResourceAttr("stackit_authorization_project_role_assignment.pra", "subject", testutil.ConvertConfigVariable(testConfigVarsProjectRoleAssignmentUpdated()["subject"])), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccFolderRoleAssignmentResource(t *testing.T) { + t.Log("Testing folder role assignment resource") + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + // deleting folder will also delete project role assignments + CheckDestroy: testAccCheckResourceManagerFoldersDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigVarsFolderRoleAssignment, + Config: testutil.AuthorizationProviderConfig() + "\n" + resourceFolderRoleAssignment, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.folder", "name", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignment["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.folder", "owner_email", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignment["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.folder", "parent_container_id", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignment["parent_container_id"])), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.folder", "folder_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.folder", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.folder", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.folder", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.folder", "update_time"), - if !slices.ContainsFunc(*members.Members, func(m authorization.Member) bool { - return *m.Role == "reader" && *m.Subject == testutil.TestProjectServiceAccountEmail - }) { - t.Log(members.Members) - return errors.New("Membership not found") + resource.TestCheckResourceAttrSet("stackit_authorization_folder_role_assignment.fra", "resource_id"), + resource.TestCheckResourceAttrSet("stackit_authorization_folder_role_assignment.fra", "id"), + resource.TestCheckResourceAttr("stackit_authorization_folder_role_assignment.fra", "role", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignment["role"])), + resource.TestCheckResourceAttr("stackit_authorization_folder_role_assignment.fra", "subject", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignment["subject"])), + ), + }, + // Import + { + ConfigVariables: testConfigVarsProjectRoleAssignment, + ResourceName: "stackit_authorization_folder_role_assignment.fra", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_authorization_folder_role_assignment.fra"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_authorization_folder_role_assignment.fra") + } + resourceId, ok := r.Primary.Attributes["resource_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute resource_id") } - return nil + role, ok := r.Primary.Attributes["role"] + if !ok { + return "", fmt.Errorf("couldn't find attribute role") + } + subject, ok := r.Primary.Attributes["subject"] + if !ok { + return "", fmt.Errorf("couldn't find attribute subject") + } + + return fmt.Sprintf("%s,%s,%s", resourceId, role, subject), nil }, + ImportState: true, + ImportStateVerify: true, }, + // Update { - // Assign a resource to an organization - ConfigVariables: testConfigVars, - Config: testutil.AuthorizationProviderConfig() + prerequisites + organizationRole, + ConfigVariables: testConfigVarsFolderRoleAssignmentUpdated(), + Config: testutil.AuthorizationProviderConfig() + "\n" + resourceFolderRoleAssignment, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.folder", "name", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignmentUpdated()["name"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.folder", "owner_email", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignmentUpdated()["owner_email"])), + resource.TestCheckResourceAttr("stackit_resourcemanager_folder.folder", "parent_container_id", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignmentUpdated()["parent_container_id"])), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.folder", "folder_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.folder", "container_id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.folder", "id"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.folder", "creation_time"), + resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.folder", "update_time"), + + resource.TestCheckResourceAttrSet("stackit_authorization_folder_role_assignment.fra", "resource_id"), + resource.TestCheckResourceAttrSet("stackit_authorization_folder_role_assignment.fra", "id"), + resource.TestCheckResourceAttr("stackit_authorization_folder_role_assignment.fra", "role", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignmentUpdated()["role"])), + resource.TestCheckResourceAttr("stackit_authorization_folder_role_assignment.fra", "subject", testutil.ConvertConfigVariable(testConfigVarsFolderRoleAssignmentUpdated()["subject"])), + ), }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccOrgRoleAssignmentResource(t *testing.T) { + t.Log("Testing org role assignment resource") + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + // only deleting the role assignment of org level + CheckDestroy: testAccCheckOrganizationRoleAssignmentDestroy, + Steps: []resource.TestStep{ + // Creation { - // The Service Account inherits owner permissions for the project from the organization. Check if you can still assign owner permissions on the project explicitly - ConfigVariables: testConfigVars, - Config: testutil.AuthorizationProviderConfig() + prerequisites + organizationRole + projectOwner, + ConfigVariables: testConfigVarsOrgRoleAssignment, + Config: testutil.AuthorizationProviderConfig() + "\n" + resourceOrgRoleAssignment, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_authorization_organization_role_assignment.ora", "resource_id"), + resource.TestCheckResourceAttrSet("stackit_authorization_organization_role_assignment.ora", "id"), + resource.TestCheckResourceAttr("stackit_authorization_organization_role_assignment.ora", "role", testutil.ConvertConfigVariable(testConfigVarsOrgRoleAssignment["role"])), + resource.TestCheckResourceAttr("stackit_authorization_organization_role_assignment.ora", "subject", testutil.ConvertConfigVariable(testConfigVarsOrgRoleAssignment["subject"])), + ), }, + // Import { - // Expect failure on creating an already existing role_assignment - // Would be bad, since two resources could be created and deletion of one would lead to state drift for the second TF resource - ConfigVariables: testConfigVars, - Config: testutil.AuthorizationProviderConfig() + prerequisites + doubleDefinition, - ExpectError: regexp.MustCompile(".+"), + ConfigVariables: testConfigVarsProjectRoleAssignment, + ResourceName: "stackit_authorization_organization_role_assignment.ora", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_authorization_organization_role_assignment.ora"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_authorization_organization_role_assignment.ora") + } + resourceId, ok := r.Primary.Attributes["resource_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute resource_id") + } + role, ok := r.Primary.Attributes["role"] + if !ok { + return "", fmt.Errorf("couldn't find attribute role") + } + subject, ok := r.Primary.Attributes["subject"] + if !ok { + return "", fmt.Errorf("couldn't find attribute subject") + } + + return fmt.Sprintf("%s,%s,%s", resourceId, role, subject), nil + }, + ImportState: true, + ImportStateVerify: true, }, + // Update { - // Assign a non-existent role. Expect failure - ConfigVariables: testConfigVars, - Config: testutil.AuthorizationProviderConfig() + prerequisites + invalidRole, - ExpectError: regexp.MustCompile(".+"), + ConfigVariables: testConfigVarsOrgRoleAssignmentUpdated(), + Config: testutil.AuthorizationProviderConfig() + "\n" + resourceOrgRoleAssignment, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("stackit_authorization_organization_role_assignment.ora", "resource_id"), + resource.TestCheckResourceAttrSet("stackit_authorization_organization_role_assignment.ora", "id"), + resource.TestCheckResourceAttr("stackit_authorization_organization_role_assignment.ora", "role", testutil.ConvertConfigVariable(testConfigVarsOrgRoleAssignmentUpdated()["role"])), + resource.TestCheckResourceAttr("stackit_authorization_organization_role_assignment.ora", "subject", testutil.ConvertConfigVariable(testConfigVarsOrgRoleAssignmentUpdated()["subject"])), + ), }, + // Deletion is done by the framework implicitly }, }) } -func authApiClient() (*authorization.APIClient, error) { +func testAccCheckResourceManagerProjectsDestroy(s *terraform.State) error { + ctx := context.Background() + var client *resourcemanager.APIClient + var err error + if testutil.ResourceManagerCustomEndpoint == "" { + client, err = resourcemanager.NewAPIClient() + } else { + client, err = resourcemanager.NewAPIClient( + stackitSdkConfig.WithEndpoint(testutil.ResourceManagerCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + var projectsToDestroy []string + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_resourcemanager_project" { + continue + } + // project terraform ID: "[container_id]" + containerId := rs.Primary.ID + projectsToDestroy = append(projectsToDestroy, containerId) + } + + if testutil.OrganizationId == "" { + return fmt.Errorf("no Org-ID is set") + } + containerParentId := testutil.OrganizationId + + projectsResp, err := client.ListProjects(ctx).ContainerParentId(containerParentId).Execute() + if err != nil { + return fmt.Errorf("getting projectsResp: %w", err) + } + + items := *projectsResp.Items + for i := range items { + if *items[i].LifecycleState == resourcemanager.LIFECYCLESTATE_DELETING { + continue + } + if !utils.Contains(projectsToDestroy, *items[i].ContainerId) { + continue + } + + err := client.DeleteProjectExecute(ctx, *items[i].ContainerId) + if err != nil { + return fmt.Errorf("destroying project %s during CheckDestroy: %w", *items[i].ContainerId, err) + } + _, err = wait.DeleteProjectWaitHandler(ctx, client, *items[i].ContainerId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("destroying project %s during CheckDestroy: waiting for deletion %w", *items[i].ContainerId, err) + } + } + return nil +} + +func testAccCheckResourceManagerFoldersDestroy(s *terraform.State) error { + ctx := context.Background() + var client *resourcemanager.APIClient + var err error + if testutil.ResourceManagerCustomEndpoint == "" { + client, err = resourcemanager.NewAPIClient() + } else { + client, err = resourcemanager.NewAPIClient( + stackitSdkConfig.WithEndpoint(testutil.ResourceManagerCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + foldersToDestroy := []string{} + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_resourcemanager_folder" { + continue + } + // project terraform ID: "[container_id]" + containerId := rs.Primary.ID + foldersToDestroy = append(foldersToDestroy, containerId) + } + + if testutil.OrganizationId == "" { + return fmt.Errorf("no Org-ID is set") + } + containerParentId := testutil.OrganizationId + + foldersResponse, err := client.ListFolders(ctx).ContainerParentId(containerParentId).Execute() + if err != nil { + return fmt.Errorf("getting foldersResponse: %w", err) + } + + items := *foldersResponse.Items + for i := range items { + if !utils.Contains(foldersToDestroy, *items[i].ContainerId) { + continue + } + + err := client.DeleteFolder(ctx, *items[i].ContainerId).Execute() + if err != nil { + return fmt.Errorf("destroying folder %s during CheckDestroy: %w", *items[i].ContainerId, err) + } + } + return nil +} + +func testAccCheckOrganizationRoleAssignmentDestroy(s *terraform.State) error { + ctx := context.Background() var client *authorization.APIClient var err error if testutil.AuthorizationCustomEndpoint == "" { - client, err = authorization.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) + client, err = authorization.NewAPIClient() } else { client, err = authorization.NewAPIClient( stackitSdkConfig.WithEndpoint(testutil.AuthorizationCustomEndpoint), ) } if err != nil { - return nil, fmt.Errorf("creating client: %w", err) + return fmt.Errorf("creating client: %w", err) + } + + var orgRoleAssignmentsToDestroy []authorization.Member + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_authorization_organization_role_assignment" { + continue + } + // project terraform ID: "resource_id,role,subject" + terraformId := strings.Split(rs.Primary.ID, ",") + + orgRoleAssignmentsToDestroy = append( + orgRoleAssignmentsToDestroy, + authorization.Member{ + Role: utils.Ptr(terraformId[1]), + Subject: utils.Ptr(terraformId[2]), + }, + ) + } + + if testutil.OrganizationId == "" { + return fmt.Errorf("no Org-ID is set") } - return client, nil + containerParentId := testutil.OrganizationId + + payload := authorization.RemoveMembersPayload{ + ResourceType: utils.Ptr("organization"), + Members: &orgRoleAssignmentsToDestroy, + } + + // Ignore error. If this request errors the org role assignment has been successfully deleted by terraform itself. + _, _ = client.RemoveMembers(ctx, containerParentId).RemoveMembersPayload(payload).Execute() + return nil } diff --git a/stackit/internal/services/authorization/roleassignments/resource.go b/stackit/internal/services/authorization/roleassignments/resource.go index 32d5909df..7e52c4063 100644 --- a/stackit/internal/services/authorization/roleassignments/resource.go +++ b/stackit/internal/services/authorization/roleassignments/resource.go @@ -2,16 +2,10 @@ package roleassignments import ( "context" - "encoding/json" "errors" "fmt" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - authorizationUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -21,8 +15,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + authorizationUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -30,6 +27,7 @@ import ( var roleTargets = []string{ "project", "organization", + "folder", } // Ensure the implementation satisfies the expected interfaces. @@ -38,11 +36,9 @@ var ( _ resource.ResourceWithConfigure = &roleAssignmentResource{} _ resource.ResourceWithImportState = &roleAssignmentResource{} - errRoleAssignmentNotFound = errors.New("response members did not contain expected role assignment") - errRoleAssignmentDuplicateFound = errors.New("found a duplicate role assignment.") + errRoleAssignmentNotFound = errors.New("response members did not contain expected role assignment") ) -// Provider's internal model type Model struct { Id types.String `tfsdk:"id"` // needed by TF ResourceId types.String `tfsdk:"resource_id"` @@ -50,7 +46,7 @@ type Model struct { Subject types.String `tfsdk:"subject"` } -// NewProjectRoleAssignmentResource is a helper function to simplify the provider implementation. +// NewRoleAssignmentResources is a helper function to simplify the provider implementation. func NewRoleAssignmentResources() []func() resource.Resource { resources := make([]func() resource.Resource, 0) for _, v := range roleTargets { @@ -97,10 +93,10 @@ func (r *roleAssignmentResource) Configure(ctx context.Context, req resource.Con // Schema defines the schema for the resource. func (r *roleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ - "main": features.AddExperimentDescription(fmt.Sprintf("%s Role Assignment resource schema.", r.apiName), features.IamExperiment, core.Resource), - "id": "Terraform's internal resource identifier. It is structured as \"[resource_id],[role],[subject]\".", + "main": features.AddExperimentDescription(fmt.Sprintf("%s Role Assignment resource schema.", fmt.Sprintf("%s%s", strings.ToUpper(r.apiName[:1]), strings.ToLower(r.apiName[1:]))), features.IamExperiment, core.Resource), + "id": "Terraform's internal resource identifier. It is structured as \"`resource_id`,`role`,`subject`\".", "resource_id": fmt.Sprintf("%s Resource to assign the role to.", r.apiName), - "role": "Role to be assigned", + "role": "Role to be assigned. Available roles can be queried using stackit-cli: `stackit curl https://authorization.api.stackit.cloud/v2/permissions`", "subject": "Identifier of user, service account or client. Usually email address or name in case of clients", } @@ -152,31 +148,35 @@ func (r *roleAssignmentResource) Create(ctx context.Context, req resource.Create return } - ctx = r.annotateLogger(ctx, &model) - - if err := r.checkDuplicate(ctx, model); err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error while checking for duplicate role assignments", err.Error()) - return - } + ctx = tflog.SetField(ctx, "subject", model.Subject.ValueString()) + ctx = tflog.SetField(ctx, "role", model.Role.ValueString()) + ctx = tflog.SetField(ctx, "resource_type", r.apiName) // Create new project role assignment - payload, err := r.toCreatePayload(&model) + payload, err := toCreatePayload(&model, &r.apiName) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Creating API payload: %v", err)) return } + createResp, err := r.authorizationClient.AddMembers(ctx, model.ResourceId.ValueString()).AddMembersPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error creating %s role assignment", r.apiName), fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema - err = mapMembersResponse(createResp, &model) + listMembersResponse, err := authorizationUtils.TypeConverter[authorization.ListMembersResponse](createResp) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error creating %s role assignment", r.apiName), fmt.Sprintf("Processing API payload: %v", err)) return } + + err = mapListMembersResponse(listMembersResponse, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error creating %s role assignment", r.apiName), fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -194,7 +194,10 @@ func (r *roleAssignmentResource) Read(ctx context.Context, req resource.ReadRequ return } - ctx = r.annotateLogger(ctx, &model) + ctx = tflog.SetField(ctx, "subject", model.Subject.ValueString()) + ctx = tflog.SetField(ctx, "role", model.Role.ValueString()) + ctx = tflog.SetField(ctx, "resource_type", r.apiName) + ctx = tflog.SetField(ctx, "resource_id", model.ResourceId.ValueString()) listResp, err := r.authorizationClient.ListMembers(ctx, r.apiName, model.ResourceId.ValueString()).Subject(model.Subject.ValueString()).Execute() if err != nil { @@ -232,12 +235,18 @@ func (r *roleAssignmentResource) Delete(ctx context.Context, req resource.Delete return } - ctx = r.annotateLogger(ctx, &model) + ctx = tflog.SetField(ctx, "subject", model.Subject.ValueString()) + ctx = tflog.SetField(ctx, "role", model.Role.ValueString()) + ctx = tflog.SetField(ctx, "resource_type", r.apiName) + ctx = tflog.SetField(ctx, "resource_id", model.ResourceId.ValueString()) payload := authorization.RemoveMembersPayload{ ResourceType: &r.apiName, Members: &[]authorization.Member{ - *authorization.NewMember(model.Role.ValueString(), model.Subject.ValueString()), + { + Role: model.Role.ValueStringPointer(), + Subject: model.Subject.ValueStringPointer(), + }, }, } @@ -268,7 +277,32 @@ func (r *roleAssignmentResource) ImportState(ctx context.Context, req resource.I tflog.Info(ctx, fmt.Sprintf("%s role assignment state imported", r.apiName)) } -// Maps project role assignment fields to the provider's internal model. +// toCreatePayload builds the payload to add a member to a resource +func toCreatePayload(model *Model, apiName *string) (*authorization.AddMembersPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + if model.Role.IsUnknown() || model.Role.ValueString() == "" { + return nil, fmt.Errorf("invalid model role") + } + + if model.Subject.IsUnknown() || model.Subject.ValueString() == "" { + return nil, fmt.Errorf("invalid model subject") + } + + return &authorization.AddMembersPayload{ + ResourceType: apiName, + Members: &[]authorization.Member{ + { + Role: model.Role.ValueStringPointer(), + Subject: model.Subject.ValueStringPointer(), + }, + }, + }, nil +} + +// mapListMembersResponse maps project role assignment fields from the API response to the provider's internal model func mapListMembersResponse(resp *authorization.ListMembersResponse, model *Model) error { if resp == nil { return fmt.Errorf("response input is nil") @@ -276,12 +310,15 @@ func mapListMembersResponse(resp *authorization.ListMembersResponse, model *Mode if resp.Members == nil { return fmt.Errorf("response members are nil") } + if resp.ResourceId == nil { + return fmt.Errorf("response resource_id is nil") + } if model == nil { return fmt.Errorf("model input is nil") } - model.Id = utils.BuildInternalTerraformId(model.ResourceId.ValueString(), model.Role.ValueString(), model.Subject.ValueString()) model.ResourceId = types.StringPointerValue(resp.ResourceId) + model.Id = utils.BuildInternalTerraformId(model.ResourceId.ValueString(), model.Role.ValueString(), model.Subject.ValueString()) for _, m := range *resp.Members { if *m.Role == model.Role.ValueString() && *m.Subject == model.Subject.ValueString() { @@ -290,69 +327,6 @@ func mapListMembersResponse(resp *authorization.ListMembersResponse, model *Mode return nil } } - return errRoleAssignmentNotFound -} - -func mapMembersResponse(resp *authorization.MembersResponse, model *Model) error { - listMembersResponse, err := typeConverter[authorization.ListMembersResponse](resp) - if err != nil { - return err - } - return mapListMembersResponse(listMembersResponse, model) -} - -// Helper to convert objects with equal JSON tags -func typeConverter[R any](data any) (*R, error) { - var result R - b, err := json.Marshal(&data) - if err != nil { - return nil, err - } - err = json.Unmarshal(b, &result) - if err != nil { - return nil, err - } - return &result, err -} -// Build Createproject role assignmentPayload from provider's model -func (r *roleAssignmentResource) toCreatePayload(model *Model) (*authorization.AddMembersPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &authorization.AddMembersPayload{ - ResourceType: &r.apiName, - Members: &[]authorization.Member{ - *authorization.NewMember(model.Role.ValueString(), model.Subject.ValueString()), - }, - }, nil -} - -func (r *roleAssignmentResource) annotateLogger(ctx context.Context, model *Model) context.Context { - resourceId := model.ResourceId.ValueString() - ctx = tflog.SetField(ctx, "resource_id", resourceId) - ctx = tflog.SetField(ctx, "subject", model.Subject.ValueString()) - ctx = tflog.SetField(ctx, "role", model.Role.ValueString()) - ctx = tflog.SetField(ctx, "resource_type", r.apiName) - return ctx -} - -// returns an error if duplicate role assignment exists -func (r *roleAssignmentResource) checkDuplicate(ctx context.Context, model Model) error { //nolint:gocritic // A read only copy is required since an api response is parsed into the model and this check should not affect the model parameter - listResp, err := r.authorizationClient.ListMembers(ctx, r.apiName, model.ResourceId.ValueString()).Subject(model.Subject.ValueString()).Execute() - if err != nil { - return err - } - - // Map response body to schema - err = mapListMembersResponse(listResp, &model) - - if err != nil { - if errors.Is(err, errRoleAssignmentNotFound) { - return nil - } - return err - } - return errRoleAssignmentDuplicateFound + return errRoleAssignmentNotFound } diff --git a/stackit/internal/services/authorization/roleassignments/resource_test.go b/stackit/internal/services/authorization/roleassignments/resource_test.go new file mode 100644 index 000000000..09971c64a --- /dev/null +++ b/stackit/internal/services/authorization/roleassignments/resource_test.go @@ -0,0 +1,231 @@ +package roleassignments + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" + tfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func TestToCreatePayload(t *testing.T) { + apiName := "test-resource" + + tests := []struct { + name string + input *Model + apiName *string + expectError bool + expected *authorization.AddMembersPayload + }{ + { + name: "valid model", + input: &Model{ + Role: types.StringValue("editor"), + Subject: types.StringValue("foo.bar@stackit.cloud"), + }, + apiName: &apiName, + expectError: false, + expected: &authorization.AddMembersPayload{ + ResourceType: &apiName, + Members: &[]authorization.Member{ + { + Role: utils.Ptr("editor"), + Subject: utils.Ptr("foo.bar@stackit.cloud"), + }, + }, + }, + }, + { + name: "nil model", + input: nil, + apiName: &apiName, + expectError: true, + }, + { + name: "unknown role", + input: &Model{ + Role: types.StringUnknown(), + Subject: types.StringValue("foo.bar@stackit.cloud"), + }, + apiName: &apiName, + expectError: true, + }, + { + name: "empty role value", + input: &Model{ + Role: types.StringValue(""), + Subject: types.StringValue("foo.bar@stackit.cloud"), + }, + apiName: &apiName, + expectError: true, + }, + { + name: "unknown subject", + input: &Model{ + Role: types.StringValue("editor"), + Subject: types.StringUnknown(), + }, + apiName: &apiName, + expectError: true, + }, + { + name: "empty subject value", + input: &Model{ + Role: types.StringValue("editor"), + Subject: types.StringValue(""), + }, + apiName: &apiName, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toCreatePayload(tt.input, tt.apiName) + + if tt.expectError && err == nil { + t.Fatalf("Expected error but got nil") + } + if !tt.expectError && err != nil { + t.Fatalf("Expected no error but got: %v", err) + } + + if !tt.expectError { + if diff := cmp.Diff(tt.expected, got); diff != "" { + t.Errorf("Payload mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestMapListMembersResponse(t *testing.T) { + role := "editor" + subject := "foo.bar@stackit.cloud" + resourceID := "project" + + tests := []struct { + name string + resp *authorization.ListMembersResponse + inputModel *Model + expectError bool + expected *Model + }{ + { + name: "successfully maps values", + resp: &authorization.ListMembersResponse{ + ResourceId: &resourceID, + Members: &[]authorization.Member{ + { + Role: &role, + Subject: &subject, + }, + }, + }, + inputModel: &Model{ + Role: types.StringValue(role), + Subject: types.StringValue(subject), + }, + expectError: false, + expected: &Model{ + ResourceId: types.StringPointerValue(&resourceID), + Role: types.StringPointerValue(&role), + Subject: types.StringPointerValue(&subject), + Id: tfUtils.BuildInternalTerraformId(resourceID, role, subject), + }, + }, + { + name: "nil response input", + resp: nil, + inputModel: &Model{ + Role: types.StringValue(role), + Subject: types.StringValue(subject), + }, + expectError: true, + }, + { + name: "nil members input", + resp: &authorization.ListMembersResponse{ + ResourceId: &resourceID, + Members: nil, + }, + inputModel: &Model{ + Role: types.StringValue(role), + Subject: types.StringValue(subject), + }, + expectError: true, + }, + { + name: "nil resource_id input", + resp: &authorization.ListMembersResponse{ + ResourceId: nil, + Members: &[]authorization.Member{ + { + Role: &role, + Subject: &subject, + }, + }, + }, + inputModel: &Model{ + Role: types.StringValue(role), + Subject: types.StringValue(subject), + }, + expectError: true, + }, + { + name: "nil model input", + resp: &authorization.ListMembersResponse{ + ResourceId: &resourceID, + Members: &[]authorization.Member{ + { + Role: &role, + Subject: &subject, + }, + }, + }, + inputModel: nil, + expectError: true, + }, + { + name: "no matching role/subject pair", + resp: &authorization.ListMembersResponse{ + ResourceId: &resourceID, + Members: &[]authorization.Member{ + { + Role: utils.Ptr("reader"), + Subject: utils.Ptr("foo.bar@stackit.cloud"), + }, + }, + }, + inputModel: &Model{ + Role: types.StringValue(role), + Subject: types.StringValue(subject), + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + model := tt.inputModel // copy pointer to avoid overriding test data + + err := mapListMembersResponse(tt.resp, model) + + if tt.expectError && err == nil { + t.Fatalf("Expected error but got none") + } + if !tt.expectError && err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + + if !tt.expectError { + if diff := cmp.Diff(tt.expected, model); diff != "" { + t.Errorf("Mapped model mismatch (-want +got):\n%s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/authorization/testdata/resource-folder-role-assignment.tf b/stackit/internal/services/authorization/testdata/resource-folder-role-assignment.tf new file mode 100644 index 000000000..f46750776 --- /dev/null +++ b/stackit/internal/services/authorization/testdata/resource-folder-role-assignment.tf @@ -0,0 +1,17 @@ +variable "name" {} +variable "role" {} +variable "owner_email" {} +variable "subject" {} +variable "parent_container_id" {} + +resource "stackit_resourcemanager_folder" "folder" { + name = var.name + owner_email = var.owner_email + parent_container_id = var.parent_container_id +} + +resource "stackit_authorization_folder_role_assignment" "fra" { + resource_id = stackit_resourcemanager_folder.folder.folder_id + role = var.role + subject = var.owner_email +} \ No newline at end of file diff --git a/stackit/internal/services/authorization/testdata/resource-org-role-assignment.tf b/stackit/internal/services/authorization/testdata/resource-org-role-assignment.tf new file mode 100644 index 000000000..d8e5035be --- /dev/null +++ b/stackit/internal/services/authorization/testdata/resource-org-role-assignment.tf @@ -0,0 +1,9 @@ +variable "role" {} +variable "subject" {} +variable "parent_container_id" {} + +resource "stackit_authorization_organization_role_assignment" "ora" { + resource_id = var.parent_container_id + role = var.role + subject = var.subject +} \ No newline at end of file diff --git a/stackit/internal/services/authorization/testdata/resource-project-role-assignment.tf b/stackit/internal/services/authorization/testdata/resource-project-role-assignment.tf new file mode 100644 index 000000000..e8c5f6c36 --- /dev/null +++ b/stackit/internal/services/authorization/testdata/resource-project-role-assignment.tf @@ -0,0 +1,17 @@ +variable "name" {} +variable "role" {} +variable "owner_email" {} +variable "subject" {} +variable "parent_container_id" {} + +resource "stackit_resourcemanager_project" "project" { + name = var.name + owner_email = var.owner_email + parent_container_id = var.parent_container_id +} + +resource "stackit_authorization_project_role_assignment" "pra" { + resource_id = stackit_resourcemanager_project.project.project_id + role = var.role + subject = var.owner_email +} \ No newline at end of file diff --git a/stackit/internal/services/authorization/testfiles/double-definition.tf b/stackit/internal/services/authorization/testfiles/double-definition.tf deleted file mode 100644 index 78db7598d..000000000 --- a/stackit/internal/services/authorization/testfiles/double-definition.tf +++ /dev/null @@ -1,6 +0,0 @@ - -resource "stackit_authorization_project_role_assignment" "serviceaccount_duplicate" { - resource_id = var.project_id - role = "reader" - subject = var.test_service_account -} diff --git a/stackit/internal/services/authorization/testfiles/invalid-role.tf b/stackit/internal/services/authorization/testfiles/invalid-role.tf deleted file mode 100644 index 67ee43f64..000000000 --- a/stackit/internal/services/authorization/testfiles/invalid-role.tf +++ /dev/null @@ -1,6 +0,0 @@ - -resource "stackit_authorization_project_role_assignment" "invalid_role" { - resource_id = var.project_id - role = "thisrolesdoesnotexist" - subject = var.test_service_account -} diff --git a/stackit/internal/services/authorization/testfiles/organization-role.tf b/stackit/internal/services/authorization/testfiles/organization-role.tf deleted file mode 100644 index 800d8bc11..000000000 --- a/stackit/internal/services/authorization/testfiles/organization-role.tf +++ /dev/null @@ -1,6 +0,0 @@ - -resource "stackit_authorization_organization_role_assignment" "serviceaccount" { - resource_id = var.organization_id - role = "organization.member" - subject = var.test_service_account -} \ No newline at end of file diff --git a/stackit/internal/services/authorization/testfiles/prerequisites.tf b/stackit/internal/services/authorization/testfiles/prerequisites.tf deleted file mode 100644 index 4188842a3..000000000 --- a/stackit/internal/services/authorization/testfiles/prerequisites.tf +++ /dev/null @@ -1,10 +0,0 @@ - -variable "project_id" {} -variable "test_service_account" {} -variable "organization_id" {} - -resource "stackit_authorization_project_role_assignment" "serviceaccount" { - resource_id = var.project_id - role = "reader" - subject = var.test_service_account -} diff --git a/stackit/internal/services/authorization/testfiles/project-owner.tf b/stackit/internal/services/authorization/testfiles/project-owner.tf deleted file mode 100644 index d1f288fd3..000000000 --- a/stackit/internal/services/authorization/testfiles/project-owner.tf +++ /dev/null @@ -1,6 +0,0 @@ - -resource "stackit_authorization_project_role_assignment" "serviceaccount_project_owner" { - resource_id = var.project_id - role = "owner" - subject = var.test_service_account -} diff --git a/stackit/internal/services/authorization/utils/util.go b/stackit/internal/services/authorization/utils/util.go index 99694780a..0f6a538f2 100644 --- a/stackit/internal/services/authorization/utils/util.go +++ b/stackit/internal/services/authorization/utils/util.go @@ -2,6 +2,7 @@ package utils import ( "context" + "encoding/json" "fmt" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -11,6 +12,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) +// ConfigureClient configures an API-Client to communicate with the authorization API func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *authorization.APIClient { apiClientConfigOptions := []config.ConfigurationOption{ config.WithCustomAuth(providerData.RoundTripper), @@ -27,3 +29,17 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags return apiClient } + +// TypeConverter converts objects with equal JSON tags +func TypeConverter[R any](data any) (*R, error) { + var result R + b, err := json.Marshal(&data) + if err != nil { + return nil, err + } + err = json.Unmarshal(b, &result) + if err != nil { + return nil, err + } + return &result, err +} diff --git a/stackit/internal/services/authorization/utils/util_test.go b/stackit/internal/services/authorization/utils/util_test.go index 794f255a6..e4f2d5115 100644 --- a/stackit/internal/services/authorization/utils/util_test.go +++ b/stackit/internal/services/authorization/utils/util_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" "github.com/stackitcloud/stackit-sdk-go/core/config" + testUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/authorization" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" @@ -91,3 +92,57 @@ func TestConfigureClient(t *testing.T) { }) } } + +func TestTypeConverter(t *testing.T) { + tests := []struct { + name string + input authorization.MembersResponse + expected *authorization.ListMembersResponse + expectError bool + }{ + { + name: "success - all fields populated", + input: authorization.MembersResponse{ + Members: &[]authorization.Member{ + { + Role: testUtils.Ptr("editor"), + Subject: testUtils.Ptr("foo.bar@stackit.cloud"), + }, + }, + ResourceId: testUtils.Ptr("project-123"), + ResourceType: testUtils.Ptr("project"), + }, + expected: &authorization.ListMembersResponse{ + Members: &[]authorization.Member{ + { + Role: testUtils.Ptr("editor"), + Subject: testUtils.Ptr("foo.bar@stackit.cloud"), + }, + }, + ResourceId: testUtils.Ptr("project-123"), + ResourceType: testUtils.Ptr("project"), + }, + expectError: false, + }, + { + name: "success - completely empty input", + input: authorization.MembersResponse{}, + expected: &authorization.ListMembersResponse{}, + expectError: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual, err := TypeConverter[authorization.ListMembersResponse](tc.input) + + if (err != nil) != tc.expectError { + t.Fatalf("unexpected error: got error=%v, expectError=%v", err, tc.expectError) + } + + if !tc.expectError && !reflect.DeepEqual(actual, tc.expected) { + t.Errorf("\nUnexpected result:\nactual: %+v\nexpected: %+v", actual, tc.expected) + } + }) + } +}