Skip to content

Commit 235812a

Browse files
committed
Added make_run_as fixture
1 parent f93056b commit 235812a

File tree

9 files changed

+154
-16
lines changed

9 files changed

+154
-16
lines changed

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ def test_something(env_or_skip):
255255
assert token is not None
256256
```
257257

258-
See also [`acc`](#acc-fixture), [`make_udf`](#make_udf-fixture), [`sql_backend`](#sql_backend-fixture), [`debug_env`](#debug_env-fixture), [`is_in_debug`](#is_in_debug-fixture).
258+
See also [`acc`](#acc-fixture), [`make_run_as`](#make_run_as-fixture), [`make_udf`](#make_udf-fixture), [`sql_backend`](#sql_backend-fixture), [`debug_env`](#debug_env-fixture), [`is_in_debug`](#is_in_debug-fixture).
259259

260260

261261
[[back to top](#python-testing-for-databricks)]
@@ -278,7 +278,7 @@ def test_workspace_operations(ws):
278278
assert len(clusters) >= 0
279279
```
280280

281-
See also [`log_workspace_link`](#log_workspace_link-fixture), [`make_alert_permissions`](#make_alert_permissions-fixture), [`make_authorization_permissions`](#make_authorization_permissions-fixture), [`make_catalog`](#make_catalog-fixture), [`make_cluster`](#make_cluster-fixture), [`make_cluster_permissions`](#make_cluster_permissions-fixture), [`make_cluster_policy`](#make_cluster_policy-fixture), [`make_cluster_policy_permissions`](#make_cluster_policy_permissions-fixture), [`make_dashboard_permissions`](#make_dashboard_permissions-fixture), [`make_directory`](#make_directory-fixture), [`make_directory_permissions`](#make_directory_permissions-fixture), [`make_experiment`](#make_experiment-fixture), [`make_experiment_permissions`](#make_experiment_permissions-fixture), [`make_feature_table`](#make_feature_table-fixture), [`make_feature_table_permissions`](#make_feature_table_permissions-fixture), [`make_group`](#make_group-fixture), [`make_instance_pool`](#make_instance_pool-fixture), [`make_instance_pool_permissions`](#make_instance_pool_permissions-fixture), [`make_job`](#make_job-fixture), [`make_job_permissions`](#make_job_permissions-fixture), [`make_lakeview_dashboard_permissions`](#make_lakeview_dashboard_permissions-fixture), [`make_model`](#make_model-fixture), [`make_notebook`](#make_notebook-fixture), [`make_notebook_permissions`](#make_notebook_permissions-fixture), [`make_pipeline`](#make_pipeline-fixture), [`make_pipeline_permissions`](#make_pipeline_permissions-fixture), [`make_query`](#make_query-fixture), [`make_query_permissions`](#make_query_permissions-fixture), [`make_registered_model_permissions`](#make_registered_model_permissions-fixture), [`make_repo`](#make_repo-fixture), [`make_repo_permissions`](#make_repo_permissions-fixture), [`make_secret_scope`](#make_secret_scope-fixture), [`make_secret_scope_acl`](#make_secret_scope_acl-fixture), [`make_serving_endpoint`](#make_serving_endpoint-fixture), [`make_serving_endpoint_permissions`](#make_serving_endpoint_permissions-fixture), [`make_storage_credential`](#make_storage_credential-fixture), [`make_udf`](#make_udf-fixture), [`make_user`](#make_user-fixture), [`make_volume`](#make_volume-fixture), [`make_warehouse`](#make_warehouse-fixture), [`make_warehouse_permissions`](#make_warehouse_permissions-fixture), [`make_workspace_file`](#make_workspace_file-fixture), [`make_workspace_file_path_permissions`](#make_workspace_file_path_permissions-fixture), [`make_workspace_file_permissions`](#make_workspace_file_permissions-fixture), [`spark`](#spark-fixture), [`sql_backend`](#sql_backend-fixture), [`debug_env`](#debug_env-fixture), [`product_info`](#product_info-fixture).
281+
See also [`log_workspace_link`](#log_workspace_link-fixture), [`make_alert_permissions`](#make_alert_permissions-fixture), [`make_authorization_permissions`](#make_authorization_permissions-fixture), [`make_catalog`](#make_catalog-fixture), [`make_cluster`](#make_cluster-fixture), [`make_cluster_permissions`](#make_cluster_permissions-fixture), [`make_cluster_policy`](#make_cluster_policy-fixture), [`make_cluster_policy_permissions`](#make_cluster_policy_permissions-fixture), [`make_dashboard_permissions`](#make_dashboard_permissions-fixture), [`make_directory`](#make_directory-fixture), [`make_directory_permissions`](#make_directory_permissions-fixture), [`make_experiment`](#make_experiment-fixture), [`make_experiment_permissions`](#make_experiment_permissions-fixture), [`make_feature_table`](#make_feature_table-fixture), [`make_feature_table_permissions`](#make_feature_table_permissions-fixture), [`make_group`](#make_group-fixture), [`make_instance_pool`](#make_instance_pool-fixture), [`make_instance_pool_permissions`](#make_instance_pool_permissions-fixture), [`make_job`](#make_job-fixture), [`make_job_permissions`](#make_job_permissions-fixture), [`make_lakeview_dashboard_permissions`](#make_lakeview_dashboard_permissions-fixture), [`make_model`](#make_model-fixture), [`make_notebook`](#make_notebook-fixture), [`make_notebook_permissions`](#make_notebook_permissions-fixture), [`make_pipeline`](#make_pipeline-fixture), [`make_pipeline_permissions`](#make_pipeline_permissions-fixture), [`make_query`](#make_query-fixture), [`make_query_permissions`](#make_query_permissions-fixture), [`make_registered_model_permissions`](#make_registered_model_permissions-fixture), [`make_repo`](#make_repo-fixture), [`make_repo_permissions`](#make_repo_permissions-fixture), [`make_run_as`](#make_run_as-fixture), [`make_secret_scope`](#make_secret_scope-fixture), [`make_secret_scope_acl`](#make_secret_scope_acl-fixture), [`make_serving_endpoint`](#make_serving_endpoint-fixture), [`make_serving_endpoint_permissions`](#make_serving_endpoint_permissions-fixture), [`make_storage_credential`](#make_storage_credential-fixture), [`make_udf`](#make_udf-fixture), [`make_user`](#make_user-fixture), [`make_volume`](#make_volume-fixture), [`make_warehouse`](#make_warehouse-fixture), [`make_warehouse_permissions`](#make_warehouse_permissions-fixture), [`make_workspace_file`](#make_workspace_file-fixture), [`make_workspace_file_path_permissions`](#make_workspace_file_path_permissions-fixture), [`make_workspace_file_permissions`](#make_workspace_file_permissions-fixture), [`spark`](#spark-fixture), [`sql_backend`](#sql_backend-fixture), [`debug_env`](#debug_env-fixture), [`product_info`](#product_info-fixture).
282282

283283

284284
[[back to top](#python-testing-for-databricks)]
@@ -305,7 +305,7 @@ def test_listing_workspaces(acc):
305305
assert len(workspaces) >= 1
306306
```
307307

308-
See also [`make_acc_group`](#make_acc_group-fixture), [`debug_env`](#debug_env-fixture), [`product_info`](#product_info-fixture), [`env_or_skip`](#env_or_skip-fixture).
308+
See also [`make_acc_group`](#make_acc_group-fixture), [`make_run_as`](#make_run_as-fixture), [`debug_env`](#debug_env-fixture), [`product_info`](#product_info-fixture), [`env_or_skip`](#env_or_skip-fixture).
309309

310310

311311
[[back to top](#python-testing-for-databricks)]
@@ -349,6 +349,14 @@ Fetch all rows from a SQL statement.
349349
See also [`sql_backend`](#sql_backend-fixture).
350350

351351

352+
[[back to top](#python-testing-for-databricks)]
353+
354+
### `make_run_as` fixture
355+
_No description yet._
356+
357+
See also [`acc`](#acc-fixture), [`ws`](#ws-fixture), [`make_random`](#make_random-fixture), [`env_or_skip`](#env_or_skip-fixture).
358+
359+
352360
[[back to top](#python-testing-for-databricks)]
353361

354362
### `make_random` fixture
@@ -372,7 +380,7 @@ random_string = make_random(k=8)
372380
assert len(random_string) == 8
373381
```
374382

375-
See also [`make_acc_group`](#make_acc_group-fixture), [`make_catalog`](#make_catalog-fixture), [`make_cluster`](#make_cluster-fixture), [`make_cluster_policy`](#make_cluster_policy-fixture), [`make_directory`](#make_directory-fixture), [`make_experiment`](#make_experiment-fixture), [`make_feature_table`](#make_feature_table-fixture), [`make_group`](#make_group-fixture), [`make_instance_pool`](#make_instance_pool-fixture), [`make_job`](#make_job-fixture), [`make_model`](#make_model-fixture), [`make_notebook`](#make_notebook-fixture), [`make_pipeline`](#make_pipeline-fixture), [`make_query`](#make_query-fixture), [`make_repo`](#make_repo-fixture), [`make_schema`](#make_schema-fixture), [`make_secret_scope`](#make_secret_scope-fixture), [`make_serving_endpoint`](#make_serving_endpoint-fixture), [`make_table`](#make_table-fixture), [`make_udf`](#make_udf-fixture), [`make_user`](#make_user-fixture), [`make_volume`](#make_volume-fixture), [`make_warehouse`](#make_warehouse-fixture), [`make_workspace_file`](#make_workspace_file-fixture).
383+
See also [`make_acc_group`](#make_acc_group-fixture), [`make_catalog`](#make_catalog-fixture), [`make_cluster`](#make_cluster-fixture), [`make_cluster_policy`](#make_cluster_policy-fixture), [`make_directory`](#make_directory-fixture), [`make_experiment`](#make_experiment-fixture), [`make_feature_table`](#make_feature_table-fixture), [`make_group`](#make_group-fixture), [`make_instance_pool`](#make_instance_pool-fixture), [`make_job`](#make_job-fixture), [`make_model`](#make_model-fixture), [`make_notebook`](#make_notebook-fixture), [`make_pipeline`](#make_pipeline-fixture), [`make_query`](#make_query-fixture), [`make_repo`](#make_repo-fixture), [`make_run_as`](#make_run_as-fixture), [`make_schema`](#make_schema-fixture), [`make_secret_scope`](#make_secret_scope-fixture), [`make_serving_endpoint`](#make_serving_endpoint-fixture), [`make_table`](#make_table-fixture), [`make_udf`](#make_udf-fixture), [`make_user`](#make_user-fixture), [`make_volume`](#make_volume-fixture), [`make_warehouse`](#make_warehouse-fixture), [`make_workspace_file`](#make_workspace_file-fixture).
376384

377385

378386
[[back to top](#python-testing-for-databricks)]

scripts/gen-readme.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def discover_fixtures() -> list[Fixture]:
7070
upstreams = []
7171
sig = inspect.signature(fn)
7272
for param in sig.parameters.values():
73-
if param.name in {'fresh_local_wheel_file', 'monkeypatch', 'log_workspace_link'}:
73+
if param.name in {'fresh_local_wheel_file', 'monkeypatch', 'log_workspace_link', 'request'}:
7474
continue
7575
upstreams.append(param.name)
7676
see_also[param.name].add(fixture)

src/databricks/labs/pytester/fixtures/baseline.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,15 @@ def inner(name: str, path: str, *, anchor: bool = True):
222222
_LOG.info(f'Created {name}: {url}')
223223

224224
return inner
225+
226+
227+
@fixture
228+
def log_account_link(acc):
229+
"""Returns a function to log an account link."""
230+
231+
def inner(name: str, path: str, *, anchor: bool = False):
232+
a = '#' if anchor else ''
233+
url = f'https://{acc.config.hostname}/{a}{path}'
234+
_LOG.info(f'Created {name}: {url}')
235+
236+
return inner

src/databricks/labs/pytester/fixtures/catalog.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
logger = logging.getLogger(__name__)
2626

2727

28+
# TODO: replace with LSQL implementation
2829
def escape_sql_identifier(path: str, *, maxsplit: int = 2) -> str:
2930
"""
3031
Escapes the path components to make them SQL safe.

src/databricks/labs/pytester/fixtures/iam.py

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
from collections.abc import Callable, Generator
44
from datetime import timedelta
55

6+
from _pytest.fixtures import SubRequest, FixtureDef, fixture
7+
from _pytest.scope import Scope
8+
69
from pytest import fixture
7-
from databricks.sdk import AccountGroupsAPI, GroupsAPI, WorkspaceClient
10+
from databricks.sdk import AccountGroupsAPI, GroupsAPI, WorkspaceClient, AccountClient
811
from databricks.sdk.config import Config
912
from databricks.sdk.errors import ResourceConflict, NotFound
1013
from databricks.sdk.retries import retried
1114
from databricks.sdk.service import iam
12-
from databricks.sdk.service.iam import User, Group
15+
from databricks.sdk.service.iam import User, Group, ServicePrincipal, Patch, PatchOp, ComplexValue, PatchSchema, \
16+
WorkspacePermission
1317

1418
from databricks.labs.pytester.fixtures.baseline import factory
1519

@@ -183,3 +187,86 @@ def create(
183187
return group
184188

185189
yield from factory(name, create, lambda item: interface.delete(item.id))
190+
191+
192+
class RunAs:
193+
def __init__(self, service_principal: ServicePrincipal, workspace_client: WorkspaceClient, request: SubRequest):
194+
self._request = SubRequest(
195+
request,
196+
Scope.Function,
197+
None,
198+
request.param_index,
199+
FixtureDef(
200+
config=request.config,
201+
baseid='ws',
202+
argname='ws',
203+
func=lambda: workspace_client,
204+
scope=None,
205+
params=None,
206+
),
207+
)
208+
self._request._arg2fixturedefs = {}
209+
self._request._fixture_defs = {}
210+
self._service_principal = service_principal
211+
212+
def __getattr__(self, item: str):
213+
if item in self.__dict__:
214+
return self.__dict__[item]
215+
fixture_value = self._request.getfixturevalue(item)
216+
return fixture_value
217+
218+
@property
219+
def display_name(self) -> str:
220+
return self._service_principal.display_name
221+
222+
@property
223+
def application_id(self) -> str:
224+
return self._service_principal.application_id
225+
226+
def __repr__(self):
227+
return f'RunAs({self.display_name})'
228+
229+
230+
@fixture
231+
def make_run_as(acc: AccountClient, ws: WorkspaceClient, make_random, env_or_skip, request, log_account_link):
232+
"""
233+
This fixture provides a function to create a service principal and assign it to a workspace. The service principal
234+
is removed after the test is complete. The fixture returns an instance of `RunAs` that proxies the calls to
235+
the other fixtures supported by the plugin, for example, `sql_fetch_all`.
236+
237+
The service principal is created with a random display name and assigned to the workspace with
238+
"""
239+
240+
def create(*, account_groups: list[str] = None):
241+
workspace_id = ws.get_workspace_id()
242+
service_principal = acc.service_principals.create(display_name=f'spn-{make_random()}')
243+
created_secret = acc.service_principal_secrets.create(service_principal.id)
244+
if account_groups:
245+
group_mapping = {}
246+
for group in acc.groups.list(attributes='id,displayName'):
247+
group_mapping[group.display_name] = group.id
248+
for group_name in account_groups:
249+
if group_name not in group_mapping:
250+
raise ValueError(f'Group {group_name} does not exist')
251+
group_id = group_mapping[group_name]
252+
acc.groups.patch(
253+
group_id,
254+
operations=[Patch(PatchOp.ADD, 'members', [ComplexValue(value=service_principal.id).as_dict()])],
255+
schemas=[PatchSchema.URN_IETF_PARAMS_SCIM_API_MESSAGES_2_0_PATCH_OP],
256+
)
257+
permissions = [WorkspacePermission.USER]
258+
acc.workspace_assignment.update(workspace_id, service_principal.id, permissions=permissions)
259+
ws_as_spn = WorkspaceClient(
260+
host=ws.config.host,
261+
client_id=service_principal.application_id,
262+
client_secret=created_secret.secret,
263+
)
264+
265+
log_account_link('account service principal', f'users/serviceprincipals/{service_principal.id}')
266+
267+
return RunAs(service_principal, ws_as_spn, request)
268+
269+
def remove(run_as: RunAs):
270+
acc.service_principals.delete(run_as._service_principal.id)
271+
272+
yield from factory("service principal", create, remove)

src/databricks/labs/pytester/fixtures/notebooks.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,18 @@ def create(
5656
default_content = "SELECT 1"
5757
else:
5858
raise ValueError(f"Unsupported language: {language}")
59-
path = path or f"/Users/{ws.current_user.me().user_name}/dummy-{make_random(4)}-{watchdog_purge_suffix}"
59+
current_user = ws.current_user.me()
60+
path = path or f"/Users/{current_user.user_name}/dummy-{make_random(4)}-{watchdog_purge_suffix}"
61+
workspace_path = WorkspacePath(ws, path)
62+
if '@' not in current_user.user_name:
63+
# If current user is a service principal added with `make_run_as`, there might be no home folder
64+
workspace_path.parent.mkdir(exist_ok=True)
6065
content = content or default_content
6166
if isinstance(content, str):
6267
content = io.BytesIO(content.encode(encoding))
6368
if isinstance(ws, Mock): # For testing
6469
ws.workspace.download.return_value = content if isinstance(content, io.BytesIO) else io.BytesIO(content)
6570
ws.workspace.upload(path, content, language=language, format=format, overwrite=overwrite)
66-
workspace_path = WorkspacePath(ws, path)
6771
logger.info(f"Created notebook: {workspace_path.as_uri()}")
6872
return workspace_path
6973

@@ -110,10 +114,14 @@ def create(
110114
suffix = ".sql"
111115
else:
112116
raise ValueError(f"Unsupported language: {language}")
113-
path = path or f"/Users/{ws.current_user.me().user_name}/dummy-{make_random(4)}-{watchdog_purge_suffix}{suffix}"
117+
current_user = ws.current_user.me()
118+
path = path or f"/Users/{current_user.user_name}/dummy-{make_random(4)}-{watchdog_purge_suffix}{suffix}"
114119
content = content or default_content
115120
encoding = encoding or _DEFAULT_ENCODING
116121
workspace_path = WorkspacePath(ws, path)
122+
if '@' not in current_user.user_name:
123+
# If current user is a service principal added with `make_run_as`, there might be no home folder
124+
workspace_path.parent.mkdir(exist_ok=True)
117125
if isinstance(content, bytes):
118126
workspace_path.write_bytes(content)
119127
else:

src/databricks/labs/pytester/fixtures/plugin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
make_random,
77
product_info,
88
log_workspace_link,
9+
log_account_link,
910
)
1011
from databricks.labs.pytester.fixtures.sql import sql_backend, sql_exec, sql_fetch_all
1112
from databricks.labs.pytester.fixtures.compute import (
@@ -16,7 +17,7 @@
1617
make_pipeline,
1718
make_warehouse,
1819
)
19-
from databricks.labs.pytester.fixtures.iam import make_group, make_acc_group, make_user
20+
from databricks.labs.pytester.fixtures.iam import make_group, make_acc_group, make_user, make_run_as
2021
from databricks.labs.pytester.fixtures.catalog import (
2122
make_udf,
2223
make_catalog,
@@ -105,6 +106,7 @@
105106
'make_warehouse_permissions',
106107
'make_lakeview_dashboard_permissions',
107108
'log_workspace_link',
109+
'log_account_link',
108110
'make_dashboard_permissions',
109111
'make_alert_permissions',
110112
'make_query',
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
def test_run_as(make_run_as, ws):
2+
run_as = make_run_as(account_groups=['role.labs.lsql.write'])
3+
through_query = next(run_as.sql_fetch_all("SELECT CURRENT_USER() AS my_name"))
4+
me = ws.current_user.me()
5+
assert me.user_name != through_query.my_name
6+
7+
8+
def test_notebooks_are_created_by_different_users(make_run_as, make_notebook):
9+
notebook_by_current_user = make_notebook()
10+
a = notebook_by_current_user.parent.as_posix()
11+
12+
run_as = make_run_as(account_groups=['role.labs.lsql.write'])
13+
notebook_by_ephemeral_principal = run_as.make_notebook()
14+
b = notebook_by_ephemeral_principal.parent.as_posix()
15+
16+
assert a != b

tests/unit/fixtures/test_catalog.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
from unittest.mock import ANY
22

3-
from databricks.sdk.service.catalog import TableInfo, TableType, DataSourceFormat, FunctionInfo, SchemaInfo, VolumeType, VolumeInfo
3+
from databricks.sdk.service.catalog import (
4+
TableInfo,
5+
TableType,
6+
DataSourceFormat,
7+
FunctionInfo,
8+
SchemaInfo,
9+
VolumeType,
10+
)
411

512
from databricks.labs.pytester.fixtures.unwrap import call_stateful
613
from databricks.labs.pytester.fixtures.catalog import (
@@ -169,9 +176,6 @@ def test_make_volume_noargs():
169176
def test_make_volume_with_name():
170177
ctx, info = call_stateful(make_volume, name='test_volume')
171178
ctx['ws'].volumes.create.assert_called_once_with(
172-
name='test_volume',
173-
catalog_name="dummy_crandom",
174-
schema_name="dummy_srandom",
175-
volume_type=VolumeType.MANAGED
179+
name='test_volume', catalog_name="dummy_crandom", schema_name="dummy_srandom", volume_type=VolumeType.MANAGED
176180
)
177181
assert info is not None

0 commit comments

Comments
 (0)