Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies = [
"msal>=1.34,<2",
"msal_extensions",
"azure-core>=1.29.0",
"azure-identity>=1.15.0",
"questionary",
"prompt_toolkit>=3.0.41",
"cachetools>=5.5.0",
Expand Down
19 changes: 18 additions & 1 deletion src/fabric_cli/commands/auth/fab_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
def init(args: Namespace) -> Any:
auth_options = [
"Interactive with a web browser",
"Azure CLI (reuse existing 'az login' session)",
"Service principal authentication with secret",
"Service principal authentication with certificate",
"Service principal authentication with federated credential",
Expand All @@ -27,7 +28,15 @@ def init(args: Namespace) -> Any:
# Clean up stale context files when logging in
Context().cleanup_context_files(cleanup_all_stale=True, cleanup_current=False)

if args.identity:
if getattr(args, "azure_cli", False):
FabAuth().set_access_mode("azure_cli", args.tenant)
FabAuth().set_azure_cli(args.tenant)
Comment on lines +31 to +33
FabAuth().get_access_token(scope=fab_constant.SCOPE_FABRIC_DEFAULT)
FabAuth().get_access_token(scope=fab_constant.SCOPE_ONELAKE_DEFAULT)
FabAuth().get_access_token(scope=fab_constant.SCOPE_AZURE_DEFAULT)
Context().context = FabAuth().get_tenant()

elif args.identity:
FabAuth().set_access_mode("managed_identity")
FabAuth().set_managed_identity(args.username)
FabAuth().get_access_token(scope=fab_constant.SCOPE_FABRIC_DEFAULT)
Expand Down Expand Up @@ -73,6 +82,13 @@ def init(args: Namespace) -> Any:
FabAuth().get_access_token(scope=fab_constant.SCOPE_ONELAKE_DEFAULT)
FabAuth().get_access_token(scope=fab_constant.SCOPE_AZURE_DEFAULT)
Context().context = FabAuth().get_tenant()
elif selected_auth.startswith("Azure CLI"):
FabAuth().set_access_mode("azure_cli", args.tenant)
FabAuth().set_azure_cli(args.tenant)
FabAuth().get_access_token(scope=fab_constant.SCOPE_FABRIC_DEFAULT)
FabAuth().get_access_token(scope=fab_constant.SCOPE_ONELAKE_DEFAULT)
FabAuth().get_access_token(scope=fab_constant.SCOPE_AZURE_DEFAULT)
Context().context = FabAuth().get_tenant()
elif selected_auth.startswith("Service principal authentication"):
fab_logger.log_warning(
"Ensure tenant setting is enabled for Service Principal auth"
Expand Down Expand Up @@ -275,6 +291,7 @@ def __mask_token(scope):

auth_data = {
"logged_in": is_logged_in,
"auth_source": auth.get_identity_type() or "N/A",
"account": upn,
"principal_id": oid,
"tenant_id": tid,
Expand Down
48 changes: 48 additions & 0 deletions src/fabric_cli/core/fab_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,52 @@ def set_managed_identity(self, client_id=None):
}
)

def set_azure_cli(self, tenant_id=None):
"""Configure Azure CLI as the authentication source."""
self._set_auth_properties(
{
con.IDENTITY_TYPE: "azure_cli",
}
)
if tenant_id:
self.set_tenant(tenant_id)

def _acquire_token_from_azure_cli(self, scope: list[str]) -> dict:
"""Acquire a token using Azure CLI's AzureCliCredential."""
try:
from azure.identity import AzureCliCredential, CredentialUnavailableError
except ImportError:
raise FabricCLIError(
"Azure CLI auth requires the 'azure-identity' package. "
"Install it with: pip install azure-identity",
status_code=con.ERROR_AUTHENTICATION_FAILED,
)
Comment on lines +436 to +440

tenant_id = self.get_tenant_id()
try:
credential = AzureCliCredential(tenant_id=tenant_id) if tenant_id else AzureCliCredential()
# AzureCliCredential.get_token expects scopes as positional args
azure_token = credential.get_token(scope[0])
Comment on lines +442 to +446
return {
"access_token": azure_token.token,
"expires_on": azure_token.expires_on,
}
except CredentialUnavailableError:
raise FabricCLIError(
"Azure CLI is not installed or not logged in. "
"Run 'az login' to authenticate, then retry.",
status_code=con.ERROR_AUTHENTICATION_FAILED,
)
except Exception as e:
# Sanitize: never include token content in error messages
error_msg = str(e)
if "accessToken" in error_msg or "token" in error_msg.lower():
error_msg = "Azure CLI token acquisition failed. Run 'az account get-access-token' manually to diagnose."
raise FabricCLIError(
f"Azure CLI authentication failed: {error_msg}",
status_code=con.ERROR_AUTHENTICATION_FAILED,
)

def print_auth_info(self):
utils_ui.print_grey(json.dumps(self._get_auth_info(), indent=2))

Expand Down Expand Up @@ -480,6 +526,8 @@ def acquire_token(self, scope: list[str], interactive_renew=True) -> dict:
ErrorMessages.Auth.managed_identity_token_failed(),
status_code=con.ERROR_AUTHENTICATION_FAILED,
)
elif identity_type == "azure_cli":
token = self._acquire_token_from_azure_cli(scope)
elif env_var_token:
token = {
"access_token": env_var_token,
Expand Down
2 changes: 1 addition & 1 deletion src/fabric_cli/core/fab_constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@

AUTH_KEYS = {
FAB_TENANT_ID: [],
IDENTITY_TYPE: ["user", "service_principal", "managed_identity"],
IDENTITY_TYPE: ["user", "service_principal", "managed_identity", "azure_cli"],
}

FAB_HOST_APP_ENV_VAR = "FAB_HOST_APP"
Expand Down
11 changes: 11 additions & 0 deletions src/fabric_cli/parsers/fab_auth_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def register_parser(subparsers: _SubParsersAction) -> None:
"$ auth login\n",
"# command_line mode",
"$ fab auth login\n",
"# command_line mode using Azure CLI auth",
"$ fab auth login --azure-cli\n",
"# command_line mode using Azure CLI auth with specific tenant",
"$ fab auth login --azure-cli --tenant <tenant_id>\n",
"# command_line mode using service principal auth",
"$ fab auth login -u <client_id> -p <client_secret> --tenant <tenant_id>\n",
"# command_line mode using system assigned managed identity auth",
Expand Down Expand Up @@ -84,6 +88,13 @@ def register_parser(subparsers: _SubParsersAction) -> None:
required=False,
help="Federated token that can be used for OIDC token exchange. Optional, only for service principal auth",
)
login_parser.add_argument(
"--azure-cli",
required=False,
action="store_true",
dest="azure_cli",
help="Use Azure CLI authentication (reuse existing 'az login' session)",
)

login_parser.usage = f"{utils_error_parser.get_usage_prog(login_parser)}"
login_parser.set_defaults(func=lazy_command(_auth_module_path, 'init'))
Expand Down
Loading
Loading