Skip to content
Open
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 NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

* Added output attribute `endpoint_url` in `databricks_model_serving` ([#4877](https://github.com/databricks/terraform-provider-databricks/pull/4877)).
* Deprecate `egg` library type in `databricks_cluster`, `databricks_job`, and `databricks_library` ([#4881](https://github.com/databricks/terraform-provider-databricks/pull/4881)).
* Added `databricks_groups` data source to retrieve multiple groups matching a filter criteria ([#4864](https://github.com/databricks/terraform-provider-databricks/pull/4864)).

### Bug Fixes

Expand Down
110 changes: 110 additions & 0 deletions docs/data-sources/groups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
subcategory: "Security"
---
# databricks_groups Data Source

Retrieves information about multiple [databricks_group](../resources/group.md) objects matching a filter criteria. This data source allows you to search for groups using SCIM filter expressions and returns detailed information about matching groups including their members, entitlements and instance profiles.

-> This data source can be used with an account or workspace-level provider.

## Example Usage

Retrieve all groups with "admin" in the name:

```hcl
data "databricks_groups" "admin_groups" {
filter = "displayName co \"admin\""
}

resource "databricks_permissions" "cluster_permissions" {
cluster_id = databricks_cluster.example.id

dynamic "access_control" {
for_each = data.databricks_groups.admin_groups.groups
content {
group_name = access_control.value.display_name
permission_level = "CAN_MANAGE"
}
}
}
```

Find groups by exact display name:

```hcl
data "databricks_groups" "specific_teams" {
filter = "displayName eq \"team-data\" or displayName eq \"team-ml\""
}

locals {
team_principals = [for group in data.databricks_groups.specific_teams.groups : group.acl_principal_id]
}
```

Find all groups without a filter (retrieve all groups):

```hcl
data "databricks_groups" "all_groups" {
filter = ""
}

output "total_groups" {
value = length(data.databricks_groups.all_groups.groups)
}
```

## Argument Reference

Data source allows you to filter groups by the following attributes:

* `filter` - (Optional) SCIM filter expression to match groups. Uses the same filter syntax as the Databricks SCIM API. Common examples:
* `displayName co "admin"` - Groups containing "admin" in the display name
* `displayName eq "admins"` - Groups with exact display name "admins"
* `displayName sw "team-"` - Groups whose display name starts with "team-"
* Empty string or omitted - Returns all groups

## Attribute Reference

Data source exposes the following attributes:

* `groups` - List of group objects matching the filter criteria. Each group object contains:
* `display_name` - Display name of the group.
* `external_id` - ID of the group in an external identity provider.
* `acl_principal_id` - Identifier for use in [databricks_access_control_rule_set](../resources/access_control_rule_set.md), formatted as `groups/{display_name}`.
* `members` - Set of all group member identifiers (users, service principals, and child groups combined).
* `users` - Set of [databricks_user](../resources/user.md) identifiers that are members of this group.
* `service_principals` - Set of [databricks_service_principal](../resources/service_principal.md) identifiers that are members of this group.
* `child_groups` - Set of [databricks_group](../resources/group.md) identifiers that are child groups of this group.
* `groups` - Set of parent [databricks_group](../resources/group.md) identifiers that this group belongs to.
* `instance_profiles` - Set of [instance profile](../resources/instance_profile.md) ARNs associated with this group.

-> **Note**: The `groups` attribute returns a list sorted by `display_name` for consistent ordering. All member sets within each group are also sorted alphabetically.

## SCIM Filter Syntax

The `filter` parameter supports SCIM 2.0 filter expressions. Common operators include:

* `co` - Contains (case insensitive)
* `eq` - Equals (case sensitive)
* `ne` - Not equals
* `sw` - Starts with
* `ew` - Ends with
* `and` - Logical AND
* `or` - Logical OR

Examples:
* `displayName co "admin"` - Contains "admin"
* `displayName sw "team-" and displayName ew "-prod"` - Starts with "team-" and ends with "-prod"
* `displayName eq "admins" or displayName eq "developers"` - Exact match for either name

## Related Resources

The following resources are used in the same context:

* [End to end workspace management](../guides/workspace-management.md) guide
* [databricks_group](group.md) to retrieve information about a single group
* [databricks_group](../resources/group.md) to manage groups in Databricks workspace
* [databricks_group_member](../resources/group_member.md) to manage group membership
* [databricks_permissions](../resources/permissions.md) to manage [access control](https://docs.databricks.com/security/access-control/index.html) in Databricks workspace
* [databricks_user](../resources/user.md) to manage users that can be added to groups
* [databricks_service_principal](../resources/service_principal.md) to manage service principals that can be added to groups
1 change: 1 addition & 0 deletions internal/providers/sdkv2/sdkv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func DatabricksProvider(opts ...SdkV2ProviderOption) *schema.Provider {
"databricks_external_location": catalog.DataSourceExternalLocation().ToResource(),
"databricks_external_locations": catalog.DataSourceExternalLocations().ToResource(),
"databricks_group": scim.DataSourceGroup().ToResource(),
"databricks_groups": scim.DataSourceGroups().ToResource(),
"databricks_instance_pool": pools.DataSourceInstancePool().ToResource(),
"databricks_instance_profiles": aws.DataSourceInstanceProfiles().ToResource(),
"databricks_jobs": jobs.DataSourceJobs().ToResource(),
Expand Down
94 changes: 94 additions & 0 deletions scim/data_groups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package scim

import (
"context"
"fmt"
"sort"
"strings"

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

// DataSourceGroups returns information about groups that match the specified filter
func DataSourceGroups() common.Resource {
type groupEntity 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"`
}

type groupsData struct {
Filter string `json:"filter,omitempty"`
Groups []groupEntity `json:"groups,omitempty" tf:"computed"`
}

return common.DataResource(groupsData{}, func(ctx context.Context, e any, c *common.DatabricksClient) error {
response := e.(*groupsData)
groupsAPI := NewGroupsAPI(ctx, c)

groupList, err := groupsAPI.Filter(response.Filter)
if err != nil {
return err
}

// Initialize empty groups slice to ensure we return empty array when no matches
response.Groups = []groupEntity{}

for _, group := range groupList.Resources {
// Convert each Group to the expected entity shape
entity := groupEntity{
DisplayName: group.DisplayName,
ExternalID: group.ExternalID,
AclPrincipalID: fmt.Sprintf("groups/%s", group.DisplayName),
}

// Process members to extract different types
for _, member := range group.Members {
entity.Members = append(entity.Members, member.Value)
if strings.HasPrefix(member.Ref, "Users/") {
entity.Users = append(entity.Users, member.Value)
}
if strings.HasPrefix(member.Ref, "Groups/") {
entity.ChildGroups = append(entity.ChildGroups, member.Value)
}
if strings.HasPrefix(member.Ref, "ServicePrincipals/") {
entity.ServicePrincipals = append(entity.ServicePrincipals, member.Value)
}
}

// Process roles (instance profiles)
for _, role := range group.Roles {
entity.InstanceProfiles = append(entity.InstanceProfiles, role.Value)
}

// Process parent groups
for _, parentGroup := range group.Groups {
entity.Groups = append(entity.Groups, parentGroup.Value)
}

// Sort all slices for consistent output
sort.Strings(entity.Members)
sort.Strings(entity.Users)
sort.Strings(entity.ServicePrincipals)
sort.Strings(entity.ChildGroups)
sort.Strings(entity.Groups)
sort.Strings(entity.InstanceProfiles)

response.Groups = append(response.Groups, entity)
}

// Sort groups by display name for consistent output
sort.Slice(response.Groups, func(i, j int) bool {
return response.Groups[i].DisplayName < response.Groups[j].DisplayName
})

return nil
})
}
146 changes: 146 additions & 0 deletions scim/data_groups_acc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package scim_test

import (
"testing"

"github.com/databricks/terraform-provider-databricks/internal/acceptance"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const groupsDataSourceTemplate = `
resource "databricks_user" "test_user" {
user_name = "tf-groups-user-{var.RANDOM}@example.com"
}

resource "databricks_service_principal" "test_sp" {
application_id = "{var.RANDOM_UUID}"
display_name = "tf-groups-sp-{var.RANDOM}"
force = true
}

resource "databricks_group" "test_group_1" {
display_name = "tf-groups-test-{var.RANDOM}-alpha"
}

resource "databricks_group" "test_group_2" {
display_name = "tf-groups-test-{var.RANDOM}-beta"
}

resource "databricks_group" "other_group" {
display_name = "tf-other-{var.RANDOM}"
}

resource "databricks_group_member" "member_1_user" {
group_id = databricks_group.test_group_1.id
member_id = databricks_user.test_user.id
}

resource "databricks_group_member" "member_1_sp" {
group_id = databricks_group.test_group_1.id
member_id = databricks_service_principal.test_sp.id
}

resource "databricks_group_member" "member_2_group" {
group_id = databricks_group.test_group_2.id
member_id = databricks_group.test_group_1.id
}

data "databricks_groups" "test_filter" {
filter = "displayName co \"tf-groups-test-{var.RANDOM}\""
depends_on = [
databricks_group_member.member_1_user,
databricks_group_member.member_1_sp,
databricks_group_member.member_2_group
]
}

data "databricks_groups" "exact_match" {
filter = "displayName eq \"${databricks_group.test_group_1.display_name}\""
depends_on = [
databricks_group_member.member_1_user,
databricks_group_member.member_1_sp
]
}

data "databricks_groups" "empty_filter" {
filter = "displayName co \"nonexistent-group-{var.RANDOM}\""
}`

func checkGroupsDataSourcePopulated(t *testing.T) func(s *terraform.State) error {
return func(s *terraform.State) error {
// Check the filtered data source that should return 2 groups
r, ok := s.Modules[0].Resources["data.databricks_groups.test_filter"]
require.True(t, ok, "data.databricks_groups.test_filter has to be there")
attr := r.Primary.Attributes

// Should find 2 groups matching the filter
assert.Equal(t, "2", attr["groups.#"])

// Groups should be sorted alphabetically (alpha before beta)
group1DisplayName := attr["groups.0.display_name"]
group2DisplayName := attr["groups.1.display_name"]

require.Contains(t, group1DisplayName, "alpha", "First group should be alpha (alphabetically first)")
require.Contains(t, group2DisplayName, "beta", "Second group should be beta (alphabetically second)")

// Verify group 1 (alpha) has the expected members
assert.NotEmpty(t, attr["groups.0.users.0"], "Group 1 should have users")
assert.NotEmpty(t, attr["groups.0.service_principals.0"], "Group 1 should have service principals")
assert.Contains(t, attr["groups.0.acl_principal_id"], "groups/", "ACL principal ID should have groups/ prefix")

// Verify group 2 (beta) has the expected child group
assert.NotEmpty(t, attr["groups.1.child_groups.0"], "Group 2 should have child groups")
assert.Contains(t, attr["groups.1.acl_principal_id"], "groups/", "ACL principal ID should have groups/ prefix")

// Check the exact match data source that should return 1 group
exactMatch, ok := s.Modules[0].Resources["data.databricks_groups.exact_match"]
require.True(t, ok, "data.databricks_groups.exact_match has to be there")
exactAttr := exactMatch.Primary.Attributes

assert.Equal(t, "1", exactAttr["groups.#"])
assert.Contains(t, exactAttr["groups.0.display_name"], "alpha", "Exact match should return the alpha group")

// Check empty result data source
emptyResult, ok := s.Modules[0].Resources["data.databricks_groups.empty_filter"]
require.True(t, ok, "data.databricks_groups.empty_filter has to be there")
emptyAttr := emptyResult.Primary.Attributes

assert.Equal(t, "0", emptyAttr["groups.#"], "Filter matching no groups should return empty list")

return nil
}
}

func TestMwsAccGroupsDataSource(t *testing.T) {
acceptance.GetEnvOrSkipTest(t, "ARM_CLIENT_ID")
acceptance.AccountLevel(t, acceptance.Step{
Template: groupsDataSourceTemplate,
Check: checkGroupsDataSourcePopulated(t),
})
}

func TestAccGroupsDataSource(t *testing.T) {
acceptance.GetEnvOrSkipTest(t, "ARM_CLIENT_ID")
acceptance.WorkspaceLevel(t, acceptance.Step{
Template: groupsDataSourceTemplate,
Check: checkGroupsDataSourcePopulated(t),
})
}

func TestAccGroupsDataSourceOnAWS(t *testing.T) {
acceptance.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE")
acceptance.WorkspaceLevel(t, acceptance.Step{
Template: groupsDataSourceTemplate,
Check: checkGroupsDataSourcePopulated(t),
})
}

func TestAccGroupsDataSourceOnGCP(t *testing.T) {
acceptance.GetEnvOrSkipTest(t, "GOOGLE_CREDENTIALS")
acceptance.WorkspaceLevel(t, acceptance.Step{
Template: groupsDataSourceTemplate,
Check: checkGroupsDataSourcePopulated(t),
})
}
Loading
Loading