Skip to content

Commit 0ea8d98

Browse files
authored
direct: Support permissions (#3724)
## Changes Implement permissions support for all resources that have it. Note, $resources references in permissions are not supported yet. ## Tests New and existing acceptance tests.
1 parent 4d8abe4 commit 0ea8d98

File tree

241 files changed

+7085
-264
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

241 files changed

+7085
-264
lines changed

acceptance/bundle/deploy/mlops-stacks/out.test.toml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

acceptance/bundle/deploy/mlops-stacks/test.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ Ignore = [
77
"config.json"
88
]
99

10+
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"]
11+
# On direct, this fails with
12+
# +Endpoint: PUT [DATABRICKS_URL]/api/2.0/permissions/registered-models/%5Bdev%20[USERNAME]%5D%20dev-project_name_[UNIQUE_NAME]-model
13+
# +HTTP Status: 400 Bad Request
14+
# +API error_code: INVALID_PARAMETER_VALUE
15+
# +API message: '[dev [USERNAME]] dev-project_name_[UNIQUE_NAME]-model' is not a valid registered model ID.
16+
17+
1018
[[Repls]]
1119
Old = "aws|azure|gcp"
1220
New = "[CLOUD_ENV_BASE]"

acceptance/bundle/refschema/out.fields.txt

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ resources.alerts.*.parent_path string ALL
4040
resources.alerts.*.permissions []resources.AlertPermission INPUT
4141
resources.alerts.*.permissions[*] resources.AlertPermission INPUT
4242
resources.alerts.*.permissions[*].group_name string INPUT
43-
resources.alerts.*.permissions[*].level string INPUT
43+
resources.alerts.*.permissions[*].level resources.AlertPermissionLevel INPUT
4444
resources.alerts.*.permissions[*].service_principal_name string INPUT
4545
resources.alerts.*.permissions[*].user_name string INPUT
4646
resources.alerts.*.query_text string ALL
@@ -145,6 +145,13 @@ resources.apps.*.updater string ALL
145145
resources.apps.*.url string ALL
146146
resources.apps.*.user_api_scopes []string ALL
147147
resources.apps.*.user_api_scopes[*] string ALL
148+
resources.apps.*.permissions.object_id string ALL
149+
resources.apps.*.permissions.permissions []iam.AccessControlRequest ALL
150+
resources.apps.*.permissions.permissions[*] iam.AccessControlRequest ALL
151+
resources.apps.*.permissions.permissions[*].group_name string ALL
152+
resources.apps.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL
153+
resources.apps.*.permissions.permissions[*].service_principal_name string ALL
154+
resources.apps.*.permissions.permissions[*].user_name string ALL
148155
resources.clusters.*.apply_policy_default_values bool INPUT STATE
149156
resources.clusters.*.autoscale *compute.AutoScale ALL
150157
resources.clusters.*.autoscale.max_workers int ALL
@@ -401,6 +408,13 @@ resources.clusters.*.workload_type *compute.WorkloadType ALL
401408
resources.clusters.*.workload_type.clients compute.ClientsTypes ALL
402409
resources.clusters.*.workload_type.clients.jobs bool ALL
403410
resources.clusters.*.workload_type.clients.notebooks bool ALL
411+
resources.clusters.*.permissions.object_id string ALL
412+
resources.clusters.*.permissions.permissions []iam.AccessControlRequest ALL
413+
resources.clusters.*.permissions.permissions[*] iam.AccessControlRequest ALL
414+
resources.clusters.*.permissions.permissions[*].group_name string ALL
415+
resources.clusters.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL
416+
resources.clusters.*.permissions.permissions[*].service_principal_name string ALL
417+
resources.clusters.*.permissions.permissions[*].user_name string ALL
404418
resources.database_catalogs.*.create_database_if_not_exists bool ALL
405419
resources.database_catalogs.*.database_instance_name string ALL
406420
resources.database_catalogs.*.database_name string ALL
@@ -465,6 +479,13 @@ resources.database_instances.*.stopped bool ALL
465479
resources.database_instances.*.uid string ALL
466480
resources.database_instances.*.url string INPUT
467481
resources.database_instances.*.usage_policy_id string ALL
482+
resources.database_instances.*.permissions.object_id string ALL
483+
resources.database_instances.*.permissions.permissions []iam.AccessControlRequest ALL
484+
resources.database_instances.*.permissions.permissions[*] iam.AccessControlRequest ALL
485+
resources.database_instances.*.permissions.permissions[*].group_name string ALL
486+
resources.database_instances.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL
487+
resources.database_instances.*.permissions.permissions[*].service_principal_name string ALL
488+
resources.database_instances.*.permissions.permissions[*].user_name string ALL
468489
resources.experiments.*.artifact_location string ALL
469490
resources.experiments.*.creation_time int64 REMOTE
470491
resources.experiments.*.experiment_id string REMOTE
@@ -486,6 +507,13 @@ resources.experiments.*.tags[*] ml.ExperimentTag ALL
486507
resources.experiments.*.tags[*].key string ALL
487508
resources.experiments.*.tags[*].value string ALL
488509
resources.experiments.*.url string INPUT
510+
resources.experiments.*.permissions.object_id string ALL
511+
resources.experiments.*.permissions.permissions []iam.AccessControlRequest ALL
512+
resources.experiments.*.permissions.permissions[*] iam.AccessControlRequest ALL
513+
resources.experiments.*.permissions.permissions[*].group_name string ALL
514+
resources.experiments.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL
515+
resources.experiments.*.permissions.permissions[*].service_principal_name string ALL
516+
resources.experiments.*.permissions.permissions[*].user_name string ALL
489517
resources.jobs.*.budget_policy_id string INPUT STATE
490518
resources.jobs.*.continuous *jobs.Continuous INPUT STATE
491519
resources.jobs.*.continuous.pause_status jobs.PauseStatus INPUT STATE
@@ -2205,6 +2233,13 @@ resources.jobs.*.webhook_notifications.on_streaming_backlog_exceeded[*].id strin
22052233
resources.jobs.*.webhook_notifications.on_success []jobs.Webhook INPUT STATE
22062234
resources.jobs.*.webhook_notifications.on_success[*] jobs.Webhook INPUT STATE
22072235
resources.jobs.*.webhook_notifications.on_success[*].id string INPUT STATE
2236+
resources.jobs.*.permissions.object_id string ALL
2237+
resources.jobs.*.permissions.permissions []iam.AccessControlRequest ALL
2238+
resources.jobs.*.permissions.permissions[*] iam.AccessControlRequest ALL
2239+
resources.jobs.*.permissions.permissions[*].group_name string ALL
2240+
resources.jobs.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL
2241+
resources.jobs.*.permissions.permissions[*].service_principal_name string ALL
2242+
resources.jobs.*.permissions.permissions[*].user_name string ALL
22082243
resources.models.*.creation_timestamp int64 REMOTE
22092244
resources.models.*.description string ALL
22102245
resources.models.*.id string INPUT REMOTE
@@ -2244,6 +2279,13 @@ resources.models.*.tags[*].key string ALL
22442279
resources.models.*.tags[*].value string ALL
22452280
resources.models.*.url string INPUT
22462281
resources.models.*.user_id string REMOTE
2282+
resources.models.*.permissions.object_id string ALL
2283+
resources.models.*.permissions.permissions []iam.AccessControlRequest ALL
2284+
resources.models.*.permissions.permissions[*] iam.AccessControlRequest ALL
2285+
resources.models.*.permissions.permissions[*].group_name string ALL
2286+
resources.models.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL
2287+
resources.models.*.permissions.permissions[*].service_principal_name string ALL
2288+
resources.models.*.permissions.permissions[*].user_name string ALL
22472289
resources.pipelines.*.allow_duplicate_names bool INPUT STATE
22482290
resources.pipelines.*.budget_policy_id string INPUT STATE
22492291
resources.pipelines.*.catalog string INPUT STATE
@@ -2838,6 +2880,13 @@ resources.pipelines.*.trigger.cron.quartz_cron_schedule string INPUT STATE
28382880
resources.pipelines.*.trigger.cron.timezone_id string INPUT STATE
28392881
resources.pipelines.*.trigger.manual *pipelines.ManualTrigger INPUT STATE
28402882
resources.pipelines.*.url string INPUT
2883+
resources.pipelines.*.permissions.object_id string ALL
2884+
resources.pipelines.*.permissions.permissions []iam.AccessControlRequest ALL
2885+
resources.pipelines.*.permissions.permissions[*] iam.AccessControlRequest ALL
2886+
resources.pipelines.*.permissions.permissions[*].group_name string ALL
2887+
resources.pipelines.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL
2888+
resources.pipelines.*.permissions.permissions[*].service_principal_name string ALL
2889+
resources.pipelines.*.permissions.permissions[*].user_name string ALL
28412890
resources.registered_models.*.aliases []catalog.RegisteredModelAlias ALL
28422891
resources.registered_models.*.aliases[*] catalog.RegisteredModelAlias ALL
28432892
resources.registered_models.*.aliases[*].alias_name string ALL
@@ -2951,6 +3000,13 @@ resources.sql_warehouses.*.tags.custom_tags[*].value string ALL
29513000
resources.sql_warehouses.*.url string INPUT
29523001
resources.sql_warehouses.*.warehouse_type sql.CreateWarehouseRequestWarehouseType INPUT STATE
29533002
resources.sql_warehouses.*.warehouse_type sql.GetWarehouseResponseWarehouseType REMOTE
3003+
resources.sql_warehouses.*.permissions.object_id string ALL
3004+
resources.sql_warehouses.*.permissions.permissions []iam.AccessControlRequest ALL
3005+
resources.sql_warehouses.*.permissions.permissions[*] iam.AccessControlRequest ALL
3006+
resources.sql_warehouses.*.permissions.permissions[*].group_name string ALL
3007+
resources.sql_warehouses.*.permissions.permissions[*].permission_level iam.PermissionLevel ALL
3008+
resources.sql_warehouses.*.permissions.permissions[*].service_principal_name string ALL
3009+
resources.sql_warehouses.*.permissions.permissions[*].user_name string ALL
29543010
resources.synced_database_tables.*.data_synchronization_status *database.SyncedTableStatus ALL
29553011
resources.synced_database_tables.*.data_synchronization_status.continuous_update_status *database.SyncedTableContinuousUpdateStatus ALL
29563012
resources.synced_database_tables.*.data_synchronization_status.continuous_update_status.initial_pipeline_sync_progress *database.SyncedTablePipelineProgress ALL
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,2 @@
11
Cloud = false
22
Local = false
3-
4-
# Permissions with alerts does not work with direct deployment yet.
5-
# So we skip this test on direct-exp until we have permissions support in
6-
# direct deployment.
7-
[EnvMatrix]
8-
DATABRICKS_BUNDLE_ENGINE = ["terraform"]

acceptance/bundle/resources/experiments/basic/test.toml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,3 @@ RecordRequests = false
55
[[Repls]]
66
Old = '\d{3,}'
77
New = "[NUMID]"
8-
9-
# Test both terraform and direct deployment engines
10-
[EnvMatrix]
11-
DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct-exp"]
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Analyze all requests recorded in subtests to highlight differences between direct and terraform.
4+
"""
5+
6+
import os
7+
import re
8+
import json
9+
import sys
10+
from pathlib import Path
11+
from difflib import unified_diff
12+
13+
14+
def read_json_many(file):
15+
with open(file) as fobj:
16+
s = fobj.read()
17+
18+
# fix invalid json due to replacement: x: [NUMID] --> x: "[NUMID]"
19+
s = re.sub(r": \[(.*?)\]", r': "[\1]"', s)
20+
21+
result = []
22+
23+
try:
24+
dec = json.JSONDecoder()
25+
pos = 0
26+
n = len(s)
27+
while True:
28+
# skip whitespace between objects
29+
while pos < n and s[pos].isspace():
30+
pos += 1
31+
if pos >= n:
32+
break
33+
obj, idx = dec.raw_decode(s, pos)
34+
result.append(obj)
35+
pos = idx
36+
37+
except Exception as ex:
38+
sys.exit(f"Failed to parse {file}: {ex}")
39+
40+
return result
41+
42+
43+
def normalize_acls(data):
44+
"""Recursively normalize ACLs in the data structure by sorting them."""
45+
if isinstance(data, dict):
46+
result = {}
47+
for key, value in data.items():
48+
if key == "access_control_list" and isinstance(value, list):
49+
# Sort ACLs by all fields to normalize order
50+
result[key] = sorted(value, key=lambda x: json.dumps(x, sort_keys=True))
51+
else:
52+
result[key] = normalize_acls(value)
53+
return result
54+
elif isinstance(data, list):
55+
return [normalize_acls(item) for item in data]
56+
else:
57+
return data
58+
59+
60+
def compare_files(file1, file2):
61+
"""Compare two JSON files and return comparison result."""
62+
data1 = read_json_many(file1)
63+
data2 = read_json_many(file2)
64+
65+
if data1 == data2:
66+
return "EXACT", ""
67+
68+
normalized1 = normalize_acls(data1)
69+
normalized2 = normalize_acls(data2)
70+
71+
if normalized1 == normalized2:
72+
return "MATCH", ""
73+
74+
json1_str = json.dumps(normalized1, indent=2, sort_keys=True)
75+
json2_str = json.dumps(normalized2, indent=2, sort_keys=True)
76+
77+
diff_lines = list(
78+
unified_diff(
79+
json1_str.splitlines(keepends=True),
80+
json2_str.splitlines(keepends=True),
81+
fromfile=str(file1),
82+
tofile=str(file2),
83+
n=3,
84+
)
85+
)
86+
87+
return "DIFF ", "\n" + to_slash("".join(diff_lines).rstrip())
88+
89+
90+
def to_slash(x):
91+
return str(x).replace("\\", "/")
92+
93+
94+
def main():
95+
current_dir = Path(".")
96+
97+
direct_files = list(current_dir.glob("**/*.direct-exp.json"))
98+
99+
for direct_file in sorted(direct_files):
100+
if direct_file.name.startswith("out.plan"):
101+
# expected difference
102+
continue
103+
104+
terraform_file = direct_file.parent / direct_file.name.replace(".direct-exp.", ".terraform.")
105+
106+
fname = to_slash(direct_file)
107+
108+
if terraform_file.exists():
109+
result, diff = compare_files(direct_file, terraform_file)
110+
print(result + " " + fname + diff)
111+
else:
112+
print(f"ERROR {fname}: Missing terraform file {to_slash(terraform_file)}")
113+
114+
115+
if __name__ == "__main__":
116+
main()

acceptance/bundle/resources/permissions/apps/current_can_manage/out.plan.direct-exp.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,41 @@
88
"name": "foo"
99
}
1010
}
11+
},
12+
"resources.apps.foo.permissions": {
13+
"depends_on": [
14+
{
15+
"node": "resources.apps.foo",
16+
"label": "${resources.apps.foo.id}"
17+
}
18+
],
19+
"action": "create",
20+
"new_state": {
21+
"config": {
22+
"object_id": "",
23+
"permissions": [
24+
{
25+
"permission_level": "CAN_USE",
26+
"user_name": "[email protected]"
27+
},
28+
{
29+
"group_name": "data-team",
30+
"permission_level": "CAN_MANAGE"
31+
},
32+
{
33+
"permission_level": "CAN_MANAGE",
34+
"service_principal_name": "[UUID]"
35+
},
36+
{
37+
"permission_level": "CAN_MANAGE",
38+
"user_name": "[USERNAME]"
39+
}
40+
]
41+
},
42+
"vars": {
43+
"object_id": "/apps/${resources.apps.foo.id}"
44+
}
45+
}
1146
}
1247
}
1348
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"method": "PUT",
3+
"path": "/api/2.0/permissions/apps/foo",
4+
"body": {
5+
"access_control_list": [
6+
{
7+
"permission_level": "CAN_USE",
8+
"user_name": "[email protected]"
9+
},
10+
{
11+
"group_name": "data-team",
12+
"permission_level": "CAN_MANAGE"
13+
},
14+
{
15+
"permission_level": "CAN_MANAGE",
16+
"service_principal_name": "[UUID]"
17+
},
18+
{
19+
"permission_level": "CAN_MANAGE",
20+
"user_name": "[USERNAME]"
21+
}
22+
]
23+
}
24+
}

acceptance/bundle/resources/permissions/apps/other_can_manage/out.plan.direct-exp.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,41 @@
88
"name": "foo"
99
}
1010
}
11+
},
12+
"resources.apps.foo.permissions": {
13+
"depends_on": [
14+
{
15+
"node": "resources.apps.foo",
16+
"label": "${resources.apps.foo.id}"
17+
}
18+
],
19+
"action": "create",
20+
"new_state": {
21+
"config": {
22+
"object_id": "",
23+
"permissions": [
24+
{
25+
"permission_level": "CAN_USE",
26+
"user_name": "[email protected]"
27+
},
28+
{
29+
"group_name": "data-team",
30+
"permission_level": "CAN_MANAGE"
31+
},
32+
{
33+
"permission_level": "CAN_MANAGE",
34+
"service_principal_name": "[UUID]"
35+
},
36+
{
37+
"permission_level": "CAN_MANAGE",
38+
"user_name": "[USERNAME]"
39+
}
40+
]
41+
},
42+
"vars": {
43+
"object_id": "/apps/${resources.apps.foo.id}"
44+
}
45+
}
1146
}
1247
}
1348
}

0 commit comments

Comments
 (0)