Skip to content

Commit 9a03826

Browse files
authored
Merge pull request #745 from hashicorp/TF-5568-add-support-for-project-custom-permissions
TF-5568 add support for project custom permissions
2 parents 55351d9 + d29dd12 commit 9a03826

4 files changed

+342
-5
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
## Enhancements
66
* Added BETA support for including `projects` relationship and `projects-count` attribute to policy_set on create by @hs26gill [#737](https://github.com/hashicorp/go-tfe/pull/737)
77
* Added BETA method `AddProjects` and `RemoveProjects` for attaching/detaching policy set to projects by @Netra2104 [#735](https://github.com/hashicorp/go-tfe/pull/735)
8+
* Added BETA support for adding and updating custom permissions to `TeamProjectAccesses`. A `TeamProjectAccessType` of `"custom"` can set various permissions applied at
9+
the project level to the project itself (`TeamProjectAccessProjectPermissionsOptions`) and all of the workspaces in a project (`TeamProjectAccessWorkspacePermissionsOptions`) by @rberecka [#745](https://github.com/hashicorp/go-tfe/pull/745)
810

911
# v1.30.0
1012

team_project_access.go

+103-4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const (
4747
TeamProjectAccessMaintain TeamProjectAccessType = "maintain"
4848
TeamProjectAccessWrite TeamProjectAccessType = "write"
4949
TeamProjectAccessRead TeamProjectAccessType = "read"
50+
TeamProjectAccessCustom TeamProjectAccessType = "custom"
5051
)
5152

5253
// TeamProjectAccessList represents a list of team project accesses
@@ -57,14 +58,106 @@ type TeamProjectAccessList struct {
5758

5859
// TeamProjectAccess represents a project access for a team
5960
type TeamProjectAccess struct {
60-
ID string `jsonapi:"primary,team-projects"`
61-
Access TeamProjectAccessType `jsonapi:"attr,access"`
61+
ID string `jsonapi:"primary,team-projects"`
62+
Access TeamProjectAccessType `jsonapi:"attr,access"`
63+
ProjectAccess *TeamProjectAccessProjectPermissions `jsonapi:"attr,project-access"`
64+
WorkspaceAccess *TeamProjectAccessWorkspacePermissions `jsonapi:"attr,workspace-access"`
6265

6366
// Relations
6467
Team *Team `jsonapi:"relation,team"`
6568
Project *Project `jsonapi:"relation,project"`
6669
}
6770

71+
// ProjectPermissions represents the team's permissions on its project
72+
type TeamProjectAccessProjectPermissions struct {
73+
ProjectSettingsPermission ProjectSettingsPermissionType `jsonapi:"attr,settings"`
74+
ProjectTeamsPermission ProjectTeamsPermissionType `jsonapi:"attr,teams"`
75+
}
76+
77+
// WorkspacePermissions represents the team's permission on all workspaces in its project
78+
type TeamProjectAccessWorkspacePermissions struct {
79+
WorkspaceRunsPermission WorkspaceRunsPermissionType `jsonapi:"attr,runs"`
80+
WorkspaceSentinelMocksPermission WorkspaceSentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks"`
81+
WorkspaceStateVersionsPermission WorkspaceStateVersionsPermissionType `jsonapi:"attr,state-versions"`
82+
WorkspaceVariablesPermission WorkspaceVariablesPermissionType `jsonapi:"attr,variables"`
83+
WorkspaceCreatePermission bool `jsonapi:"attr,create"`
84+
WorkspaceLockingPermission bool `jsonapi:"attr,locking"`
85+
WorkspaceMovePermission bool `jsonapi:"attr,move"`
86+
WorkspaceDeletePermission bool `jsonapi:"attr,delete"`
87+
WorkspaceRunTasksPermission bool `jsonapi:"attr,run-tasks"`
88+
}
89+
90+
// ProjectSettingsPermissionType represents the permissiontype to a project's settings
91+
type ProjectSettingsPermissionType string
92+
93+
const (
94+
ProjectSettingsPermissionRead ProjectSettingsPermissionType = "read"
95+
ProjectSettingsPermissionUpdate ProjectSettingsPermissionType = "update"
96+
ProjectSettingsPermissionDelete ProjectSettingsPermissionType = "delete"
97+
)
98+
99+
// ProjectTeamsPermissionType represents the permissiontype to a project's teams
100+
type ProjectTeamsPermissionType string
101+
102+
const (
103+
ProjectTeamsPermissionNone ProjectTeamsPermissionType = "none"
104+
ProjectTeamsPermissionRead ProjectTeamsPermissionType = "read"
105+
ProjectTeamsPermissionManage ProjectTeamsPermissionType = "manage"
106+
)
107+
108+
// WorkspaceRunsPermissionType represents the permissiontype to project workspaces' runs
109+
type WorkspaceRunsPermissionType string
110+
111+
const (
112+
WorkspaceRunsPermissionRead WorkspaceRunsPermissionType = "read"
113+
WorkspaceRunsPermissionPlan WorkspaceRunsPermissionType = "plan"
114+
WorkspaceRunsPermissionApply WorkspaceRunsPermissionType = "apply"
115+
)
116+
117+
// WorkspaceSentinelMocksPermissionType represents the permissiontype to project workspaces' sentinel-mocks
118+
type WorkspaceSentinelMocksPermissionType string
119+
120+
const (
121+
WorkspaceSentinelMocksPermissionNone WorkspaceSentinelMocksPermissionType = "none"
122+
WorkspaceSentinelMocksPermissionRead WorkspaceSentinelMocksPermissionType = "read"
123+
)
124+
125+
// WorkspaceStateVersionsPermissionType represents the permissiontype to project workspaces' state-versions
126+
type WorkspaceStateVersionsPermissionType string
127+
128+
const (
129+
WorkspaceStateVersionsPermissionNone WorkspaceStateVersionsPermissionType = "none"
130+
WorkspaceStateVersionsPermissionReadOutputs WorkspaceStateVersionsPermissionType = "read-outputs"
131+
WorkspaceStateVersionsPermissionRead WorkspaceStateVersionsPermissionType = "read"
132+
WorkspaceStateVersionsPermissionWrite WorkspaceStateVersionsPermissionType = "write"
133+
)
134+
135+
// WorkspaceVariablesPermissionType represents the permissiontype to project workspaces' variables
136+
type WorkspaceVariablesPermissionType string
137+
138+
const (
139+
WorkspaceVariablesPermissionNone WorkspaceVariablesPermissionType = "none"
140+
WorkspaceVariablesPermissionRead WorkspaceVariablesPermissionType = "read"
141+
WorkspaceVariablesPermissionWrite WorkspaceVariablesPermissionType = "write"
142+
)
143+
144+
type TeamProjectAccessProjectPermissionsOptions struct {
145+
Settings *ProjectSettingsPermissionType `json:"settings,omitempty"`
146+
Teams *ProjectTeamsPermissionType `json:"teams,omitempty"`
147+
}
148+
149+
type TeamProjectAccessWorkspacePermissionsOptions struct {
150+
Runs *WorkspaceRunsPermissionType `json:"runs,omitempty"`
151+
SentinelMocks *WorkspaceSentinelMocksPermissionType `json:"sentinel-mocks,omitempty"`
152+
StateVersions *WorkspaceStateVersionsPermissionType `json:"state-versions,omitempty"`
153+
Variables *WorkspaceVariablesPermissionType `json:"variables,omitempty"`
154+
Create *bool `json:"create,omitempty"`
155+
Locking *bool `json:"locking,omitempty"`
156+
Move *bool `json:"move,omitempty"`
157+
Delete *bool `json:"delete,omitempty"`
158+
RunTasks *bool `json:"run-tasks,omitempty"`
159+
}
160+
68161
// TeamProjectAccessListOptions represents the options for listing team project accesses
69162
type TeamProjectAccessListOptions struct {
70163
ListOptions
@@ -80,6 +173,9 @@ type TeamProjectAccessAddOptions struct {
80173
Type string `jsonapi:"primary,team-projects"`
81174
// The type of access to grant.
82175
Access TeamProjectAccessType `jsonapi:"attr,access"`
176+
// The levels that project and workspace permissions grant
177+
ProjectAccess *TeamProjectAccessProjectPermissionsOptions `jsonapi:"attr,project-access,omitempty"`
178+
WorkspaceAccess *TeamProjectAccessWorkspacePermissionsOptions `jsonapi:"attr,workspace-access,omitempty"`
83179

84180
// The team to add to the project
85181
Team *Team `jsonapi:"relation,team"`
@@ -95,7 +191,9 @@ type TeamProjectAccessUpdateOptions struct {
95191
// https://jsonapi.org/format/#crud-creating
96192
Type string `jsonapi:"primary,team-projects"`
97193
// The type of access to grant.
98-
Access *TeamProjectAccessType `jsonapi:"attr,access,omitempty"`
194+
Access *TeamProjectAccessType `jsonapi:"attr,access,omitempty"`
195+
ProjectAccess *TeamProjectAccessProjectPermissionsOptions `jsonapi:"attr,project-access,omitempty"`
196+
WorkspaceAccess *TeamProjectAccessWorkspacePermissionsOptions `jsonapi:"attr,workspace-access,omitempty"`
99197
}
100198

101199
// List all team accesses for a given project.
@@ -229,7 +327,8 @@ func validateTeamProjectAccessType(t TeamProjectAccessType) error {
229327
case TeamProjectAccessAdmin,
230328
TeamProjectAccessMaintain,
231329
TeamProjectAccessWrite,
232-
TeamProjectAccessRead:
330+
TeamProjectAccessRead,
331+
TeamProjectAccessCustom:
233332
// do nothing
234333
default:
235334
return ErrInvalidTeamProjectAccessType

team_project_access_integration_test.go

+207-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ package tfe
55

66
import (
77
"context"
8-
"github.com/stretchr/testify/assert"
98
"testing"
109

10+
"github.com/stretchr/testify/assert"
11+
1112
"github.com/stretchr/testify/require"
1213
)
1314

@@ -164,6 +165,102 @@ func TestTeamProjectAccessesAdd(t *testing.T) {
164165
}
165166
})
166167

168+
t.Run("with valid options for all custom TeamProject permissions", func(t *testing.T) {
169+
skipUnlessBeta(t)
170+
options := TeamProjectAccessAddOptions{
171+
Access: *ProjectAccess(TeamProjectAccessCustom),
172+
Team: tmTest,
173+
Project: pTest,
174+
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
175+
Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate),
176+
Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage),
177+
},
178+
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
179+
Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply),
180+
SentinelMocks: WorkspaceSentinelMocksPermission(WorkspaceSentinelMocksPermissionRead),
181+
StateVersions: WorkspaceStateVersionsPermission(WorkspaceStateVersionsPermissionWrite),
182+
Variables: WorkspaceVariablesPermission(WorkspaceVariablesPermissionWrite),
183+
Create: Bool(true),
184+
Locking: Bool(true),
185+
Move: Bool(true),
186+
Delete: Bool(false),
187+
RunTasks: Bool(false),
188+
},
189+
}
190+
191+
tpa, err := client.TeamProjectAccess.Add(ctx, options)
192+
defer func() {
193+
err := client.TeamProjectAccess.Remove(ctx, tpa.ID)
194+
if err != nil {
195+
t.Logf("error removing team access (%s): %s", tpa.ID, err)
196+
}
197+
}()
198+
199+
require.NoError(t, err)
200+
201+
// Get a refreshed view from the API.
202+
refreshed, err := client.TeamProjectAccess.Read(ctx, tpa.ID)
203+
require.NoError(t, err)
204+
205+
for _, item := range []*TeamProjectAccess{
206+
tpa,
207+
refreshed,
208+
} {
209+
assert.NotEmpty(t, item.ID)
210+
assert.Equal(t, options.Access, item.Access)
211+
assert.Equal(t, *options.ProjectAccess.Settings, item.ProjectAccess.ProjectSettingsPermission)
212+
assert.Equal(t, *options.ProjectAccess.Teams, item.ProjectAccess.ProjectTeamsPermission)
213+
assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission)
214+
assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, item.WorkspaceAccess.WorkspaceSentinelMocksPermission)
215+
assert.Equal(t, *options.WorkspaceAccess.StateVersions, item.WorkspaceAccess.WorkspaceStateVersionsPermission)
216+
assert.Equal(t, *options.WorkspaceAccess.Variables, item.WorkspaceAccess.WorkspaceVariablesPermission)
217+
assert.Equal(t, item.WorkspaceAccess.WorkspaceCreatePermission, true)
218+
assert.Equal(t, item.WorkspaceAccess.WorkspaceLockingPermission, true)
219+
assert.Equal(t, item.WorkspaceAccess.WorkspaceMovePermission, true)
220+
assert.Equal(t, item.WorkspaceAccess.WorkspaceDeletePermission, false)
221+
assert.Equal(t, item.WorkspaceAccess.WorkspaceRunTasksPermission, false)
222+
}
223+
})
224+
225+
t.Run("with valid options for some custom TeamProject permissions", func(t *testing.T) {
226+
skipUnlessBeta(t)
227+
options := TeamProjectAccessAddOptions{
228+
Access: *ProjectAccess(TeamProjectAccessCustom),
229+
Team: tmTest,
230+
Project: pTest,
231+
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
232+
Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate),
233+
},
234+
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
235+
Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionApply),
236+
},
237+
}
238+
239+
tpa, err := client.TeamProjectAccess.Add(ctx, options)
240+
t.Cleanup(func() {
241+
err := client.TeamProjectAccess.Remove(ctx, tpa.ID)
242+
if err != nil {
243+
t.Logf("error removing team access (%s): %s", tpa.ID, err)
244+
}
245+
})
246+
247+
require.NoError(t, err)
248+
249+
// Get a refreshed view from the API.
250+
refreshed, err := client.TeamProjectAccess.Read(ctx, tpa.ID)
251+
require.NoError(t, err)
252+
253+
for _, item := range []*TeamProjectAccess{
254+
tpa,
255+
refreshed,
256+
} {
257+
assert.NotEmpty(t, item.ID)
258+
assert.Equal(t, options.Access, item.Access)
259+
assert.Equal(t, *options.ProjectAccess.Settings, item.ProjectAccess.ProjectSettingsPermission)
260+
assert.Equal(t, *options.WorkspaceAccess.Runs, item.WorkspaceAccess.WorkspaceRunsPermission)
261+
}
262+
})
263+
167264
t.Run("when the team already has access to the project", func(t *testing.T) {
168265
_, tpaTestCleanup := createTeamProjectAccess(t, client, tmTest, pTest, nil)
169266
defer tpaTestCleanup()
@@ -205,6 +302,20 @@ func TestTeamProjectAccessesAdd(t *testing.T) {
205302
assert.Equal(t, err, ErrRequiredProject)
206303
})
207304

305+
t.Run("when invalid custom project permission is provided in options", func(t *testing.T) {
306+
skipUnlessBeta(t)
307+
tpa, err := client.TeamProjectAccess.Add(ctx, TeamProjectAccessAddOptions{
308+
Access: *ProjectAccess(TeamProjectAccessCustom),
309+
Team: tmTest,
310+
Project: pTest,
311+
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
312+
Teams: ProjectTeamsPermission(badIdentifier),
313+
},
314+
})
315+
assert.Nil(t, tpa)
316+
assert.Error(t, err)
317+
})
318+
208319
t.Run("when invalid access is provided in options", func(t *testing.T) {
209320
tpa, err := client.TeamProjectAccess.Add(ctx, TeamProjectAccessAddOptions{
210321
Access: badIdentifier,
@@ -242,6 +353,101 @@ func TestTeamProjectAccessesUpdate(t *testing.T) {
242353

243354
assert.Equal(t, tpa.Access, TeamProjectAccessRead)
244355
})
356+
357+
t.Run("with valid custom permissions attributes for all permissions", func(t *testing.T) {
358+
skipUnlessBeta(t)
359+
options := TeamProjectAccessUpdateOptions{
360+
Access: ProjectAccess(TeamProjectAccessCustom),
361+
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
362+
Settings: ProjectSettingsPermission(ProjectSettingsPermissionUpdate),
363+
Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage),
364+
},
365+
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
366+
Runs: WorkspaceRunsPermission(WorkspaceRunsPermissionPlan),
367+
SentinelMocks: WorkspaceSentinelMocksPermission(WorkspaceSentinelMocksPermissionNone),
368+
StateVersions: WorkspaceStateVersionsPermission(WorkspaceStateVersionsPermissionReadOutputs),
369+
Variables: WorkspaceVariablesPermission(WorkspaceVariablesPermissionRead),
370+
Create: Bool(false),
371+
Locking: Bool(false),
372+
Move: Bool(false),
373+
Delete: Bool(true),
374+
RunTasks: Bool(true),
375+
},
376+
}
377+
378+
tpa, err := client.TeamProjectAccess.Update(ctx, tpaTest.ID, options)
379+
require.NoError(t, err)
380+
require.NotNil(t, options.ProjectAccess)
381+
require.NotNil(t, options.WorkspaceAccess)
382+
assert.Equal(t, tpa.Access, TeamProjectAccessCustom)
383+
assert.Equal(t, *options.ProjectAccess.Teams, tpa.ProjectAccess.ProjectTeamsPermission)
384+
assert.Equal(t, *options.ProjectAccess.Settings, tpa.ProjectAccess.ProjectSettingsPermission)
385+
assert.Equal(t, *options.WorkspaceAccess.Runs, tpa.WorkspaceAccess.WorkspaceRunsPermission)
386+
assert.Equal(t, *options.WorkspaceAccess.SentinelMocks, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission)
387+
assert.Equal(t, *options.WorkspaceAccess.StateVersions, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission)
388+
assert.Equal(t, *options.WorkspaceAccess.Variables, tpa.WorkspaceAccess.WorkspaceVariablesPermission)
389+
assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceCreatePermission)
390+
assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceLockingPermission)
391+
assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceMovePermission)
392+
assert.Equal(t, true, tpa.WorkspaceAccess.WorkspaceDeletePermission)
393+
assert.Equal(t, true, tpa.WorkspaceAccess.WorkspaceRunTasksPermission)
394+
})
395+
396+
t.Run("with valid custom permissions attributes for some permissions", func(t *testing.T) {
397+
skipUnlessBeta(t)
398+
// create tpaCustomTest to verify unupdated attributes stay the same for custom permissions
399+
// because going from admin to read to custom changes the values of all custom permissions
400+
tm2Test, tm2TestCleanup := createTeam(t, client, orgTest)
401+
defer tm2TestCleanup()
402+
403+
TpaOptions := TeamProjectAccessAddOptions{
404+
Access: *ProjectAccess(TeamProjectAccessCustom),
405+
Team: tm2Test,
406+
Project: pTest,
407+
}
408+
409+
tpaCustomTest, err := client.TeamProjectAccess.Add(ctx, TpaOptions)
410+
require.NoError(t, err)
411+
412+
options := TeamProjectAccessUpdateOptions{
413+
Access: ProjectAccess(TeamProjectAccessCustom),
414+
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
415+
Teams: ProjectTeamsPermission(ProjectTeamsPermissionManage),
416+
},
417+
WorkspaceAccess: &TeamProjectAccessWorkspacePermissionsOptions{
418+
Create: Bool(false),
419+
},
420+
}
421+
422+
tpa, err := client.TeamProjectAccess.Update(ctx, tpaCustomTest.ID, options)
423+
require.NoError(t, err)
424+
require.NotNil(t, options.ProjectAccess)
425+
require.NotNil(t, options.WorkspaceAccess)
426+
assert.Equal(t, *options.ProjectAccess.Teams, tpa.ProjectAccess.ProjectTeamsPermission)
427+
assert.Equal(t, false, tpa.WorkspaceAccess.WorkspaceCreatePermission)
428+
// assert that other attributes remain the same
429+
assert.Equal(t, tpaCustomTest.ProjectAccess.ProjectSettingsPermission, tpa.ProjectAccess.ProjectSettingsPermission)
430+
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceLockingPermission, tpa.WorkspaceAccess.WorkspaceLockingPermission)
431+
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceMovePermission, tpa.WorkspaceAccess.WorkspaceMovePermission)
432+
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceDeletePermission, tpa.WorkspaceAccess.WorkspaceDeletePermission)
433+
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceRunsPermission, tpa.WorkspaceAccess.WorkspaceRunsPermission)
434+
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceSentinelMocksPermission, tpa.WorkspaceAccess.WorkspaceSentinelMocksPermission)
435+
assert.Equal(t, tpaCustomTest.WorkspaceAccess.WorkspaceStateVersionsPermission, tpa.WorkspaceAccess.WorkspaceStateVersionsPermission)
436+
})
437+
t.Run("with invalid custom permissions attributes", func(t *testing.T) {
438+
skipUnlessBeta(t)
439+
options := TeamProjectAccessUpdateOptions{
440+
Access: ProjectAccess(TeamProjectAccessCustom),
441+
ProjectAccess: &TeamProjectAccessProjectPermissionsOptions{
442+
Teams: ProjectTeamsPermission(badIdentifier),
443+
},
444+
}
445+
446+
tpa, err := client.TeamProjectAccess.Update(ctx, tpaTest.ID, options)
447+
448+
assert.Nil(t, tpa)
449+
assert.Error(t, err)
450+
})
245451
}
246452

247453
func TestTeamProjectAccessesRemove(t *testing.T) {

0 commit comments

Comments
 (0)