Skip to content

Commit e320aa1

Browse files
alexottmgyucht
andauthored
Don't rely on having @ to check if it's user or SP (#2765)
* Fix: don't rely on having `@` to check if it's user or SP There are some installations where user name isn't equal to email - for them our detection of username vs SP ID is broken, and lead to some problems, like, generation of erroneous `run_as` blocks in job definitions, etc. This PR fixes this by validating a given string as UUID, and if it's matches, then uses it as SP ID, and the rest as user name. Changes affect following: * `databricks_job` API - besides validation, it also doesn't fill the `RunAs` structure on read when creator user name == run as user name. * `databricks_current_user` data source - should fix generation of `acl_principal_id` * Update jobs/data_job_test.go Co-authored-by: Miles Yucht <[email protected]> * Add integration test --------- Co-authored-by: Miles Yucht <[email protected]>
1 parent a9f0ed6 commit e320aa1

File tree

8 files changed

+192
-12
lines changed

8 files changed

+192
-12
lines changed

common/util.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package common
2+
3+
import (
4+
"regexp"
5+
)
6+
7+
var (
8+
uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
9+
)
10+
11+
func StringIsUUID(s string) bool {
12+
return uuidRegex.MatchString(s)
13+
}

common/util_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package common
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestStringIsUUID(t *testing.T) {
10+
assert.True(t, StringIsUUID("3f670caf-9a4b-4479-8143-1a0878da8f57"))
11+
assert.False(t, StringIsUUID("abc"))
12+
}

exporter/importables.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ var (
4242
nameNormalizationRegex = regexp.MustCompile(`\W+`)
4343
jobClustersRegex = regexp.MustCompile(`^((job_cluster|task)\.[0-9]+\.new_cluster\.[0-9]+\.)`)
4444
dltClusterRegex = regexp.MustCompile(`^(cluster\.[0-9]+\.)`)
45-
uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
4645
predefinedClusterPolicies = []string{"Personal Compute", "Job Compute", "Power User Compute", "Shared Compute"}
4746
secretPathRegex = regexp.MustCompile(`^\{\{secrets\/([^\/]+)\/([^}]+)\}\}$`)
4847
sqlParentRegexp = regexp.MustCompile(`^folders/(\d+)$`)

exporter/util.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,15 @@ func (ic *importContext) emitUserOrServicePrincipal(userOrSPName string) {
9696
if userOrSPName == "" {
9797
return
9898
}
99+
// TODO: think about another way of checking for a user. ideally we need to check against the
100+
// list of users/SPs obtained via SCIM API - this will be done in the refactoring requested by the SCIM team
99101
if strings.Contains(userOrSPName, "@") {
100102
ic.Emit(&resource{
101103
Resource: "databricks_user",
102104
Attribute: "user_name",
103105
Value: userOrSPName,
104106
})
105-
} else if uuidRegex.MatchString(userOrSPName) {
107+
} else if common.StringIsUUID(userOrSPName) {
106108
ic.Emit(&resource{
107109
Resource: "databricks_service_principal",
108110
Attribute: "application_id",
@@ -126,8 +128,10 @@ func (ic *importContext) IsUserOrServicePrincipalDirectory(path, prefix string)
126128
}
127129
parts := strings.SplitN(path, "/", 4)
128130
if len(parts) == 3 || (len(parts) == 4 && parts[3] == "") {
131+
// TODO: think about another way of checking for a user. ideally we need to check against the
132+
// list of users/SPs obtained via SCIM API - this will be done in the refactoring requested by the SCIM team
129133
userOrSPName := parts[2]
130-
return strings.Contains(userOrSPName, "@") || uuidRegex.MatchString(userOrSPName)
134+
return strings.Contains(userOrSPName, "@") || common.StringIsUUID(userOrSPName)
131135
}
132136
return false
133137
}

internal/acceptance/job_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/databricks/databricks-sdk-go"
1111
"github.com/databricks/databricks-sdk-go/service/jobs"
1212
"github.com/databricks/terraform-provider-databricks/common"
13+
"github.com/databricks/terraform-provider-databricks/qa"
1314
"github.com/stretchr/testify/assert"
1415
)
1516

@@ -216,3 +217,65 @@ func TestAccJobControlRunState(t *testing.T) {
216217
Check: resourceCheck("databricks_job.this", waitForRunToStart),
217218
})
218219
}
220+
221+
func runAsTemplate(runAs string) string {
222+
return `
223+
data "databricks_current_user" "me" {}
224+
data "databricks_spark_version" "latest" {}
225+
data "databricks_node_type" "smallest" {
226+
local_disk = true
227+
}
228+
229+
resource "databricks_notebook" "this" {
230+
path = "${data.databricks_current_user.me.home}/Terraform{var.RANDOM}"
231+
language = "PYTHON"
232+
content_base64 = base64encode(<<-EOT
233+
# created from ${abspath(path.module)}
234+
display(spark.range(10))
235+
EOT
236+
)
237+
}
238+
239+
resource "databricks_job" "this" {
240+
name = "{var.RANDOM}"
241+
242+
job_cluster {
243+
job_cluster_key = "j"
244+
new_cluster {
245+
num_workers = 20
246+
spark_version = data.databricks_spark_version.latest.id
247+
node_type_id = data.databricks_node_type.smallest.id
248+
}
249+
}
250+
251+
task {
252+
task_key = "c"
253+
job_cluster_key = "j"
254+
notebook_task {
255+
notebook_path = databricks_notebook.this.path
256+
}
257+
}
258+
259+
run_as {
260+
` + runAs + `
261+
}
262+
}`
263+
}
264+
265+
func TestAccJobRunAsUser(t *testing.T) {
266+
workspaceLevel(t, step{
267+
Template: `
268+
resource "databricks_user" "this" {
269+
user_name = "` + qa.RandomEmail() + `"
270+
}
271+
` + runAsTemplate(`user_name = databricks_user.this.user_name`),
272+
})
273+
}
274+
275+
func TestAccJobRunAsServicePrincipal(t *testing.T) {
276+
loadDebugEnvIfRunsFromIDE(t, "ucws")
277+
spId := GetEnvOrSkipTest(t, "ACCOUNT_LEVEL_SERVICE_PRINCIPAL_ID")
278+
unityWorkspaceLevel(t, step{
279+
Template: runAsTemplate(`service_principal_name = "` + spId + `"`),
280+
})
281+
}

jobs/data_job_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,97 @@ func TestDataSourceQueryableJobMatchesId(t *testing.T) {
8484
})
8585
}
8686

