From b9656c1539286b9d8f94060917fcb1b007469af3 Mon Sep 17 00:00:00 2001 From: rberecka Date: Mon, 7 Aug 2023 11:16:37 -0700 Subject: [PATCH] TF-5569 add support for custom project permissions (#983) * Add customizable project level permissions in dataSourceTFETeamProjectAccess * Add customizable project level permission in resourceTFETeamProjectAccess --- CHANGELOG.md | 3 + go.mod | 2 +- go.sum | 4 +- tfe/data_source_team_project_access.go | 75 +++- tfe/data_source_team_project_access_test.go | 89 +++++ tfe/resource_tfe_team_project_access.go | 312 +++++++++++++++- tfe/resource_tfe_team_project_access_test.go | 332 ++++++++++++++++++ .../docs/r/team_project_access.html.markdown | 69 +++- 8 files changed, 875 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b89d03b5e..b8a6ccda5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ FEATURES: * `d/tfe_saml_settings`: Add PrivateKey (sensitive), SignatureSigningMethod, and SignatureDigestMethod attributes, by @karvounis-form3 [970](https://github.com/hashicorp/terraform-provider-tfe/pull/970) * **New Resource**: `r/tfe_project_policy_set` is a new resource to attach/detach an existing `project` to an existing `policy set`, by @Netra2104 [972](https://github.com/hashicorp/terraform-provider-tfe/pull/972) * `d/tfe_policy_set`: Add `project_ids` attribute, by @Netra2104 [974](https://github.com/hashicorp/terraform-provider-tfe/pull/974/files) +* `r/tfe_team_project_access`: Add a `custom` option to the `access` attribute as well as `project_access` and `workspace_access` attributes with +various customizable permissions options to apply to a project and all of the workspaces therein, by @rberecka [983](https://github.com/hashicorp/terraform-provider-tfe/pull/983) +* `d/team_project_access`: Add a `custom` option to the `access` attribute as well as `project_access` and `workspace_access` attributes, by @rberecka [983](https://github.com/hashicorp/terraform-provider-tfe/pull/983) NOTES: * The provider is now using go-tfe [v1.30.0](https://github.com/hashicorp/go-tfe/releases/tag/v1.30.0), by @karvounis-form3 [970](https://github.com/hashicorp/terraform-provider-tfe/pull/970) diff --git a/go.mod b/go.mod index 4b46e7cc8..0a78cb741 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.4 // indirect github.com/hashicorp/go-slug v0.12.0 - github.com/hashicorp/go-tfe v1.31.0 + github.com/hashicorp/go-tfe v1.32.0 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.17.0 // indirect diff --git a/go.sum b/go.sum index 2b6aae7e6..c2d5144f9 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZn github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-slug v0.12.0 h1:y1ArGp5RFF85uvD8nq5VZug/bup/kGN5Ft4xFOQ5GPM= github.com/hashicorp/go-slug v0.12.0/go.mod h1:JZVtycnZZbiJ4oxpJ/zfhyfBD8XxT4f0uOSyjNLCqFY= -github.com/hashicorp/go-tfe v1.31.0 h1:R1CokrAVBHxrsvRw1vKes7RQxTRTWcula7gjQK7Jfsk= -github.com/hashicorp/go-tfe v1.31.0/go.mod h1:vcfy2u52JQ4sYLFi941qcQXQYfUq2RjEW466tZ+m97Y= +github.com/hashicorp/go-tfe v1.32.0 h1:wyUQJHPrqF5IwD5Y4YJFTlU3A08LXoJ2PLF7x80febU= +github.com/hashicorp/go-tfe v1.32.0/go.mod h1:vcfy2u52JQ4sYLFi941qcQXQYfUq2RjEW466tZ+m97Y= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= diff --git a/tfe/data_source_team_project_access.go b/tfe/data_source_team_project_access.go index 82595f315..8db748213 100644 --- a/tfe/data_source_team_project_access.go +++ b/tfe/data_source_team_project_access.go @@ -5,6 +5,7 @@ package tfe import ( "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" tfe "github.com/hashicorp/go-tfe" @@ -30,18 +31,88 @@ func dataSourceTFETeamProjectAccess() *schema.Resource { Type: schema.TypeString, Required: true, }, + + "project_access": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "settings": { + Type: schema.TypeString, + Computed: true, + }, + + "teams": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "workspace_access": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "create": { + Type: schema.TypeBool, + Computed: true, + }, + + "locking": { + Type: schema.TypeBool, + Computed: true, + }, + + "move": { + Type: schema.TypeBool, + Computed: true, + }, + + "delete": { + Type: schema.TypeBool, + Computed: true, + }, + + "run_tasks": { + Type: schema.TypeBool, + Computed: true, + }, + + "runs": { + Type: schema.TypeString, + Computed: true, + }, + + "sentinel_mocks": { + Type: schema.TypeString, + Computed: true, + }, + + "state_versions": { + Type: schema.TypeString, + Computed: true, + }, + + "variables": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, }, } } func dataSourceTFETeamProjectAccessRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { config := meta.(ConfiguredClient) - // Get the team ID. teamID := d.Get("team_id").(string) - // Get the project projectID := d.Get("project_id").(string) + proj, err := config.Client.Projects.Read(ctx, projectID) if err != nil { return diag.Errorf( diff --git a/tfe/data_source_team_project_access_test.go b/tfe/data_source_team_project_access_test.go index 4f02bc7c4..be36e7822 100644 --- a/tfe/data_source_team_project_access_test.go +++ b/tfe/data_source_team_project_access_test.go @@ -37,6 +37,55 @@ func TestAccTFETeamProjectAccessDataSource_basic(t *testing.T) { }) } +func TestAccTFETeamProjectCustomAccessDataSource_basic(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, orgCleanup := createBusinessOrganization(t, tfeClient) + t.Cleanup(orgCleanup) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamProjectCustomAccessDataSourceConfig(org.Name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.tfe_team_project_access.foobar_custom", "id"), + resource.TestCheckResourceAttrSet("data.tfe_team_project_access.foobar_custom", "team_id"), + resource.TestCheckResourceAttrSet("data.tfe_team_project_access.foobar_custom", "project_id"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "access", "custom"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "project_access.0.settings", "delete"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "project_access.0.teams", "manage"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "workspace_access.0.state_versions", "write"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "workspace_access.0.sentinel_mocks", "read"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "workspace_access.0.runs", "apply"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "workspace_access.0.variables", "write"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "workspace_access.0.create", "true"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "workspace_access.0.locking", "true"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "workspace_access.0.move", "true"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "workspace_access.0.delete", "false"), + resource.TestCheckResourceAttr( + "data.tfe_team_project_access.foobar_custom", "workspace_access.0.run_tasks", "false"), + ), + }, + }, + }) +} + func testAccTFETeamProjectAccessDataSourceConfig(organization string) string { return fmt.Sprintf(` resource "tfe_team" "foobar" { @@ -61,3 +110,43 @@ data "tfe_team_project_access" "foobar" { depends_on = [tfe_team_project_access.foobar] }`, organization, organization) } + +func testAccTFETeamProjectCustomAccessDataSourceConfig(organization string) string { + return fmt.Sprintf(` +resource "tfe_team" "foobar_custom" { + name = "team-test2" + organization = "%s" +} + +resource "tfe_project" "foobar_custom" { + name = "projecttest2" + organization = "%s" +} + +resource "tfe_team_project_access" "foobar_custom" { + access = "custom" + team_id = tfe_team.foobar_custom.id + project_id = tfe_project.foobar_custom.id + project_access { + settings = "delete" + teams = "manage" + } + workspace_access { + state_versions = "write" + sentinel_mocks = "read" + runs = "apply" + variables = "write" + create = true + locking = true + move = true + delete = false + run_tasks = false + } +} + +data "tfe_team_project_access" "foobar_custom" { + team_id = tfe_team.foobar_custom.id + project_id = tfe_project.foobar_custom.id + depends_on = [tfe_team_project_access.foobar_custom] +}`, organization, organization) +} diff --git a/tfe/resource_tfe_team_project_access.go b/tfe/resource_tfe_team_project_access.go index 1294672af..a72ecc7c3 100644 --- a/tfe/resource_tfe_team_project_access.go +++ b/tfe/resource_tfe_team_project_access.go @@ -6,6 +6,7 @@ package tfe import ( "context" "errors" + "fmt" "log" tfe "github.com/hashicorp/go-tfe" @@ -26,6 +27,7 @@ func resourceTFETeamProjectAccess() *schema.Resource { SchemaVersion: 1, + CustomizeDiff: checkForCustomPermissions, Schema: map[string]*schema.Schema{ "access": { Type: schema.TypeString, @@ -36,6 +38,7 @@ func resourceTFETeamProjectAccess() *schema.Resource { string(tfe.TeamProjectAccessWrite), string(tfe.TeamProjectAccessMaintain), string(tfe.TeamProjectAccessRead), + string(tfe.TeamProjectAccessCustom), }, false, ), @@ -56,6 +59,138 @@ func resourceTFETeamProjectAccess() *schema.Resource { "must be a valid project ID (prj-)", ), }, + + "project_access": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "settings": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice( + []string{ + string(tfe.ProjectSettingsPermissionRead), + string(tfe.ProjectSettingsPermissionUpdate), + string(tfe.ProjectSettingsPermissionDelete), + }, + false, + ), + }, + + "teams": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice( + []string{ + string(tfe.ProjectTeamsPermissionNone), + string(tfe.ProjectTeamsPermissionRead), + string(tfe.ProjectTeamsPermissionManage), + }, + false, + ), + }, + }, + }, + }, + + "workspace_access": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "create": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "locking": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "move": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "delete": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "run_tasks": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "runs": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice( + []string{ + string(tfe.WorkspaceRunsPermissionRead), + string(tfe.WorkspaceRunsPermissionPlan), + string(tfe.WorkspaceRunsPermissionApply), + }, + false, + ), + }, + + "sentinel_mocks": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice( + []string{ + string(tfe.WorkspaceSentinelMocksPermissionNone), + string(tfe.WorkspaceSentinelMocksPermissionRead), + }, + false, + ), + }, + + "state_versions": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice( + []string{ + string(tfe.WorkspaceStateVersionsPermissionNone), + string(tfe.WorkspaceStateVersionsPermissionReadOutputs), + string(tfe.WorkspaceStateVersionsPermissionRead), + string(tfe.WorkspaceStateVersionsPermissionWrite), + }, + false, + ), + }, + + "variables": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice( + []string{ + string(tfe.WorkspaceVariablesPermissionNone), + string(tfe.WorkspaceVariablesPermissionRead), + string(tfe.WorkspaceVariablesPermissionWrite), + }, + false, + ), + }, + }, + }, + }, }, } } @@ -83,9 +218,55 @@ func resourceTFETeamProjectAccessCreate(ctx context.Context, d *schema.ResourceD // Create a new options struct. options := tfe.TeamProjectAccessAddOptions{ - Access: *tfe.ProjectAccess(tfe.TeamProjectAccessType(access)), - Team: tm, - Project: proj, + Access: *tfe.ProjectAccess(tfe.TeamProjectAccessType(access)), + Team: tm, + Project: proj, + ProjectAccess: &tfe.TeamProjectAccessProjectPermissionsOptions{}, + WorkspaceAccess: &tfe.TeamProjectAccessWorkspacePermissionsOptions{}, + } + + if v, ok := d.GetOk("project_access.0.settings"); ok { + options.ProjectAccess.Settings = tfe.ProjectSettingsPermission(tfe.ProjectSettingsPermissionType(v.(string))) + } + + if v, ok := d.GetOk("project_access.0.teams"); ok { + options.ProjectAccess.Teams = tfe.ProjectTeamsPermission(tfe.ProjectTeamsPermissionType(v.(string))) + } + + if v, ok := d.GetOk("workspace_access.0.state_versions"); ok { + options.WorkspaceAccess.StateVersions = tfe.WorkspaceStateVersionsPermission(tfe.WorkspaceStateVersionsPermissionType(v.(string))) + } + + if v, ok := d.GetOk("workspace_access.0.sentinel_mocks"); ok { + options.WorkspaceAccess.SentinelMocks = tfe.WorkspaceSentinelMocksPermission(tfe.WorkspaceSentinelMocksPermissionType(v.(string))) + } + + if v, ok := d.GetOk("workspace_access.0.runs"); ok { + options.WorkspaceAccess.Runs = tfe.WorkspaceRunsPermission(tfe.WorkspaceRunsPermissionType(v.(string))) + } + + if v, ok := d.GetOk("workspace_access.0.variables"); ok { + options.WorkspaceAccess.Variables = tfe.WorkspaceVariablesPermission(tfe.WorkspaceVariablesPermissionType(v.(string))) + } + + if v, ok := d.GetOk("workspace_access.0.create"); ok { + options.WorkspaceAccess.Create = tfe.Bool(v.(bool)) + } + + if v, ok := d.GetOk("workspace_access.0.locking"); ok { + options.WorkspaceAccess.Locking = tfe.Bool(v.(bool)) + } + + if v, ok := d.GetOk("workspace_access.0.move"); ok { + options.WorkspaceAccess.Move = tfe.Bool(v.(bool)) + } + + if v, ok := d.GetOk("workspace_access.0.delete"); ok { + options.WorkspaceAccess.Delete = tfe.Bool(v.(bool)) + } + + if v, ok := d.GetOk("workspace_access.0.run_tasks"); ok { + options.WorkspaceAccess.RunTasks = tfe.Bool(v.(bool)) } log.Printf("[DEBUG] Give team %s %s access to project: %s", tm.Name, access, proj.Name) @@ -129,6 +310,31 @@ func resourceTFETeamProjectAccessRead(ctx context.Context, d *schema.ResourceDat d.Set("project_id", "") } + project_access := []map[string]interface{}{{ + "settings": tmAccess.ProjectAccess.ProjectSettingsPermission, + "teams": tmAccess.ProjectAccess.ProjectTeamsPermission, + }} + + workspace_access := []map[string]interface{}{{ + "state_versions": tmAccess.WorkspaceAccess.WorkspaceStateVersionsPermission, + "sentinel_mocks": tmAccess.WorkspaceAccess.WorkspaceSentinelMocksPermission, + "runs": tmAccess.WorkspaceAccess.WorkspaceRunsPermission, + "variables": tmAccess.WorkspaceAccess.WorkspaceVariablesPermission, + "create": tmAccess.WorkspaceAccess.WorkspaceCreatePermission, + "locking": tmAccess.WorkspaceAccess.WorkspaceLockingPermission, + "move": tmAccess.WorkspaceAccess.WorkspaceMovePermission, + "delete": tmAccess.WorkspaceAccess.WorkspaceDeletePermission, + "run_tasks": tmAccess.WorkspaceAccess.WorkspaceRunTasksPermission, + }} + + if err := d.Set("project_access", project_access); err != nil { + return diag.Errorf("Error setting configuration of team project access %s: %v", d.Id(), err) + } + + if err := d.Set("workspace_access", workspace_access); err != nil { + return diag.Errorf("Error setting configuration of team workspace access %s: %v", d.Id(), err) + } + return nil } @@ -136,12 +342,88 @@ func resourceTFETeamProjectAccessUpdate(ctx context.Context, d *schema.ResourceD config := meta.(ConfiguredClient) // create an options struct - options := tfe.TeamProjectAccessUpdateOptions{} + options := tfe.TeamProjectAccessUpdateOptions{ + ProjectAccess: &tfe.TeamProjectAccessProjectPermissionsOptions{}, + WorkspaceAccess: &tfe.TeamProjectAccessWorkspacePermissionsOptions{}, + } // Set access level access := d.Get("access").(string) options.Access = tfe.ProjectAccess(tfe.TeamProjectAccessType(access)) + if d.HasChange("project_access.0.settings") { + if settings, ok := d.GetOk("project_access.0.settings"); ok { + projectSettingsPermissionType := tfe.ProjectSettingsPermissionType(settings.(string)) + options.ProjectAccess.Settings = &projectSettingsPermissionType + } + } + + if d.HasChange("project_access.0.teams") { + if teams, ok := d.GetOk("project_access.0.teams"); ok { + projectTeamsPermissionType := tfe.ProjectTeamsPermissionType(teams.(string)) + options.ProjectAccess.Teams = &projectTeamsPermissionType + } + } + + if d.HasChange("workspace_access.0.state_versions") { + if state_versions, ok := d.GetOk("workspace_access.0.state_versions"); ok { + workspaceStateVersionsPermissionType := tfe.WorkspaceStateVersionsPermissionType(state_versions.(string)) + options.WorkspaceAccess.StateVersions = &workspaceStateVersionsPermissionType + } + } + + if d.HasChange("workspace_access.0.sentinel_mocks") { + if sentinel_mocks, ok := d.GetOk("workspace_access.0.sentinel_mocks"); ok { + workspaceSentinelMocksPermissionType := tfe.WorkspaceSentinelMocksPermissionType(sentinel_mocks.(string)) + options.WorkspaceAccess.SentinelMocks = &workspaceSentinelMocksPermissionType + } + } + + if d.HasChange("workspace_access.0.runs") { + if runs, ok := d.GetOk("workspace_access.0.runs"); ok { + workspaceRunsPermissionType := tfe.WorkspaceRunsPermissionType(runs.(string)) + options.WorkspaceAccess.Runs = &workspaceRunsPermissionType + } + } + + if d.HasChange("workspace_access.0.variables") { + if variables, ok := d.GetOk("workspace_access.0.variables"); ok { + workspaceVariablesPermissionType := tfe.WorkspaceVariablesPermissionType(variables.(string)) + options.WorkspaceAccess.Variables = &workspaceVariablesPermissionType + } + } + + if d.HasChange("workspace_access.0.create") { + if create, ok := d.GetOkExists("workspace_access.0.create"); ok { + create := tfe.Bool(create.(bool)) + options.WorkspaceAccess.Create = create + } + } + + if d.HasChange("workspace_access.0.locking") { + if locking, ok := d.GetOkExists("workspace_access.0.locking"); ok { + options.WorkspaceAccess.Locking = tfe.Bool(locking.(bool)) + } + } + + if d.HasChange("workspace_access.0.move") { + if move, ok := d.GetOkExists("workspace_access.0.move"); ok { + options.WorkspaceAccess.Move = tfe.Bool(move.(bool)) + } + } + + if d.HasChange("workspace_access.0.delete") { + if delete, ok := d.GetOkExists("workspace_access.0.delete"); ok { + options.WorkspaceAccess.Delete = tfe.Bool(delete.(bool)) + } + } + + if d.HasChange("workspace_access.0.run_tasks") { + if run_tasks, ok := d.GetOkExists("workspace_access.0.run_tasks"); ok { + options.WorkspaceAccess.RunTasks = tfe.Bool(run_tasks.(bool)) + } + } + log.Printf("[DEBUG] Update team project access: %s", d.Id()) _, err := config.Client.TeamProjectAccess.Update(ctx, d.Id(), options) if err != nil { @@ -149,7 +431,7 @@ func resourceTFETeamProjectAccessUpdate(ctx context.Context, d *schema.ResourceD "Error updating team project access %s: %v", d.Id(), err) } - return nil + return resourceTFETeamProjectAccessRead(ctx, d, meta) } func resourceTFETeamProjectAccessDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -166,3 +448,23 @@ func resourceTFETeamProjectAccessDelete(ctx context.Context, d *schema.ResourceD return nil } + +// You cannot set custom permissions when access level is not "custom" +func checkForCustomPermissions(_ context.Context, d *schema.ResourceDiff, meta interface{}) error { + + if access, ok := d.GetOk("access"); ok && access != "custom" { + // is an empty [] if project_access is not in the config + project_access := d.GetRawConfig().GetAttr("project_access").AsValueSet().Values() + if len(project_access) != 0 { + return fmt.Errorf("you can only set project_access permissions with access level custom") + } + + // is an empty [] if project_access is not in the config + workspace_access := d.GetRawConfig().GetAttr("workspace_access").AsValueSet().Values() + if len(workspace_access) != 0 { + return fmt.Errorf("you can only set workspace_access permissions with access level custom") + } + } + + return nil +} diff --git a/tfe/resource_tfe_team_project_access_test.go b/tfe/resource_tfe_team_project_access_test.go index 1888ebd09..3d7f3d183 100644 --- a/tfe/resource_tfe_team_project_access_test.go +++ b/tfe/resource_tfe_team_project_access_test.go @@ -6,6 +6,7 @@ package tfe import ( "fmt" "math/rand" + "regexp" "testing" "time" @@ -38,6 +39,39 @@ func TestAccTFETeamProjectAccess(t *testing.T) { } } +func TestAccTFETeamProjectCustomAccess(t *testing.T) { + tmAccess := &tfe.TeamProjectAccess{} + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + access := tfe.TeamProjectAccessCustom + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFETeamProjectAccessDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamProjectCustomAccess(rInt, access), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamProjectAccessExists( + "tfe_team_project_access.custom_foobar", tmAccess), + testAccCheckTFETeamProjectAccessAttributesAccessIs(tmAccess, access), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "access", string(access)), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.settings", "delete"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.teams", "manage"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.state_versions", "write"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.sentinel_mocks", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.runs", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.variables", "write"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.create", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.locking", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.move", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.delete", "false"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.run_tasks", "false"), + ), + }, + }, + }) +} func TestAccTFETeamProjectAccess_import(t *testing.T) { rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() @@ -58,6 +92,151 @@ func TestAccTFETeamProjectAccess_import(t *testing.T) { }) } +func TestAccTFETeamProjectCustomAccess_import(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + tmAccess := &tfe.TeamProjectAccess{} + access := tfe.TeamProjectAccessCustom + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFETeamProjectAccessDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamProjectCustomAccess(rInt, access), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamProjectAccessExists( + "tfe_team_project_access.custom_foobar", tmAccess), + testAccCheckTFETeamProjectAccessAttributesAccessIs(tmAccess, access), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "access", string(access)), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.settings", "delete"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.teams", "manage"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.state_versions", "write"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.sentinel_mocks", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.runs", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.variables", "write"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.create", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.locking", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.move", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.delete", "false"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.run_tasks", "false"), + ), + }, + { + ResourceName: "tfe_team_project_access.custom_foobar", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccTFETeamProjectCustomAccess_full_update(t *testing.T) { + tmAccess := &tfe.TeamProjectAccess{} + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + access := tfe.TeamProjectAccessCustom + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamProjectCustomAccess(rInt, access), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamProjectAccessExists( + "tfe_team_project_access.custom_foobar", tmAccess), + testAccCheckTFETeamProjectAccessAttributesAccessIs(tmAccess, access), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "access", string(access)), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.settings", "delete"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.teams", "manage"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.state_versions", "write"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.sentinel_mocks", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.runs", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.variables", "write"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.create", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.locking", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.move", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.delete", "false"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.run_tasks", "false"), + ), + }, + { + Config: testAccTFETeamProjectCustomAccess_full_update(rInt, access), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamProjectAccessExists( + "tfe_team_project_access.custom_foobar", tmAccess), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "access", string(access)), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.settings", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.teams", "none"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.state_versions", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.sentinel_mocks", "none"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.runs", "apply"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.variables", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.create", "false"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.locking", "false"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.move", "false"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.delete", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.run_tasks", "true"), + ), + }, + }, + }) +} + +func TestAccTFETeamProjectCustomAccess_partial_update(t *testing.T) { + tmAccess := &tfe.TeamProjectAccess{} + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + access := tfe.TeamProjectAccessCustom + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamProjectCustomAccess(rInt, access), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamProjectAccessExists( + "tfe_team_project_access.custom_foobar", tmAccess), + testAccCheckTFETeamProjectAccessAttributesAccessIs(tmAccess, access), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "access", string(access)), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.settings", "delete"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.teams", "manage"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.state_versions", "write"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.sentinel_mocks", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.variables", "write"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.create", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.locking", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.move", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.delete", "false"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.run_tasks", "false"), + ), + }, + { + Config: testAccTFETeamProjectCustomAccess_partial_update(rInt, access), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFETeamProjectAccessExists( + "tfe_team_project_access.custom_foobar", tmAccess), + testAccCheckTFETeamProjectAccessAttributesAccessIs(tmAccess, access), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "access", string(access)), + // changed access levels + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.settings", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.delete", "true"), + // unchanged access levels + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "project_access.0.teams", "manage"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.state_versions", "write"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.sentinel_mocks", "read"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.variables", "write"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.create", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.locking", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.move", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.delete", "true"), + resource.TestCheckResourceAttr("tfe_team_project_access.custom_foobar", "workspace_access.0.run_tasks", "false"), + ), + }, + }, + }) +} + func testAccCheckTFETeamProjectAccessExists( n string, tmAccess *tfe.TeamProjectAccess) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -87,6 +266,22 @@ func testAccCheckTFETeamProjectAccessExists( } } +func TestAccTFETeamProjectCustomAccess_invalid_custom_access(t *testing.T) { + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFETeamProjectAccessDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFETeamProjectCustomAccess_invalid_custom_config(rInt), + ExpectError: regexp.MustCompile("you can only set workspace_access permissions with access level custom"), + }, + }, + }) +} + func testAccCheckTFETeamProjectAccessAttributesAccessIs(tmAccess *tfe.TeamProjectAccess, access tfe.TeamProjectAccessType) resource.TestCheckFunc { return func(s *terraform.State) error { if tmAccess.Access != access { @@ -140,3 +335,140 @@ resource "tfe_team_project_access" "foobar" { project_id = tfe_project.foobar.id }`, rInt, access) } + +func testAccTFETeamProjectCustomAccess(rInt int, access tfe.TeamProjectAccessType) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar_2" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_team" "foobar_2" { + name = "team-test" + organization = tfe_organization.foobar_2.id +} + +resource "tfe_project" "foobar_2" { + name = "projecttest" + organization = tfe_organization.foobar_2.id +} + +resource "tfe_team_project_access" "custom_foobar" { + access = "%s" + team_id = tfe_team.foobar_2.id + project_id = tfe_project.foobar_2.id + project_access { + settings = "delete" + teams = "manage" + } + workspace_access { + state_versions = "write" + sentinel_mocks = "read" + runs = "read" + variables = "write" + create = true + locking = true + move = true + delete = false + run_tasks = false + } + +}`, rInt, access) +} + +func testAccTFETeamProjectCustomAccess_full_update(rInt int, access tfe.TeamProjectAccessType) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar_2" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_team" "foobar_2" { + name = "team-test" + organization = tfe_organization.foobar_2.id +} + +resource "tfe_project" "foobar_2" { + name = "projecttest" + organization = tfe_organization.foobar_2.id +} + +resource "tfe_team_project_access" "custom_foobar" { + access = "%s" + team_id = tfe_team.foobar_2.id + project_id = tfe_project.foobar_2.id + project_access { + settings = "read" + teams = "none" + } + workspace_access { + state_versions = "read" + sentinel_mocks = "none" + runs = "apply" + variables = "read" + create = false + locking = false + move = false + delete = true + run_tasks = true + } +}`, rInt, access) +} + +func testAccTFETeamProjectCustomAccess_partial_update(rInt int, access tfe.TeamProjectAccessType) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar_2" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_team" "foobar_2" { + name = "team-test" + organization = tfe_organization.foobar_2.id +} + +resource "tfe_project" "foobar_2" { + name = "projecttest" + organization = tfe_organization.foobar_2.id +} + +resource "tfe_team_project_access" "custom_foobar" { + access = "%s" + team_id = tfe_team.foobar_2.id + project_id = tfe_project.foobar_2.id + project_access { + settings = "read" + } + workspace_access { + delete = true + } +}`, rInt, access) +} + +func testAccTFETeamProjectCustomAccess_invalid_custom_config(rInt int) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar_2" { + name = "tst-terraform-%d" + email = "admin@company.com" +} + +resource "tfe_team" "foobar_invalid" { + name = "team-test" + organization = tfe_organization.foobar_2.id +} + +resource "tfe_project" "foobar_invalid" { + name = "projecttest" + organization = tfe_organization.foobar_2.id +} + +resource "tfe_team_project_access" "custom_invalid" { + access = "read" + team_id = tfe_team.foobar_invalid.id + project_id = tfe_project.foobar_invalid.id + + workspace_access { + delete = true + } +}`, rInt) +} diff --git a/website/docs/r/team_project_access.html.markdown b/website/docs/r/team_project_access.html.markdown index 945c63e4f..31c5c5710 100644 --- a/website/docs/r/team_project_access.html.markdown +++ b/website/docs/r/team_project_access.html.markdown @@ -37,7 +37,74 @@ The following arguments are supported: * `team_id` - (Required) ID of the team to add to the project. * `project_id` - (Required) ID of the project to which the team will be added. -* `access` - (Required) Type of fixed access to grant. Valid values are `admin`, `maintain`, `write`, or `read`. +* `access` - (Required) Type of fixed access to grant. Valid values are `admin`, `maintain`, `write`, `read`, or `custom`. + +## Custom Access + +If using `custom` for `access`, you can set the levels of individual permissions +that affect the project itself and all workspaces in the project, by using `project_access` and `workspace_access` arguments and their associated permission attributes. When using custom access, if attributes are not set they will be given a default value. Some permissions have values that are specific "strings" that denote the level of the permission, while other permissions are simple booleans. + +The following permissions apply to the project itself. + +| project-access | Description, Default, Valid Values | +|---------------------|---------------------------------------------| +| `settings` | The permission to grant for the project's settings. Default: `read`. Valid strings: `read`, `update`, or `delete` | +| `teams` | The permission to grant for the project's teams. Default: `none`, Valid strings: `none`, `read`, or `manage` | + + + + + +The following permissions apply to all workpsaces (and future workspaces) in the project. + +| workspace-access | Description, Default, Valid Values | +|----------------------|-------------------------------------------------------| +| `runs` | The permission to grant project's workspaces' runs. Default: `read`. Valid strings: `read`, `plan`, or `apply`. | +| `sentinel-mocks` | The permission to grant project's workspaces' Sentinel mocks. Default: `none`. Valid strings: `none`, or `read`. | +| `state-versions` | The permission to grant project's workspaces' state versions. Default: `none` Valid strings: `none`, `read-outputs`, `read`, or `write`.| +| `variables` | The permission to grant project's workspaces' variables. Default `none`. Valid strings: `none`, `read`, or `write`. | +| `create` | The permission to create project's workspaces in the project. Default: `false`. Valid booleans `true`, `false` | +| `locking` | The permission to manually lock or unlock the project's workspaces. Default `false`. Valid booleans `true`, `false` | +| `delete` | The permission to delete the project's workspaces. Default: `false`. Valid booleans: `true`, `false` | +| `move` | This permission to move workspaces into and out of the project. The team must also have permissions to the project(s) receiving the the workspace(s). Default: `false`. Valid booleans: `true`, `false` | +| `run-tasks` | The permission to manage run tasks within the project's workspaces. Default `false`. Valid booleans: `true`, `false` | + + +## Example Usage with Custom Project Permissions + +```hcl +resource "tfe_team" "dev" { + name = "my-dev-team" + organization = "my-org-name" +} + +resource "tfe_project" "test" { + name = "myproject" + organization = "my-org-name" +} + +resource "tfe_team_project_access" "custom" { + access = "custom" + team_id = tfe_team.dev.id + project_id = tfe_project.test.id + + project_access { + settings = "read" + teams = "none" + } + workspace_access { + state_versions = "write" + sentinel_mocks = "none" + runs = "apply" + variables = "write" + create = true + locking = true + move = false + delete = false + run_tasks = false + } +} +``` ## Attributes Reference