Skip to content
2 changes: 2 additions & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
### Exporter

### Internal Changes

* Make `databricks_entitlements` forward-compatible with new entitlements ([#4763](https://github.com/databricks/terraform-provider-databricks/pull/4763))
40 changes: 18 additions & 22 deletions scim/data_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,35 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

type groupData struct {
entitlements
DisplayName string `json:"display_name"`
Recursive bool `json:"recursive,omitempty"`
Members []string `json:"members,omitempty" tf:"slice_set,computed"`
Users []string `json:"users,omitempty" tf:"slice_set,computed"`
ServicePrincipals []string `json:"service_principals,omitempty" tf:"slice_set,computed"`
ChildGroups []string `json:"child_groups,omitempty" tf:"slice_set,computed"`
Groups []string `json:"groups,omitempty" tf:"slice_set,computed"`
InstanceProfiles []string `json:"instance_profiles,omitempty" tf:"slice_set,computed"`
ExternalID string `json:"external_id,omitempty" tf:"computed"`
AclPrincipalID string `json:"acl_principal_id,omitempty" tf:"computed"`
}

// DataSourceGroup returns information about group specified by display name
func DataSourceGroup() common.Resource {
type entity struct {
DisplayName string `json:"display_name"`
Recursive bool `json:"recursive,omitempty"`
Members []string `json:"members,omitempty" tf:"slice_set,computed"`
Users []string `json:"users,omitempty" tf:"slice_set,computed"`
ServicePrincipals []string `json:"service_principals,omitempty" tf:"slice_set,computed"`
ChildGroups []string `json:"child_groups,omitempty" tf:"slice_set,computed"`
Groups []string `json:"groups,omitempty" tf:"slice_set,computed"`
InstanceProfiles []string `json:"instance_profiles,omitempty" tf:"slice_set,computed"`
ExternalID string `json:"external_id,omitempty" tf:"computed"`
AclPrincipalID string `json:"acl_principal_id,omitempty" tf:"computed"`
}

s := common.StructToSchema(entity{}, func(
s := common.StructToSchema(groupData{}, func(
s map[string]*schema.Schema) map[string]*schema.Schema {
// nolint once SDKv2 has Diagnostics-returning validators, change
s["display_name"].ValidateFunc = validation.StringIsNotEmpty
s["recursive"].Default = true
s["members"].Deprecated = "Please use `users`, `service_principals`, and `child_groups` instead"
addEntitlementsToSchema(s)
return s
})

return common.Resource{
Schema: s,
Read: func(ctx context.Context, d *schema.ResourceData, m *common.DatabricksClient) error {
var this entity
var this groupData
var group Group
var err error
common.DataToStructPointer(d, s, &this)
Expand Down Expand Up @@ -79,7 +79,7 @@ func DataSourceGroup() common.Resource {
for _, x := range current.Roles {
this.InstanceProfiles = append(this.InstanceProfiles, x.Value)
}
current.Entitlements.readIntoData(d)
this.entitlements = mergeEntitlements(this.entitlements, newEntitlements(ctx, current.Entitlements))
for _, x := range current.Groups {
this.Groups = append(this.Groups, x.Value)
if this.Recursive {
Expand All @@ -99,11 +99,7 @@ func DataSourceGroup() common.Resource {
sort.Strings(this.ChildGroups)
sort.Strings(this.ServicePrincipals)
sort.Strings(this.InstanceProfiles)
err = common.StructToData(this, s, d)
if err != nil {
return err
}
return nil
return common.StructToData(this, s, d)
},
}
}
1 change: 1 addition & 0 deletions scim/data_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func TestDataSourceGroup(t *testing.T) {
},
Read: true,
NonWritable: true,
New: true,
Resource: DataSourceGroup(),
ID: ".",
State: map[string]any{
Expand Down
83 changes: 83 additions & 0 deletions scim/entitlements.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package scim

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

type entitlements struct {
AllowClusterCreate bool `json:"allow_cluster_create,omitempty"`
AllowInstancePoolCreate bool `json:"allow_instance_pool_create,omitempty"`
DatabricksSQLAccess bool `json:"databricks_sql_access,omitempty"`
WorkspaceAccess bool `json:"workspace_access,omitempty"`
WorkspaceConsume bool `json:"workspace_consume,omitempty"`
}

func (e entitlements) toComplexValueList() []ComplexValue {
result := []ComplexValue{}
if e.AllowClusterCreate {
result = append(result, ComplexValue{
Value: "allow-cluster-create",
})
}
if e.AllowInstancePoolCreate {
result = append(result, ComplexValue{
Value: "allow-instance-pool-create",
})
}
if e.DatabricksSQLAccess {
result = append(result, ComplexValue{
Value: "databricks-sql-access",
})
}
if e.WorkspaceAccess {
result = append(result, ComplexValue{
Value: "workspace-access",
})
}
if e.WorkspaceConsume {
result = append(result, ComplexValue{
Value: "workspace-consume",
})
}
return result
}

func newEntitlements(ctx context.Context, cv []ComplexValue) entitlements {
var e entitlements
for _, c := range cv {
switch c.Value {
case "allow-cluster-create":
e.AllowClusterCreate = true
case "allow-instance-pool-create":
e.AllowInstancePoolCreate = true
case "databricks-sql-access":
e.DatabricksSQLAccess = true
case "workspace-access":
e.WorkspaceAccess = true
case "workspace-consume":
e.WorkspaceConsume = true
default:
tflog.Warn(ctx, fmt.Sprintf("Ignoring unknown entitlement: %s", c.Value))
}
}
return e
}

func mergeEntitlements(e1, e2 entitlements) entitlements {
return entitlements{
AllowClusterCreate: e1.AllowClusterCreate || e2.AllowClusterCreate,
AllowInstancePoolCreate: e1.AllowInstancePoolCreate || e2.AllowInstancePoolCreate,
DatabricksSQLAccess: e1.DatabricksSQLAccess || e2.DatabricksSQLAccess,
WorkspaceAccess: e1.WorkspaceAccess || e2.WorkspaceAccess,
WorkspaceConsume: e1.WorkspaceConsume || e2.WorkspaceConsume,
}
}

func customizeEntitlementsSchema(m map[string]*schema.Schema) map[string]*schema.Schema {
m["workspace_consume"].ConflictsWith = []string{"workspace_access", "databricks_sql_access"}
return m
}
2 changes: 1 addition & 1 deletion scim/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (a GroupsAPI) Patch(groupID string, r patchRequest) error {
return a.client.Scim(a.context, http.MethodPatch, fmt.Sprintf("/preview/scim/v2/Groups/%v", groupID), r, nil)
}

func (a GroupsAPI) UpdateNameAndEntitlements(groupID string, name string, externalID string, e entitlements) error {
func (a GroupsAPI) UpdateNameAndEntitlements(groupID string, name string, externalID string, e []ComplexValue) error {
g, err := a.Read(groupID, "displayName,entitlements,groups,members,externalId")
if err != nil {
return err
Expand Down
68 changes: 36 additions & 32 deletions scim/resource_entitlement.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,82 +9,86 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

type entitlementsResource struct {
entitlements
GroupId string `json:"group_id,omitempty" tf:"force_new"`
UserId string `json:"user_id,omitempty" tf:"force_new"`
SpnId string `json:"service_principal_id,omitempty" tf:"force_new"`
}

// ResourceGroup manages user groups
func ResourceEntitlements() common.Resource {
type entity struct {
GroupId string `json:"group_id,omitempty" tf:"force_new"`
UserId string `json:"user_id,omitempty" tf:"force_new"`
SpnId string `json:"service_principal_id,omitempty" tf:"force_new"`
}
entitlementSchema := common.StructToSchema(entity{},
entitlementSchema := common.StructToSchema(entitlementsResource{},
func(m map[string]*schema.Schema) map[string]*schema.Schema {
addEntitlementsToSchema(m)
alof := []string{"group_id", "user_id", "service_principal_id"}
for _, field := range alof {
m[field].AtLeastOneOf = alof
}
return m
return customizeEntitlementsSchema(m)
})
addEntitlementsToSchema(entitlementSchema)
return common.Resource{
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
if c.Config.IsAccountClient() {
return fmt.Errorf("entitlements can only be managed with a provider configured at the workspace-level")
}
err := patchEntitlements(ctx, d, c, "replace")
var e entitlementsResource
common.DataToStructPointer(d, entitlementSchema, &e)
err := patchEntitlements(ctx, e, c, "replace")
if err != nil {
return err
}
d.SetId(getId(d))
d.SetId(getId(e))
return nil
},
Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
split := strings.SplitN(d.Id(), "/", 2)
if len(split) != 2 {
return fmt.Errorf("ID must be two elements: %s", d.Id())
}
var entitlements entitlementsResource
switch strings.ToLower(split[0]) {
case "group":
group, err := NewGroupsAPI(ctx, c).Read(split[1], "entitlements")
if err != nil {
return err
}
d.Set("group_id", split[1])
group.Entitlements.generateEmpty(d)
return group.Entitlements.readIntoData(d)
entitlements.GroupId = split[1]
entitlements.entitlements = newEntitlements(ctx, group.Entitlements)
case "user":
user, err := NewUsersAPI(ctx, c).Read(split[1], "entitlements")
if err != nil {
return err
}
d.Set("user_id", split[1])
user.Entitlements.generateEmpty(d)
return user.Entitlements.readIntoData(d)
entitlements.UserId = split[1]
entitlements.entitlements = newEntitlements(ctx, user.Entitlements)
case "spn":
spn, err := NewServicePrincipalsAPI(ctx, c).Read(split[1], "entitlements")
if err != nil {
return err
}
d.Set("service_principal_id", split[1])
spn.Entitlements.generateEmpty(d)
return spn.Entitlements.readIntoData(d)
entitlements.SpnId = split[1]
entitlements.entitlements = newEntitlements(ctx, spn.Entitlements)
}
return nil
return common.StructToData(entitlements, entitlementSchema, d)
},
Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
return patchEntitlements(ctx, d, c, "replace")
var e entitlementsResource
common.DataToStructPointer(d, entitlementSchema, &e)
return patchEntitlements(ctx, e, c, "replace")
},
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
return patchEntitlements(ctx, d, c, "remove")
var e entitlementsResource
common.DataToStructPointer(d, entitlementSchema, &e)
return patchEntitlements(ctx, e, c, "remove")
},
Schema: entitlementSchema,
}
}

func getId(d *schema.ResourceData) string {
groupId := d.Get("group_id").(string)
userId := d.Get("user_id").(string)
spnId := d.Get("service_principal_id").(string)
func getId(e entitlementsResource) string {
groupId := e.GroupId
userId := e.UserId
spnId := e.SpnId
if groupId != "" {
return "group/" + groupId
}
Expand All @@ -97,12 +101,12 @@ func getId(d *schema.ResourceData) string {
return ""
}

func patchEntitlements(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient, op string) error {
groupId := d.Get("group_id").(string)
userId := d.Get("user_id").(string)
spnId := d.Get("service_principal_id").(string)
func patchEntitlements(ctx context.Context, e entitlementsResource, c *common.DatabricksClient, op string) error {
groupId := e.GroupId
userId := e.UserId
spnId := e.SpnId
noEntitlementMessage := "invalidPath No such attribute with the name : entitlements in the current resource"
entitlements := readEntitlementsFromData(d)
entitlements := e.toComplexValueList()
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

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

The method call should be e.entitlements.toComplexValueList() to access the embedded entitlements field, not called directly on the entitlementsResource struct.

Suggested change
entitlements := e.toComplexValueList()
entitlements := e.entitlements.toComplexValueList()

Copilot uses AI. Check for mistakes.

if len(entitlements) == 1 && entitlements[0].Value == "" && op == "remove" {
// No updates are needed, so return early
return nil
Expand Down
6 changes: 1 addition & 5 deletions scim/resource_entitlement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,7 @@ var addRequest = PatchRequestComplexValue([]patchOperation{

var emptyAddRequest = PatchRequestComplexValue([]patchOperation{
{
"replace", "entitlements", []ComplexValue{
{
Value: "",
},
},
"replace", "entitlements", []ComplexValue{},
},
})

Expand Down
Loading
Loading