Skip to content

Commit

Permalink
Added databricks_mws_permission_assignment resource (databricks#1491)
Browse files Browse the repository at this point in the history
These resources are invoked in the account context. Provider must have `account_id` attribute configured. Example usage in account context, adding account-level group to a workspace:

```hcl
provider "databricks" {
  // <other properties>
  account_id = "<databricks account id>"
}

resource "databricks_group" "data_eng" {
  display_name = "Data Engineering"
}

resource "databricks_mws_permission_assignment" "add_admin_group" {
  workspace_id = databricks_mws_workspaces.this.workspace_id
  principal_id = databricks_group.data_eng.id
  permissions  = ["ADMIN"]
}
```
  • Loading branch information
nfx authored Aug 4, 2022
1 parent fbc0eff commit 241201e
Show file tree
Hide file tree
Showing 12 changed files with 593 additions and 13 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
| [databricks_mws_customer_managed_keys](docs/resources/mws_customer_managed_keys.md)
| [databricks_mws_log_delivery](docs/resources/mws_log_delivery.md)
| [databricks_mws_networks](docs/resources/mws_networks.md)
| [databricks_mws_permission_assignment](docs/resources/mws_permission_assignment.md)
| [databricks_mws_private_access_settings](docs/resources/mws_private_access_settings.md)
| [databricks_mws_storage_configurations](docs/resources/mws_storage_configurations.md)
| [databricks_mws_vpc_endpoint](docs/resources/mws_vpc_endpoint.md)
Expand Down
115 changes: 115 additions & 0 deletions access/resource_permission_assignment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package access

import (
"context"
"fmt"
"strconv"

"github.com/databricks/terraform-provider-databricks/common"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func NewPermissionAssignmentAPI(ctx context.Context, m any) PermissionAssignmentAPI {
return PermissionAssignmentAPI{m.(*common.DatabricksClient), ctx}
}

type PermissionAssignmentAPI struct {
client *common.DatabricksClient
context context.Context
}

type Permissions struct {
Permissions []string `json:"permissions"`
}

func (a PermissionAssignmentAPI) CreateOrUpdate(principalId int64, r Permissions) error {
path := fmt.Sprintf("/preview/permissionassignments/principals/%d", principalId)
return a.client.Put(a.context, path, r)
}

func (a PermissionAssignmentAPI) Remove(principalId string) error {
path := fmt.Sprintf("/preview/permissionassignments/principals/%s", principalId)
return a.client.Delete(a.context, path, nil)
}

type Principal struct {
DisplayName string `json:"display_name"`
PrincipalID int64 `json:"principal_id"`
ServicePrincipalName string `json:"service_principal_name,omitempty"`
UserName string `json:"user_name,omitempty"`
GroupName string `json:"group_name,omitempty"`
}

type PermissionAssignment struct {
Permissions []string `json:"permissions"`
Principal Principal
}

type PermissionAssignmentList struct {
PermissionAssignments []PermissionAssignment `json:"permission_assignments"`
}

func (l PermissionAssignmentList) ForPrincipal(principalId int64) (res Permissions, err error) {
for _, v := range l.PermissionAssignments {
if v.Principal.PrincipalID != principalId {
continue
}
return Permissions{v.Permissions}, nil
}
return res, common.NotFound(fmt.Sprintf("%d not found", principalId))
}

func (a PermissionAssignmentAPI) List() (list PermissionAssignmentList, err error) {
err = a.client.Get(a.context, "/preview/permissionassignments", nil, &list)
return
}

func mustInt64(s string) int64 {
n, err := strconv.ParseInt(s, 10, 0)
if err != nil {
panic(err)
}
return n
}

// ResourcePermissionAssignment performs of users to a workspace
// from a workspace context, though it requires additional set
// data resource for "workspace account scim", whicl will be added later.
func ResourcePermissionAssignment() *schema.Resource {
type entity struct {
PrincipalId int64 `json:"principal_id"`
Permissions []string `json:"permissions" tf:"slice_as_set"`
}
s := common.StructToSchema(entity{},
func(m map[string]*schema.Schema) map[string]*schema.Schema {
return m
})
return common.Resource{
Schema: s,
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
var assignment entity
common.DataToStructPointer(d, s, &assignment)
api := NewPermissionAssignmentAPI(ctx, c)
err := api.CreateOrUpdate(assignment.PrincipalId, Permissions{assignment.Permissions})
if err != nil {
return err
}
d.SetId(fmt.Sprintf("%d", assignment.PrincipalId))
return nil
},
Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
list, err := NewPermissionAssignmentAPI(ctx, c).List()
if err != nil {
return err
}
permissions, err := list.ForPrincipal(mustInt64(d.Id()))
if err != nil {
return err
}
return common.StructToData(permissions, s, d)
},
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
return NewPermissionAssignmentAPI(ctx, c).Remove(d.Id())
},
}.ToResource()
}
72 changes: 72 additions & 0 deletions access/resource_permission_assignment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package access

