diff --git a/bitbucket/provider.go b/bitbucket/provider.go index a29719b9..e241b1df 100644 --- a/bitbucket/provider.go +++ b/bitbucket/provider.go @@ -85,6 +85,8 @@ func Provider() *schema.Provider { "bitbucket_project": resourceProject(), "bitbucket_project_branching_model": resourceProjectBranchingModel(), "bitbucket_project_default_reviewers": resourceProjectDefaultReviewers(), + "bitbucket_project_group_permission": resourceProjectGroupPermission(), + "bitbucket_project_user_permission": resourceProjectUserPermission(), "bitbucket_repository": resourceRepository(), "bitbucket_repository_group_permission": resourceRepositoryGroupPermission(), "bitbucket_repository_user_permission": resourceRepositoryUserPermission(), diff --git a/bitbucket/resource_project_group_permission.go b/bitbucket/resource_project_group_permission.go new file mode 100644 index 00000000..d36732ae --- /dev/null +++ b/bitbucket/resource_project_group_permission.go @@ -0,0 +1,187 @@ +package bitbucket + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "strings" + + "github.com/DrFaust92/bitbucket-go-client" + "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" +) + +type ProjectGroupPermission struct { + Permission string `json:"permission"` + Group *ProjectGroup `json:"group,omitempty"` +} + +type ProjectGroup struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Workspace bitbucket.Workspace `json:"workspace,omitempty"` +} + +func resourceProjectGroupPermission() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceProjectGroupPermissionPut, + ReadWithoutTimeout: resourceProjectGroupPermissionRead, + UpdateWithoutTimeout: resourceProjectGroupPermissionPut, + DeleteWithoutTimeout: resourceProjectGroupPermissionDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "workspace": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "project_key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "group_slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "permission": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"admin", "write", "read", "create-repo"}, false), + }, + }, + } +} + +func createProjectGroupPermission(d *schema.ResourceData) *ProjectGroupPermission { + + permission := &ProjectGroupPermission{ + Permission: d.Get("permission").(string), + } + + return permission +} + +func resourceProjectGroupPermissionPut(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(Clients).httpClient + permission := createProjectGroupPermission(d) + + payload, err := json.Marshal(permission) + if err != nil { + return diag.FromErr(err) + } + + workspace := d.Get("workspace").(string) + projectKey := d.Get("project_key").(string) + groupSlug := d.Get("group_slug").(string) + + permissionReq, err := client.Put(fmt.Sprintf("2.0/workspaces/%s/projects/%s/permissions-config/groups/%s", + workspace, + projectKey, + groupSlug, + ), bytes.NewBuffer(payload)) + + if err != nil { + return diag.FromErr(err) + } + + body, readerr := io.ReadAll(permissionReq.Body) + if readerr != nil { + return diag.FromErr(readerr) + } + + decodeerr := json.Unmarshal(body, &permission) + if decodeerr != nil { + return diag.FromErr(decodeerr) + } + + if d.IsNewResource() { + d.SetId(fmt.Sprintf("%s:%s:%s", workspace, projectKey, groupSlug)) + } + + return resourceProjectGroupPermissionRead(ctx, d, m) +} + +func resourceProjectGroupPermissionRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(Clients).httpClient + + workspace, projectKey, groupSlug, err := projectGroupPermissionId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + permissionReq, err := client.Get(fmt.Sprintf("2.0/workspaces/%s/projects/%s/permissions-config/groups/%s", + workspace, + projectKey, + groupSlug, + )) + + if permissionReq.StatusCode == http.StatusNotFound { + log.Printf("[WARN] Project Group Permission (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(err) + } + + var permission ProjectGroupPermission + + body, readerr := io.ReadAll(permissionReq.Body) + if readerr != nil { + return diag.FromErr(readerr) + } + + log.Printf("Project Group Permission raw is: %#v", string(body)) + + decodeerr := json.Unmarshal(body, &permission) + if decodeerr != nil { + return diag.FromErr(decodeerr) + } + + log.Printf("Project Group Permission decoded is: %#v", permission) + + d.Set("permission", permission.Permission) + d.Set("group_slug", permission.Group.Slug) + d.Set("workspace", permission.Group.Workspace.Slug) + d.Set("project_key", projectKey) + + return nil +} + +func resourceProjectGroupPermissionDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(Clients).httpClient + + workspace, projectKey, groupSlug, err := projectGroupPermissionId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + _, err = client.Delete(fmt.Sprintf("2.0/workspaces/%s/projects/%s/permissions-config/groups/%s", + workspace, + projectKey, + groupSlug, + )) + + return diag.FromErr(err) +} + +func projectGroupPermissionId(id string) (string, string, string, error) { + parts := strings.Split(id, ":") + + if len(parts) != 3 { + return "", "", "", fmt.Errorf("unexpected format of ID (%q), expected WORKSPACE:PROJECT-KEY:GROUP-SLUG", id) + } + + return parts[0], parts[1], parts[2], nil +} diff --git a/bitbucket/resource_project_group_permission_test.go b/bitbucket/resource_project_group_permission_test.go new file mode 100644 index 00000000..8386db0c --- /dev/null +++ b/bitbucket/resource_project_group_permission_test.go @@ -0,0 +1,108 @@ +package bitbucket + +import ( + "fmt" + "net/http" + "os" + "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" +) + +func TestAccBitbucketProjectGroupPermission_basic(t *testing.T) { + var projectGroupPermission ProjectGroupPermission + resourceName := "bitbucket_project_group_permission.test" + workspace := os.Getenv("BITBUCKET_TEAM") + rName := acctest.RandomWithPrefix("tf-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBitbucketProjectGroupPermissionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBitbucketProjectGroupPermissionConfig(workspace, rName, "read"), + Check: resource.ComposeTestCheckFunc( + testAccCheckBitbucketProjectGroupPermissionExists(resourceName, &projectGroupPermission), + resource.TestCheckResourceAttrPair(resourceName, "project_key", "bitbucket_project.test", "key"), + resource.TestCheckResourceAttrPair(resourceName, "group_slug", "bitbucket_group.test", "slug"), + resource.TestCheckResourceAttr(resourceName, "workspace", workspace), + resource.TestCheckResourceAttr(resourceName, "permission", "read"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBitbucketProjectGroupPermissionConfig(workspace, rName, "write"), + Check: resource.ComposeTestCheckFunc( + testAccCheckBitbucketProjectGroupPermissionExists(resourceName, &projectGroupPermission), + resource.TestCheckResourceAttrPair(resourceName, "project_key", "bitbucket_project.test", "key"), + resource.TestCheckResourceAttrPair(resourceName, "group_slug", "bitbucket_group.test", "slug"), + resource.TestCheckResourceAttr(resourceName, "workspace", workspace), + resource.TestCheckResourceAttr(resourceName, "permission", "write"), + ), + }, + }, + }) +} + +func testAccCheckBitbucketProjectGroupPermissionDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(Clients).httpClient + for _, rs := range s.RootModule().Resources { + if rs.Type != "bitbucket_project_group_permission" { + continue + } + + response, err := client.Get(fmt.Sprintf("2.0/repositories/%s/%s/permissions-config/groups/%s", rs.Primary.Attributes["workspace"], rs.Primary.Attributes["project_key"], rs.Primary.Attributes["group_slug"])) + + if err == nil { + return fmt.Errorf("The resource was found should have errored") + } + + if response.StatusCode != http.StatusNotFound { + return fmt.Errorf("Project Group Permission still exists") + } + + } + return nil +} + +func testAccCheckBitbucketProjectGroupPermissionExists(n string, projectGroupPermission *ProjectGroupPermission) 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 fmt.Errorf("No Project Group Permission ID is set") + } + return nil + } +} + +func testAccBitbucketProjectGroupPermissionConfig(workspace, rName, permission string) string { + return fmt.Sprintf(` +resource "bitbucket_project" "test" { + owner = %[1]q + name = %[2]q + key = "GRPPERM" +} + +resource "bitbucket_group" "test" { + workspace = %[1]q + name = %[2]q +} + +resource "bitbucket_project_group_permission" "test" { + workspace = %[1]q + project_key = bitbucket_project.test.key + group_slug = bitbucket_group.test.slug + permission = %[3]q +} +`, workspace, rName, permission) +} diff --git a/bitbucket/resource_project_user_permission.go b/bitbucket/resource_project_user_permission.go new file mode 100644 index 00000000..9cc39f41 --- /dev/null +++ b/bitbucket/resource_project_user_permission.go @@ -0,0 +1,184 @@ +package bitbucket + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "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" +) + +type ProjectUserPermission struct { + Permission string `json:"permission"` + User *ProjectUser `json:"user,omitempty"` +} + +type ProjectUser struct { + UUID string `json:"uuid,omitempty"` +} + +func resourceProjectUserPermission() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceProjectUserPermissionPut, + ReadWithoutTimeout: resourceProjectUserPermissionRead, + UpdateWithoutTimeout: resourceProjectUserPermissionPut, + DeleteWithoutTimeout: resourceProjectUserPermissionDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "workspace": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "project_key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "user_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "permission": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"admin", "write", "read", "create-repo"}, false), + }, + }, + } +} + +func createProjectUserPermission(d *schema.ResourceData) *ProjectUserPermission { + + permission := &ProjectUserPermission{ + Permission: d.Get("permission").(string), + } + + return permission +} + +func resourceProjectUserPermissionPut(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(Clients).httpClient + permission := createProjectUserPermission(d) + + payload, err := json.Marshal(permission) + if err != nil { + return diag.FromErr(err) + } + + workspace := d.Get("workspace").(string) + projectKey := d.Get("project_key").(string) + userSlug := d.Get("user_id").(string) + + permissionReq, err := client.Put(fmt.Sprintf("2.0/workspaces/%s/projects/%s/permissions-config/users/%s", + workspace, + projectKey, + userSlug, + ), bytes.NewBuffer(payload)) + + if err != nil { + return diag.FromErr(err) + } + + body, readerr := io.ReadAll(permissionReq.Body) + if readerr != nil { + return diag.FromErr(readerr) + } + + decodeerr := json.Unmarshal(body, &permission) + if decodeerr != nil { + return diag.FromErr(decodeerr) + } + + if d.IsNewResource() { + d.SetId(fmt.Sprintf("%s:%s:%s", workspace, projectKey, userSlug)) + } + + return resourceProjectUserPermissionRead(ctx, d, m) +} + +func resourceProjectUserPermissionRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(Clients).httpClient + + workspace, projectKey, userSlug, err := ProjectUserPermissionId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + permissionReq, err := client.Get(fmt.Sprintf("2.0/workspaces/%s/projects/%s/permissions-config/users/%s", + workspace, + projectKey, + userSlug, + )) + + if permissionReq.StatusCode == http.StatusNotFound { + log.Printf("[WARN] Project User Permission (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(err) + } + + var permission ProjectUserPermission + + body, readerr := io.ReadAll(permissionReq.Body) + if readerr != nil { + return diag.FromErr(readerr) + } + + log.Printf("Project User Permission raw is: %#v", string(body)) + + decodeerr := json.Unmarshal(body, &permission) + if decodeerr != nil { + return diag.FromErr(decodeerr) + } + + log.Printf("Project User Permission decoded is: %#v", permission) + + d.Set("permission", permission.Permission) + d.Set("user_id", permission.User.UUID) + d.Set("workspace", workspace) + d.Set("project_key", projectKey) + + return nil +} + +func resourceProjectUserPermissionDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(Clients).httpClient + + workspace, projectKey, userSlug, err := ProjectUserPermissionId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + _, err = client.Delete(fmt.Sprintf("2.0/workspaces/%s/projects/%s/permissions-config/users/%s", + workspace, + projectKey, + userSlug, + )) + + return diag.FromErr(err) +} + +func ProjectUserPermissionId(id string) (string, string, string, error) { + parts := strings.Split(id, ":") + + if len(parts) != 3 { + return "", "", "", fmt.Errorf("unexpected format of ID (%q), expected WORKSPACE:REPO-SLUG:GROUP-SLUG", id) + } + + return parts[0], parts[1], parts[2], nil +} diff --git a/bitbucket/resource_project_user_permission_test.go b/bitbucket/resource_project_user_permission_test.go new file mode 100644 index 00000000..e62ea22b --- /dev/null +++ b/bitbucket/resource_project_user_permission_test.go @@ -0,0 +1,105 @@ +package bitbucket + +import ( + "fmt" + "net/http" + "os" + "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" +) + +func TestAccBitbucketProjectUserPermission_basic(t *testing.T) { + var projectUserPermission ProjectUserPermission + resourceName := "bitbucket_project_user_permission.test" + workspace := os.Getenv("BITBUCKET_TEAM") + rName := acctest.RandomWithPrefix("tf-test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBitbucketProjectUserPermissionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBitbucketProjectUserPermissionConfig(workspace, rName, "read"), + Check: resource.ComposeTestCheckFunc( + testAccCheckBitbucketProjectUserPermissionExists(resourceName, &projectUserPermission), + resource.TestCheckResourceAttrPair(resourceName, "project_key", "bitbucket_project.test", "key"), + resource.TestCheckResourceAttrPair(resourceName, "user_id", "data.bitbucket_current_user.test", "id"), + resource.TestCheckResourceAttr(resourceName, "workspace", workspace), + resource.TestCheckResourceAttr(resourceName, "permission", "read"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBitbucketProjectUserPermissionConfig(workspace, rName, "write"), + Check: resource.ComposeTestCheckFunc( + testAccCheckBitbucketProjectUserPermissionExists(resourceName, &projectUserPermission), + resource.TestCheckResourceAttrPair(resourceName, "project_key", "bitbucket_project.test", "key"), + resource.TestCheckResourceAttrPair(resourceName, "user_id", "data.bitbucket_current_user.test", "id"), + resource.TestCheckResourceAttr(resourceName, "workspace", workspace), + resource.TestCheckResourceAttr(resourceName, "permission", "write"), + ), + }, + }, + }) +} + +func testAccCheckBitbucketProjectUserPermissionDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(Clients).httpClient + for _, rs := range s.RootModule().Resources { + if rs.Type != "bitbucket_project_user_permission" { + continue + } + + response, err := client.Get(fmt.Sprintf("2.0/repositories/%s/%s/permissions-config/users/%s", rs.Primary.Attributes["workspace"], rs.Primary.Attributes["project_key"], rs.Primary.Attributes["user_id"])) + + if err == nil { + return fmt.Errorf("The resource was found should have errored") + } + + if response.StatusCode != http.StatusNotFound { + return fmt.Errorf("Project User Permission still exists") + } + + } + return nil +} + +func testAccCheckBitbucketProjectUserPermissionExists(n string, projectUserPermission *ProjectUserPermission) 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 fmt.Errorf("No Project User Permission ID is set") + } + return nil + } +} + +func testAccBitbucketProjectUserPermissionConfig(workspace, rName, permission string) string { + return fmt.Sprintf(` +resource "bitbucket_project" "test" { + owner = %[1]q + name = %[2]q + key = "USERPERM" +} + +data "bitbucket_current_user" "test" {} + +resource "bitbucket_project_user_permission" "test" { + workspace = %[1]q + project_key = bitbucket_project.test.key + user_id = data.bitbucket_current_user.test.id + permission = %[3]q +} +`, workspace, rName, permission) +} diff --git a/docs/resources/project_group_permission.md b/docs/resources/project_group_permission.md new file mode 100644 index 00000000..6da6fcd4 --- /dev/null +++ b/docs/resources/project_group_permission.md @@ -0,0 +1,45 @@ +--- +layout: "bitbucket" +page_title: "Bitbucket: bitbucket_project_group_permission" +sidebar_current: "docs-bitbucket-resource-project-group-permission" +description: |- + Provides a Bitbucket Repository Group Permission Resource +--- + +# bitbucket\_project\_group\_permission + +Provides a Bitbucket Repository Group Permission Resource. + +This allows you set explicit group permission for a project. + +OAuth2 Scopes: `project:admin` + +Note: can only be used when authenticating with Bitbucket Cloud using an _app password_. Authenticating via an OAuth flow gives a 403 error due to a [restriction in the Bitbucket Cloud API](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/#api-repositories-workspace-project-key-permissions-config-groups-group-slug-put). + +## Example Usage + +```hcl +resource "bitbucket_project_group_permission" "example" { + workspace = "example" + project_key = bitbucket_project.example.key + group_slug = bitbucket_group.example.slug + permission = "read" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `workspace` - (Required) The workspace id. +* `project_key` - (Required) The project key. +* `group_slug` - (Required) Slug of the requested group. +* `permission` - (Required) Permissions can be one of `read`, `write`, `create-repo`, and `admin`. + +## Import + +Repository Group Permissions can be imported using their `workspace:project-key:group-slug` ID, e.g. + +```sh +terraform import bitbucket_project_group_permission.example workspace:project-key:group-slug +``` diff --git a/docs/resources/project_user_permission.md b/docs/resources/project_user_permission.md new file mode 100644 index 00000000..f42da31c --- /dev/null +++ b/docs/resources/project_user_permission.md @@ -0,0 +1,43 @@ +--- +layout: "bitbucket" +page_title: "Bitbucket: bitbucket_project_user_permission" +sidebar_current: "docs-bitbucket-resource-project-user-permission" +description: |- + Provides a Bitbucket Repository User Permission Resource +--- + +# bitbucket\_project\_user\_permission + +Provides a Bitbucket Repository User Permission Resource. + +This allows you set explicit user permission for a project. + +OAuth2 Scopes: `project:admin` + +## Example Usage + +```hcl +resource "bitbucket_project_user_permission" "example" { + workspace = "example" + project_key = bitbucket_project.example.key + user_id = "user-id" + permission = "read" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `workspace` - (Required) The workspace id. +* `project_key` - (Required) The project key. +* `user_id` - (Required) The UUID of the user. +* `permission` - (Required) Permissions can be one of `read`, `write`, `create-repo`, and `admin`. + +## Import + +Repository User Permissions can be imported using their `workspace:project-key:user-id` ID, e.g. + +```sh +terraform import bitbucket_project_user_permission.example workspace:project-key:user-id +```