Skip to content

Commit

Permalink
feat: add team_access resource (#398)
Browse files Browse the repository at this point in the history
* Fix payloads finally

* Test team access for users

* Fix description to show team_access

* Validate 'user' or 'service_account' values

* Add examples

* Touch up description
  • Loading branch information
mitchnielsen authored Mar 4, 2025
1 parent 9383bda commit c319331
Show file tree
Hide file tree
Showing 9 changed files with 621 additions and 0 deletions.
70 changes: 70 additions & 0 deletions docs/resources/team_access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "prefect_team_access Resource - prefect"
subcategory: ""
description: |-
The resource team_access grants access to a team for a user or service account. For more information, see manage teams https://docs.prefect.io/v3/manage/cloud/manage-users/manage-teams.
---

# prefect_team_access (Resource)

The resource `team_access` grants access to a team for a user or service account. For more information, see [manage teams](https://docs.prefect.io/v3/manage/cloud/manage-users/manage-teams).

## Example Usage

```terraform
# Example: granting access to a service account.
resource "prefect_service_account" "test" {
name = "my-service-account"
}
resource "prefect_team" "test" {
name = "my-team"
description = "test-team-description"
}
resource "prefect_team_access" "test" {
member_type = "service_account"
member_id = prefect_service_account.test.id
member_actor_id = prefect_service_account.test.actor_id
team_id = prefect_team.test.id
}
# Example: granting access to a user.
data "prefect_account_member" "test" {
email = "[email protected]"
}
resource "prefect_team" "test" {
name = "my-team"
description = "test-team-description"
}
resource "prefect_team_access" "test" {
team_id = prefect_team.test.id
member_type = "user"
member_id = data.prefect_account_member.test.user_id
member_actor_id = data.prefect_account_member.test.actor_id
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `member_actor_id` (String) Member Actor ID (UUID)
- `member_id` (String) Member ID (UUID)
- `member_type` (String) Member Type (user | service_account)
- `team_id` (String) Team ID (UUID)

### Optional

- `account_id` (String) Account ID (UUID)

### Read-Only

- `id` (String) Team Access ID
36 changes: 36 additions & 0 deletions examples/resources/prefect_team_access/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Example: granting access to a service account.

resource "prefect_service_account" "test" {
name = "my-service-account"
}

resource "prefect_team" "test" {
name = "my-team"
description = "test-team-description"
}

resource "prefect_team_access" "test" {
member_type = "service_account"
member_id = prefect_service_account.test.id
member_actor_id = prefect_service_account.test.actor_id
team_id = prefect_team.test.id
}


# Example: granting access to a user.

data "prefect_account_member" "test" {
email = "[email protected]"
}

resource "prefect_team" "test" {
name = "my-team"
description = "test-team-description"
}

resource "prefect_team_access" "test" {
team_id = prefect_team.test.id
member_type = "user"
member_id = data.prefect_account_member.test.user_id
member_actor_id = data.prefect_account_member.test.actor_id
}
1 change: 1 addition & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type PrefectClient interface {
DeploymentAccess(accountID uuid.UUID, workspaceID uuid.UUID) (DeploymentAccessClient, error)
DeploymentSchedule(accountID uuid.UUID, workspaceID uuid.UUID) (DeploymentScheduleClient, error)
GlobalConcurrencyLimits(accountID uuid.UUID, workspaceID uuid.UUID) (GlobalConcurrencyLimitsClient, error)
TeamAccess(accountID uuid.UUID, teamID uuid.UUID) (TeamAccessClient, error)
Teams(accountID uuid.UUID) (TeamsClient, error)
Flows(accountID uuid.UUID, workspaceID uuid.UUID) (FlowsClient, error)
TaskRunConcurrencyLimits(accountID uuid.UUID, workspaceID uuid.UUID) (TaskRunConcurrencyLimitsClient, error)
Expand Down
47 changes: 47 additions & 0 deletions internal/api/team_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package api

import (
"context"

"github.com/google/uuid"
)

// TeamAccessClient is a client for the TeamAccess resource.
type TeamAccessClient interface {
Read(ctx context.Context, teamID, memberID, memberActorID uuid.UUID) (*TeamAccess, error)
Upsert(ctx context.Context, memberType string, memberID uuid.UUID) error
Delete(ctx context.Context, memberID uuid.UUID) error
}

// TeamAccess is a representation of a team access.
type TeamAccess struct {
BaseModel

TeamID uuid.UUID `json:"team_id"`

MemberID uuid.UUID `json:"member_id"`
MemberActorID uuid.UUID `json:"member_actor_id"`
MemberType string `json:"member_type"`
}

// TeamAccessUpsert defines the payload for an upsert request.
type TeamAccessUpsert struct {
Members []TeamAccessMember `json:"members"`
}

// TeamAccessMember is a representation of a team access member.
type TeamAccessMember struct {
MemberID uuid.UUID `json:"member_id"`
MemberType string `json:"member_type"`
}

// TeamAccessRead defines the response payload for a get request.
type TeamAccessRead struct {
Memberships []Membership `json:"memberships"`
}

// Membership is a representation of a team access membership.
type Membership struct {
ActorID uuid.UUID `json:"id"`
Type string `json:"type"`
}
122 changes: 122 additions & 0 deletions internal/client/team_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package client

import (
"context"
"fmt"
"net/http"

"github.com/google/uuid"
"github.com/prefecthq/terraform-provider-prefect/internal/api"
)

var _ = api.TeamAccessClient(&TeamAccessClient{})

// TeamAccessClient is a client for the TeamAccess resource.
type TeamAccessClient struct {
hc *http.Client
apiKey string
basicAuthKey string
routePrefix string
}

// TeamAccess is a factory that initializes and returns a TeamAccessClient.
//
//nolint:ireturn // required to support PrefectClient mocking
func (c *Client) TeamAccess(accountID uuid.UUID, teamID uuid.UUID) (api.TeamAccessClient, error) {
if accountID == uuid.Nil {
accountID = c.defaultAccountID
}

return &TeamAccessClient{
hc: c.hc,
apiKey: c.apiKey,
basicAuthKey: c.basicAuthKey,
routePrefix: fmt.Sprintf("%s/accounts/%s/teams/%s", c.endpoint, accountID.String(), teamID.String()),
}, nil
}

// Upsert creates or updates access to a team for a member.
func (c *TeamAccessClient) Upsert(ctx context.Context, memberType string, memberID uuid.UUID) error {
payload := api.TeamAccessUpsert{
Members: []api.TeamAccessMember{
{
MemberID: memberID,
MemberType: memberType,
},
},
}

cfg := requestConfig{
method: http.MethodPut,
url: fmt.Sprintf("%s/members", c.routePrefix),
body: &payload,
apiKey: c.apiKey,
basicAuthKey: c.basicAuthKey,
successCodes: successCodesStatusOK,
}

resp, err := request(ctx, c.hc, cfg)
if err != nil {
return fmt.Errorf("failed to upsert team access: %w", err)
}
defer resp.Body.Close()

return nil
}

// Read fetches a team access by member actor ID.
func (c *TeamAccessClient) Read(ctx context.Context, teamID, memberID, memberActorID uuid.UUID) (*api.TeamAccess, error) {
cfg := requestConfig{
method: http.MethodGet,
url: c.routePrefix,
apiKey: c.apiKey,
basicAuthKey: c.basicAuthKey,
successCodes: successCodesStatusOK,
}

var teamAccessRead api.TeamAccessRead
if err := requestWithDecodeResponse(ctx, c.hc, cfg, &teamAccessRead); err != nil {
return nil, fmt.Errorf("failed to get team access: %w", err)
}

// Find the memberships entry that matches the member ID.
var teamAccess *api.TeamAccess
for _, membership := range teamAccessRead.Memberships {
if membership.ActorID == memberActorID {
teamAccess = &api.TeamAccess{
TeamID: teamID,
MemberID: memberID,
MemberActorID: memberActorID,
MemberType: membership.Type,
}

break
}
}

if teamAccess == nil {
return nil, fmt.Errorf("client.Read: team access not found for member ID: %s", memberActorID.String())
}

return teamAccess, nil
}

// Delete deletes a team access by member ID.
func (c *TeamAccessClient) Delete(ctx context.Context, memberID uuid.UUID) error {
cfg := requestConfig{
method: http.MethodDelete,
url: fmt.Sprintf("%s/members/%s", c.routePrefix, memberID.String()),
body: http.NoBody,
apiKey: c.apiKey,
basicAuthKey: c.basicAuthKey,
successCodes: successCodesStatusNoContent,
}

resp, err := request(ctx, c.hc, cfg)
if err != nil {
return fmt.Errorf("failed to delete team access: %w", err)
}
defer resp.Body.Close()

return nil
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ func (p *PrefectProvider) Resources(_ context.Context) []func() resource.Resourc
resources.NewGlobalConcurrencyLimitResource,
resources.NewServiceAccountResource,
resources.NewTaskRunConcurrencyLimitResource,
resources.NewTeamAccessResource,
resources.NewTeamResource,
resources.NewVariableResource,
resources.NewWebhookResource,
Expand Down
Loading

0 comments on commit c319331

Please sign in to comment.