import (
"testing"

"github.com/databricks/terraform-provider-databricks/qa"
)

func TestPermissionAssignmentCreate(t *testing.T) {
qa.ResourceFixture{
Fixtures: []qa.HTTPFixture{
{
Method: "PUT",
Resource: "/api/2.0/preview/permissionassignments/principals/345",
ExpectedRequest: Permissions{
Permissions: []string{"USER"},
},
},
{
Method: "GET",
Resource: "/api/2.0/preview/permissionassignments",
Response: PermissionAssignmentList{
PermissionAssignments: []PermissionAssignment{
{
Permissions: []string{"USER"},
Principal: Principal{
PrincipalID: 345,
},
},
},
},
},
},
Resource: ResourcePermissionAssignment(),
Create: true,
AccountID: "abc",
HCL: `
principal_id = 345
permissions = ["USER"]
`,
}.ApplyNoError(t)
}

func TestPermissionAssignmentReadNotFound(t *testing.T) {
qa.ResourceFixture{
Fixtures: []qa.HTTPFixture{
{
Method: "GET",
Resource: "/api/2.0/preview/permissionassignments",
Response: PermissionAssignmentList{
PermissionAssignments: []PermissionAssignment{
{
Permissions: []string{"USER"},
Principal: Principal{
PrincipalID: 345,
},
},
},
},
},
},
Resource: ResourcePermissionAssignment(),
Read: true,
Removed: true,
AccountID: "abc",
ID: "123",
}.ApplyNoError(t)
}

func TestPermissionAssignmentFuzz(t *testing.T) {
qa.ResourceCornerCases(t, ResourcePermissionAssignment())
}
5 changes: 4 additions & 1 deletion catalog/resource_metastore_assignment.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ func ResourceMetastoreAssignment() *schema.Resource {
func(m map[string]*schema.Schema) map[string]*schema.Schema {
return m
})
pi := common.NewPairID("workspace_id", "metastore_id")
pi := common.NewPairID("workspace_id", "metastore_id").Schema(
func(m map[string]*schema.Schema) map[string]*schema.Schema {
return s
})
return common.Resource{
Schema: s,
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
Expand Down
26 changes: 16 additions & 10 deletions common/pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,23 @@ func (p *Pair) Unpack(d *schema.ResourceData) (string, string, error) {
d.SetId("")
return "", "", fmt.Errorf("%s cannot be empty", p.right)
}
d.Set(p.left, parts[0])
if p.schema[p.right].Type == schema.TypeInt {
i64, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return parts[0], parts[1], err
}
d.Set(p.right, i64)
} else {
d.Set(p.right, parts[1])
err := p.setField(d, p.left, parts[0])
if err != nil {
return parts[0], parts[1], err
}
return parts[0], parts[1], nil
err = p.setField(d, p.right, parts[1])
return parts[0], parts[1], err
}

func (p *Pair) setField(d *schema.ResourceData, col, val string) error {
if p.schema[col].Type != schema.TypeInt {
return d.Set(col, val)
}
i64, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return err
}
return d.Set(col, i64)
}

// Pack data attributes to ID
Expand Down
65 changes: 65 additions & 0 deletions docs/resources/mws_permission_assignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
subcategory: "Unity Catalog"
---
# databricks_mws_permission_assignment Resource

