Skip to content

Commit

Permalink
Merge pull request #123 from hashicorp/cwa
Browse files Browse the repository at this point in the history
Add Custom workspace access support / Team Access updates
  • Loading branch information
chrisarcand authored Jun 15, 2020
2 parents 7e10622 + 6fda71c commit 45afb2a
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 7 deletions.
103 changes: 96 additions & 7 deletions team_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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.
Expand All @@ -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"`
Expand Down Expand Up @@ -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"`

Expand Down Expand Up @@ -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) {
Expand Down
83 changes: 83 additions & 0 deletions team_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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)
})
Expand All @@ -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()
Expand Down
20 changes: 20 additions & 0 deletions type_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 45afb2a

Please sign in to comment.