87+
func TestDataSourceQueryableJobRunAsSP(t *testing.T) {
88+
spID := "3f670caf-9a4b-4479-8143-1a0878da8f57"
89+
qa.ResourceFixture{
90+
Fixtures: []qa.HTTPFixture{
91+
{
92+
Method: "GET",
93+
Resource: "/api/2.0/jobs/get?job_id=234",
94+
Response: Job{
95+
JobID: 234,
96+
Settings: &JobSettings{
97+
Name: "Second",
98+
},
99+
CreatorUserName: "[email protected]",
100+
RunAsUserName: spID,
101+
},
102+
},
103+
},
104+
Resource: DataSourceJob(),
105+
Read: true,
106+
New: true,
107+
NonWritable: true,
108+
HCL: `job_id = "234"`,
109+
ID: "234",
110+
}.ApplyAndExpectData(t, map[string]any{
111+
"job_id": "234",
112+
"id": "234",
113+
"job_settings.0.settings.0.name": "Second",
114+
"job_settings.0.settings.0.run_as.0.service_principal_name": spID,
115+
})
116+
}
117+
118+
func TestDataSourceQueryableJobRunAsSameUser(t *testing.T) {
119+
qa.ResourceFixture{
120+
Fixtures: []qa.HTTPFixture{
121+
{
122+
Method: "GET",
123+
Resource: "/api/2.0/jobs/get?job_id=234",
124+
Response: Job{
125+
JobID: 234,
126+
Settings: &JobSettings{
127+
Name: "Second",
128+
},
129+
CreatorUserName: "[email protected]",
130+
RunAsUserName: "[email protected]",
131+
},
132+
},
133+
},
134+
Resource: DataSourceJob(),
135+
Read: true,
136+
New: true,
137+
NonWritable: true,
138+
HCL: `job_id = "234"`,
139+
ID: "234",
140+
}.ApplyAndExpectData(t, map[string]any{
141+
"job_id": "234",
142+
"id": "234",
143+
"job_settings.0.settings.0.name": "Second",
144+
"job_settings.0.settings.0.run_as.0": map[string]any{},
145+
})
146+
}
147+
148+
func TestDataSourceQueryableJobRunAsAnotherUser(t *testing.T) {
149+
qa.ResourceFixture{
150+
Fixtures: []qa.HTTPFixture{
151+
{
152+
Method: "GET",
153+
Resource: "/api/2.0/jobs/get?job_id=234",
154+
Response: Job{
155+
JobID: 234,
156+
Settings: &JobSettings{
157+
Name: "Second",
158+
},
159+
CreatorUserName: "[email protected]",
160+
RunAsUserName: "[email protected]",
161+
},
162+
},
163+
},
164+
Resource: DataSourceJob(),
165+
Read: true,
166+
New: true,
167+
NonWritable: true,
168+
HCL: `job_id = "234"`,
169+
ID: "234",
170+
}.ApplyAndExpectData(t, map[string]any{
171+
"job_id": "234",
172+
"id": "234",
173+
"job_settings.0.settings.0.name": "Second",
174+
"job_settings.0.settings.0.run_as.0.user_name": "[email protected]",
175+
})
176+
}
177+
87178
func TestDataSourceQueryableJobMatchesName(t *testing.T) {
88179
qa.ResourceFixture{
89180
Fixtures: commonFixtures("First"),

jobs/resource_job.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -572,16 +572,14 @@ func (a JobsAPI) Read(id string) (job Job, err error) {
572572
job.Settings.sortWebhooksByID()
573573
}
574574

575-
if job.RunAsUserName != "" && job.Settings != nil {
576-
userNameIsEmail := strings.Contains(job.RunAsUserName, "@")
577-
578-
if userNameIsEmail {
575+
if job.Settings != nil && job.RunAsUserName != "" && job.RunAsUserName != job.CreatorUserName {
576+
if common.StringIsUUID(job.RunAsUserName) {
579577
job.Settings.RunAs = &JobRunAs{
580-
UserName: job.RunAsUserName,
578+
ServicePrincipalName: job.RunAsUserName,
581579
}
582580
} else {
583581
job.Settings.RunAs = &JobRunAs{
584-
ServicePrincipalName: job.RunAsUserName,
582+
UserName: job.RunAsUserName,
585583
}
586584
}
587585
}

scim/data_current_user.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ func DataSourceCurrentUser() *schema.Resource {
5959
d.Set("user_name", me.UserName)
6060
d.Set("home", fmt.Sprintf("/Users/%s", me.UserName))
6161
d.Set("repos", fmt.Sprintf("/Repos/%s", me.UserName))
62-
if strings.Contains(me.UserName, "@") {
63-
d.Set("acl_principal_id", fmt.Sprintf("users/%s", me.UserName))
64-
} else {
62+
if common.StringIsUUID(me.UserName) {
6563
d.Set("acl_principal_id", fmt.Sprintf("servicePrincipals/%s", me.UserName))
64+
} else {
65+
d.Set("acl_principal_id", fmt.Sprintf("users/%s", me.UserName))
6666
}
6767
d.Set("external_id", me.ExternalId)
6868
splits := strings.Split(me.UserName, "@")

0 commit comments

Comments
 (0)