Skip to content

Commit 171d7ca

Browse files
karoluszalexotttanmay-db
authored
Issue 2971: Service principal data source retrieval by SCIM ID (#3142)
## Changes Allows for retrieval of service principals as data sources by SCIM ID. Slight changes to allow for the use of `ExactlyOneOf` in schema definition, which helps with config validation. Commit 54810c4 has a working and tested implementation closer to the original one, if so desired. Closes #2971 ## Tests Unrelated unit tests failing in the local dev environment. - [x] `make test` run locally - [x] relevant change in `docs/` folder - [ ] covered with integration tests in `internal/acceptance` - [ ] relevant acceptance tests are passing - [ ] using Go SDK --------- Co-authored-by: Alex Ott <[email protected]> Co-authored-by: Alex Ott <[email protected]> Co-authored-by: Tanmay Rustagi <[email protected]>
1 parent 43e9a8a commit 171d7ca

File tree

5 files changed

+226
-49
lines changed

5 files changed

+226
-49
lines changed

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
* Add `bearer_token` to the list of sensitive options in `databricks_connection` ([#4812](https://github.com/databricks/terraform-provider-databricks/pull/4812)).
1010
* Use single-node cluster for `databricks_sql_permissions` ([#4813](https://github.com/databricks/terraform-provider-databricks/pull/4813)).
11+
* Allow to retrieve service principal data by SCIM ID ([#3142](https://github.com/databricks/terraform-provider-databricks/pull/3142)).
1112
* Add support for Lakebase `databricks_database_instance` in `databricks_permissions` ([#4824](https://github.com/databricks/terraform-provider-databricks/pull/4824)).
1213
* Added support for Alert V2 in `databricks_permissions` ([#4831](https://github.com/databricks/terraform-provider-databricks/pull/4831)).
1314
* Replace instead of dropping Delta `databricks_sql_table` ([#2424](https://github.com/databricks/terraform-provider-databricks/pull/2424)).

docs/data-sources/service_principal.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,18 @@ resource "databricks_group_member" "my_member_a" {
3131

3232
Data source allows you to pick service principals by one of the following attributes (only one of them):
3333

34-
- `application_id` - (Required if `display_name` isn't used) ID of the service principal. The service principal must exist before this resource can be retrieved.
35-
- `display_name` - (Required if `application_id` isn't used) Exact display name of the service principal. The service principal must exist before this resource can be retrieved. In case if there are several service principals with the same name, an error is thrown.
34+
- `application_id` - (Required if neither `display_name` nor `scim_id` is used) ID of the service principal. The service principal must exist before this resource can be retrieved.
35+
- `display_name` - (Required if neither `application_id` nor `scim_id` is used) Exact display name of the service principal. The service principal must exist before this resource can be retrieved. In case if there are several service principals with the same name, an error is thrown.
36+
- `scim_id` - (Required if neither `application_id` nor `display_name` is used) Unique SCIM ID for a service principal in the Databricks workspace. The service principal must exist before this resource can be retrieved.
3637

3738
## Attribute Reference
3839

3940
Data source exposes the following attributes:
4041

41-
- `id` - The id of the service principal.
42+
- `id` - The id of the service principal (SCIM ID).
4243
- `external_id` - ID of the service principal in an external identity provider.
4344
- `display_name` - Display name of the [service principal](../resources/service_principal.md), e.g. `Foo SPN`.
45+
- `scim_id` - same as `id`.
4446
- `home` - Home folder of the [service principal](../resources/service_principal.md), e.g. `/Users/11111111-2222-3333-4444-555666777888`.
4547
- `repos` - Repos location of the [service principal](../resources/service_principal.md), e.g. `/Repos/11111111-2222-3333-4444-555666777888`.
4648
- `active` - Whether service principal is active or not.

scim/data_service_principal.go

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,79 @@ import (
55
"fmt"
66

77
"github.com/databricks/terraform-provider-databricks/common"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
89
)
910

10-
// DataSourceServicePrincipal returns information about the spn specified by the application_id
11+
// DataSourceServicePrincipal returns information about the spn specified by the application_id, id, display_name, or scim_id
1112
func DataSourceServicePrincipal() common.Resource {
1213
type spnData struct {
1314
ApplicationID string `json:"application_id,omitempty" tf:"computed"`
1415
DisplayName string `json:"display_name,omitempty" tf:"computed"`
15-
SpID string `json:"sp_id,omitempty" tf:"computed"`
16+
ScimID string `json:"scim_id,omitempty" tf:"computed"`
1617
ID string `json:"id,omitempty" tf:"computed"`
1718
Home string `json:"home,omitempty" tf:"computed"`
1819
Repos string `json:"repos,omitempty" tf:"computed"`
1920
Active bool `json:"active,omitempty" tf:"computed"`
2021
ExternalID string `json:"external_id,omitempty" tf:"computed"`
2122
AclPrincipalID string `json:"acl_principal_id,omitempty" tf:"computed"`
2223
}
23-
return common.DataResource(spnData{}, func(ctx context.Context, e any, c *common.DatabricksClient) error {
24-
response := e.(*spnData)
25-
spnAPI := NewServicePrincipalsAPI(ctx, c)
26-
var spList []User
27-
var err error
28-
if response.ApplicationID != "" && response.DisplayName != "" {
29-
return fmt.Errorf("please specify only one of application_id or display_name")
30-
}
31-
if response.ApplicationID != "" {
32-
spList, err = spnAPI.Filter(fmt.Sprintf(`applicationId eq "%s"`, response.ApplicationID), true)
33-
} else if response.DisplayName != "" {
34-
spList, err = spnAPI.Filter(fmt.Sprintf(`displayName eq "%s"`, response.DisplayName), true)
35-
} else {
36-
return fmt.Errorf("please specify either application_id or display_name")
37-
}
38-
if err != nil {
39-
return err
40-
}
41-
if len(spList) == 0 {
24+
25+
s := common.StructToSchema(spnData{}, func(
26+
s map[string]*schema.Schema) map[string]*schema.Schema {
27+
s["application_id"].ExactlyOneOf = []string{"application_id", "display_name", "scim_id"}
28+
s["display_name"].ExactlyOneOf = []string{"application_id", "display_name", "scim_id"}
29+
s["scim_id"].ExactlyOneOf = []string{"application_id", "display_name", "scim_id"}
30+
return s
31+
})
32+
33+
return common.Resource{
34+
Schema: s,
35+
Read: func(ctx context.Context, d *schema.ResourceData, m *common.DatabricksClient) error {
36+
var response spnData
37+
var spList []User
38+
var err error
39+
40+
common.DataToStructPointer(d, s, &response)
41+
spnAPI := NewServicePrincipalsAPI(ctx, m)
42+
4243
if response.ApplicationID != "" {
43-
return fmt.Errorf("cannot find SP with ID %s", response.ApplicationID)
44+
spList, err = spnAPI.Filter(fmt.Sprintf(`applicationId eq "%s"`, response.ApplicationID), true)
45+
} else if response.ScimID != "" {
46+
spList, err = spnAPI.Filter(fmt.Sprintf(`id eq "%s"`, response.ScimID), true)
47+
} else if response.DisplayName != "" {
48+
spList, err = spnAPI.Filter(fmt.Sprintf(`displayName eq "%s"`, response.DisplayName), true)
4449
} else {
45-
return fmt.Errorf("cannot find SP with name %s", response.DisplayName)
50+
return fmt.Errorf("please specify either application_id, display_name, or scim_id")
4651
}
47-
} else if len(spList) > 1 {
48-
return fmt.Errorf("there are more than 1 service principal with name %s", response.DisplayName)
49-
}
50-
51-
sp := spList[0]
52-
response.DisplayName = sp.DisplayName
53-
response.ApplicationID = sp.ApplicationID
54-
response.Home = fmt.Sprintf("/Users/%s", sp.ApplicationID)
55-
response.Repos = fmt.Sprintf("/Repos/%s", sp.ApplicationID)
56-
response.AclPrincipalID = fmt.Sprintf("servicePrincipals/%s", sp.ApplicationID)
57-
response.ExternalID = sp.ExternalID
58-
response.Active = sp.Active
59-
response.SpID = sp.ID
60-
response.ID = sp.ID
61-
return nil
62-
})
52+
if err != nil {
53+
return err
54+
}
55+
56+
if len(spList) == 0 {
57+
if response.ApplicationID != "" {
58+
return fmt.Errorf("cannot find SP with an application ID %s", response.ApplicationID)
59+
} else if response.ScimID != "" {
60+
return fmt.Errorf("cannot find SP with an ID %s", response.ScimID)
61+
} else {
62+
return fmt.Errorf("cannot find SP with name %s", response.DisplayName)
63+
}
64+
} else if len(spList) > 1 {
65+
return fmt.Errorf("there are %d Service Principals with name %s", len(spList), response.DisplayName)
66+
}
67+
68+
sp := spList[0]
69+
response.DisplayName = sp.DisplayName
70+
response.ApplicationID = sp.ApplicationID
71+
response.Home = fmt.Sprintf("/Users/%s", sp.ApplicationID)
72+
response.Repos = fmt.Sprintf("/Repos/%s", sp.ApplicationID)
73+
response.AclPrincipalID = fmt.Sprintf("servicePrincipals/%s", sp.ApplicationID)
74+
response.ExternalID = sp.ExternalID
75+
response.Active = sp.Active
76+
response.ScimID = sp.ID
77+
78+
err = common.StructToData(response, s, d)
79+
d.SetId(sp.ID)
80+
return err
81+
},
82+
}
6383
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package scim_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/databricks/terraform-provider-databricks/internal/acceptance"
7+
)
8+
9+
const spnBySCIMID = `
10+
resource "databricks_service_principal" "this" {
11+
display_name = "SPN {var.RANDOM}"
12+
}
13+
14+
data "databricks_service_principal" "this" {
15+
scim_id = databricks_service_principal.this.id
16+
depends_on = [databricks_service_principal.this]
17+
}`
18+
19+
const azureSpnBySCIMID = `
20+
resource "databricks_service_principal" "this" {
21+
application_id = "{var.RANDOM_UUID}"
22+
display_name = "SPN {var.RANDOM}"
23+
force = true
24+
}
25+
26+
data "databricks_service_principal" "this" {
27+
scim_id = databricks_service_principal.this.id
28+
depends_on = [databricks_service_principal.this]
29+
}`
30+
31+
func TestAccDataSourceSPNOnAWSBySCIMID(t *testing.T) {
32+
acceptance.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE")
33+
acceptance.WorkspaceLevel(t, acceptance.Step{
34+
Template: spnBySCIMID,
35+
})
36+
}
37+
38+
func TestAccDataSourceSPNOnGCPBySCIMID(t *testing.T) {
39+
acceptance.GetEnvOrSkipTest(t, "GOOGLE_CREDENTIALS")
40+
acceptance.WorkspaceLevel(t, acceptance.Step{
41+
Template: spnBySCIMID,
42+
})
43+
}
44+
45+
func TestAccDataSourceSPNOnAzureBySCIMID(t *testing.T) {
46+
acceptance.GetEnvOrSkipTest(t, "ARM_CLIENT_ID")
47+
acceptance.WorkspaceLevel(t, acceptance.Step{
48+
Template: azureSpnBySCIMID,
49+
})
50+
}

scim/data_service_principal_test.go

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestDataServicePrincipalReadByAppId(t *testing.T) {
4141
NonWritable: true,
4242
ID: "abc",
4343
}.ApplyAndExpectData(t, map[string]any{
44-
"sp_id": "abc",
44+
"scim_id": "abc",
4545
"id": "abc",
4646
"application_id": "abc",
4747
"display_name": "Example Service Principal",
@@ -52,7 +52,95 @@ func TestDataServicePrincipalReadByAppId(t *testing.T) {
5252
})
5353
}
5454

55-
func TestDataServicePrincipalReadByIdNotFound(t *testing.T) {
55+
func TestDataServicePrincipalReadBySpId(t *testing.T) {
56+
qa.ResourceFixture{
57+
Fixtures: []qa.HTTPFixture{
58+
{
59+
Method: "GET",
60+
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?excludedAttributes=roles&filter=id%20eq%20%22abc%22",
61+
Response: UserList{
62+
Resources: []User{
63+
{
64+
ID: "abc",
65+
DisplayName: "Example Service Principal",
66+
Active: true,
67+
ApplicationID: "abc",
68+
Groups: []ComplexValue{
69+
{
70+
Display: "admins",
71+
Value: "4567",
72+
},
73+
{
74+
Display: "ds",
75+
Value: "9877",
76+
},
77+
},
78+
},
79+
},
80+
},
81+
},
82+
},
83+
Resource: DataSourceServicePrincipal(),
84+
HCL: `scim_id = "abc"`,
85+
Read: true,
86+
NonWritable: true,
87+
ID: "abc",
88+
}.ApplyAndExpectData(t, map[string]any{
89+
"scim_id": "abc",
90+
"id": "abc",
91+
"application_id": "abc",
92+
"display_name": "Example Service Principal",
93+
"active": true,
94+
"home": "/Users/abc",
95+
"repos": "/Repos/abc",
96+
"acl_principal_id": "servicePrincipals/abc",
97+
})
98+
}
99+
func TestDataServicePrincipalReadByDisplayName(t *testing.T) {
100+
qa.ResourceFixture{
101+
Fixtures: []qa.HTTPFixture{
102+
{
103+
Method: "GET",
104+
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?excludedAttributes=roles&filter=displayName%20eq%20%22testsp%22",
105+
Response: UserList{
106+
Resources: []User{
107+
{
108+
ID: "abc",
109+
DisplayName: "testsp",
110+
Active: true,
111+
ApplicationID: "abc",
112+
Groups: []ComplexValue{
113+
{
114+
Display: "admins",
115+
Value: "4567",
116+
},
117+
{
118+
Display: "ds",
119+
Value: "9877",
120+
},
121+
},
122+
},
123+
},
124+
},
125+
},
126+
},
127+
Resource: DataSourceServicePrincipal(),
128+
HCL: `display_name = "testsp"`,
129+
Read: true,
130+
NonWritable: true,
131+
ID: "abc",
132+
}.ApplyAndExpectData(t, map[string]any{
133+
"scim_id": "abc",
134+
"id": "abc",
135+
"application_id": "abc",
136+
"display_name": "testsp",
137+
"active": true,
138+
"home": "/Users/abc",
139+
"repos": "/Repos/abc",
140+
"acl_principal_id": "servicePrincipals/abc",
141+
})
142+
}
143+
func TestDataServicePrincipalReadByAppIdNotFound(t *testing.T) {
56144
qa.ResourceFixture{
57145
Fixtures: []qa.HTTPFixture{
58146
{
@@ -66,9 +154,25 @@ func TestDataServicePrincipalReadByIdNotFound(t *testing.T) {
66154
Read: true,
67155
NonWritable: true,
68156
ID: "_",
69-
}.ExpectError(t, "cannot find SP with ID abc")
157+
}.ExpectError(t, "cannot find SP with an application ID abc")
70158
}
71159

160+
func TestDataServicePrincipalReadByIdNotFound(t *testing.T) {
161+
qa.ResourceFixture{
162+
Fixtures: []qa.HTTPFixture{
163+
{
164+
Method: "GET",
165+
Resource: "/api/2.0/preview/scim/v2/ServicePrincipals?excludedAttributes=roles&filter=id%20eq%20%22abc%22",
166+
Response: UserList{},
167+
},
168+
},
169+
Resource: DataSourceServicePrincipal(),
170+
HCL: `scim_id = "abc"`,
171+
Read: true,
172+
NonWritable: true,
173+
ID: "_",
174+
}.ExpectError(t, "cannot find SP with an ID abc")
175+
}
72176
func TestDataServicePrincipalReadByNameNotFound(t *testing.T) {
73177
qa.ResourceFixture{
74178
Fixtures: []qa.HTTPFixture{
@@ -133,7 +237,7 @@ func TestDataServicePrincipalReadByNameDuplicates(t *testing.T) {
133237
Read: true,
134238
NonWritable: true,
135239
ID: "abc",
136-
}.ExpectError(t, "there are more than 1 service principal with name abc")
240+
}.ExpectError(t, "there are 2 Service Principals with name abc")
137241
}
138242

139243
func TestDataServicePrincipalReadNoParams(t *testing.T) {
@@ -143,16 +247,16 @@ func TestDataServicePrincipalReadNoParams(t *testing.T) {
143247
Read: true,
144248
NonWritable: true,
145249
ID: "_",
146-
}.ExpectError(t, "please specify either application_id or display_name")
250+
}.ExpectError(t, "please specify either application_id, display_name, or scim_id")
147251
}
148252

149-
func TestDataServicePrincipalReadBothParams(t *testing.T) {
253+
func TestDataServicePrincipalReadInvalidConfig(t *testing.T) {
150254
qa.ResourceFixture{
151255
Resource: DataSourceServicePrincipal(),
152256
HCL: `display_name = "abc"
153257
application_id = "abc"`,
154258
Read: true,
155259
NonWritable: true,
156260
ID: "_",
157-
}.ExpectError(t, "please specify only one of application_id or display_name")
261+
}.ExpectError(t, "invalid config supplied. [application_id] Invalid combination of arguments. [display_name] Invalid combination of arguments. [scim_id] Invalid combination of arguments")
158262
}

0 commit comments

Comments
 (0)