diff --git a/team_access.go b/team_access.go index a69ffa17e..c04301111 100644 --- a/team_access.go +++ b/team_access.go @@ -25,6 +25,9 @@ type TeamAccesses interface { // Read a team access by its ID. Read(ctx context.Context, teamAccessID string) (*TeamAccess, error) + // Update a team access by its ID. + Update(ctx context.Context, teamAccessID string, options TeamAccessUpdateOptions) (*TeamAccess, error) + // Remove team access from a workspace. Remove(ctx context.Context, teamAccessID string) error } @@ -37,12 +40,44 @@ type teamAccesses struct { // AccessType represents a team access type. type AccessType string -// List all available team access types. +// RunsPermissionType represents the permissiontype to a workspace's runs. +type RunsPermissionType string + +// VariablesPermissionType represents the permissiontype to a workspace's variables. +type VariablesPermissionType string + +// StateVersionsPermissionType represents the permissiontype to a workspace's state versions. +type StateVersionsPermissionType string + +// SentinelMocksPermissionType represents the permissiontype to a workspace's Sentinel mocks. +type SentinelMocksPermissionType string + +// WorkspaceLockingPermissionType represents the permissiontype to lock or unlock a workspace. +type WorkspaceLockingPermissionType bool + +// List all available team access types and permissions. const ( - AccessAdmin AccessType = "admin" - AccessPlan AccessType = "plan" - AccessRead AccessType = "read" - AccessWrite AccessType = "write" + AccessAdmin AccessType = "admin" + AccessPlan AccessType = "plan" + AccessRead AccessType = "read" + AccessWrite AccessType = "write" + AccessCustom AccessType = "custom" + + RunsPermissionRead RunsPermissionType = "read" + RunsPermissionPlan RunsPermissionType = "plan" + RunsPermissionApply RunsPermissionType = "apply" + + VariablesPermissionNone VariablesPermissionType = "none" + VariablesPermissionRead VariablesPermissionType = "read" + VariablesPermissionWrite VariablesPermissionType = "write" + + StateVersionsPermissionNone StateVersionsPermissionType = "none" + StateVersionsPermissionReadOutputs StateVersionsPermissionType = "read-outputs" + StateVersionsPermissionRead StateVersionsPermissionType = "read" + StateVersionsPermissionWrite StateVersionsPermissionType = "write" + + SentinelMocksPermissionNone SentinelMocksPermissionType = "none" + SentinelMocksPermissionRead SentinelMocksPermissionType = "read" ) // TeamAccessList represents a list of team accesses. @@ -53,8 +88,13 @@ type TeamAccessList struct { // TeamAccess represents the workspace access for a team. type TeamAccess struct { - ID string `jsonapi:"primary,team-workspaces"` - Access AccessType `jsonapi:"attr,access"` + ID string `jsonapi:"primary,team-workspaces"` + Access AccessType `jsonapi:"attr,access"` + Runs RunsPermissionType `jsonapi:"attr,runs"` + Variables VariablesPermissionType `jsonapi:"attr,variables"` + StateVersions StateVersionsPermissionType `jsonapi:"attr,state-versions"` + SentinelMocks SentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks"` + WorkspaceLocking bool `jsonapi:"attr,workspace-locking"` // Relations Team *Team `jsonapi:"relation,team"` @@ -105,6 +145,14 @@ type TeamAccessAddOptions struct { // The type of access to grant. Access *AccessType `jsonapi:"attr,access"` + // Custom workspace access permissions. These can only be edited when Access is 'custom'; otherwise, they are + // read-only and reflect the Access level's implicit permissions. + Runs *RunsPermissionType `jsonapi:"attr,runs,omitempty"` + Variables *VariablesPermissionType `jsonapi:"attr,variables,omitempty"` + StateVersions *StateVersionsPermissionType `jsonapi:"attr,state-versions,omitempty"` + SentinelMocks *SentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks,omitempty"` + WorkspaceLocking *bool `jsonapi:"attr,workspace-locking,omitempty"` + // The team to add to the workspace Team *Team `jsonapi:"relation,team"` @@ -169,6 +217,47 @@ func (s *teamAccesses) Read(ctx context.Context, teamAccessID string) (*TeamAcce return ta, nil } +// TeamAccessUpdateOptions represents the options for updating team access. +type TeamAccessUpdateOptions struct { + // For internal use only! + ID string `jsonapi:"primary,team-workspaces"` + + // The type of access to grant. + Access *AccessType `jsonapi:"attr,access,omitempty"` + + // Custom workspace access permissions. These can only be edited when Access is 'custom'; otherwise, they are + // read-only and reflect the Access level's implicit permissions. + Runs *RunsPermissionType `jsonapi:"attr,runs,omitempty"` + Variables *VariablesPermissionType `jsonapi:"attr,variables,omitempty"` + StateVersions *StateVersionsPermissionType `jsonapi:"attr,state-versions,omitempty"` + SentinelMocks *SentinelMocksPermissionType `jsonapi:"attr,sentinel-mocks,omitempty"` + WorkspaceLocking *bool `jsonapi:"attr,workspace-locking,omitempty"` +} + +// Update team access for a workspace +func (s *teamAccesses) Update(ctx context.Context, teamAccessID string, options TeamAccessUpdateOptions) (*TeamAccess, error) { + if !validStringID(&teamAccessID) { + return nil, errors.New("invalid value for team access ID") + } + + // Make sure we don't send a user provided ID. + options.ID = "" + + u := fmt.Sprintf("team-workspaces/%s", url.QueryEscape(teamAccessID)) + req, err := s.client.newRequest("PATCH", u, &options) + if err != nil { + return nil, err + } + + ta := &TeamAccess{} + err = s.client.do(ctx, req, ta) + if err != nil { + return nil, err + } + + return ta, err +} + // Remove team access from a workspace. func (s *teamAccesses) Remove(ctx context.Context, teamAccessID string) error { if !validStringID(&teamAccessID) { diff --git a/team_access_test.go b/team_access_test.go index fea595e98..16b0665e2 100644 --- a/team_access_test.go +++ b/team_access_test.go @@ -92,6 +92,35 @@ func TestTeamAccessesAdd(t *testing.T) { } ta, err := client.TeamAccess.Add(ctx, options) + defer client.TeamAccess.Remove(ctx, ta.ID) + + require.NoError(t, err) + + // Get a refreshed view from the API. + refreshed, err := client.TeamAccess.Read(ctx, ta.ID) + require.NoError(t, err) + + for _, item := range []*TeamAccess{ + ta, + refreshed, + } { + assert.NotEmpty(t, item.ID) + assert.Equal(t, *options.Access, item.Access) + } + }) + + t.Run("with valid custom options", func(t *testing.T) { + options := TeamAccessAddOptions{ + Access: Access(AccessCustom), + Runs: RunsPermission(RunsPermissionRead), + StateVersions: StateVersionsPermission(StateVersionsPermissionNone), + Team: tmTest, + Workspace: wTest, + } + + ta, err := client.TeamAccess.Add(ctx, options) + defer client.TeamAccess.Remove(ctx, ta.ID) + require.NoError(t, err) // Get a refreshed view from the API. @@ -107,7 +136,23 @@ func TestTeamAccessesAdd(t *testing.T) { } }) + t.Run("with invalid custom options", func(t *testing.T) { + options := TeamAccessAddOptions{ + Access: Access(AccessRead), + Runs: RunsPermission(RunsPermissionApply), + Team: tmTest, + Workspace: wTest, + } + + _, err := client.TeamAccess.Add(ctx, options) + + assert.EqualError(t, err, "invalid attribute\n\nRuns is read-only when access level is 'read'; use the 'custom' access level to set this attribute.") + }) + t.Run("when the team already has access", func(t *testing.T) { + _, taTestCleanup := createTeamAccess(t, client, tmTest, wTest, nil) + defer taTestCleanup() + options := TeamAccessAddOptions{ Access: Access(AccessAdmin), Team: tmTest, @@ -159,6 +204,14 @@ func TestTeamAccessesRead(t *testing.T) { assert.Equal(t, AccessAdmin, ta.Access) + t.Run("permission attributes are decoded", func(t *testing.T) { + assert.Equal(t, RunsPermissionApply, ta.Runs) + assert.Equal(t, VariablesPermissionWrite, ta.Variables) + assert.Equal(t, StateVersionsPermissionWrite, ta.StateVersions) + assert.Equal(t, SentinelMocksPermissionRead, ta.SentinelMocks) + assert.Equal(t, true, ta.WorkspaceLocking) + }) + t.Run("team relationship is decoded", func(t *testing.T) { assert.NotEmpty(t, ta.Team) }) @@ -181,6 +234,36 @@ func TestTeamAccessesRead(t *testing.T) { }) } +func TestTeamAccessesUpdate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wTest, wTestCleanup := createWorkspace(t, client, orgTest) + defer wTestCleanup() + + tmTest, tmTestCleanup := createTeam(t, client, orgTest) + defer tmTestCleanup() + + taTest, taTestCleanup := createTeamAccess(t, client, tmTest, wTest, nil) + defer taTestCleanup() + + t.Run("with valid attributes", func(t *testing.T) { + options := TeamAccessUpdateOptions{ + Access: Access(AccessCustom), + Runs: RunsPermission(RunsPermissionPlan), + } + + ta, err := client.TeamAccess.Update(ctx, taTest.ID, options) + require.NoError(t, err) + + assert.Equal(t, ta.Access, AccessCustom) + assert.Equal(t, ta.Runs, RunsPermissionPlan) + }) +} + func TestTeamAccessesRemove(t *testing.T) { client := testClient(t) ctx := context.Background() diff --git a/type_helpers.go b/type_helpers.go index 9296e4e6f..6b38125b1 100644 --- a/type_helpers.go +++ b/type_helpers.go @@ -5,6 +5,26 @@ func Access(v AccessType) *AccessType { return &v } +// RunsPermission returns a pointer to the given team runs permission type. +func RunsPermission(v RunsPermissionType) *RunsPermissionType { + return &v +} + +// VariablesPermission returns a pointer to the given team variables permission type. +func VariablesPermission(v VariablesPermissionType) *VariablesPermissionType { + return &v +} + +// StateVersionsPermission returns a pointer to the given team state versions permission type. +func StateVersionsPermission(v StateVersionsPermissionType) *StateVersionsPermissionType { + return &v +} + +// SentinelMocksPermission returns a pointer to the given team Sentinel mocks permission type. +func SentinelMocksPermission(v SentinelMocksPermissionType) *SentinelMocksPermissionType { + return &v +} + // AuthPolicy returns a pointer to the given authentication poliy. func AuthPolicy(v AuthPolicyType) *AuthPolicyType { return &v