alan-turing-institute / data-safe-haven

https://data-safe-haven.readthedocs.io
BSD 3-Clause "New" or "Revised" License
61 stars 15 forks source link

Catch Graph API timeout exception #2142

Closed jemrobinson closed 2 months ago

jemrobinson commented 2 months ago

:white_check_mark: Checklist

:vertical_traffic_light: Depends on

n/a

:arrow_heading_up: Summary

Catch a Graph API timeout exception when deploying

Full traceback without this fix ```bash $ hatch run dsh sre deploy fuschia You are logged into the Azure CLI as: user: James Robinson (aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee) tenant: turing.ac.uk (11111111-2222-3333-4444-555555555555) Are these details correct? [y/n] (y): y Go to https://microsoft.com/devicelogin in a web browser and enter the code GM98MDND2 at the prompt. Use global administrator credentials for your Entra ID directory to sign-in. ╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮ │ /path/to/code/data_safe_haven/commands/sre.py: │ │ 69 in deploy │ │ │ │ 66 │ │ │ config=sre_config, │ │ 67 │ │ │ pulumi_config=pulumi_config, │ │ 68 │ │ │ create_project=True, │ │ ❱ 69 │ │ │ graph_api_token=graph_api.token, │ │ 70 │ │ ) │ │ 71 │ │ # Set Azure options │ │ 72 │ │ stack.add_option( │ │ │ │ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │ │ │ context = Context( │ │ │ │ │ admin_group_name='ADMIN GROUP NAME', │ │ │ │ │ subscription_name='SUBSCRIPTION NAME', │ │ │ │ │ description='DESCRIPTION', │ │ │ │ │ name='projects' │ │ │ │ ) │ │ │ │ force = False │ │ │ │ graph_api = │ │ │ │ logger = │ │ │ │ name = 'sandbox' │ │ │ │ pulumi_config = DSHPulumiConfig( │ │ │ │ │ │ │ │ │ encrypted_key='TmMzVWc1TzRRZElfcGY4NnZZYkJraE5PcUhXcXVxbE1HNFJ1WVBDSzQ4Zldp… │ │ │ │ │ projects={ │ │ │ │ │ │ 'sandbox': DSHPulumiProject( │ │ │ │ │ │ │ stack_config={ │ │ │ │ │ │ │ │ 'azure-native:location': 'uksouth', │ │ │ │ │ │ │ │ 'azure-native:subscriptionId': │ │ │ │ '22222222-3333-4444-5555-666666666666', │ │ │ │ │ │ │ │ 'azure-native:tenantId': │ │ │ │ '11111111-2222-3333-4444-555555555555', │ │ │ │ │ │ │ │ 'data-safe-haven:shm-admin-group-id': │ │ │ │ '33333333-4444-5555-6666-777777777777', │ │ │ │ │ │ │ │ 'data-safe-haven:shm-entra-tenant-id': │ │ │ │ '44444444-5555-6666-7777-888888888888', │ │ │ │ │ │ │ │ 'data-safe-haven:shm-fqdn': │ │ │ │ 'projects.example.org' │ │ │ │ │ │ │ } │ │ │ │ │ │ ) │ │ │ │ │ } │ │ │ │ ) │ │ │ │ shm_config = SHMConfig( │ │ │ │ │ azure=ConfigSectionAzure( │ │ │ │ │ │ location='uksouth', │ │ │ │ │ │ subscription_id='22222222-3333-4444-5555-666666666666', │ │ │ │ │ │ tenant_id='11111111-2222-3333-4444-555555555555' │ │ │ │ │ ), │ │ │ │ │ shm=ConfigSectionSHM( │ │ │ │ │ │ admin_group_id='33333333-4444-5555-6666-777777777777', │ │ │ │ │ │ entra_tenant_id='44444444-5555-6666-7777-888888888888', │ │ │ │ │ │ fqdn='projects.example.org' │ │ │ │ │ ) │ │ │ │ ) │ │ │ │ sre_config = SREConfig( │ │ │ │ │ azure=ConfigSectionAzure( │ │ │ │ │ │ location='uksouth', │ │ │ │ │ │ subscription_id='22222222-3333-4444-5555-666666666666', │ │ │ │ │ │ tenant_id='11111111-2222-3333-4444-555555555555' │ │ │ │ │ ), │ │ │ │ │ description='sandbox Project', │ │ │ │ │ dockerhub=ConfigSectionDockerHub( │ │ │ │ │ │ access_token='dckr_pat_example', │ │ │ │ │ │ username='jemrobinson' │ │ │ │ │ ), │ │ │ │ │ name='sandbox', │ │ │ │ │ sre=ConfigSectionSRE( │ │ │ │ │ │ admin_email_address='admin@example.org', │ │ │ │ │ │ admin_ip_addresses=[ │ │ │ │ │ │ │ '1.1.1.1/32', │ │ │ │ │ │ ], │ │ │ │ │ │ databases=[ │ │ │ │ │ │ │ , │ │ │ │ │ │ │ │ │ │ │ │ │ ], │ │ │ │ │ │ data_provider_ip_addresses=[ │ │ │ │ │ │ │ '1.1.1.1/32', │ │ │ │ │ │ ], │ │ │ │ │ │ remote_desktop=ConfigSubsectionRemoteDesktopOpts( │ │ │ │ │ │ │ allow_copy=True, │ │ │ │ │ │ │ allow_paste=True │ │ │ │ │ │ ), │ │ │ │ │ │ research_user_ip_addresses=[ │ │ │ │ │ │ │ '1.1.1.1/32', │ │ │ │ │ │ ], │ │ │ │ │ │ storage_quota_gb=ConfigSubsectionStorageQuotaGB( │ │ │ │ │ │ │ home=100, │ │ │ │ │ │ │ shared=100 │ │ │ │ │ │ ), │ │ │ │ │ │ software_packages=, │ │ │ │ │ │ timezone='Europe/Berlin', │ │ │ │ │ │ workspace_skus=['Standard_D2s_v5'] │ │ │ │ │ ) │ │ │ │ ) │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ /path/to/code/data_safe_haven/external/api/gra │ │ ph_api.py:95 in token │ │ │ │ 92 │ │ │ 93 │ @property │ │ 94 │ def token(self) -> str: │ │ ❱ 95 │ │ return self.credential.token │ │ 96 │ │ │ 97 │ def add_custom_domain(self, domain_name: str) -> str: │ │ 98 │ │ """Add Entra ID custom domain │ │ │ │ ╭──────────────────────────────────── locals ────────────────────────────────────╮ │ │ │ self = │ │ │ ╰────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ /path/to/code/data_safe_haven/external/api/cre │ │ dentials.py:46 in token │ │ │ │ 43 │ @property │ │ 44 │ def token(self) -> str: │ │ 45 │ │ """Get a token from the credential provider.""" │ │ ❱ 46 │ │ return str(self.get_token(*self.scopes, tenant_id=self.tenant_id).token) │ │ 47 │ │ │ 48 │ @classmethod │ │ 49 │ def decode_token(cls, auth_token: str) -> dict[str, Any]: │ │ │ │ ╭────────────────────────────────────────── locals ──────────────────────────────────────────╮ │ │ │ self = │ │ │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ /path/to/code/data_safe_haven/external/api/cre │ │ dentials.py:109 in get_token │ │ │ │ 106 │ │ ): │ │ 107 │ │ │ # Generate a new token and store it at class-level token │ │ 108 │ │ │ DeferredCredential.tokens_[combined_scopes] = ( │ │ ❱ 109 │ │ │ │ self.get_credential().get_token(*scopes, **kwargs) │ │ 110 │ │ │ ) │ │ 111 │ │ return DeferredCredential.tokens_[combined_scopes] │ │ 112 │ │ │ │ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │ │ │ combined_scopes = 'Application.ReadWrite.All AppRoleAssignment.ReadWrite.All │ │ │ │ Directory.ReadWrite.Al'+21 │ │ │ │ kwargs = {'tenant_id': '44444444-5555-6666-7777-888888888888'} │ │ │ │ scopes = ( │ │ │ │ │ 'Application.ReadWrite.All', │ │ │ │ │ 'AppRoleAssignment.ReadWrite.All', │ │ │ │ │ 'Directory.ReadWrite.All', │ │ │ │ │ 'Group.ReadWrite.All' │ │ │ │ ) │ │ │ │ self = │ │ │ │ validity_cutoff = 1724080942.105617 │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ /path/to/code/data_safe_haven/external/api/cre │ │ dentials.py:206 in get_credential │ │ │ │ 203 │ │ ) │ │ 204 │ │ │ │ 205 │ │ # Write out an authentication record for this credential │ │ ❱ 206 │ │ new_auth_record = credential.authenticate(scopes=self.scopes) │ │ 207 │ │ with open(authentication_record_path, "w") as f_auth: │ │ 208 │ │ │ f_auth.write(new_auth_record.serialize()) │ │ 209 │ │ │ │ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │ │ │ authentication_record_path = PosixPath('/path/to/Library/Application │ │ │ │ Support/data_safe_haven/.msal-authentication-cache-dsh-1122334… │ │ │ │ cache_name = 'dsh-44444444-5555-6666-7777-888888888888' │ │ │ │ callback = .callback │ │ │ │ at 0x106fa5a80> │ │ │ │ credential = │ │ │ │ existing_auth_record = │ │ │ │ f_auth = <_io.TextIOWrapper name='/path/to/Library/Application │ │ │ │ Support/data_safe_haven/.msal-authentication-cache-dsh-1122334… │ │ │ │ mode='r' encoding='UTF-8'> │ │ │ │ kwargs = { │ │ │ │ │ 'authentication_record': │ │ │ │ │ │ │ │ } │ │ │ │ self = │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ /path/to/Library/Application │ │ Support/hatch/env/virtual/data-safe-haven/Svgq2p76/data-safe-haven/lib/python3.12/site-packages/ │ │ azure/identity/_internal/interactive.py:213 in authenticate │ │ │ │ 210 │ │ │ │ │ 211 │ │ │ scopes = _DEFAULT_AUTHENTICATE_SCOPES[self._authority] │ │ 212 │ │ │ │ ❱ 213 │ │ _ = self.get_token(*scopes, _allow_prompt=True, claims=claims, **kwargs) │ │ 214 │ │ return self._auth_record # type: ignore │ │ 215 │ │ │ 216 │ @wrap_exceptions │ │ │ │ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │ │ │ claims = None │ │ │ │ kwargs = {} │ │ │ │ scopes = [ │ │ │ │ │ 'Application.ReadWrite.All', │ │ │ │ │ 'AppRoleAssignment.ReadWrite.All', │ │ │ │ │ 'Directory.ReadWrite.All', │ │ │ │ │ 'Group.ReadWrite.All' │ │ │ │ ] │ │ │ │ self = │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ /path/to/Library/Application │ │ Support/hatch/env/virtual/data-safe-haven/Svgq2p76/data-safe-haven/lib/python3.12/site-packages/ │ │ azure/identity/_internal/interactive.py:173 in get_token │ │ │ │ 170 │ │ │ if "access_token" not in result: │ │ 171 │ │ │ │ message = "Authentication failed: {}".format(result.get("error_descripti │ │ 172 │ │ │ │ response = self._client.get_error_response(result) │ │ ❱ 173 │ │ │ │ raise ClientAuthenticationError(message=message, response=response) │ │ 174 │ │ │ │ │ 175 │ │ │ # this may be the first authentication, or the user may have authenticated a │ │ 176 │ │ │ self._auth_record = _build_auth_record(result) │ │ │ │ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │ │ │ allow_prompt = True │ │ │ │ claims = None │ │ │ │ enable_cae = False │ │ │ │ kwargs = {} │ │ │ │ message = "Authentication failed: AADSTS70020: The provided value for the input │ │ │ │ parameter '"+188 │ │ │ │ now = 1724080342 │ │ │ │ response = │ │ │ │ result = { │ │ │ │ │ 'error': 'expired_token', │ │ │ │ │ 'error_description': "AADSTS70020: The provided value for the input │ │ │ │ parameter 'device_code' is not val"+165, │ │ │ │ │ 'error_codes': [70020], │ │ │ │ │ 'timestamp': '2024-08-19 15:13:16Z', │ │ │ │ │ 'trace_id': 'c32ddf22-432f-4da2-8e81-c6644eddf600', │ │ │ │ │ 'correlation_id': '0f0dbe2c-4a01-466c-989c-771f44226a66', │ │ │ │ │ 'error_uri': 'https://login.microsoftonline.com/error?code=70020' │ │ │ │ } │ │ │ │ scopes = ( │ │ │ │ │ 'Application.ReadWrite.All', │ │ │ │ │ 'AppRoleAssignment.ReadWrite.All', │ │ │ │ │ 'Directory.ReadWrite.All', │ │ │ │ │ 'Group.ReadWrite.All' │ │ │ │ ) │ │ │ │ self = │ │ │ │ tenant_id = None │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ClientAuthenticationError: Authentication failed: AADSTS70020: The provided value for the input parameter 'device_code' is not valid. This device code has expired. Trace ID: c32ddf22-432f-4da2-8e81-c6644eddf600 Correlation ID: 0f0dbe2c-4a01-466c-989c-771f44226a66 Timestamp: 2024-08-19 15:13:16Z Content: {"error":"expired_token","error_description":"AADSTS70020: The provided value for the input parameter 'device_code' is not valid. This device code has expired. Trace ID: c32ddf22-432f-4da2-8e81-c6644eddf600 Correlation ID: 0f0dbe2c-4a01-466c-989c-771f44226a66 Timestamp: 2024-08-19 15:13:16Z","error_codes":[70020],"timestamp":"2024-08-19 15:13:16Z","trace_id":"c32ddf22-432f-4da2-8e81-c6644eddf600","correlation_id":"0f0dbe2c-4a01-466c-989c-771f44226a66","error_uri":"https://login.microsoftonline.com/error?code=70020"} ``` ### :closed_umbrella: Related issues n/a ### :microscope: Tests Test by logging out of the Graph API, attempting to deploy without responding to the device code prompt. After a minute it will time out.
github-actions[bot] commented 2 months ago

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  data_safe_haven/external/api
  credentials.py 211-214
Project Total  

This report was generated by python-coverage-comment-action

craddm commented 2 months ago

What's the error this catches? is it the one where it says the device code has expired?

jemrobinson commented 2 months ago

@craddm : I've added a traceback example in the top-level comment

craddm commented 2 months ago

Yep, been meaning to report this one. LGTM