-
Notifications
You must be signed in to change notification settings - Fork 63
refactor: Add user drift detection to azure cli auth mode #285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
377dd24
e7cf03c
f9a4a1a
05fd139
79baef2
2ce2458
c99100b
64ec2e2
b2929c9
8ac7334
75842c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -328,6 +328,11 @@ def get_tenant_id(self): | |
| def get_identity_type(self): | ||
| return self._get_auth_property(con.IDENTITY_TYPE) | ||
|
|
||
| def validate_azure_cli_identity(self) -> None: | ||
| """Validate the current Azure CLI identity when that mode is active.""" | ||
| if self.get_identity_type() == "azure_cli": | ||
| self.get_access_token(con.SCOPE_FABRIC_DEFAULT, interactive_renew=False) | ||
|
|
||
| def set_access_mode(self, mode, tenant_id=None): | ||
| if mode not in con.AUTH_KEYS[con.IDENTITY_TYPE]: | ||
| raise FabricCLIError( | ||
|
|
@@ -338,6 +343,10 @@ def set_access_mode(self, mode, tenant_id=None): | |
| ) | ||
| if mode != self.get_identity_type(): | ||
| self.logout() | ||
| elif mode == "azure_cli": | ||
| # Clear the baseline so an explicit re-login establishes the | ||
| # current Azure CLI identity instead of reporting identity drift | ||
| self._clear_azure_cli_identity_baseline() | ||
| if tenant_id and self.get_tenant_id() != tenant_id: | ||
| self.set_tenant(tenant_id) | ||
| self._set_auth_property(con.IDENTITY_TYPE, mode) | ||
|
|
@@ -421,10 +430,10 @@ def set_managed_identity(self, client_id=None): | |
| ) | ||
|
|
||
| def _acquire_token_from_azure_cli(self, scope: list[str]) -> dict: | ||
| """Acquire a token using the current Azure CLI authentication context. | ||
| """Acquire a token and validate its identity against the stored baseline. | ||
|
|
||
| Synchronizes Fabric CLI's tenant, resource caches, and command context | ||
| with the tenant claim in the acquired token. | ||
| Records the tenant and principal on first use. If either identity later | ||
| changes, logs out and clears cached Fabric CLI state. | ||
| """ | ||
| from azure.core.exceptions import ( | ||
| ClientAuthenticationError, | ||
|
|
@@ -440,11 +449,9 @@ def _acquire_token_from_azure_cli(self, scope: list[str]) -> dict: | |
| # AzureCliCredential.get_token expects scopes as positional args | ||
| azure_token = self._azure_cli_credential.get_token(scope[0]) | ||
|
|
||
| # Keep tenant-scoped context and caches aligned with Azure CLI | ||
| # Extract claims from the acquired token to determine the current Azure CLI identity | ||
| claims = self._decode_jwt_token(azure_token.token) | ||
| tid = claims.get("tid") | ||
| if tid and tid != self.get_tenant_id(): | ||
| self._synchronize_azure_cli_tenant(tid) | ||
| self._check_azure_cli_identity(claims) | ||
|
|
||
| token_result = { | ||
| "access_token": azure_token.token, | ||
|
|
@@ -478,14 +485,68 @@ def _acquire_token_from_azure_cli(self, scope: list[str]) -> dict: | |
| status_code=con.ERROR_AUTHENTICATION_FAILED, | ||
| ) | ||
|
|
||
| def _synchronize_azure_cli_tenant(self, tenant_id: str) -> None: | ||
| """Synchronize state when the active Azure CLI tenant changes.""" | ||
| def _check_azure_cli_identity(self, claims: dict) -> None: | ||
| """Records Azure CLI tenant and principal IDs and rejects identity drift.""" | ||
| from fabric_cli.core.fab_context import Context | ||
| from fabric_cli.utils import fab_mem_store | ||
|
|
||
| self._set_auth_properties({con.FAB_TENANT_ID: tenant_id}) | ||
| fab_mem_store.clear_caches() | ||
| Context().context = self.get_tenant() | ||
| # Get the tenant and principal IDs from the claims | ||
| tenant_id = claims.get("tid") | ||
| principal_id = claims.get("oid") | ||
|
|
||
| if tenant_id is None or principal_id is None: | ||
| raise FabricCLIError( | ||
| ErrorMessages.Auth.azure_cli_identity_claims_missing(), | ||
| status_code=con.ERROR_AUTHENTICATION_FAILED, | ||
| ) | ||
|
|
||
| # Get the current tenant and principal IDs from the auth context | ||
| current_tenant_id = self.get_tenant_id() | ||
| current_principal_id = self._get_auth_property(con.FAB_PRINCIPAL_ID) | ||
|
|
||
| # Determine if there is an identity drift (tenant or principal) | ||
| tenant_drifted = ( | ||
| current_tenant_id is not None and tenant_id != current_tenant_id | ||
| ) | ||
| principal_drifted = ( | ||
| current_principal_id is not None and principal_id != current_principal_id | ||
| ) | ||
| # If any drift is detected, set the changed identity description | ||
| if tenant_drifted or principal_drifted: | ||
| if tenant_drifted and principal_drifted: | ||
| changed_identity = "Tenant ID and Principal ID" | ||
| elif tenant_drifted: | ||
| changed_identity = "Tenant ID" | ||
| else: | ||
| changed_identity = "Principal ID" | ||
| fab_logger.log_warning(f"Change detected in Azure CLI {changed_identity}") | ||
|
|
||
| # Logout, clear caches, and reset context before raising an error | ||
| self.logout() | ||
| fab_mem_store.clear_caches() | ||
| Context().reset_context() | ||
|
|
||
| # Raise an error to stop the current operation upon detecting identity drift | ||
| raise FabricCLIError( | ||
| ErrorMessages.Auth.azure_cli_identity_changed(), | ||
| status_code=con.ERROR_AUTHENTICATION_FAILED, | ||
| ) | ||
|
Comment on lines
+529
to
+532
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why we raise an error?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We raise error to prevent the command from completing when identity drift it detected. Without it, even when identity drift is detected, if you do |
||
|
|
||
| # Record the initial Azure CLI identity as the baseline for future drift checks | ||
| auth_properties: dict[str, str] = {} | ||
|
|
||
| if current_tenant_id is None: | ||
| auth_properties[con.FAB_TENANT_ID] = tenant_id | ||
| if current_principal_id is None: | ||
| auth_properties[con.FAB_PRINCIPAL_ID] = principal_id | ||
| if auth_properties: | ||
| self._set_auth_properties(auth_properties) | ||
| Context().context = self.get_tenant() | ||
|
shirasassoon marked this conversation as resolved.
|
||
|
|
||
| def _clear_azure_cli_identity_baseline(self): | ||
| self._auth_info.pop(con.FAB_TENANT_ID, None) | ||
| self._auth_info.pop(con.FAB_PRINCIPAL_ID, None) | ||
| self._azure_cli_credential = None | ||
| self._save_auth() | ||
|
Comment on lines
+546
to
+549
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we want to remove all auth_info? consider using self.logout() method. note that if we choose to use self.logout(), it also removes the msal's cache.bin file and resets the config. perhaps we can extract the common code (reset auth file, save auth, set azure_cli_cred to None) to another method and use both, or add a new argument to logout method that tells it to skip remove cache and config..
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is used in the following context: when the user explicitly run When switching from another authentication mode to Azure CLI, the first branch calls the full |
||
|
|
||
| def print_auth_info(self): | ||
| utils_ui.print_grey(json.dumps(self._get_auth_info(), indent=2)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| ERROR_UNAUTHORIZED, | ||
| EXIT_CODE_AUTHORIZATION_REQUIRED, | ||
| EXIT_CODE_ERROR, | ||
| FAB_MODE_INTERACTIVE, | ||
| ) | ||
| from fabric_cli.core.fab_exceptions import FabricCLIError | ||
| from fabric_cli.utils import fab_ui | ||
|
|
@@ -21,7 +22,7 @@ def getinstance(*args, **kwargs): | |
| if class_ not in instances: | ||
| instances[class_] = class_(*args, **kwargs) | ||
| return instances[class_] | ||
|
|
||
| return getinstance | ||
|
|
||
|
|
||
|
|
@@ -64,8 +65,14 @@ def decorator(func): | |
| def wrapper(*args, **kwargs): | ||
| # Import Context locally to avoid circular import | ||
| from fabric_cli.core.fab_context import Context | ||
| Context().command = args[0].command_path | ||
| Context().fabric_skill = getattr(args[0], "skill", None) | ||
|
|
||
| context = Context() | ||
| context.command = args[0].command_path | ||
| context.fabric_skill = getattr(args[0], "skill", None) | ||
| if context.get_runtime_mode() == FAB_MODE_INTERACTIVE: | ||
| from fabric_cli.core.fab_auth import FabAuth | ||
|
|
||
| FabAuth().validate_azure_cli_identity() | ||
|
Comment on lines
+72
to
+75
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you explain this change?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interactive mode keeps authentication, cache, and hierarchy context alive across commands. The user may change their Azure CLI account or tenant in another terminal during that session. This check detects the change before the next command runs, preventing use of stale identity-specific context. |
||
| return func(*args, **kwargs) | ||
|
|
||
| return wrapper | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.