Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added databricks_mws_permission_assignment resource #1491

Merged
merged 7 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 will there be a documentation for access/permission_assignment? For that one API doesn't need workspace id or account id, they can be derived from the workspace url.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have a way to retrieve account-level IDs from workspace in the provider yet. please "publish" the api ;)


## Example Usage

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

Comment on lines +10 to +11

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that we only have docs for updating role assignment in the account context. I think thats fine and sufficient but it will not work for workspace admin personas who are not account admins.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, we'll add that "workspace level account scim user listing" in one of the following releases.

```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"]
}
```
nfx marked this conversation as resolved.
Show resolved Hide resolved
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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, what does this test do? Does it test that the template can be accepted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it does create, read, update, and delete test assignments of test principals

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