These resources are invoked in the account context. Provider must have `account_id` attribute configured.

## Example Usage

In account context, adding account-level group to a workspace:

```hcl
provider "databricks" {
// <other properties>
account_id = "<databricks account id>"
}
resource "databricks_group" "data_eng" {
display_name = "Data Engineering"
}
resource "databricks_mws_permission_assignment" "add_admin_group" {
workspace_id = databricks_mws_workspaces.this.workspace_id
principal_id = databricks_group.data_eng.id
permissions = ["ADMIN"]
}
```

In account context, adding account-level user to a workspace:

```hcl
provider "databricks" {
// <other properties>
account_id = "<databricks account id>"
}
resource "databricks_user" "me" {
user_name = "me@example.com"
}
resource "databricks_mws_permission_assignment" "add_user" {
workspace_id = databricks_mws_workspaces.this.workspace_id
principal_id = databricks_user.me.id
permissions = ["USER"]
}
```

In account context, adding account-level service principal to a workspace:

```hcl
provider "databricks" {
// <other properties>
account_id = "<databricks account id>"
}
resource "databricks_service_principal" "sp" {
display_name = "Automation-only SP"
}
resource "databricks_mws_permission_assignment" "add_admin_spn" {
workspace_id = databricks_mws_workspaces.this.workspace_id
principal_id = databricks_service_principal.sp.id
permissions = ["ADMIN"]
}
```
3 changes: 2 additions & 1 deletion internal/acceptance/acceptance.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ func Test(t *testing.T, steps []Step, otherVars ...map[string]string) {
awsAttrs = "aws_attributes {}"
}
instancePoolID := ""
if cloudEnv != "MWS" && cloudEnv != "gcp-accounts" {
if cloudEnv != "MWS" && cloudEnv != "gcp-accounts" && !strings.HasPrefix(cloudEnv, "unity-catalog-") {
// TODO: replace this with data resource
instancePoolID = compute.CommonInstancePoolID()
}
vars := map[string]string{
Expand Down
64 changes: 64 additions & 0 deletions mws/acceptance/mws_permissionassignments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package acceptance

import (
"testing"

"github.com/databricks/terraform-provider-databricks/internal/acceptance"
"github.com/databricks/terraform-provider-databricks/qa"
)

func TestAccAssignGroupToWorkspace(t *testing.T) {
qa.RequireCloudEnv(t, "unity-catalog-account")
acceptance.Test(t, []acceptance.Step{
{
Template: `
resource "databricks_group" "this" {
display_name = "TF {var.RANDOM}"
}
resource "databricks_mws_permission_assignment" "this" {
workspace_id = {env.TEST_UC_WORKSPACE_ID}
principal_id = databricks_group.this.id
permissions = ["USER"]
}`,
},
{
Template: `
resource "databricks_group" "this" {
display_name = "TF {var.RANDOM}"
}
resource "databricks_mws_permission_assignment" "this" {
workspace_id = {env.TEST_UC_WORKSPACE_ID}
principal_id = databricks_group.this.id
permissions = ["ADMIN"]
}`,
},
{
Template: `
resource "databricks_group" "this" {
display_name = "TF {var.RANDOM}"
}
resource "databricks_mws_permission_assignment" "this" {
workspace_id = {env.TEST_UC_WORKSPACE_ID}
principal_id = databricks_group.this.id
permissions = ["USER"]
}`,
},
})
}

func TestAccAssignSpnToWorkspace(t *testing.T) {
qa.RequireCloudEnv(t, "unity-catalog-account")
acceptance.Test(t, []acceptance.Step{
{
Template: `
resource "databricks_service_principal" "this" {
display_name = "TF {var.RANDOM}"
}
resource "databricks_mws_permission_assignment" "this" {
workspace_id = {env.TEST_UC_WORKSPACE_ID}
principal_id = databricks_service_principal.this.id
permissions = ["USER"]
}`,
},
})
}
Loading

0 comments on commit 241201e

Please sign in to comment.