Skip to content

Commit

Permalink
feat(Deployments): support access control (#294)
Browse files Browse the repository at this point in the history
* feat(Deployments): support access control

Adds support for access control on Deployments.

Closes https://linear.app/prefect/issue/PLA-439/deployments-support-access

* Expected sucecss response is 204

* Adhere to the example set by block access

* Reuse ObjectActorAccess object

* Get tests working

* Reduce verbosity in tests

* Add docs, example

* Generate Terraform Docs

* Require cloud endpoint for deployment access

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
mitchnielsen and github-actions[bot] authored Nov 4, 2024
1 parent 0beb8cd commit 9bf9b8b
Show file tree
Hide file tree
Showing 9 changed files with 882 additions and 37 deletions.
102 changes: 102 additions & 0 deletions docs/resources/deployment_access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "prefect_deployment_access Resource - prefect"
subcategory: ""
description: |-
The resource deployment_access represents a connection between an accessor (User, Service Account or Team) with a Deployment. This resource specifies an actor's access level to a specific Deployment in the Account.
---

# prefect_deployment_access (Resource)

The resource `deployment_access` represents a connection between an accessor (User, Service Account or Team) with a Deployment. This resource specifies an actor's access level to a specific Deployment in the Account.

## Example Usage

```terraform
provider "prefect" {}
# All Deployments are scoped to a Workspace.
data "prefect_workspace" "test" {
handle = "my-workspace"
}
# Be sure to grant all Actors/Teams who need Deployment access to first be
# invited to the Workspace (with a role).
data "prefect_workspace_role" "developer" {
name = "Developer"
}
# Example: invite a Service Account to the Workspace and grant it Developer access
resource "prefect_service_account" "test" {
name = "my-service-account"
}
resource "prefect_workspace_access" "test" {
accessor_type = "SERVICE_ACCOUNT"
accessor_id = prefect_service_account.test.id
workspace_role_id = data.prefect_workspace_role.developer.id
workspace_id = data.prefect_workspace.test.id
}
# Example: invite a Team to the Workspace and grant it Developer access
data "prefect_team" "test" {
name = "my-team"
}
resource "prefect_workspace_access" "test_team" {
accessor_type = "TEAM"
accessor_id = data.prefect_team.test.id
workspace_role_id = data.prefect_workspace_role.developer.id
workspace_id = data.prefect_workspace.test.id
}
# Define the Flow and Deployment, and grant access to the Deployment
resource "prefect_flow" "test" {
name = "my-flow"
workspace_id = data.prefect_workspace.test.id
tags = ["test"]
}
resource "prefect_deployment" "test" {
name = "my-deployment"
workspace_id = data.prefect_workspace.test.id
flow_id = prefect_flow.test.id
}
resource "prefect_deployment_access" "test" {
workspace_id = data.prefect_workspace.test.id
deployment_id = prefect_deployment.test.id
manage_actor_ids = [prefect_service_account.test.actor_id]
run_actor_ids = [prefect_service_account.test.actor_id]
view_actor_ids = [prefect_service_account.test.actor_id]
manage_team_ids = [data.prefect_team.test.id]
run_team_ids = [data.prefect_team.test.id]
view_team_ids = [data.prefect_team.test.id]
}
```

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

### Required

- `deployment_id` (String) Deployment ID (UUID)

### Optional

- `account_id` (String) Account ID (UUID)
- `manage_actor_ids` (List of String) List of actor IDs with manage access to the Deployment
- `manage_team_ids` (List of String) List of team IDs with manage access to the Deployment
- `run_actor_ids` (List of String) List of actor IDs with run access to the Deployment
- `run_team_ids` (List of String) List of team IDs with run access to the Deployment
- `view_actor_ids` (List of String) List of actor IDs with view access to the Deployment
- `view_team_ids` (List of String) List of team IDs with view access to the Deployment
- `workspace_id` (String) Workspace ID (UUID)
68 changes: 68 additions & 0 deletions examples/resources/prefect_deployment_access/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
provider "prefect" {}

# All Deployments are scoped to a Workspace.
data "prefect_workspace" "test" {
handle = "my-workspace"
}

# Be sure to grant all Actors/Teams who need Deployment access to first be
# invited to the Workspace (with a role).
data "prefect_workspace_role" "developer" {
name = "Developer"
}


# Example: invite a Service Account to the Workspace and grant it Developer access

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

resource "prefect_workspace_access" "test" {
accessor_type = "SERVICE_ACCOUNT"
accessor_id = prefect_service_account.test.id
workspace_role_id = data.prefect_workspace_role.developer.id
workspace_id = data.prefect_workspace.test.id
}


# Example: invite a Team to the Workspace and grant it Developer access

data "prefect_team" "test" {
name = "my-team"
}

resource "prefect_workspace_access" "test_team" {
accessor_type = "TEAM"
accessor_id = data.prefect_team.test.id
workspace_role_id = data.prefect_workspace_role.developer.id
workspace_id = data.prefect_workspace.test.id
}


# Define the Flow and Deployment, and grant access to the Deployment

resource "prefect_flow" "test" {
name = "my-flow"
workspace_id = data.prefect_workspace.test.id
tags = ["test"]
}

resource "prefect_deployment" "test" {
name = "my-deployment"
workspace_id = data.prefect_workspace.test.id
flow_id = prefect_flow.test.id
}

resource "prefect_deployment_access" "test" {
workspace_id = data.prefect_workspace.test.id
deployment_id = prefect_deployment.test.id

manage_actor_ids = [prefect_service_account.test.actor_id]
run_actor_ids = [prefect_service_account.test.actor_id]
view_actor_ids = [prefect_service_account.test.actor_id]

manage_team_ids = [data.prefect_team.test.id]
run_team_ids = [data.prefect_team.test.id]
view_team_ids = [data.prefect_team.test.id]
}
1 change: 1 addition & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type PrefectClient interface {
BlockTypes(accountID uuid.UUID, workspaceID uuid.UUID) (BlockTypeClient, error)
Collections(accountID uuid.UUID, workspaceID uuid.UUID) (CollectionsClient, error)
Deployments(accountID uuid.UUID, workspaceID uuid.UUID) (DeploymentsClient, error)
DeploymentAccess(accountID uuid.UUID, workspaceID uuid.UUID) (DeploymentAccessClient, error)
Teams(accountID uuid.UUID) (TeamsClient, error)
Flows(accountID uuid.UUID, workspaceID uuid.UUID) (FlowsClient, error)
Workspaces(accountID uuid.UUID) (WorkspacesClient, error)
Expand Down
37 changes: 0 additions & 37 deletions internal/api/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,40 +82,3 @@ type DeploymentUpdate struct {
// {"deployments": {"handle": {"any_": ["test"]}}}.
type DeploymentFilter struct {
}

type DeploymentAccess struct {
BaseModel
AccountID uuid.UUID `json:"account_id"`
WorkspaceID uuid.UUID `json:"workspace_id"`
DeploymentID uuid.UUID `json:"deployment_id"`
AccessControl DeploymentAccessControl `json:"access_control"`
}

// DeploymentAccessSet is a subset of DeploymentAccess used when Setting deployment access control.
type DeploymentAccessSet struct {
AccessControl DeploymentAccessControlSet `json:"access_control"`
}

// DeploymentAccessControlSet is a definition of deployment access control.
type DeploymentAccessControlSet struct {
ManageActorIDs []string `json:"manage_actor_ids"`
RunActorIDs []string `json:"run_actor_ids"`
ViewActorIDs []string `json:"view_actor_ids"`
ManageTeamIDs []string `json:"manage_team_ids"`
RunTeamIDs []string `json:"run_team_ids"`
ViewTeamIDs []string `json:"view_team_ids"`
}

// DeploymentAccessControl is a definition of deployment access control.
type DeploymentAccessControl struct {
ManageActors []Actor `json:"manage_actors"`
RunActors []Actor `json:"run_actors"`
ViewActors []Actor `json:"view_actors"`
}

type Actor struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Type string `json:"type"`
}
44 changes: 44 additions & 0 deletions internal/api/deployments_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package api

import (
"context"

"github.com/google/uuid"
)

// DeploymentAccessClient is a client for working with deployment access.
type DeploymentAccessClient interface {
Read(ctx context.Context, deploymentID uuid.UUID) (*DeploymentAccessControl, error)
Set(ctx context.Context, deploymentID uuid.UUID, accessControl DeploymentAccessSet) error
}

// DeploymentAccess is a representation of a deployment access.
type DeploymentAccess struct {
BaseModel
AccountID uuid.UUID `json:"account_id"`
WorkspaceID uuid.UUID `json:"workspace_id"`
DeploymentID uuid.UUID `json:"deployment_id"`
AccessControl DeploymentAccessControl `json:"access_control"`
}

// DeploymentAccessSet is a subset of DeploymentAccess used when setting deployment access control.
type DeploymentAccessSet struct {
AccessControl DeploymentAccessControlSet `json:"access_control"`
}

// DeploymentAccessControlSet is a definition of deployment access control.
type DeploymentAccessControlSet struct {
ManageActorIDs []string `json:"manage_actor_ids"`
RunActorIDs []string `json:"run_actor_ids"`
ViewActorIDs []string `json:"view_actor_ids"`
ManageTeamIDs []string `json:"manage_team_ids"`
RunTeamIDs []string `json:"run_team_ids"`
ViewTeamIDs []string `json:"view_team_ids"`
}

// DeploymentAccessControl is a definition of deployment access control.
type DeploymentAccessControl struct {
ManageActors []ObjectActorAccess `json:"manage_actors"`
RunActors []ObjectActorAccess `json:"run_actors"`
ViewActors []ObjectActorAccess `json:"view_actors"`
}
105 changes: 105 additions & 0 deletions internal/client/deployment_access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package client

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"

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

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

type DeploymentAccessClient struct {
hc *http.Client
routePrefix string
apiKey string
}

// DeploymentAccess returns a DeploymentAccessClient.
//
//nolint:ireturn // required to support PrefectClient mocking
func (c *Client) DeploymentAccess(accountID uuid.UUID, workspaceID uuid.UUID) (api.DeploymentAccessClient, error) {
if accountID == uuid.Nil {
accountID = c.defaultAccountID
}

if workspaceID == uuid.Nil {
workspaceID = c.defaultWorkspaceID
}

if helpers.IsCloudEndpoint(c.endpoint) && (accountID == uuid.Nil || workspaceID == uuid.Nil) {
return nil, fmt.Errorf("prefect Cloud endpoints require an account_id and workspace_id to be set on either the provider or the resource")
}

return &DeploymentAccessClient{
hc: c.hc,
routePrefix: getWorkspaceScopedURL(c.endpoint, accountID, workspaceID, "deployments"),
apiKey: c.apiKey,
}, nil
}

func (c *DeploymentAccessClient) Read(ctx context.Context, deploymentID uuid.UUID) (*api.DeploymentAccessControl, error) {
url := fmt.Sprintf("%s/%s/access", c.routePrefix, deploymentID.String())

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}

setDefaultHeaders(req, c.apiKey)

resp, err := c.hc.Do(req)
if err != nil {
return nil, fmt.Errorf("http error: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
errorBody, _ := io.ReadAll(resp.Body)

return nil, fmt.Errorf("status code %s, error=%s", resp.Status, errorBody)
}

var accessControl api.DeploymentAccessControl
if err := json.NewDecoder(resp.Body).Decode(&accessControl); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &accessControl, nil
}

func (c *DeploymentAccessClient) Set(ctx context.Context, deploymentID uuid.UUID, accessControl api.DeploymentAccessSet) error {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&accessControl); err != nil {
return fmt.Errorf("failed to encode access control: %w", err)
}

url := fmt.Sprintf("%s/%s/access", c.routePrefix, deploymentID.String())

req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, &buf)
if err != nil {
return fmt.Errorf("error creating request: %w", err)
}

setDefaultHeaders(req, c.apiKey)

resp, err := c.hc.Do(req)
if err != nil {
return fmt.Errorf("http error: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent {
errorBody, _ := io.ReadAll(resp.Body)

return fmt.Errorf("status code %s, error=%s", resp.Status, errorBody)
}

return nil
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ func (p *PrefectProvider) Resources(_ context.Context) []func() resource.Resourc
resources.NewAccountResource,
resources.NewFlowResource,
resources.NewDeploymentResource,
resources.NewDeploymentAccessResource,
resources.NewServiceAccountResource,
resources.NewVariableResource,
resources.NewWorkPoolResource,
Expand Down
Loading

0 comments on commit 9bf9b8b

Please sign in to comment.