Skip to content

Commit 5773358

Browse files
Added permission migration support for feature tables and the root permissions for models and feature tables (#997)
1 parent 3942069 commit 5773358

File tree

6 files changed

+213
-1
lines changed

6 files changed

+213
-1
lines changed

src/databricks/labs/ucx/mixins/fixtures.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,12 @@ def _path(ws, path):
283283
[PermissionLevel.CAN_VIEW, PermissionLevel.CAN_MANAGE],
284284
_simple,
285285
),
286+
(
287+
"feature_table",
288+
"feature-tables",
289+
[PermissionLevel.CAN_VIEW_METADATA, PermissionLevel.CAN_EDIT_METADATA, PermissionLevel.CAN_MANAGE],
290+
_simple,
291+
),
286292
]
287293

288294

@@ -1126,3 +1132,23 @@ def remove(endpoint_name: str):
11261132
logger.info(f"Can't remove endpoint {e}")
11271133

11281134
yield from factory("Serving endpoint", create, remove)
1135+
1136+
1137+
@pytest.fixture
1138+
def make_feature_table(ws, make_random):
1139+
def create():
1140+
feature_table_name = make_random(6) + "." + make_random(6)
1141+
table = ws.api_client.do(
1142+
"POST",
1143+
"/api/2.0/feature-store/feature-tables/create",
1144+
body={"name": feature_table_name, "primary_keys": [{"name": "pk", "data_type": "string"}]},
1145+
)
1146+
return table['feature_table']
1147+
1148+
def remove(table: dict):
1149+
try:
1150+
ws.api_client.do("DELETE", "/api/2.0/feature-store/feature-tables/delete", body={"name": table["name"]})
1151+
except RuntimeError as e:
1152+
logger.info(f"Can't remove feature table {e}")
1153+
1154+
yield from factory("Feature table", create, remove)

src/databricks/labs/ucx/workspace_access/generic.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,34 @@ def inner() -> Iterator[ml.Experiment]:
410410
return inner
411411

412412

413+
def feature_store_listing(ws: WorkspaceClient):
414+
def inner() -> list[GenericPermissionsInfo]:
415+
feature_tables = []
416+
token = None
417+
while True:
418+
result = ws.api_client.do(
419+
"GET", "/api/2.0/feature-store/feature-tables/search", query={"page_token": token, "max_results": 200}
420+
)
421+
for table in result.get("feature_tables", []):
422+
feature_tables.append(GenericPermissionsInfo(table["id"], "feature-tables"))
423+
424+
if "next_page_token" not in result:
425+
break
426+
token = result["next_page_token"] # type: ignore[index]
427+
428+
return feature_tables
429+
430+
return inner
431+
432+
433+
def feature_tables_root_page():
434+
return [GenericPermissionsInfo("/root", "feature-tables")]
435+
436+
437+
def models_root_page():
438+
return [GenericPermissionsInfo("/root", "registered-models")]
439+
440+
413441
def tokens_and_passwords():
414442
for _value in ("tokens", "passwords"):
415443
yield GenericPermissionsInfo(_value, "authorization")

