From 02f9b60bc9cdb89b6e2de5ab0ed7effb63f75ec7 Mon Sep 17 00:00:00 2001 From: Ron DeFreitas Date: Mon, 21 Mar 2022 10:43:11 -0400 Subject: [PATCH 1/4] Add data source for service principals --- docs/data-sources/current_user.md | 1 + docs/data-sources/service_principal.md | 57 ++++++++++++++++++++++++++ provider/provider.go | 1 + scim/data_service_principal.go | 52 +++++++++++++++++++++++ scim/data_service_principal_test.go | 47 +++++++++++++++++++++ scim/data_user.go | 5 +++ 6 files changed, 163 insertions(+) create mode 100644 docs/data-sources/service_principal.md create mode 100644 scim/data_service_principal.go create mode 100644 scim/data_service_principal_test.go diff --git a/docs/data-sources/current_user.md b/docs/data-sources/current_user.md index 7c1ac092f2..b44b76bece 100644 --- a/docs/data-sources/current_user.md +++ b/docs/data-sources/current_user.md @@ -56,6 +56,7 @@ output "job_url" { Data source exposes the following attributes: * `id` - The id of the calling user. +* `application_id` - Application ID of the [service principal](../resources/service_principal.md) if the currently logged-in user is a service principal, e.g. `813073e7-3b08-4c0d-8619-39bcfa4ac632` * `external_id` - ID of the user in an external identity provider. * `user_name` - Name of the [user](../resources/user.md), e.g. `mr.foo@example.com`. * `home` - Home folder of the [user](../resources/user.md), e.g. `/Users/mr.foo@example.com`. diff --git a/docs/data-sources/service_principal.md b/docs/data-sources/service_principal.md new file mode 100644 index 0000000000..a811238a9a --- /dev/null +++ b/docs/data-sources/service_principal.md @@ -0,0 +1,57 @@ +--- +subcategory: "Security" +--- + +# databricks_service_principal Data Source + +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. + +Retrieves information about [databricks_service_principal](../resources/user.md). + +## Example Usage + +Adding service principal "813073e7-3b08-4c0d-8619-39bcfa4ac632" to administrative group + +```hcl +data "databricks_group" "admins" { + display_name = "admins" +} + +data "databricks_service_principal" "spn" { + application_id = "813073e7-3b08-4c0d-8619-39bcfa4ac632" +} + +resource "databricks_group_member" "my_member_a" { + group_id = data.databricks_group.admins.id + member_id = data.databricks_service_principal.spn.id +} +``` + +## Argument Reference + +Data source allows you to pick service principals by the following attributes + +- `application_id` - (Required) ID of the service principal. The service principal must exist before this resource can be retrieved. + +## Attribute Reference + +Data source exposes the following attributes: + +- `id` - The id of the service principal. +- `external_id` - ID of the service principal in an external identity provider. +- `display_name` - Display name of the [service principal](../resources/service_principal.md), e.g. `Foo SPN`. +- `home` - Home folder of the [service principal](../resources/service_principal.md), e.g. `/Users/813073e7-3b08-4c0d-8619-39bcfa4ac632`. +- `repos` - Repos location of the [service principal](../resources/service_principal.md), e.g. `/Repos/813073e7-3b08-4c0d-8619-39bcfa4ac632`. + +## Related Resources + +The following resources are used in the same context: + +* [End to end workspace management](../guides/passthrough-cluster-per-user.md) guide +* [databricks_current_user](current_user.md) data to retrieve information about [databricks_user](../resources/user.md) or [databricks_service_principal](../resources/service_principal.md), that is calling Databricks REST API. +* [databricks_group](../resources/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). +* [databricks_group](group.md) data to retrieve information about [databricks_group](../resources/group.md) members, entitlements and instance profiles. +* [databricks_group_instance_profile](../resources/group_instance_profile.md) to attach [databricks_instance_profile](../resources/instance_profile.md) (AWS) to [databricks_group](../resources/group.md). +* [databricks_group_member](../resources/group_member.md) to attach [users](../resources/user.md) and [groups](../resources/group.md) as group members. +* [databricks_permissions](../resources/permissions.md) to manage [access control](https://docs.databricks.com/security/access-control/index.html) in Databricks workspace. +* [databricks_service principal](../resources/service_principal.md) to manage [service principals](../resources/service_principal.md) \ No newline at end of file diff --git a/provider/provider.go b/provider/provider.go index 4467dcf9a4..1e5eab01cb 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -50,6 +50,7 @@ func DatabricksProvider() *schema.Provider { "databricks_notebook": workspace.DataSourceNotebook(), "databricks_notebook_paths": workspace.DataSourceNotebookPaths(), "databricks_schemas": catalog.DataSourceSchemas(), + "databricks_service_principal": scim.DataSourceServicePrincipal(), "databricks_spark_version": clusters.DataSourceSparkVersion(), "databricks_tables": catalog.DataSourceTables(), "databricks_user": scim.DataSourceUser(), diff --git a/scim/data_service_principal.go b/scim/data_service_principal.go new file mode 100644 index 0000000000..49bf90a9fc --- /dev/null +++ b/scim/data_service_principal.go @@ -0,0 +1,52 @@ +package scim + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// DataSourceUser returns information about user specified by user name +func DataSourceServicePrincipal() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "display_name": { + Type: schema.TypeString, + Computed: true, + }, + "application_id": { + Type: schema.TypeString, + Computed: true + }, + "home": { + Type: schema.TypeString, + Computed: true, + }, + "repos": { + Type: schema.TypeString, + Computed: true, + }, + "external_id": { + Type: schema.TypeString, + Computed: true, + } + }, + ReadContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + spnAPI := NewServicePrincipalsAPI(ctx, m) + spn, err := spnAPI.read(d.Get("application_id").(string)) + if err != nil { + return diag.FromErr(err) + } + d.Set("display_name", spn.DisplayName) + d.Set("home", fmt.Sprintf("/Users/%s", spn.UserName)) + d.Set("repos", fmt.Sprintf("/Repos/%s", spn.UserName)) + d.Set("external_id", spn.ExternalID) + d.Set("application_id", spn.ApplicationID) + d.SetId(spn.ID) + return nil + }, + } +} \ No newline at end of file diff --git a/scim/data_service_principal_test.go b/scim/data_service_principal_test.go new file mode 100644 index 0000000000..965b4815ab --- /dev/null +++ b/scim/data_service_principal_test.go @@ -0,0 +1,47 @@ +package scim + +import ( + "context" + "os" + "testing" + + "github.com/databrickslabs/terraform-provider-databricks/common" + + "github.com/databrickslabs/terraform-provider-databricks/qa" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResourceServicePrincipalRead(t *testing.T) { + d, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc", + Response: User{ + ID: "abc", + DisplayName: "Example Service Principal", + Groups: []ComplexValue{ + { + Display: "admins", + Value: "4567", + }, + { + Display: "ds", + Value: "9877", + }, + }, + }, + }, + }, + Resource: DataSourceServicePrincipal(), + HCL: `display_name = "Sylens"`, + New: true, + Read: true, + ID: "abc", + }.Apply(t) + require.NoError(t, err, err) + assert.Equal(t, "abc", d.Id(), "Id should not be empty") + assert.Equal(t, d.Get("application_id"), d.Id(), "Id should match application Id") + assert.Equal(t, "Example Service Principal", d.Get("display_name")) +} diff --git a/scim/data_user.go b/scim/data_user.go index 5b4bae5844..4d932cf2f1 100644 --- a/scim/data_user.go +++ b/scim/data_user.go @@ -59,6 +59,10 @@ func DataSourceUser() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "application_id": { + Type: schema.TypeString, + Computed: true + } }, ReadContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { usersAPI := NewUsersAPI(ctx, m) @@ -71,6 +75,7 @@ func DataSourceUser() *schema.Resource { d.Set("home", fmt.Sprintf("/Users/%s", user.UserName)) d.Set("repos", fmt.Sprintf("/Repos/%s", user.UserName)) d.Set("external_id", user.ExternalID) + d.Set("application_id", user.ApplicationID) splits := strings.Split(user.UserName, "@") norm := nonAlphanumeric.ReplaceAllLiteralString(splits[0], "_") norm = strings.ToLower(norm) From 45a8a68f2a6013df780f9508e91d81e6564858aa Mon Sep 17 00:00:00 2001 From: Vuong Nguyen Date: Thu, 9 Jun 2022 20:47:47 +0100 Subject: [PATCH 2/4] add-spn-data-source --- docs/data-sources/current_user.md | 2 +- docs/data-sources/service_principal.md | 14 +-- docs/data-sources/service_principals.md | 59 +++++++++++ provider/provider.go | 1 + scim/data_service_principal.go | 94 ++++++++++-------- scim/data_service_principal_test.go | 124 +++++++++++++++++++----- scim/data_user.go | 4 +- 7 files changed, 223 insertions(+), 75 deletions(-) create mode 100644 docs/data-sources/service_principals.md diff --git a/docs/data-sources/current_user.md b/docs/data-sources/current_user.md index b44b76bece..f5c574c4d1 100644 --- a/docs/data-sources/current_user.md +++ b/docs/data-sources/current_user.md @@ -56,7 +56,7 @@ output "job_url" { Data source exposes the following attributes: * `id` - The id of the calling user. -* `application_id` - Application ID of the [service principal](../resources/service_principal.md) if the currently logged-in user is a service principal, e.g. `813073e7-3b08-4c0d-8619-39bcfa4ac632` +* `application_id` - Application ID of the [service principal](../resources/service_principal.md) if the currently logged-in user is a service principal, e.g. `11111111-2222-3333-4444-555666777888` * `external_id` - ID of the user in an external identity provider. * `user_name` - Name of the [user](../resources/user.md), e.g. `mr.foo@example.com`. * `home` - Home folder of the [user](../resources/user.md), e.g. `/Users/mr.foo@example.com`. diff --git a/docs/data-sources/service_principal.md b/docs/data-sources/service_principal.md index a811238a9a..ab8147144f 100644 --- a/docs/data-sources/service_principal.md +++ b/docs/data-sources/service_principal.md @@ -6,11 +6,11 @@ subcategory: "Security" -> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. -Retrieves information about [databricks_service_principal](../resources/user.md). +Retrieves information about [databricks_service_principal](../resources/service_principal.md). ## Example Usage -Adding service principal "813073e7-3b08-4c0d-8619-39bcfa4ac632" to administrative group +Adding service principal `11111111-2222-3333-4444-555666777888` to administrative group ```hcl data "databricks_group" "admins" { @@ -18,7 +18,7 @@ data "databricks_group" "admins" { } data "databricks_service_principal" "spn" { - application_id = "813073e7-3b08-4c0d-8619-39bcfa4ac632" + application_id = "11111111-2222-3333-4444-555666777888" } resource "databricks_group_member" "my_member_a" { @@ -37,11 +37,13 @@ Data source allows you to pick service principals by the following attributes Data source exposes the following attributes: -- `id` - The id of the service principal. +- `sp_id` - The id of the service principal. +- `application_id` - This is the application id of the given service principal - `external_id` - ID of the service principal in an external identity provider. - `display_name` - Display name of the [service principal](../resources/service_principal.md), e.g. `Foo SPN`. -- `home` - Home folder of the [service principal](../resources/service_principal.md), e.g. `/Users/813073e7-3b08-4c0d-8619-39bcfa4ac632`. -- `repos` - Repos location of the [service principal](../resources/service_principal.md), e.g. `/Repos/813073e7-3b08-4c0d-8619-39bcfa4ac632`. +- `home` - Home folder of the [service principal](../resources/service_principal.md), e.g. `/Users/11111111-2222-3333-4444-555666777888`. +- `repos` - Repos location of the [service principal](../resources/service_principal.md), e.g. `/Repos/11111111-2222-3333-4444-555666777888`. +- `active` - Whether service principal is active or not. ## Related Resources diff --git a/docs/data-sources/service_principals.md b/docs/data-sources/service_principals.md new file mode 100644 index 0000000000..3de3b215b0 --- /dev/null +++ b/docs/data-sources/service_principals.md @@ -0,0 +1,59 @@ +--- +subcategory: "Security" +--- + +# databricks_service_principals Data Source + +-> **Note** If you have a fully automated setup with workspaces created by [databricks_mws_workspaces](../resources/mws_workspaces.md) or [azurerm_databricks_workspace](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/databricks_workspace), please make sure to add [depends_on attribute](../index.md#data-resources-and-authentication-is-not-configured-errors) in order to prevent _authentication is not configured for provider_ errors. + +Retrieves `application_ids` of all [databricks_service_principal](../resources/service_principal.md) based on their `display_name` + +## Example Usage + +Adding all service principals with display name `my-spn` to admin group + +```hcl +data "databricks_group" "admins" { + display_name = "admins" +} + +data "databricks_service_principals" "spns" { + display_name = "my-spn" +} + +data "databricks_service_principal" "spn" { + for_each = toset(data.databricks_service_principals.spns.application_ids) + application_id = each.value +} + +resource "databricks_group_member" "my_member_spn" { + for_each = toset(data.databricks_service_principals.spns.application_ids) + group_id = data.databricks_group.admins.id + member_id = data.databricks_service_principal.spn[each.value].sp_id +} +``` + +## Argument Reference + +Data source allows you to pick service principals by the following attributes + +- `display_name` - (Required) Display name of the service principals. The service principals must exist before this resource can be retrieved. + +## Attribute Reference + +Data source exposes the following attributes: + +- `application_ids` - List of `application_ids` of service principals with the display name. Individual service principal can be retrieved using [databricks_service_principal](databricks_service_principal.md) data source + +## Related Resources + +The following resources are used in the same context: + +* [End to end workspace management](../guides/passthrough-cluster-per-user.md) guide +* [databricks_current_user](current_user.md) data to retrieve information about [databricks_user](../resources/user.md) or [databricks_service_principal](../resources/service_principal.md), that is calling Databricks REST API. +* [databricks_group](../resources/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). +* [databricks_group](group.md) data to retrieve information about [databricks_group](../resources/group.md) members, entitlements and instance profiles. +* [databricks_group_instance_profile](../resources/group_instance_profile.md) to attach [databricks_instance_profile](../resources/instance_profile.md) (AWS) to [databricks_group](../resources/group.md). +* [databricks_group_member](../resources/group_member.md) to attach [users](../resources/user.md) and [groups](../resources/group.md) as group members. +* [databricks_permissions](../resources/permissions.md) to manage [access control](https://docs.databricks.com/security/access-control/index.html) in Databricks workspace. +* [databricks_service principal](../resources/service_principal.md) to manage [service principals](../resources/service_principal.md) \ No newline at end of file diff --git a/provider/provider.go b/provider/provider.go index 436ac16027..fc68e4ad47 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -51,6 +51,7 @@ func DatabricksProvider() *schema.Provider { "databricks_notebook_paths": workspace.DataSourceNotebookPaths(), "databricks_schemas": catalog.DataSourceSchemas(), "databricks_service_principal": scim.DataSourceServicePrincipal(), + "databricks_service_principals": scim.DataSourceServicePrincipals(), "databricks_spark_version": clusters.DataSourceSparkVersion(), "databricks_tables": catalog.DataSourceTables(), "databricks_views": catalog.DataSourceViews(), diff --git a/scim/data_service_principal.go b/scim/data_service_principal.go index 49bf90a9fc..bbd74879a2 100644 --- a/scim/data_service_principal.go +++ b/scim/data_service_principal.go @@ -3,50 +3,62 @@ package scim import ( "context" "fmt" - "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/databrickslabs/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -// DataSourceUser returns information about user specified by user name +// DataSourceServicePrincipal returns information about the spn specified by the application_id func DataSourceServicePrincipal() *schema.Resource { - return &schema.Resource{ - Schema: map[string]*schema.Schema{ - "display_name": { - Type: schema.TypeString, - Computed: true, - }, - "application_id": { - Type: schema.TypeString, - Computed: true - }, - "home": { - Type: schema.TypeString, - Computed: true, - }, - "repos": { - Type: schema.TypeString, - Computed: true, - }, - "external_id": { - Type: schema.TypeString, - Computed: true, - } - }, - ReadContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - spnAPI := NewServicePrincipalsAPI(ctx, m) - spn, err := spnAPI.read(d.Get("application_id").(string)) - if err != nil { - return diag.FromErr(err) - } - d.Set("display_name", spn.DisplayName) - d.Set("home", fmt.Sprintf("/Users/%s", spn.UserName)) - d.Set("repos", fmt.Sprintf("/Repos/%s", spn.UserName)) - d.Set("external_id", spn.ExternalID) - d.Set("application_id", spn.ApplicationID) - d.SetId(spn.ID) - return nil - }, + type spnData struct { + ApplicationID string `json:"application_id,omitempty" tf:"computed"` + DisplayName string `json:"display_name,omitempty" tf:"computed"` + SpID string `json:"sp_id,omitempty" tf:"computed"` + Home string `json:"home,omitempty" tf:"computed"` + Repos string `json:"repos,omitempty" tf:"computed"` + Active bool `json:"active,omitempty" tf:"computed"` + ExternalID string `json:"external_id,omitempty" tf:"computed"` } -} \ No newline at end of file + return common.DataResource(spnData{}, func(ctx context.Context, e interface{}, c *common.DatabricksClient) error { + response := e.(*spnData) + spnAPI := NewServicePrincipalsAPI(ctx, c) + spList, err := spnAPI.filter(fmt.Sprintf("applicationId eq '%s'", response.ApplicationID)) + if err != nil { + return err + } + if len(spList) == 0 { + return fmt.Errorf("cannot find SP with ID %s", response.ApplicationID) + } + sp := spList[0] + response.DisplayName = sp.DisplayName + response.Home = fmt.Sprintf("/Users/%s", sp.ApplicationID) + response.Repos = fmt.Sprintf("/Repos/%s", sp.ApplicationID) + response.ExternalID = sp.ExternalID + response.Active = sp.Active + response.SpID = sp.ID + return nil + }) +} + +// DataSourceServicePrincipal returns information about the spn specified by the application_id +func DataSourceServicePrincipals() *schema.Resource { + type spnsData struct { + DisplayName string `json:"display_name,omitempty" tf:"computed"` + ApplicationIDs []string `json:"application_ids,omitempty" tf:"computed,slice_set"` + } + return common.DataResource(spnsData{}, func(ctx context.Context, e interface{}, c *common.DatabricksClient) error { + response := e.(*spnsData) + spnAPI := NewServicePrincipalsAPI(ctx, c) + spList, err := spnAPI.filter(fmt.Sprintf("displayName eq '%s'", response.DisplayName)) + if err != nil { + return err + } + if len(spList) == 0 { + return fmt.Errorf("cannot find SPs with display name %s", response.DisplayName) + } + for _, sp := range spList { + response.ApplicationIDs = append(response.ApplicationIDs, sp.ApplicationID) + } + return nil + }) +} diff --git a/scim/data_service_principal_test.go b/scim/data_service_principal_test.go index 965b4815ab..1e979f3a66 100644 --- a/scim/data_service_principal_test.go +++ b/scim/data_service_principal_test.go @@ -1,47 +1,121 @@ package scim import ( - "context" - "os" "testing" - "github.com/databrickslabs/terraform-provider-databricks/common" - "github.com/databrickslabs/terraform-provider-databricks/qa" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestResourceServicePrincipalRead(t *testing.T) { - d, err := qa.ResourceFixture{ +func TestDataServicePrincipalReadByAppId(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=applicationId%20eq%20%27abc%27", + Response: UserList{ + Resources: []User{ + { + ID: "abc", + DisplayName: "Example Service Principal", + Active: true, + ApplicationID: "abc", + Groups: []ComplexValue{ + { + Display: "admins", + Value: "4567", + }, + { + Display: "ds", + Value: "9877", + }, + }, + }, + }, + }, + }, + }, + Resource: DataSourceServicePrincipal(), + HCL: `application_id = "abc"`, + Read: true, + NonWritable: true, + ID: "_", + }.ApplyAndExpectData(t, map[string]interface{}{ + "sp_id": "abc", + "application_id": "abc", + "display_name": "Example Service Principal", + "active": true, + "home": "/Users/abc", + "repos": "/Repos/abc", + }) +} + +func TestDataServicePrincipalsReadByDisplayName(t *testing.T) { + qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ { Method: "GET", - Resource: "/api/2.0/preview/scim/v2/ServicePrincipals/abc", - Response: User{ - ID: "abc", - DisplayName: "Example Service Principal", - Groups: []ComplexValue{ + Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=displayName%20eq%20%27def%27", + Response: UserList{ + Resources: []User{ { - Display: "admins", - Value: "4567", + ID: "abc1", + DisplayName: "def", + Active: true, + ApplicationID: "123", }, { - Display: "ds", - Value: "9877", + ID: "abc2", + DisplayName: "def", + Active: true, + ApplicationID: "124", }, }, }, }, }, - Resource: DataSourceServicePrincipal(), - HCL: `display_name = "Sylens"`, - New: true, - Read: true, - ID: "abc", + Resource: DataSourceServicePrincipals(), + HCL: `display_name = "def"`, + Read: true, + NonWritable: true, + ID: "_", + }.ApplyAndExpectData(t, map[string]interface{}{ + "application_ids": []string{"123", "124"}, + }) +} + +func TestDataServicePrincipalReadNotFound(t *testing.T) { + _, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=applicationId%20eq%20%27abc%27", + Response: UserList{}, + }, + }, + Resource: DataSourceServicePrincipal(), + HCL: `application_id = "abc"`, + Read: true, + NonWritable: true, + ID: "_", + }.Apply(t) + require.Error(t, err, err) +} + +func TestDataServicePrincipalsReadNotFound(t *testing.T) { + _, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=displayName%20eq%20%27def%27", + Response: UserList{}, + }, + }, + Resource: DataSourceServicePrincipals(), + HCL: `display_name = "def"`, + Read: true, + NonWritable: true, + ID: "_", }.Apply(t) - require.NoError(t, err, err) - assert.Equal(t, "abc", d.Id(), "Id should not be empty") - assert.Equal(t, d.Get("application_id"), d.Id(), "Id should match application Id") - assert.Equal(t, "Example Service Principal", d.Get("display_name")) + require.Error(t, err, err) } diff --git a/scim/data_user.go b/scim/data_user.go index 4d932cf2f1..a9a27c58e4 100644 --- a/scim/data_user.go +++ b/scim/data_user.go @@ -61,8 +61,8 @@ func DataSourceUser() *schema.Resource { }, "application_id": { Type: schema.TypeString, - Computed: true - } + Computed: true, + }, }, ReadContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { usersAPI := NewUsersAPI(ctx, m) From ae8ba12dae9aab1b4aae0ff091fd52d7b8b4904b Mon Sep 17 00:00:00 2001 From: Vuong Nguyen Date: Fri, 10 Jun 2022 03:49:16 +0100 Subject: [PATCH 3/4] incorporated feedback --- docs/data-sources/service_principal.md | 3 +- docs/data-sources/service_principals.md | 12 +-- scim/data_service_principal.go | 23 ----- scim/data_service_principal_test.go | 44 ++-------- scim/data_service_principals.go | 33 +++++++ scim/data_service_principals_test.go | 112 ++++++++++++++++++++++++ 6 files changed, 157 insertions(+), 70 deletions(-) create mode 100644 scim/data_service_principals.go create mode 100644 scim/data_service_principals_test.go diff --git a/docs/data-sources/service_principal.md b/docs/data-sources/service_principal.md index ab8147144f..3b6a7bb02c 100644 --- a/docs/data-sources/service_principal.md +++ b/docs/data-sources/service_principal.md @@ -38,7 +38,6 @@ Data source allows you to pick service principals by the following attributes Data source exposes the following attributes: - `sp_id` - The id of the service principal. -- `application_id` - This is the application id of the given service principal - `external_id` - ID of the service principal in an external identity provider. - `display_name` - Display name of the [service principal](../resources/service_principal.md), e.g. `Foo SPN`. - `home` - Home folder of the [service principal](../resources/service_principal.md), e.g. `/Users/11111111-2222-3333-4444-555666777888`. @@ -56,4 +55,4 @@ The following resources are used in the same context: * [databricks_group_instance_profile](../resources/group_instance_profile.md) to attach [databricks_instance_profile](../resources/instance_profile.md) (AWS) to [databricks_group](../resources/group.md). * [databricks_group_member](../resources/group_member.md) to attach [users](../resources/user.md) and [groups](../resources/group.md) as group members. * [databricks_permissions](../resources/permissions.md) to manage [access control](https://docs.databricks.com/security/access-control/index.html) in Databricks workspace. -* [databricks_service principal](../resources/service_principal.md) to manage [service principals](../resources/service_principal.md) \ No newline at end of file +* [databricks_service principal](../resources/service_principal.md) to manage [service principals](../resources/service_principal.md) diff --git a/docs/data-sources/service_principals.md b/docs/data-sources/service_principals.md index 3de3b215b0..3e867e486c 100644 --- a/docs/data-sources/service_principals.md +++ b/docs/data-sources/service_principals.md @@ -10,7 +10,7 @@ Retrieves `application_ids` of all [databricks_service_principal](../resources/s ## Example Usage -Adding all service principals with display name `my-spn` to admin group +Adding all service principals of which display name contains `my-spn` to admin group ```hcl data "databricks_group" "admins" { @@ -22,12 +22,12 @@ data "databricks_service_principals" "spns" { } data "databricks_service_principal" "spn" { - for_each = toset(data.databricks_service_principals.spns.application_ids) + for_each = toset(data.databricks_service_principals.spns.application_ids) application_id = each.value } resource "databricks_group_member" "my_member_spn" { - for_each = toset(data.databricks_service_principals.spns.application_ids) + for_each = toset(data.databricks_service_principals.spns.application_ids) group_id = data.databricks_group.admins.id member_id = data.databricks_service_principal.spn[each.value].sp_id } @@ -37,13 +37,13 @@ resource "databricks_group_member" "my_member_spn" { Data source allows you to pick service principals by the following attributes -- `display_name` - (Required) Display name of the service principals. The service principals must exist before this resource can be retrieved. +- `display_name_contains` - (Optional) Only return [databricks_service_principal](databricks_service_principal.md) display name that match the given name string ## Attribute Reference Data source exposes the following attributes: -- `application_ids` - List of `application_ids` of service principals with the display name. Individual service principal can be retrieved using [databricks_service_principal](databricks_service_principal.md) data source +- `application_ids` - List of `application_ids` of service principals Individual service principal can be retrieved using [databricks_service_principal](databricks_service_principal.md) data source ## Related Resources @@ -56,4 +56,4 @@ The following resources are used in the same context: * [databricks_group_instance_profile](../resources/group_instance_profile.md) to attach [databricks_instance_profile](../resources/instance_profile.md) (AWS) to [databricks_group](../resources/group.md). * [databricks_group_member](../resources/group_member.md) to attach [users](../resources/user.md) and [groups](../resources/group.md) as group members. * [databricks_permissions](../resources/permissions.md) to manage [access control](https://docs.databricks.com/security/access-control/index.html) in Databricks workspace. -* [databricks_service principal](../resources/service_principal.md) to manage [service principals](../resources/service_principal.md) \ No newline at end of file +* [databricks_service principal](../resources/service_principal.md) to manage [service principals](../resources/service_principal.md) diff --git a/scim/data_service_principal.go b/scim/data_service_principal.go index bbd74879a2..85dbf90137 100644 --- a/scim/data_service_principal.go +++ b/scim/data_service_principal.go @@ -39,26 +39,3 @@ func DataSourceServicePrincipal() *schema.Resource { return nil }) } - -// DataSourceServicePrincipal returns information about the spn specified by the application_id -func DataSourceServicePrincipals() *schema.Resource { - type spnsData struct { - DisplayName string `json:"display_name,omitempty" tf:"computed"` - ApplicationIDs []string `json:"application_ids,omitempty" tf:"computed,slice_set"` - } - return common.DataResource(spnsData{}, func(ctx context.Context, e interface{}, c *common.DatabricksClient) error { - response := e.(*spnsData) - spnAPI := NewServicePrincipalsAPI(ctx, c) - spList, err := spnAPI.filter(fmt.Sprintf("displayName eq '%s'", response.DisplayName)) - if err != nil { - return err - } - if len(spList) == 0 { - return fmt.Errorf("cannot find SPs with display name %s", response.DisplayName) - } - for _, sp := range spList { - response.ApplicationIDs = append(response.ApplicationIDs, sp.ApplicationID) - } - return nil - }) -} diff --git a/scim/data_service_principal_test.go b/scim/data_service_principal_test.go index 1e979f3a66..11184ff8d5 100644 --- a/scim/data_service_principal_test.go +++ b/scim/data_service_principal_test.go @@ -50,40 +50,6 @@ func TestDataServicePrincipalReadByAppId(t *testing.T) { }) } -func TestDataServicePrincipalsReadByDisplayName(t *testing.T) { - qa.ResourceFixture{ - Fixtures: []qa.HTTPFixture{ - { - Method: "GET", - Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=displayName%20eq%20%27def%27", - Response: UserList{ - Resources: []User{ - { - ID: "abc1", - DisplayName: "def", - Active: true, - ApplicationID: "123", - }, - { - ID: "abc2", - DisplayName: "def", - Active: true, - ApplicationID: "124", - }, - }, - }, - }, - }, - Resource: DataSourceServicePrincipals(), - HCL: `display_name = "def"`, - Read: true, - NonWritable: true, - ID: "_", - }.ApplyAndExpectData(t, map[string]interface{}{ - "application_ids": []string{"123", "124"}, - }) -} - func TestDataServicePrincipalReadNotFound(t *testing.T) { _, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ @@ -102,17 +68,17 @@ func TestDataServicePrincipalReadNotFound(t *testing.T) { require.Error(t, err, err) } -func TestDataServicePrincipalsReadNotFound(t *testing.T) { +func TestDataServicePrincipalReadError(t *testing.T) { _, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{ { Method: "GET", - Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=displayName%20eq%20%27def%27", - Response: UserList{}, + Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=applicationId%20eq%20%27abc%27", + Status: 500, }, }, - Resource: DataSourceServicePrincipals(), - HCL: `display_name = "def"`, + Resource: DataSourceServicePrincipal(), + HCL: `application_id = "abc"`, Read: true, NonWritable: true, ID: "_", diff --git a/scim/data_service_principals.go b/scim/data_service_principals.go new file mode 100644 index 0000000000..79a72daf30 --- /dev/null +++ b/scim/data_service_principals.go @@ -0,0 +1,33 @@ +package scim + +import ( + "context" + "fmt" + + "github.com/databrickslabs/terraform-provider-databricks/common" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// DataSourceServicePrincipals searches for service principals based on display_name +func DataSourceServicePrincipals() *schema.Resource { + type spnsData struct { + DisplayNameContains string `json:"display_name_contains,omitempty" tf:"computed"` + ApplicationIDs []string `json:"application_ids,omitempty" tf:"computed,slice_set"` + } + return common.DataResource(spnsData{}, func(ctx context.Context, e interface{}, c *common.DatabricksClient) error { + response := e.(*spnsData) + spnAPI := NewServicePrincipalsAPI(ctx, c) + + spList, err := spnAPI.filter(fmt.Sprintf("displayName co '%s'", response.DisplayNameContains)) + if err != nil { + return err + } + if len(spList) == 0 { + return fmt.Errorf("cannot find SPs with display name containing %s", response.DisplayNameContains) + } + for _, sp := range spList { + response.ApplicationIDs = append(response.ApplicationIDs, sp.ApplicationID) + } + return nil + }) +} diff --git a/scim/data_service_principals_test.go b/scim/data_service_principals_test.go new file mode 100644 index 0000000000..e0c47027a7 --- /dev/null +++ b/scim/data_service_principals_test.go @@ -0,0 +1,112 @@ +package scim + +import ( + "testing" + + "github.com/databrickslabs/terraform-provider-databricks/qa" + "github.com/stretchr/testify/require" +) + +func TestDataServicePrincipalsReadByDisplayName(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=displayName%20co%20%27def%27", + Response: UserList{ + Resources: []User{ + { + ID: "abc1", + DisplayName: "def", + Active: true, + ApplicationID: "123", + }, + { + ID: "abc2", + DisplayName: "def", + Active: true, + ApplicationID: "124", + }, + }, + }, + }, + }, + Resource: DataSourceServicePrincipals(), + HCL: `display_name_contains = "def"`, + Read: true, + NonWritable: true, + ID: "_", + }.ApplyAndExpectData(t, map[string]interface{}{ + "application_ids": []string{"123", "124"}, + }) +} + +func TestDataServicePrincipalsReadNotFound(t *testing.T) { + _, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=displayName%20co%20%27def%27", + Response: UserList{}, + }, + }, + Resource: DataSourceServicePrincipals(), + HCL: `display_name_contains = "def"`, + Read: true, + NonWritable: true, + ID: "_", + }.Apply(t) + require.Error(t, err, err) +} + +func TestDataServicePrincipalsReadNoFilter(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=displayName%20co%20%27%27", + Response: UserList{ + Resources: []User{ + { + ID: "abc1", + DisplayName: "def1", + Active: true, + ApplicationID: "123", + }, + { + ID: "abc2", + DisplayName: "def2", + Active: true, + ApplicationID: "124", + }, + }, + }, + }, + }, + Resource: DataSourceServicePrincipals(), + HCL: ``, + Read: true, + NonWritable: true, + ID: "_", + }.ApplyAndExpectData(t, map[string]interface{}{ + "application_ids": []string{"123", "124"}, + }) +} + +func TestDataServicePrincipalsReadError(t *testing.T) { + _, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?filter=displayName%20co%20%27def%27", + Status: 500, + }, + }, + Resource: DataSourceServicePrincipals(), + HCL: `display_name_contains = "def"`, + Read: true, + NonWritable: true, + ID: "_", + }.Apply(t) + require.Error(t, err, err) +} From 5d837f5adb306a6ab341357765663908f437caed Mon Sep 17 00:00:00 2001 From: Vuong Nguyen Date: Fri, 10 Jun 2022 03:51:52 +0100 Subject: [PATCH 4/4] sort output for sps --- scim/data_service_principals.go | 2 ++ scim/data_service_principals_test.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scim/data_service_principals.go b/scim/data_service_principals.go index 79a72daf30..d836ccc824 100644 --- a/scim/data_service_principals.go +++ b/scim/data_service_principals.go @@ -3,6 +3,7 @@ package scim import ( "context" "fmt" + "sort" "github.com/databrickslabs/terraform-provider-databricks/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -28,6 +29,7 @@ func DataSourceServicePrincipals() *schema.Resource { for _, sp := range spList { response.ApplicationIDs = append(response.ApplicationIDs, sp.ApplicationID) } + sort.Strings(response.ApplicationIDs) return nil }) } diff --git a/scim/data_service_principals_test.go b/scim/data_service_principals_test.go index e0c47027a7..94c6ebc9f2 100644 --- a/scim/data_service_principals_test.go +++ b/scim/data_service_principals_test.go @@ -71,13 +71,13 @@ func TestDataServicePrincipalsReadNoFilter(t *testing.T) { ID: "abc1", DisplayName: "def1", Active: true, - ApplicationID: "123", + ApplicationID: "124", }, { ID: "abc2", DisplayName: "def2", Active: true, - ApplicationID: "124", + ApplicationID: "123", }, }, },