Skip to content

Commit

Permalink
feat(blocks): add prefect_block_access resource for binding ACLs to…
Browse files Browse the repository at this point in the history
… Block resources (#206)

* add block_access resource

* docs

* Generate Terraform Docs

* ok

* Generate Terraform Docs

* add delay

* add file

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
parkedwards and github-actions[bot] authored Jun 10, 2024
1 parent 2fa004f commit 39c62c1
Show file tree
Hide file tree
Showing 20 changed files with 536 additions and 81 deletions.
2 changes: 1 addition & 1 deletion docs/data-sources/account_member.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ data "prefect_account_member" "marvin" {

- `account_role_id` (String) Acount Role ID (UUID)
- `account_role_name` (String) Name of Account Role assigned to member
- `actor_id` (String) Actor ID (UUID)
- `actor_id` (String) Actor ID (UUID), used for granting access to resources like Blocks and Deployments
- `first_name` (String) Member's first name
- `handle` (String) Member handle, or a human-readable identifier
- `id` (String) Account Member ID (UUID)
Expand Down
2 changes: 1 addition & 1 deletion docs/data-sources/account_members.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Read-Only:

- `account_role_id` (String) Acount Role ID (UUID)
- `account_role_name` (String) Name of Account Role assigned to member
- `actor_id` (String) Actor ID (UUID)
- `actor_id` (String) Actor ID (UUID), used for granting access to resources like Blocks and Deployments
- `first_name` (String) Member's first name
- `handle` (String) Member handle, or a human-readable identifier
- `id` (String) Account Member ID (UUID)
Expand Down
1 change: 1 addition & 0 deletions docs/data-sources/service_account.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ data "prefect_service_account" "bot" {
### Read-Only

- `account_role_name` (String) Account Role name of the service account
- `actor_id` (String) Actor ID (UUID), used for granting access to resources like Blocks and Deployments
- `api_key` (String) API Key associated with the service account
- `api_key_created` (String) Date and time that the API Key was created in RFC 3339 format
- `api_key_expiration` (String) Date and time that the API Key expires in RFC 3339 format
Expand Down
2 changes: 1 addition & 1 deletion docs/resources/block.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ resource "prefect_block" "gcp_credentials_key" {
### Optional

- `account_id` (String) Account ID (UUID) where the Block is located
- `workspace_id` (String) Workspace ID (UUID) where the Block is located. In Prefect Cloud, either the resource or the provider's `workspace_id` must be set in order to manage the Block.
- `workspace_id` (String) Workspace ID (UUID) where the Block is located. In Prefect Cloud, either the `prefect_block` resource or the provider's `workspace_id` must be set.

### Read-Only

Expand Down
110 changes: 110 additions & 0 deletions docs/resources/block_access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "prefect_block_access Resource - prefect"
subcategory: ""
description: |-
This resource manages access control to Blocks. Accessors can be Service Accounts, Users, or Teams. Accessors can be Managers or Viewers.
All Actors/Teams must first be granted access to the Workspace where the Block resides.
Leave fields empty to use the default access controls
---

# prefect_block_access (Resource)

This resource manages access control to Blocks. Accessors can be Service Accounts, Users, or Teams. Accessors can be Managers or Viewers.

All Actors/Teams must first be granted access to the Workspace where the Block resides.

Leave fields empty to use the default access controls

## Example Usage

```terraform
provider "prefect" {}
# All Blocks are scoped to a Workspace
data "prefect_workspace" "my_workspace" {
handle = "my-workspace"
}
resource "prefect_block" "my_secret" {
name = "my-secret"
type_slug = "secret"
data = jsonencode({
"value" : "foobar"
})
workspace_id = data.prefect_workspace.my_workspace.id
}
# Be sure to grant all Actors/Teams who need Block 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
resource "prefect_service_account" "bot" {
name = "bot"
}
resource "prefect_workspace_access" "bot_developer" {
accessor_type = "SERVICE_ACCOUNT"
accessor_id = prefect_service_account.bot.id
workspace_role_id = data.prefect_workspace_role.developer.id
workspace_id = data.prefect_workspace.my_workspace.id
}
# Example: invite a User to the Workspace
data "prefect_account_member" "user" {
email = "[email protected]"
}
resource "prefect_workspace_access" "user_developer" {
accessor_type = "USER"
accessor_id = data.prefect_account_member.user.user_id
workspace_role_id = data.prefect_workspace_role.developer.id
workspace_id = data.prefect_workspace.my_workspace.id
}
# Example: invite a Team to the Workspace
data "prefect_team" "eng" {
name = "my-team"
}
resource "prefect_workspace_access" "team_developer" {
accessor_type = "TEAM"
accessor_id = data.prefect_team.eng.id
workspace_role_id = data.prefect_workspace_role.developer.id
workspace_id = data.prefect_workspace.my_workspace.id
}
# Grant all Actors/Teams the appropriate Manage or View access to the Block
resource "prefect_block_access" "custom_access" {
block_id = prefect_block.my_secret.id
manage_actor_ids = [prefect_service_account.bot.actor_id]
view_actor_ids = [data.prefect_account_member.user.actor_id]
manage_team_ids = [data.prefect_team.eng.id]
workspace_id = data.prefect_workspace.my_workspace.id
}
# Optionally, leave all fields empty to use the default access controls
resource "prefect_block_access" "default_access" {
block_id = prefect_block.my_secret.id
manage_actor_ids = []
view_actor_ids = []
manage_team_ids = []
view_team_ids = []
workspace_id = data.prefect_workspace.my_workspace.id
}
```

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

### Required

- `block_id` (String) Block ID (UUID)

### Optional

- `account_id` (String) Account ID (UUID) where the Block is located
- `manage_actor_ids` (List of String) List of actor IDs with manage access to the Block
- `manage_team_ids` (List of String) List of team IDs with manage access to the Block
- `view_actor_ids` (List of String) List of actor IDs with view access to the Block
- `view_team_ids` (List of String) List of team IDs with view access to the Block
- `workspace_id` (String) Workspace ID (UUID) where the Block is located. In Prefect Cloud, either the `prefect_block_access` resource or the provider's `workspace_id` must be set.
1 change: 1 addition & 0 deletions docs/resources/service_account.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ resource "prefect_service_account" "example" {

### Read-Only

- `actor_id` (String) Actor ID (UUID), used for granting access to resources like Blocks and Deployments
- `api_key` (String, Sensitive) API Key associated with the service account
- `api_key_created` (String) Timestamp of the API Key creation (RFC3339)
- `api_key_id` (String) API Key ID associated with the service account
Expand Down
72 changes: 72 additions & 0 deletions examples/resources/prefect_block_access/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
provider "prefect" {}

# All Blocks are scoped to a Workspace
data "prefect_workspace" "my_workspace" {
handle = "my-workspace"
}
resource "prefect_block" "my_secret" {
name = "my-secret"
type_slug = "secret"
data = jsonencode({
"value" : "foobar"
})
workspace_id = data.prefect_workspace.my_workspace.id
}

# Be sure to grant all Actors/Teams who need Block 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
resource "prefect_service_account" "bot" {
name = "bot"
}
resource "prefect_workspace_access" "bot_developer" {
accessor_type = "SERVICE_ACCOUNT"
accessor_id = prefect_service_account.bot.id
workspace_role_id = data.prefect_workspace_role.developer.id
workspace_id = data.prefect_workspace.my_workspace.id
}

# Example: invite a User to the Workspace
data "prefect_account_member" "user" {
email = "[email protected]"
}
resource "prefect_workspace_access" "user_developer" {
accessor_type = "USER"
accessor_id = data.prefect_account_member.user.user_id
workspace_role_id = data.prefect_workspace_role.developer.id
workspace_id = data.prefect_workspace.my_workspace.id
}

# Example: invite a Team to the Workspace
data "prefect_team" "eng" {
name = "my-team"
}
resource "prefect_workspace_access" "team_developer" {
accessor_type = "TEAM"
accessor_id = data.prefect_team.eng.id
workspace_role_id = data.prefect_workspace_role.developer.id
workspace_id = data.prefect_workspace.my_workspace.id
}

# Grant all Actors/Teams the appropriate Manage or View access to the Block
resource "prefect_block_access" "custom_access" {
block_id = prefect_block.my_secret.id
manage_actor_ids = [prefect_service_account.bot.actor_id]
view_actor_ids = [data.prefect_account_member.user.actor_id]
manage_team_ids = [data.prefect_team.eng.id]
workspace_id = data.prefect_workspace.my_workspace.id
}

# Optionally, leave all fields empty to use the default access controls
resource "prefect_block_access" "default_access" {
block_id = prefect_block.my_secret.id
manage_actor_ids = []
view_actor_ids = []
manage_team_ids = []
view_team_ids = []
workspace_id = data.prefect_workspace.my_workspace.id
}
18 changes: 10 additions & 8 deletions internal/api/block_documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ type BlockDocumentClient interface {
Update(ctx context.Context, id uuid.UUID, payload BlockDocumentUpdate) error
Delete(ctx context.Context, id uuid.UUID) error

GetACL(ctx context.Context, id uuid.UUID) (*BlockDocumentAccess, error)
UpdateACL(ctx context.Context, id uuid.UUID, payload BlockDocumentAccessReplace) error
GetAccess(ctx context.Context, id uuid.UUID) (*BlockDocumentAccess, error)
UpsertAccess(ctx context.Context, id uuid.UUID, payload BlockDocumentAccessUpsert) error
}

type BlockDocument struct {
Expand Down Expand Up @@ -42,15 +42,17 @@ type BlockDocumentUpdate struct {
MergeExistingData bool `json:"merge_existing_data"`
}

// BlockDocumentAccessReplace is the "update" request payload
// BlockDocumentAccessUpsert is the create/update request payload
// to modify a block document's current access control levels,
// meaning it contains the list of actors/teams + their respective access
// to a given block document.
type BlockDocumentAccessReplace struct {
ManageActorIDs []AccessActorID `json:"manage_actor_ids"`
ViewActorIDs []AccessActorID `json:"view_actor_ids"`
ManageTeamIDs []uuid.UUID `json:"manage_team_ids"`
ViewTeamIDs []uuid.UUID `json:"view_team_ids"`
type BlockDocumentAccessUpsert struct {
AccessControl struct {
ManageActorIDs []string `json:"manage_actor_ids"`
ViewActorIDs []string `json:"view_actor_ids"`
ManageTeamIDs []string `json:"manage_team_ids"`
ViewTeamIDs []string `json:"view_team_ids"`
} `json:"access_control"`
}

// BlockDocumentAccess is the API object representing a
Expand Down
61 changes: 1 addition & 60 deletions internal/api/helpers.go
Original file line number Diff line number Diff line change
@@ -1,64 +1,5 @@
package api

import (
"encoding/json"
"fmt"

"github.com/google/uuid"
)

// AccessActorID is a custom type that represents
// an API response where the value can be:
// uuid.UUID - a single ID
// "*" - a wildcard string, meaning "all".
//
// nolint:musttag // we have custom marshal/unmarshal logic for this type
type AccessActorID struct {
ID *uuid.UUID
All bool
}

// Custom JSON marshaling for AccessActorID
// so we can return uuid.UUID or "*" back to the API.
func (aid AccessActorID) MarshalJSON() ([]byte, error) {
if aid.All {
return []byte("*"), nil
}
if aid.ID != nil {
uuidByteSlice, err := json.Marshal(aid.ID)
if err != nil {
return nil, fmt.Errorf("failed to marshal AccessActorID: %w", err)
}

return uuidByteSlice, nil
}

return nil, fmt.Errorf("invalid AccessActorID: both ID and All are nil/false")
}

// Custom JSON unmarshaling for AccessActorID
// so we can accept uuid.UUID or "*" from the API
// in a structured format.
func (aid *AccessActorID) UnmarshalJSON(data []byte) error {
var id uuid.UUID
if err := json.Unmarshal(data, &id); err == nil {
aid.ID = &id
aid.All = false

return nil
}

var all string
if err := json.Unmarshal(data, &all); err == nil && all == "*" {
aid.All = true
aid.ID = nil

return nil
}

return fmt.Errorf("invalid AccessActorID format")
}

// AccessActorType represents an enum of type values
// used in our Access APIs.
type AccessActorType string
Expand All @@ -71,7 +12,7 @@ const (
)

type ObjectActorAccess struct {
ID AccessActorID `json:"id"`
ID string `json:"id"`
Name string `json:"name"`
Email *string `json:"email"`
Type AccessActorType `json:"type"`
Expand Down
1 change: 1 addition & 0 deletions internal/api/service_accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type ServiceAccountFilter struct {
// ServiceAccount is a representation of a created service account (from a Create response).
type ServiceAccount struct {
BaseModel
ActorID uuid.UUID `json:"actor_id"`
AccountID uuid.UUID `json:"account_id"`
Name string `json:"name"`
AccountRoleName string `json:"account_role_name"`
Expand Down
5 changes: 2 additions & 3 deletions internal/client/block_documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func (c *BlockDocumentClient) Delete(ctx context.Context, id uuid.UUID) error {
return nil
}

func (c *BlockDocumentClient) GetACL(ctx context.Context, id uuid.UUID) (*api.BlockDocumentAccess, error) {
func (c *BlockDocumentClient) GetAccess(ctx context.Context, id uuid.UUID) (*api.BlockDocumentAccess, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/%s/access", c.routePrefix, id.String()), http.NoBody)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
Expand Down Expand Up @@ -187,8 +187,7 @@ func (c *BlockDocumentClient) GetACL(ctx context.Context, id uuid.UUID) (*api.Bl
return &blockDocumentAccess, nil
}

func (c *BlockDocumentClient) UpdateACL(ctx context.Context, id uuid.UUID, payload api.
BlockDocumentAccessReplace) error {
func (c *BlockDocumentClient) UpsertAccess(ctx context.Context, id uuid.UUID, payload api.BlockDocumentAccessUpsert) error {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&payload); err != nil {
return fmt.Errorf("failed to encode update payload data: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/datasources/account_member.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ var accountMemberAttributesBase = map[string]schema.Attribute{
"actor_id": schema.StringAttribute{
Computed: true,
CustomType: customtypes.UUIDType{},
Description: "Actor ID (UUID)",
Description: "Actor ID (UUID), used for granting access to resources like Blocks and Deployments",
},
"user_id": schema.StringAttribute{
Computed: true,
Expand Down
6 changes: 6 additions & 0 deletions internal/provider/datasources/service_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type ServiceAccountDataSourceModel struct {
Updated customtypes.TimestampValue `tfsdk:"updated"`

Name types.String `tfsdk:"name"`
ActorID customtypes.UUIDValue `tfsdk:"actor_id"`
AccountID customtypes.UUIDValue `tfsdk:"account_id"`
AccountRoleName types.String `tfsdk:"account_role_name"`

Expand Down Expand Up @@ -85,6 +86,11 @@ var serviceAccountAttributes = map[string]schema.Attribute{
CustomType: customtypes.TimestampType{},
Description: "Timestamp of when the resource was updated (RFC3339)",
},
"actor_id": schema.StringAttribute{
Computed: true,
CustomType: customtypes.UUIDType{},
Description: "Actor ID (UUID), used for granting access to resources like Blocks and Deployments",
},
"account_id": schema.StringAttribute{
CustomType: customtypes.UUIDType{},
Description: "Account ID (UUID), defaults to the account set in the provider",
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/helpers/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ func CreateClientErrorDiagnostic(clientName string, err error) diag.Diagnostic {
func ResourceClientErrorDiagnostic(resourceName string, operation string, err error) diag.Diagnostic {
return diag.NewErrorDiagnostic(
fmt.Sprintf("Error during %s %s", operation, resourceName),
fmt.Sprintf("Could not %s %s, unexpected error: %s", operation, resourceName, err),
fmt.Sprintf("Could not %s %s, unexpected error: %s", operation, resourceName, err.Error()),
)
}
Loading

0 comments on commit 39c62c1

Please sign in to comment.