From 9b6cf024a7334d862f9e5de0698101ce386e4e2c Mon Sep 17 00:00:00 2001 From: Zahiar Ahmed <310030+zahiar@users.noreply.github.com> Date: Mon, 29 Aug 2022 03:01:08 +0100 Subject: [PATCH] GH-111 Add user permission resource & data source (#119) Along with docs. --- .github/workflows/testing.yml | 1 + README.md | 13 +- .../data_source_bitbucket_user_permission.go | 39 +++++ ...a_source_bitbucket_user_permission_test.go | 69 +++++++++ bitbucket/provider.go | 2 + .../resource_bitbucket_user_permission.go | 137 ++++++++++++++++++ ...resource_bitbucket_user_permission_test.go | 81 +++++++++++ .../data-sources/bitbucket_user_permission.md | 22 +++ docs/resources/bitbucket_user_permission.md | 31 ++++ 9 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 bitbucket/data_source_bitbucket_user_permission.go create mode 100644 bitbucket/data_source_bitbucket_user_permission_test.go create mode 100644 bitbucket/resource_bitbucket_user_permission.go create mode 100644 bitbucket/resource_bitbucket_user_permission_test.go create mode 100644 docs/data-sources/bitbucket_user_permission.md create mode 100644 docs/resources/bitbucket_user_permission.md diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0fb41cd..c5dbaab 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -31,3 +31,4 @@ jobs: env: BITBUCKET_USERNAME: ${{ secrets.BITBUCKET_USERNAME }} BITBUCKET_PASSWORD: ${{ secrets.BITBUCKET_PASSWORD }} + BITBUCKET_MEMBER_ACCOUNT_UUID: ${{ secrets.BITBUCKET_MEMBER_ACCOUNT_UUID }} diff --git a/README.md b/README.md index 6f8c674..e51db27 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,21 @@ $ make test ``` ### Acceptance Tests -This will require you to specify two environment variables (`BITBUCKET_USERNAME` & `BITBUCKET_PASSWORD`), as these tests -will provision actual resources in your account, and it will tear them down afterwards to ensure it leaves your account clean. +This will require you to specify the following environment variables, as these tests will provision actual resources in +your account, and it will tear them down afterwards to ensure it leaves your account clean. + +You will also require a UUID of another account that is a member of your workspace in order for the `bitbucket_user_permission` +tests to run, as Bitbucket's API will reject the account owner's UUID. + +* `BITBUCKET_USERNAME` - Username of the account to run the tests against +* `BITBUCKET_PASSWORD` - Password of the account to run the tests against +* `BITBUCKET_MEMBER_ACCOUNT_UUID` - Account UUID of the member who is part of your account **NOTE**: if a test fails, it may leave dangling resources in your account so please bear this in mind. If you have two-factor authentication enabled, then be sure to set up an [app password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/) and use that instead. ```shell -$ BITBUCKET_USERNAME=myUsername BITBUCKET_PASSWORD=myPassword make testacc +$ BITBUCKET_USERNAME=myUsername BITBUCKET_PASSWORD=myPassword BITBUCKET_MEMBER_ACCOUNT_UUID=myMemberUUID make testacc ``` ### Documentation diff --git a/bitbucket/data_source_bitbucket_user_permission.go b/bitbucket/data_source_bitbucket_user_permission.go new file mode 100644 index 0000000..4a17f11 --- /dev/null +++ b/bitbucket/data_source_bitbucket_user_permission.go @@ -0,0 +1,39 @@ +package bitbucket + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceBitbucketUserPermission() *schema.Resource { + return &schema.Resource{ + ReadContext: resourceBitbucketUserPermissionRead, + Schema: map[string]*schema.Schema{ + "id": { + Description: "The ID of the user permission.", + Type: schema.TypeString, + Computed: true, + }, + "workspace": { + Description: "The UUID (including the enclosing `{}`) of the workspace.", + Type: schema.TypeString, + Required: true, + }, + "repository": { + Description: "The slug of the repository (must consist of only lowercase ASCII letters, numbers, underscores and hyphens).", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateRepositoryName, + }, + "user": { + Description: "The UUID (including the enclosing `{}`) of the user.", + Type: schema.TypeString, + Required: true, + }, + "permission": { + Description: "The permission this user will have. Must be one of 'read', 'write', 'admin'.", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} diff --git a/bitbucket/data_source_bitbucket_user_permission_test.go b/bitbucket/data_source_bitbucket_user_permission_test.go new file mode 100644 index 0000000..33b1c72 --- /dev/null +++ b/bitbucket/data_source_bitbucket_user_permission_test.go @@ -0,0 +1,69 @@ +package bitbucket + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccBitbucketUserPermissionDataSource_basic(t *testing.T) { + workspaceSlug := os.Getenv("BITBUCKET_USERNAME") + projectName := "tf-acc-test-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + projectKey := strings.ToUpper(acctest.RandStringFromCharSet(3, acctest.CharSetAlpha)) + repoName := "tf-acc-test-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + user := os.Getenv("BITBUCKET_MEMBER_ACCOUNT_UUID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "bitbucket_workspace" "testacc" { + id = "%s" + } + + resource "bitbucket_project" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + name = "%s" + key = "%s" + } + + resource "bitbucket_repository" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + project_key = bitbucket_project.testacc.key + name = "%s" + } + + data "bitbucket_user" "testacc" { + id = "%s" + } + + resource "bitbucket_user_permission" "testacc" { + workspace = data.bitbucket_workspace.testacc.uuid + repository = bitbucket_repository.testacc.name + user = data.bitbucket_user.testacc.id + permission = "read" + } + + data "bitbucket_user_permission" "testacc" { + workspace = data.bitbucket_workspace.testacc.uuid + repository = bitbucket_repository.testacc.name + user = data.bitbucket_user.testacc.id + }`, workspaceSlug, projectName, projectKey, repoName, user), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.bitbucket_user_permission.testacc", "repository", repoName), + resource.TestCheckResourceAttr("data.bitbucket_user_permission.testacc", "user", user), + + resource.TestCheckResourceAttrSet("data.bitbucket_user_permission.testacc", "id"), + resource.TestCheckResourceAttrSet("data.bitbucket_user_permission.testacc", "workspace"), + resource.TestCheckResourceAttrSet("data.bitbucket_user_permission.testacc", "permission"), + ), + }, + }, + }) +} diff --git a/bitbucket/provider.go b/bitbucket/provider.go index 66a5346..9c51ae5 100644 --- a/bitbucket/provider.go +++ b/bitbucket/provider.go @@ -39,6 +39,7 @@ func Provider() *schema.Provider { "bitbucket_project": dataSourceBitbucketProject(), "bitbucket_repository": dataSourceBitbucketRepository(), "bitbucket_user": dataSourceBitbucketUser(), + "bitbucket_user_permission": dataSourceBitbucketUserPermission(), "bitbucket_user_workspace": dataSourceBitbucketUserWorkspace(), "bitbucket_webhook": dataSourceBitbucketWebhook(), "bitbucket_workspace": dataSourceBitbucketWorkspace(), @@ -56,6 +57,7 @@ func Provider() *schema.Provider { "bitbucket_pipeline_variable": resourceBitbucketPipelineVariable(), "bitbucket_project": resourceBitbucketProject(), "bitbucket_repository": resourceBitbucketRepository(), + "bitbucket_user_permission": resourceBitbucketUserPermission(), "bitbucket_webhook": resourceBitbucketWebhook(), }, diff --git a/bitbucket/resource_bitbucket_user_permission.go b/bitbucket/resource_bitbucket_user_permission.go new file mode 100644 index 0000000..4d3c127 --- /dev/null +++ b/bitbucket/resource_bitbucket_user_permission.go @@ -0,0 +1,137 @@ +package bitbucket + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/ktrysmt/go-bitbucket" +) + +func resourceBitbucketUserPermission() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceBitbucketUserPermissionCreate, + ReadContext: resourceBitbucketUserPermissionRead, + DeleteContext: resourceBitbucketUserPermissionDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceBitbucketUserPermissionImport, + }, + Schema: map[string]*schema.Schema{ + "id": { + Description: "The ID of the user permission.", + Type: schema.TypeString, + Computed: true, + }, + "workspace": { + Description: "The UUID (including the enclosing `{}`) of the workspace.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "repository": { + Description: "The slug of the repository (must consist of only lowercase ASCII letters, numbers, underscores and hyphens).", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateRepositoryName, + ForceNew: true, + }, + "user": { + Description: "The UUID (including the enclosing `{}`) of the user.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "permission": { + Description: "The permission this user will have. Must be one of 'read', 'write', 'admin'.", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"read", "write", "admin"}, false), + ForceNew: true, + }, + }, + } +} + +func resourceBitbucketUserPermissionCreate(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*Clients).V2 + + _, err := client.Repositories.Repository.SetUserPermissions(&bitbucket.RepositoryUserPermissionsOptions{ + Owner: resourceData.Get("workspace").(string), + RepoSlug: resourceData.Get("repository").(string), + User: resourceData.Get("user").(string), + Permission: resourceData.Get("permission").(string), + }) + + if err != nil { + return diag.FromErr(fmt.Errorf("unable to create user permission with error: %s", err)) + } + + return resourceBitbucketUserPermissionRead(ctx, resourceData, meta) +} + +func resourceBitbucketUserPermissionRead(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*Clients).V2 + + workspace := resourceData.Get("workspace").(string) + repository := resourceData.Get("repository").(string) + + userPermission, err := client.Repositories.Repository.GetUserPermissions(&bitbucket.RepositoryUserPermissionsOptions{ + Owner: workspace, + RepoSlug: repository, + User: resourceData.Get("user").(string), + Permission: resourceData.Get("permission").(string), + }) + if err != nil { + return diag.FromErr(fmt.Errorf("unable to get user permission with error: %s", err)) + } + + _ = resourceData.Set("user", userPermission.User.Uuid) + _ = resourceData.Set("permission", userPermission.Permission) + + resourceData.SetId(generateUserPermissionResourceId(workspace, repository, userPermission.User.Uuid)) + + return nil +} + +func resourceBitbucketUserPermissionDelete(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*Clients).V2 + + _, err := client.Repositories.Repository.DeleteUserPermissions(&bitbucket.RepositoryUserPermissionsOptions{ + Owner: resourceData.Get("workspace").(string), + RepoSlug: resourceData.Get("repository").(string), + User: resourceData.Get("user").(string), + Permission: resourceData.Get("permission").(string), + }) + + if err != nil { + return diag.FromErr(fmt.Errorf("unable to delete user permission with error: %s", err)) + } + + resourceData.SetId("") + + return nil +} + +func resourceBitbucketUserPermissionImport(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + ret := []*schema.ResourceData{resourceData} + + splitID := strings.Split(resourceData.Id(), "/") + if len(splitID) < 3 { + return ret, fmt.Errorf("invalid import ID. It must to be in this format \"//\"") + } + + _ = resourceData.Set("workspace", splitID[0]) + _ = resourceData.Set("repository", splitID[1]) + _ = resourceData.Set("user", splitID[2]) + + _ = resourceBitbucketUserPermissionRead(ctx, resourceData, meta) + + return ret, nil +} + +func generateUserPermissionResourceId(workspace string, repo string, user string) string { + return fmt.Sprintf("%s-%s-%s", workspace, repo, user) +} diff --git a/bitbucket/resource_bitbucket_user_permission_test.go b/bitbucket/resource_bitbucket_user_permission_test.go new file mode 100644 index 0000000..97e8dfa --- /dev/null +++ b/bitbucket/resource_bitbucket_user_permission_test.go @@ -0,0 +1,81 @@ +package bitbucket + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/assert" +) + +func TestAccBitbucketUserPermissionResource_basic(t *testing.T) { + workspaceSlug := os.Getenv("BITBUCKET_USERNAME") + projectName := "tf-acc-test-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + projectKey := strings.ToUpper(acctest.RandStringFromCharSet(3, acctest.CharSetAlpha)) + repoName := "tf-acc-test-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + user := os.Getenv("BITBUCKET_MEMBER_ACCOUNT_UUID") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "bitbucket_workspace" "testacc" { + id = "%s" + } + + resource "bitbucket_project" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + name = "%s" + key = "%s" + } + + resource "bitbucket_repository" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + project_key = bitbucket_project.testacc.key + name = "%s" + } + + data "bitbucket_user" "testacc" { + id = "%s" + } + + resource "bitbucket_user_permission" "testacc" { + workspace = data.bitbucket_workspace.testacc.uuid + repository = bitbucket_repository.testacc.name + user = data.bitbucket_user.testacc.id + permission = "read" + }`, workspaceSlug, projectName, projectKey, repoName, user), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("bitbucket_user_permission.testacc", "repository", repoName), + resource.TestCheckResourceAttr("bitbucket_user_permission.testacc", "user", user), + resource.TestCheckResourceAttr("bitbucket_user_permission.testacc", "permission", "read"), + + resource.TestCheckResourceAttrSet("bitbucket_user_permission.testacc", "id"), + resource.TestCheckResourceAttrSet("bitbucket_user_permission.testacc", "workspace"), + ), + }, + { + ResourceName: "bitbucket_user_permission.testacc", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(state *terraform.State) (string, error) { + resources := state.Modules[0].Resources + workspaceResourceAttr := resources["data.bitbucket_workspace.testacc"].Primary.Attributes + return fmt.Sprintf("%s/%s/%s", workspaceResourceAttr["uuid"], repoName, user), nil + }, + }, + }, + }) +} + +func TestGenerateUserPermissionResourceId(t *testing.T) { + expected := "{my-workspace-uuid}-my-test-repo-{my-user-uuid}" + result := generateUserPermissionResourceId("{my-workspace-uuid}", "my-test-repo", "{my-user-uuid}") + assert.Equal(t, expected, result) +} diff --git a/docs/data-sources/bitbucket_user_permission.md b/docs/data-sources/bitbucket_user_permission.md new file mode 100644 index 0000000..692ad55 --- /dev/null +++ b/docs/data-sources/bitbucket_user_permission.md @@ -0,0 +1,22 @@ +# Data Source: bitbucket_user_permission +Use this data source to get the user permission resource, you can then reference its attributes without having to hardcode them. + +## Example Usage +```hcl +data "bitbucket_user_permission" "example" { + workspace = "{workspace-uuid}" + repository = "example-repository" + user = "{user-uuid}" +} +``` + +## Argument Reference +The following arguments are supported: +* `workspace` - (Required) The UUID (including the enclosing `{}`) of the workspace. +* `repository` - (Required) The slug of the repository. +* `user` - (Required) The UUID (including the enclosing `{}`) of the user. + +## Attribute Reference +In addition to the arguments above, the following additional attributes are exported: +* `id` - The ID of the user permission. +* `permission` - The permission this user will have. Is one of 'read', 'write', or 'admin'. diff --git a/docs/resources/bitbucket_user_permission.md b/docs/resources/bitbucket_user_permission.md new file mode 100644 index 0000000..3177340 --- /dev/null +++ b/docs/resources/bitbucket_user_permission.md @@ -0,0 +1,31 @@ +# Resource: bitbucket_user_permission +Manage a user permission for a repository within Bitbucket. + +## Example Usage +```hcl +resource "bitbucket_user_permission" "example" { + workspace = "{workspace-uuid}" + repository = "example-repository" + user = "{user-uuid}" + permission = "read" +} +``` + +## Argument Reference +The following arguments are supported: +* `workspace` - (Required) The UUID (including the enclosing `{}`) of the workspace. +* `repository` - (Required) The slug of the repository. +* `user` - (Required) The UUID (including the enclosing `{}`) of the user. +* `permission` - (Required) The permission this user will have. Is one of 'read', 'write', or 'admin'. + +## Attribute Reference +In addition to the arguments above, the following additional attributes are exported: +* `id` - The ID of the user permission. + +## Import +Bitbucket user permission can be imported with a combination of its workspace UUID, repository slug & user UUID. + +### Example using workspace UUID, repository slug & user UUID +```sh +$ terraform import bitbucket_user_permission.example "{123ab4cd-5678-9e01-f234-5678g9h01i2j}/example-repo/{123ab4cd-5678-9e01-f234-5678g9h01i2j}" +```