Skip to content

Commit 6a6ec4c

Browse files
authored
Added databricks_entitlements resource (#1583)
1 parent cb42fdc commit 6a6ec4c

File tree

9 files changed

+1044
-3
lines changed

9 files changed

+1044
-3
lines changed

docs/resources/entitlements.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
subcategory: "Security"
3+
---
4+
# databricks_entitlements Resource
5+
6+
This resource allows you to set entitlements to existing [databricks_users](user.md), [databricks_group](group.md) or [databricks_service_principal](service_principal.md)
7+
8+
## Example Usage
9+
10+
Setting entitlements for a regular user:
11+
12+
```hcl
13+
data "databricks_user" "me" {
14+
user_name = "[email protected]"
15+
}
16+
17+
resource "databricks_entitlements" "me" {
18+
user_id = data.databricks_user.me.id
19+
allow_cluster_create = true
20+
allow_instance_pool_create = true
21+
}
22+
```
23+
24+
Setting entitlements for a service principal:
25+
26+
```hcl
27+
data "databricks_service_principal" "this" {
28+
application_id = "11111111-2222-3333-4444-555666777888"
29+
}
30+
31+
resource "databricks_entitlements" "this" {
32+
service_principal_id = data.databricks_service_principal.this.sp_id
33+
allow_cluster_create = true
34+
allow_instance_pool_create = true
35+
}
36+
```
37+
38+
Setting entitlements to all users in a workspace - referencing special `users` [databricks_group](../data-sources/group.md)
39+
40+
```hcl
41+
data "databricks_group" "users" {
42+
display_name = "users"
43+
}
44+
45+
resource "databricks_entitlements" "workspace-users" {
46+
group_id = data.databricks_group.users.id
47+
allow_cluster_create = true
48+
allow_instance_pool_create = true
49+
}
50+
```
51+
52+
## Argument Reference
53+
54+
The following arguments are available to specify the identity you need to enforce entitlements. You must specify exactly one of those arguments otherwise resource creation will fail.
55+
56+
* `user_id` - Canonical unique identifier for the user.
57+
* `group_id` - Canonical unique identifier for the group.
58+
* `service_principal_id` - Canonical unique identifier for the service principal.
59+
60+
The following entitlements are available.
61+
62+
* `allow_cluster_create` - (Optional) Allow the user to have [cluster](cluster.md) create privileges. Defaults to false. More fine grained permissions could be assigned with [databricks_permissions](permissions.md#Cluster-usage) and `cluster_id` argument. Everyone without `allow_cluster_create` argument set, but with [permission to use](permissions.md#Cluster-Policy-usage) Cluster Policy would be able to create clusters, but within boundaries of that specific policy.
63+
* `allow_instance_pool_create` - (Optional) Allow the user to have [instance pool](instance_pool.md) create privileges. Defaults to false. More fine grained permissions could be assigned with [databricks_permissions](permissions.md#Instance-Pool-usage) and [instance_pool_id](permissions.md#instance_pool_id) argument.
64+
* `databricks_sql_access` - (Optional) This is a field to allow the group to have access to [Databricks SQL](https://databricks.com/product/databricks-sql) feature in User Interface and through [databricks_sql_endpoint](sql_endpoint.md).
65+
66+
## Import
67+
68+
The resource can be imported using a synthetic identifier. Examples of valid synthetic identifiers are:
69+
70+
* `user/user_id` - user `user_id`.
71+
* `group/group_id` - group `group_id`.
72+
* `spn/spn_id` - service principal `spn_id`.
73+
74+
```bash
75+
terraform import databricks_entitlements.me user/<user-id>
76+
```
77+
78+
## Related Resources
79+
80+
The following resources are often used in the same context:
81+
82+
* [End to end workspace management](../guides/workspace-management.md) guide.
83+
* [databricks_group](group.md) to manage [groups in Databricks Workspace](https://docs.databricks.com/administration-guide/users-groups/groups.html) or [Account Console](https://accounts.cloud.databricks.com/) (for AWS deployments).
84+
* [databricks_group](../data-sources/group.md) data to retrieve information about [databricks_group](group.md) members, entitlements and instance profiles.
85+
* [databricks_group_instance_profile](group_instance_profile.md) to attach [databricks_instance_profile](instance_profile.md) (AWS) to [databricks_group](group.md).
86+
* [databricks_group_member](group_member.md) to attach [users](user.md) and [groups](group.md) as group members.
87+
* [databricks_instance_profile](instance_profile.md) to manage AWS EC2 instance profiles that users can launch [databricks_cluster](cluster.md) and access data, like [databricks_mount](mount.md).
88+
* [databricks_user](../data-sources/user.md) data to retrieve information about [databricks_user](user.md).

provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func DatabricksProvider() *schema.Provider {
7272
"databricks_cluster_policy": policies.ResourceClusterPolicy(),
7373
"databricks_dbfs_file": storage.ResourceDbfsFile(),
7474
"databricks_directory": workspace.ResourceDirectory(),
75+
"databricks_entitlements": scim.ResourceEntitlements(),
7576
"databricks_external_location": catalog.ResourceExternalLocation(),
7677
"databricks_git_credential": repos.ResourceGitCredential(),
7778
"databricks_global_init_script": workspace.ResourceGlobalInitScript(),
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package acceptance
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/databricks/terraform-provider-databricks/internal/acceptance"
8+
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
10+
)
11+
12+
func TestAccEntitlementResource(t *testing.T) {
13+
if _, ok := os.LookupEnv("CLOUD_ENV"); !ok {
14+
t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set")
15+
}
16+
t.Parallel()
17+
config := acceptance.EnvironmentTemplate(t, `
18+
resource "databricks_user" "first" {
19+
user_name = "tf-eerste+{var.RANDOM}@example.com"
20+
display_name = "Eerste {var.RANDOM}"
21+
allow_cluster_create = true
22+
allow_instance_pool_create = true
23+
}
24+
25+
resource "databricks_group" "second" {
26+
display_name = "{var.RANDOM} group"
27+
allow_cluster_create = true
28+
allow_instance_pool_create = true
29+
}
30+
31+
resource "databricks_entitlements" "first_entitlements" {
32+
user_id = databricks_user.first.id
33+
allow_cluster_create = true
34+
allow_instance_pool_create = true
35+
}
36+
37+
resource "databricks_entitlements" "second_entitlements" {
38+
group_id = databricks_group.second.id
39+
allow_cluster_create = true
40+
allow_instance_pool_create = true
41+
}
42+
`)
43+
acceptance.AccTest(t, resource.TestCase{
44+
Steps: []resource.TestStep{
45+
{
46+
Config: config,
47+
Check: resource.ComposeTestCheckFunc(
48+
resource.TestCheckResourceAttr("databricks_entitlements.first_entitlements", "allow_cluster_create", "true"),
49+
resource.TestCheckResourceAttr("databricks_entitlements.first_entitlements", "allow_instance_pool_create", "true"),
50+
resource.TestCheckResourceAttr("databricks_entitlements.second_entitlements", "allow_cluster_create", "true"),
51+
resource.TestCheckResourceAttr("databricks_entitlements.second_entitlements", "allow_instance_pool_create", "true"),
52+
),
53+
},
54+
{
55+
Config: config,
56+
},
57+
},
58+
})
59+
}
60+
61+
func TestAccServicePrincipalEntitlementsResourceOnAzure(t *testing.T) {
62+
if cloud, ok := os.LookupEnv("CLOUD_ENV"); !ok || cloud != "azure" {
63+
t.Skip("Test is only for CLOUD_ENV=azure")
64+
}
65+
t.Parallel()
66+
acceptance.Test(t, []acceptance.Step{
67+
{
68+
Template: `resource "databricks_service_principal" "this" {
69+
application_id = "00000000-1234-5678-0000-000000000001"
70+
display_name = "SPN {var.RANDOM}"
71+
allow_cluster_create = true
72+
allow_instance_pool_create = true
73+
}
74+
75+
resource "databricks_entitlements" "service_principal" {
76+
service_principal_id = databricks_service_principal.this.id
77+
allow_cluster_create = true
78+
allow_instance_pool_create = true
79+
}`,
80+
},
81+
})
82+
}
83+
84+
func TestAccServicePrincipalEntitlementsResourceOnAws(t *testing.T) {
85+
if cloud, ok := os.LookupEnv("CLOUD_ENV"); !ok || cloud != "aws" {
86+
t.Skip("Test is only for CLOUD_ENV=aws")
87+
}
88+
t.Parallel()
89+
acceptance.Test(t, []acceptance.Step{
90+
{
91+
Template: `resource "databricks_service_principal" "this" {
92+
display_name = "SPN {var.RANDOM}"
93+
allow_cluster_create = true
94+
allow_instance_pool_create = true
95+
}
96+
97+
resource "databricks_entitlements" "service_principal" {
98+
service_principal_id = databricks_service_principal.this.id
99+
allow_cluster_create = true
100+
allow_instance_pool_create = true
101+
}`,
102+
},
103+
})
104+
}

scim/groups.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ func (a GroupsAPI) UpdateNameAndEntitlements(groupID string, name string, extern
8484
}, nil)
8585
}
8686

87+
func (a GroupsAPI) UpdateEntitlements(groupID string, entitlements patchRequest) error {
88+
return a.client.Scim(a.context, http.MethodPatch,
89+
fmt.Sprintf("/preview/scim/v2/Groups/%v", groupID), entitlements, nil)
90+
}
91+
8792
// Delete deletes a group given a group id
8893
func (a GroupsAPI) Delete(groupID string) error {
8994
return a.client.Scim(a.context, http.MethodDelete,

scim/resource_entitlement.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package scim
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/databricks/terraform-provider-databricks/common"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
// ResourceGroup manages user groups
13+
func ResourceEntitlements() *schema.Resource {
14+
type entity struct {
15+
GroupId string `json:"group_id,omitempty" tf:"force_new"`
16+
UserId string `json:"user_id,omitempty" tf:"force_new"`
17+
SpnId string `json:"service_principal_id,omitempty" tf:"force_new"`
18+
}
19+
entitlementSchema := common.StructToSchema(entity{},
20+
func(m map[string]*schema.Schema) map[string]*schema.Schema {
21+
addEntitlementsToSchema(&m)
22+
alof := []string{"group_id", "user_id", "service_principal_id"}
23+
for _, field := range alof {
24+
m[field].AtLeastOneOf = alof
25+
}
26+
return m
27+
})
28+
addEntitlementsToSchema(&entitlementSchema)
29+
return common.Resource{
30+
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
31+
return patchEntitlements(ctx, d, c, "add")
32+
},
33+
Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
34+
split := strings.SplitN(d.Id(), "/", 2)
35+
if len(split) != 2 {
36+
return fmt.Errorf("ID must be two elements: %s", d.Id())
37+
}
38+
switch strings.ToLower(split[0]) {
39+
case "group":
40+
group, err := NewGroupsAPI(ctx, c).Read(split[1])
41+
if err != nil {
42+
return err
43+
}
44+
return group.Entitlements.readIntoData(d)
45+
case "user":
46+
user, err := NewUsersAPI(ctx, c).Read(split[1])
47+
if err != nil {
48+
return err
49+
}
50+
return user.Entitlements.readIntoData(d)
51+
case "spn":
52+
spn, err := NewServicePrincipalsAPI(ctx, c).Read(split[1])
53+
if err != nil {
54+
return err
55+
}
56+
return spn.Entitlements.readIntoData(d)
57+
}
58+
return nil
59+
},
60+
Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
61+
return enforceEntitlements(ctx, d, c)
62+
},
63+
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
64+
return patchEntitlements(ctx, d, c, "remove")
65+
},
66+
Schema: entitlementSchema,
67+
}.ToResource()
68+
}
69+
70+
func patchEntitlements(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient, op string) error {
71+
groupId := d.Get("group_id").(string)
72+
userId := d.Get("user_id").(string)
73+
spnId := d.Get("service_principal_id").(string)
74+
request := PatchRequestComplexValue([]patchOperation{
75+
{
76+
op,
77+
"entitlements",
78+
readEntitlementsFromData(d),
79+
},
80+
})
81+
if groupId != "" {
82+
groupsAPI := NewGroupsAPI(ctx, c)
83+
err := groupsAPI.UpdateEntitlements(groupId, request)
84+
if err != nil {
85+
return err
86+
}
87+
d.SetId("group/" + groupId)
88+
}
89+
if userId != "" {
90+
usersAPI := NewUsersAPI(ctx, c)
91+
err := usersAPI.UpdateEntitlements(userId, request)
92+
if err != nil {
93+
return err
94+
}
95+
d.SetId("user/" + userId)
96+
}
97+
if spnId != "" {
98+
spnAPI := NewServicePrincipalsAPI(ctx, c)
99+
err := spnAPI.UpdateEntitlements(spnId, request)
100+
if err != nil {
101+
return err
102+
}
103+
d.SetId("spn/" + spnId)
104+
}
105+
return nil
106+
}
107+
108+
func enforceEntitlements(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
109+
split := strings.SplitN(d.Id(), "/", 2)
110+
if len(split) != 2 {
111+
return fmt.Errorf("ID must be two elements: %s", d.Id())
112+
}
113+
identity := strings.ToLower(split[0])
114+
id := strings.ToLower(split[1])
115+
request := PatchRequestComplexValue(
116+
[]patchOperation{
117+
{
118+
"remove", "entitlements", generateFullEntitlements(),
119+
},
120+
{
121+
"add", "entitlements", readEntitlementsFromData(d),
122+
},
123+
},
124+
)
125+
switch identity {
126+
case "group":
127+
NewGroupsAPI(ctx, c).UpdateEntitlements(id, request)
128+
case "user":
129+
NewUsersAPI(ctx, c).UpdateEntitlements(id, request)
130+
case "spn":
131+
NewServicePrincipalsAPI(ctx, c).UpdateEntitlements(id, request)
132+
}
133+
return nil
134+
}

0 commit comments

Comments
 (0)