src/databricks/labs/ucx/workspace_access/manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ def factory(
5454
generic.Listing(ws.serving_endpoints.list, "id", "serving-endpoints"),
5555
generic.Listing(generic.experiments_listing(ws), "experiment_id", "experiments"),
5656
generic.Listing(generic.models_listing(ws, num_threads), "id", "registered-models"),
57+
generic.Listing(generic.models_root_page, "object_id", "registered-models"),
5758
generic.Listing(generic.tokens_and_passwords, "object_id", "authorization"),
59+
generic.Listing(generic.feature_store_listing(ws), "object_id", "feature-tables"),
60+
generic.Listing(generic.feature_tables_root_page, "object_id", "feature-tables"),
5861
generic.WorkspaceListing(
5962
ws,
6063
sql_backend=sql_backend,

tests/integration/workspace_access/test_generic.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import json
22
from datetime import timedelta
33

4+
from databricks.sdk import WorkspaceClient
45
from databricks.sdk.errors import BadRequest, NotFound
56
from databricks.sdk.retries import retried
67
from databricks.sdk.service import iam
7-
from databricks.sdk.service.iam import PermissionLevel
8+
from databricks.sdk.service.iam import AccessControlRequest, PermissionLevel
89

910
from databricks.labs.ucx.workspace_access.base import Permissions
1011
from databricks.labs.ucx.workspace_access.generic import (
1112
GenericPermissionsSupport,
1213
Listing,
1314
WorkspaceListing,
1415
experiments_listing,
16+
feature_store_listing,
17+
feature_tables_root_page,
1518
models_listing,
19+
models_root_page,
1620
tokens_and_passwords,
1721
)
1822
from databricks.labs.ucx.workspace_access.groups import MigratedGroup
@@ -439,3 +443,86 @@ def test_endpoints(
439443

440444
after = generic_permissions.load_as_dict("serving-endpoints", endpoint.response.id)
441445
assert after[group_b.display_name] == PermissionLevel.CAN_MANAGE
446+
447+
448+
def test_feature_tables(ws: WorkspaceClient, make_feature_table, make_group, make_feature_table_permissions):
449+
group_a = make_group()
450+
group_b = make_group()
451+
feature_table = make_feature_table()
452+
make_feature_table_permissions(
453+
object_id=feature_table["id"],
454+
permission_level=PermissionLevel.CAN_EDIT_METADATA,
455+
group_name=group_a.display_name,
456+
)
457+
458+
generic_permissions = GenericPermissionsSupport(
459+
ws, [Listing(feature_store_listing(ws), "object_id", "feature-tables")]
460+
)
461+
before = generic_permissions.load_as_dict("feature-tables", feature_table["id"])
462+
assert before[group_a.display_name] == PermissionLevel.CAN_EDIT_METADATA
463+
464+
apply_tasks(
465+
generic_permissions,
466+
[
467+
MigratedGroup.partial_info(group_a, group_b),
468+
],
469+
)
470+
471+
after = generic_permissions.load_as_dict("feature-tables", feature_table["id"])
472+
assert after[group_b.display_name] == PermissionLevel.CAN_EDIT_METADATA
473+
474+
475+
def test_feature_store_root_page(ws: WorkspaceClient, make_group):
476+
group_a = make_group()
477+
group_b = make_group()
478+
ws.permissions.update(
479+
"feature-tables",
480+
"/root",
481+
access_control_list=[
482+
AccessControlRequest(group_name=group_a.display_name, permission_level=PermissionLevel.CAN_EDIT_METADATA)
483+
],
484+
)
485+
486+
generic_permissions = GenericPermissionsSupport(
487+
ws, [Listing(feature_tables_root_page, "object_id", "feature-tables")]
488+
)
489+
before = generic_permissions.load_as_dict("feature-tables", "/root")
490+
assert before[group_a.display_name] == PermissionLevel.CAN_EDIT_METADATA
491+
492+
apply_tasks(
493+
generic_permissions,
494+
[
495+
MigratedGroup.partial_info(group_a, group_b),
496+
],
497+
)
498+
499+
after = generic_permissions.load_as_dict("feature-tables", "/root")
500+
assert after[group_b.display_name] == PermissionLevel.CAN_EDIT_METADATA
501+
502+
503+
def test_models_root_page(ws: WorkspaceClient, make_group):
504+
group_a = make_group()
505+
group_b = make_group()
506+
ws.permissions.update(
507+
"registered-models",
508+
"/root",
509+
access_control_list=[
510+
AccessControlRequest(
511+
group_name=group_a.display_name, permission_level=PermissionLevel.CAN_MANAGE_PRODUCTION_VERSIONS
512+
)
513+
],
514+
)
515+
516+
generic_permissions = GenericPermissionsSupport(ws, [Listing(models_root_page, "object_id", "registered-models")])
517+
before = generic_permissions.load_as_dict("registered-models", "/root")
518+
assert before[group_a.display_name] == PermissionLevel.CAN_MANAGE_PRODUCTION_VERSIONS
519+
520+
apply_tasks(
521+
generic_permissions,
522+
[
523+
MigratedGroup.partial_info(group_a, group_b),
524+
],
525+
)
526+
527+
after = generic_permissions.load_as_dict("registered-models", "/root")
528+
assert after[group_b.display_name] == PermissionLevel.CAN_MANAGE_PRODUCTION_VERSIONS

tests/unit/workspace_access/test_generic.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
WorkspaceListing,
2727
WorkspaceObjectInfo,
2828
experiments_listing,
29+
feature_store_listing,
30+
feature_tables_root_page,
2931
models_listing,
32+
models_root_page,
3033
tokens_and_passwords,
3134
)
3235
from databricks.labs.ucx.workspace_access.groups import MigrationState
@@ -849,3 +852,67 @@ def test_verify_task_should_fail_if_acls_missing():
849852

850853
with pytest.raises(ValueError):
851854
sup.get_verify_task(item)
855+
856+
857+
def test_feature_tables_listing():
858+
ws = MagicMock()
859+
860+
def do_api_side_effect(*_, query):
861+
if not query["page_token"]:
862+
return {"feature_tables": [{"id": "table1"}, {"id": "table2"}], "next_page_token": "token"}
863+
return {"feature_tables": [{"id": "table3"}, {"id": "table4"}]}
864+
865+
ws.api_client.do.side_effect = do_api_side_effect
866+
867+
wrapped = Listing(feature_store_listing(ws), id_attribute="object_id", object_type="feature-tables")
868+
result = list(wrapped)
869+
870+
assert len(result) == 4
871+
assert result[0].object_id == "table1"
872+
assert result[0].request_type == "feature-tables"
873+
874+
875+
def test_root_page_listing():
876+
ws = MagicMock()
877+
878+
basic_acl = [
879+
iam.AccessControlResponse(
880+
group_name="test",
881+
all_permissions=[iam.Permission(inherited=False, permission_level=iam.PermissionLevel.CAN_EDIT_METADATA)],
882+
)
883+
]
884+
885+
ws.permissions.get.side_effect = [
886+
iam.ObjectPermissions(object_id="/root", object_type="feature-tables", access_control_list=basic_acl),
887+
]
888+
889+
sup = GenericPermissionsSupport(ws=ws, listings=[Listing(feature_tables_root_page, "object_id", "feature-tables")])
890+
tasks = list(sup.get_crawler_tasks())
891+
assert len(tasks) == 1
892+
auth_items = [task() for task in tasks]
893+
for item in auth_items:
894+
assert item.object_id == "/root"
895+
assert item.object_type == "feature-tables"
896+
897+
898+
def test_models_page_listing():
899+
ws = MagicMock()
900+
901+
basic_acl = [
902+
iam.AccessControlResponse(
903+
group_name="test",
904+
all_permissions=[iam.Permission(inherited=False, permission_level=iam.PermissionLevel.CAN_EDIT_METADATA)],
905+
)
906+
]
907+
908+
ws.permissions.get.side_effect = [
909+
iam.ObjectPermissions(object_id="/root", object_type="registered-models", access_control_list=basic_acl),
910+
]
911+
912+
sup = GenericPermissionsSupport(ws=ws, listings=[Listing(models_root_page, "object_id", "registered-models")])
913+
tasks = list(sup.get_crawler_tasks())
914+
assert len(tasks) == 1
915+
auth_items = [task() for task in tasks]
916+
for item in auth_items:
917+
assert item.object_id == "/root"
918+
assert item.object_type == "registered-models"

tests/unit/workspace_access/test_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ def test_factory(mocker):
215215
"entitlements",
216216
"roles",
217217
'serving-endpoints',
218+
"feature-tables",
218219
"ANY FILE",
219220
"FUNCTION",
220221
"ANONYMOUS FUNCTION",

0 commit comments

Comments
 (0)