Skip to content

Commit afa9441

Browse files
shirasassoonShira Sassoon
andauthored
Closes #909 Remove defaultazurecredential fallback path (#910)
This pull request introduces a breaking change to the authentication mechanism in `FabricWorkspace`: the default credential fallback has been removed, and an explicit `token_credential` must now be provided for API requests. The codebase and tests have been updated accordingly to enforce this requirement and provide clearer error messaging. **Breaking change to authentication:** * Removed the use of `DefaultAzureCredential` as a fallback; `FabricWorkspace` now requires an explicit `token_credential` to be passed for authentication. Attempting to instantiate without a credential (outside Fabric runtime) raises an `InputError` with a clear message. [[1]](diffhunk://#diff-5cc697c08271c66e9f601e755c17d2ea45267b4a20d48d4091aabbeb721fbf72R1-R8) [[2]](diffhunk://#diff-3bd0f70ed06c7fc7a0f77378aecc6fb108eeb5263161db3b22270f3de4df99c0R106-R109) **Documentation and interface improvements:** * Updated docstrings in `fabric_workspace.py` to clarify the requirement for an explicit `token_credential` and provide usage examples. **Test suite updates:** * All tests creating a `FabricWorkspace` now explicitly provide a `DummyTokenCredential` to comply with the new requirement, ensuring tests remain valid and pass. This affects multiple test files, including `test_publish.py`, `test_fabric_workspace.py`, `test_response_collection.py`, and `test_subfolders.py`. [[1]](diffhunk://#diff-38a69298c91571d1f1a72285d551b8e0ceaebe1ba66bd7bfc7a537620b4bfa3aR125) [[2]](diffhunk://#diff-38a69298c91571d1f1a72285d551b8e0ceaebe1ba66bd7bfc7a537620b4bfa3aR1037) [[3]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R128) [[4]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R153) [[5]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R167) [[6]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R187) [[7]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R201) [[8]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R215) [[9]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R255) [[10]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R299) [[11]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R351) [[12]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R400) [[13]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R450) [[14]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R492) [[15]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R536) [[16]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R576) [[17]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R611) [[18]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R638) [[19]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R676) [[20]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R713) [[21]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R741) [[22]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R771) [[23]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R802) [[24]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R833) [[25]](diffhunk://#diff-5f95c956aa5a4e335e0df13b1416d9448f8889ce7ff8f9806e450bdca98bef87R866) [[26]](diffhunk://#diff-4e3a3effb367a354b8da3e7859fac06554b469f7a08a228c0939925720f38d81R96) [[27]](diffhunk://#diff-a335b8b04c42975e71fe88a9d1aa131d52f55012ab5992e3cf99a9b8dead9d97R13-R14) **Type annotation improvement:** * Updated the `token_credential` parameter type in `FabricWorkspace.__init__` to be explicitly `Optional[TokenCredential]` for better type clarity. --------- Co-authored-by: Shira Sassoon <shirasassoon@microsoft.com>
1 parent aad2cd6 commit afa9441

File tree

6 files changed

+73
-7
lines changed

6 files changed

+73
-7
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
kind: breaking
2+
body: Remove default credential fallback; explicit token credential is now required
3+
time: 2026-03-29T15:07:39.9569937+03:00
4+
custom:
5+
Author: shirasassoon
6+
AuthorLink: https://github.com/shirasassoon
7+
Issue: "909"
8+
IssueLink: https://github.com/microsoft/fabric-cicd/issues/909

src/fabric_cicd/fabric_workspace.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def __init__(
3535
environment: str = "N/A",
3636
workspace_id: Optional[str] = None,
3737
workspace_name: Optional[str] = None,
38-
token_credential: TokenCredential = None,
38+
token_credential: Optional[TokenCredential] = None,
3939
**kwargs,
4040
) -> None:
4141
"""
@@ -47,7 +47,7 @@ def __init__(
4747
repository_directory: Local directory path of the repository where items are to be deployed from.
4848
item_type_in_scope: Item types that should be deployed for a given workspace. If omitted, defaults to all available item types.
4949
environment: The environment to be used for parameterization.
50-
token_credential: The token credential to use for API requests.
50+
token_credential: The token credential to use for API requests (e.g., AzureCliCredential, ClientSecretCredential).
5151
kwargs: Additional keyword arguments.
5252
5353
Examples:
@@ -102,12 +102,11 @@ def __init__(
102102

103103
if token_credential is None:
104104
if _is_fabric_runtime():
105-
token_credential = _generate_fabric_credential()
105+
token_credential = validate_token_credential(_generate_fabric_credential())
106+
logger.debug("Running in Fabric runtime - using generated Fabric credential for authentication.")
106107
else:
107-
# if credential is not defined, use DefaultAzureCredential
108-
from azure.identity import DefaultAzureCredential
109-
110-
token_credential = DefaultAzureCredential()
108+
msg = "A TokenCredential is required to authenticate API requests. Please pass a 'token_credential' (e.g., AzureCliCredential, ClientSecretCredential)."
109+
raise InputError(msg, logger)
111110
else:
112111
token_credential = validate_token_credential(token_credential)
113112

tests/test_fabric_workspace.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import pytest
1111
import yaml
12+
from fixtures.credentials import DummyTokenCredential
1213

1314
from fabric_cicd.fabric_workspace import FabricWorkspace, constants
1415

@@ -120,6 +121,7 @@ def _create_workspace(workspace_id, repository_directory, item_type_in_scope=Non
120121
workspace_id=workspace_id,
121122
repository_directory=repository_directory,
122123
item_type_in_scope=item_type_in_scope,
124+
token_credential=DummyTokenCredential(),
123125
**kwargs,
124126
)
125127
# Call refresh methods to populate workspace data
@@ -1048,6 +1050,32 @@ def test_parameter_file_path_invalid_type_rejected(temp_workspace_dir, patched_f
10481050
assert not workspace.environment_parameter
10491051

10501052

1053+
def test_no_token_credential_outside_fabric_runtime_raises_error(temp_workspace_dir, valid_workspace_id):
1054+
"""Test that constructing FabricWorkspace without token_credential outside Fabric runtime raises InputError."""
1055+
from fabric_cicd._common._exceptions import InputError
1056+
1057+
# Create a simple platform file so directory validation passes
1058+
notebook_dir = temp_workspace_dir / "Test Notebook"
1059+
notebook_dir.mkdir()
1060+
platform_file = notebook_dir / ".platform"
1061+
platform_content = {
1062+
"metadata": {"type": "Notebook", "displayName": "Test Notebook"},
1063+
"config": {"logicalId": "12345678-1234-5678-abcd-1234567890ab"},
1064+
}
1065+
with platform_file.open("w", encoding="utf-8") as f:
1066+
json.dump(platform_content, f)
1067+
1068+
with patch("fabric_cicd.fabric_workspace._is_fabric_runtime", return_value=False):
1069+
with pytest.raises(InputError) as exc_info:
1070+
FabricWorkspace(
1071+
workspace_id=valid_workspace_id,
1072+
repository_directory=str(temp_workspace_dir),
1073+
)
1074+
1075+
assert "TokenCredential is required" in str(exc_info.value)
1076+
assert "token_credential" in str(exc_info.value)
1077+
1078+
10511079
def test_base_api_url_kwarg_raises_error(temp_workspace_dir, valid_workspace_id):
10521080
"""Test that passing base_api_url as kwarg raises an error."""
10531081
from fabric_cicd._common._exceptions import InputError
@@ -1072,6 +1100,7 @@ def test_base_api_url_kwarg_raises_error(temp_workspace_dir, valid_workspace_id)
10721100
workspace_id=valid_workspace_id,
10731101
repository_directory=str(temp_workspace_dir),
10741102
base_api_url="https://custom.api.url",
1103+
token_credential=DummyTokenCredential(),
10751104
)
10761105

10771106
# Verify the error message contains the expected text
@@ -1619,6 +1648,7 @@ def test_mix_of_default_and_non_default_logical_ids(temp_workspace_dir, patched_
16191648
assert workspace.repository_items["Notebook"]["Git Notebook"].logical_id == unique_logical_id
16201649
assert workspace.repository_items["DataPipeline"]["Exported Pipeline"].logical_id == constants.DEFAULT_GUID
16211650

1651+
16221652
def test_publish_variable_library_only_calls_replace_parameters(
16231653
temp_workspace_dir, patched_fabric_workspace, valid_workspace_id
16241654
):

tests/test_publish.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from unittest.mock import MagicMock, patch
1212

1313
import pytest
14+
from fixtures.credentials import DummyTokenCredential
1415

1516
import fabric_cicd.publish as publish
1617
from fabric_cicd import constants
@@ -124,6 +125,7 @@ def test_publish_only_existing_item_types(mock_endpoint, temp_workspace_dir):
124125
workspace = FabricWorkspace(
125126
workspace_id="12345678-1234-5678-abcd-1234567890ab",
126127
repository_directory=str(temp_workspace_dir),
128+
token_credential=DummyTokenCredential(),
127129
)
128130

129131
publish.publish_all_items(workspace)
@@ -148,6 +150,7 @@ def test_default_none_item_type_in_scope_includes_all_types(mock_endpoint, temp_
148150
workspace = FabricWorkspace(
149151
workspace_id="12345678-1234-5678-abcd-1234567890ab",
150152
repository_directory=str(temp_workspace_dir),
153+
token_credential=DummyTokenCredential(),
151154
)
152155

153156
expected_types = list(constants.ACCEPTED_ITEM_TYPES)
@@ -161,6 +164,7 @@ def test_empty_item_type_in_scope_list(mock_endpoint, temp_workspace_dir):
161164
workspace_id="12345678-1234-5678-abcd-1234567890ab",
162165
repository_directory=str(temp_workspace_dir),
163166
item_type_in_scope=[],
167+
token_credential=DummyTokenCredential(),
164168
)
165169
assert workspace.item_type_in_scope == []
166170

@@ -180,6 +184,7 @@ def test_invalid_item_types_in_scope(mock_endpoint, temp_workspace_dir):
180184
workspace_id="12345678-1234-5678-abcd-1234567890ab",
181185
repository_directory=str(temp_workspace_dir),
182186
item_type_in_scope=["InvalidItemType"],
187+
token_credential=DummyTokenCredential(),
183188
)
184189

185190

@@ -193,6 +198,7 @@ def test_multiple_invalid_item_types_in_scope(mock_endpoint, temp_workspace_dir)
193198
workspace_id="12345678-1234-5678-abcd-1234567890ab",
194199
repository_directory=str(temp_workspace_dir),
195200
item_type_in_scope=["FakeType", "AnotherInvalidType"],
201+
token_credential=DummyTokenCredential(),
196202
)
197203

198204

@@ -206,6 +212,7 @@ def test_mixed_valid_and_invalid_item_types_in_scope(mock_endpoint, temp_workspa
206212
workspace_id="12345678-1234-5678-abcd-1234567890ab",
207213
repository_directory=str(temp_workspace_dir),
208214
item_type_in_scope=["Notebook", "BadType", "Environment"],
215+
token_credential=DummyTokenCredential(),
209216
)
210217

211218

@@ -245,6 +252,7 @@ def test_unpublish_feature_flag_warnings(mock_endpoint, temp_workspace_dir, capl
245252
workspace_id="12345678-1234-5678-abcd-1234567890ab",
246253
repository_directory=str(temp_workspace_dir),
247254
item_type_in_scope=["Lakehouse", "Warehouse", "SQLDatabase", "Eventhouse"],
255+
token_credential=DummyTokenCredential(),
248256
)
249257

250258
publish.unpublish_all_orphan_items(workspace)
@@ -288,6 +296,7 @@ def test_unpublish_with_feature_flags_enabled(mock_endpoint, temp_workspace_dir,
288296
workspace_id="12345678-1234-5678-abcd-1234567890ab",
289297
repository_directory=str(temp_workspace_dir),
290298
item_type_in_scope=["Lakehouse"],
299+
token_credential=DummyTokenCredential(),
291300
)
292301

293302
publish.unpublish_all_orphan_items(workspace)
@@ -339,6 +348,7 @@ def track_unpublish(_self, item_name, item_type):
339348
workspace_id="12345678-1234-5678-abcd-1234567890ab",
340349
repository_directory=str(temp_workspace_dir),
341350
item_type_in_scope=["Notebook"],
351+
token_credential=DummyTokenCredential(),
342352
)
343353

344354
publish.unpublish_all_orphan_items(workspace)
@@ -387,6 +397,7 @@ def track_unpublish(_self, item_name, item_type):
387397
workspace_id="12345678-1234-5678-abcd-1234567890ab",
388398
repository_directory=str(temp_workspace_dir),
389399
item_type_in_scope=["Notebook"],
400+
token_credential=DummyTokenCredential(),
390401
)
391402

392403
publish.unpublish_all_orphan_items(workspace, item_name_exclude_regex=r"^Protected.*")
@@ -436,6 +447,7 @@ def track_unpublish(_self, item_name, item_type):
436447
workspace_id="12345678-1234-5678-abcd-1234567890ab",
437448
repository_directory=str(temp_workspace_dir),
438449
item_type_in_scope=["Notebook"],
450+
token_credential=DummyTokenCredential(),
439451
)
440452

441453
publish.unpublish_all_orphan_items(workspace, items_to_include=["TargetOrphan.Notebook"])
@@ -477,6 +489,7 @@ def track_unpublish(_self, item_name, item_type):
477489
workspace_id="12345678-1234-5678-abcd-1234567890ab",
478490
repository_directory=str(temp_workspace_dir),
479491
item_type_in_scope=["Notebook"],
492+
token_credential=DummyTokenCredential(),
480493
)
481494

482495
publish.unpublish_all_orphan_items(workspace)
@@ -520,6 +533,7 @@ def mock_publish_mirroreddatabase():
520533
workspace_id="12345678-1234-5678-abcd-1234567890ab",
521534
repository_directory=str(temp_workspace_dir),
522535
item_type_in_scope=["Lakehouse", "MirroredDatabase"],
536+
token_credential=DummyTokenCredential(),
523537
)
524538

525539
publish.publish_all_items(workspace)
@@ -559,6 +573,7 @@ def test_folder_exclusion_with_regex(mock_endpoint, temp_workspace_dir):
559573
workspace_id="12345678-1234-5678-abcd-1234567890ab",
560574
repository_directory=str(temp_workspace_dir),
561575
item_type_in_scope=["Notebook", "SemanticModel"],
576+
token_credential=DummyTokenCredential(),
562577
)
563578

564579
exclude_regex = r".*legacy.*"
@@ -593,6 +608,7 @@ def test_folder_exclusion_with_anchored_regex(mock_endpoint, temp_workspace_dir)
593608
workspace_id="12345678-1234-5678-abcd-1234567890ab",
594609
repository_directory=str(temp_workspace_dir),
595610
item_type_in_scope=["Notebook"],
611+
token_credential=DummyTokenCredential(),
596612
)
597613

598614
exclude_regex = r"^/legacy$"
@@ -619,6 +635,7 @@ def test_item_name_exclusion_still_works(mock_endpoint, temp_workspace_dir):
619635
workspace_id="12345678-1234-5678-abcd-1234567890ab",
620636
repository_directory=str(temp_workspace_dir),
621637
item_type_in_scope=["Notebook"],
638+
token_credential=DummyTokenCredential(),
622639
)
623640

624641
exclude_regex = r".*DoNotPublish.*"
@@ -656,6 +673,7 @@ def test_folder_inclusion_with_folder_path_to_include(mock_endpoint, temp_worksp
656673
workspace_id="12345678-1234-5678-abcd-1234567890ab",
657674
repository_directory=str(temp_workspace_dir),
658675
item_type_in_scope=["Notebook", "SemanticModel"],
676+
token_credential=DummyTokenCredential(),
659677
)
660678

661679
publish.publish_all_items(
@@ -692,6 +710,7 @@ def test_folder_inclusion_and_exclusion_together(mock_endpoint, temp_workspace_d
692710
workspace_id="12345678-1234-5678-abcd-1234567890ab",
693711
repository_directory=str(temp_workspace_dir),
694712
item_type_in_scope=["Notebook"],
713+
token_credential=DummyTokenCredential(),
695714
)
696715

697716
with pytest.raises(
@@ -719,6 +738,7 @@ def test_empty_folder_path_to_include_raises_error(mock_endpoint, temp_workspace
719738
workspace_id="12345678-1234-5678-abcd-1234567890ab",
720739
repository_directory=str(temp_workspace_dir),
721740
item_type_in_scope=["Notebook"],
741+
token_credential=DummyTokenCredential(),
722742
)
723743

724744
with pytest.raises(InputError, match="folder_path_to_include must not be an empty list"):
@@ -748,6 +768,7 @@ def test_folder_exclusion_with_items_to_include(mock_endpoint, temp_workspace_di
748768
workspace_id="12345678-1234-5678-abcd-1234567890ab",
749769
repository_directory=str(temp_workspace_dir),
750770
item_type_in_scope=["Notebook"],
771+
token_credential=DummyTokenCredential(),
751772
)
752773

753774
publish.publish_all_items(
@@ -778,6 +799,7 @@ def test_folder_inclusion_with_item_exclusion(mock_endpoint, temp_workspace_dir)
778799
workspace_id="12345678-1234-5678-abcd-1234567890ab",
779800
repository_directory=str(temp_workspace_dir),
780801
item_type_in_scope=["Notebook"],
802+
token_credential=DummyTokenCredential(),
781803
)
782804

783805
publish.publish_all_items(
@@ -808,6 +830,7 @@ def test_folder_inclusion_with_items_to_include(mock_endpoint, temp_workspace_di
808830
workspace_id="12345678-1234-5678-abcd-1234567890ab",
809831
repository_directory=str(temp_workspace_dir),
810832
item_type_in_scope=["Notebook"],
833+
token_credential=DummyTokenCredential(),
811834
)
812835

813836
publish.publish_all_items(
@@ -840,6 +863,7 @@ def test_all_filters_combined(mock_endpoint, temp_workspace_dir):
840863
workspace_id="12345678-1234-5678-abcd-1234567890ab",
841864
repository_directory=str(temp_workspace_dir),
842865
item_type_in_scope=["Notebook"],
866+
token_credential=DummyTokenCredential(),
843867
)
844868

845869
publish.publish_all_items(

tests/test_response_collection.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from unittest.mock import MagicMock, patch
1010

1111
import pytest
12+
from fixtures.credentials import DummyTokenCredential
1213

1314
import fabric_cicd.constants as constants
1415
import fabric_cicd.publish as publish
@@ -91,6 +92,7 @@ def test_workspace_with_notebook(mock_endpoint):
9192
workspace_id="12345678-1234-5678-abcd-1234567890ab",
9293
repository_directory=str(temp_path),
9394
item_type_in_scope=["Notebook"],
95+
token_credential=DummyTokenCredential(),
9496
)
9597
# Manually set up repository items since we're patching the refresh methods
9698
workspace.repository_items = {

tests/test_subfolders.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from unittest.mock import MagicMock, patch
1010

1111
import pytest
12+
from fixtures.credentials import DummyTokenCredential
1213

1314
from fabric_cicd.fabric_workspace import FabricWorkspace
1415

@@ -110,6 +111,7 @@ def _create_workspace(workspace_id, repository_directory, item_type_in_scope, **
110111
workspace_id=workspace_id,
111112
repository_directory=repository_directory,
112113
item_type_in_scope=item_type_in_scope,
114+
token_credential=DummyTokenCredential(),
113115
**kwargs,
114116
)
115117

@@ -268,6 +270,7 @@ def mock_invoke_side_effect(method, url, **_kwargs):
268270
workspace_id=valid_workspace_id,
269271
repository_directory=str(repository_with_subfolders),
270272
item_type_in_scope=["Notebook", "DataPipeline"],
273+
token_credential=DummyTokenCredential(),
271274
)
272275

273276
# Call methods in the intended order to populate folder structures

0 commit comments

Comments
 (0)