diff --git a/docs/explanation/cache.md b/docs/explanation/cache.md deleted file mode 100644 index b039352..0000000 --- a/docs/explanation/cache.md +++ /dev/null @@ -1,20 +0,0 @@ -# Token Cache - -## Least Privilege - -`pyvet` uses the principle of least privilege when interacting with various VA -apis. A veteran must initially request and authorize access for each api and -then `pyvet` will place the bearer token in a cache. - -The token cache will be updated on initial entry and then once the bearer -token expires, utilizing the refresh token to retrieve another token. Below is -an example of the token cache for a va api name and its token, or key value -pair respectively. - -```python3 -{ - 'veteran': 'somerandomtoken', - 'claims': 'somerandomtoken', - ... -} -``` diff --git a/docs/explanation/token_scheduler.md b/docs/explanation/token_scheduler.md new file mode 100644 index 0000000..4b84953 --- /dev/null +++ b/docs/explanation/token_scheduler.md @@ -0,0 +1,27 @@ +# Token Scheduler + +## Least Privilege + +`pyvet` uses the principle of +[least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) +when interacting with various VA apis. A veteran must initially request and +authorize access for each api and then `pyvet` will place the bearer token in a +cache. + +Below is an example of the token cache for a va api +name and its token, or key value pair respectively. + +```python3 +{ + 'veteran': 'somerandomtoken', + 'claims': 'somerandomtoken', + ... +} +``` + +### Eviction Policy + +The token cache will be updated on initial entry of a bearer token and then +once it becomes invalid (expired or revoked). The refresh token will be used +to retrieve another token. If the refresh token is expired, then the entire +authentication process is initiated for the veteran's approval. diff --git a/pyvet/benefits/claims/api.py b/pyvet/benefits/claims/api.py index 02f92ae..44ca01a 100644 --- a/pyvet/benefits/claims/api.py +++ b/pyvet/benefits/claims/api.py @@ -9,13 +9,11 @@ from pyvet.client import ( current_session as session, ) -from pyvet.client import ( - get_bearer_token, -) +from pyvet.client import token_scheduler from pyvet.creds import API_URL from pyvet.json_alias import Json -BENEFITS_INTAKE_URL = API_URL + "claims/v1/" +BENEFITS_CLAIMS_URL = API_URL + "claims/v1/" CLAIM_SCOPE = "openid profile offline_access claim.read claim.write" @@ -44,10 +42,10 @@ def get_claims( r : json Response in json format. """ - claims_url = BENEFITS_INTAKE_URL + "claims" + claims_url = BENEFITS_CLAIMS_URL + "claims" session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="claims", scope=CLAIM_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="claims", scope=CLAIM_SCOPE)}""" if session.headers.get("Authorization") is None: return None if is_representative: @@ -91,10 +89,10 @@ def get_claim( r : json Response in json format. """ - claim_url = BENEFITS_INTAKE_URL + f"claims/{claim_id}" + claim_url = BENEFITS_CLAIMS_URL + f"claims/{claim_id}" session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="claims", scope=CLAIM_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="claims", scope=CLAIM_SCOPE)}""" if session.headers.get("Authorization") is None: return None if is_representative: @@ -138,8 +136,8 @@ def submit_526( r : json Response in json format. """ - submission_url = BENEFITS_INTAKE_URL + "forms/526" - token = get_bearer_token(va_api="claims", scope=CLAIM_SCOPE) + submission_url = BENEFITS_CLAIMS_URL + "forms/526" + token = token_scheduler.get_bearer_token(va_api="claims", scope=CLAIM_SCOPE) if session.headers.get("Authorization") is None: session.headers["Authorization"] = f"Bearer {token}" @@ -275,10 +273,10 @@ def get_last_active_intent_to_file( r : json Response in json format. """ - active_intent_url = BENEFITS_INTAKE_URL + "forms/0966/active" + active_intent_url = BENEFITS_CLAIMS_URL + "forms/0966/active" session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="claims", scope=CLAIM_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="claims", scope=CLAIM_SCOPE)}""" if session.headers.get("Authorization") is None: return None if is_representative: @@ -383,10 +381,10 @@ def get_poa_status_by_id( r : json Response in json format. """ - poa_url = BENEFITS_INTAKE_URL + f"forms/2122/{poa_id}" + poa_url = BENEFITS_CLAIMS_URL + f"forms/2122/{poa_id}" session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="claims", scope=CLAIM_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="claims", scope=CLAIM_SCOPE)}""" if session.headers.get("Authorization") is None: return None if is_representative: @@ -427,10 +425,10 @@ def get_status_poa_last_active( r : json Response in json format. """ - poa_url = BENEFITS_INTAKE_URL + "forms/2122/active" + poa_url = BENEFITS_CLAIMS_URL + "forms/2122/active" session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="claims", scope=CLAIM_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="claims", scope=CLAIM_SCOPE)}""" if session.headers.get("Authorization") is None: return None if is_representative: diff --git a/pyvet/client.py b/pyvet/client.py index 6bdad51..4295a1b 100644 --- a/pyvet/client.py +++ b/pyvet/client.py @@ -3,7 +3,6 @@ """ import logging -import oidc_client as oidc import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry @@ -13,86 +12,8 @@ API_FORCE_LIST, API_KEY_HEADER, API_RETRIES, - AUTH_SERVER, - CLIENT_ID, - DEFAULT_SCOPE, - ISSUER, - REDIRECT, ) - - -class TokenCache: - """A simple token cache.""" - - def __init__(self): - self.tokens: dict[str, str] = {} - - def has_token(self, va_api: str) -> bool: - """Check if a token exists for a VA API. - Parameters - ---------- - va_api : str - The VA API to check for a token. - Returns - ------- - has_token : bool - Whether or not a token exists for the VA API. - """ - has_token = va_api in self.tokens - return has_token - - def get_token(self, va_api: str) -> str | None: - """Get a token for a VA API. - Parameters - ---------- - va_api : str - The VA API to get a token for. - Returns - ------- - token : str - A bearer token. - """ - token = self.tokens.get(va_api) - return token if token else None - - -def get_bearer_token(va_api: str, scope: str = DEFAULT_SCOPE) -> str | None: - """Get a bearer token from the VA OIDC server. - Parameters - ---------- - va_api : str - The VA API to request a token for. - scope : str - A scope to request from the VA OIDC server (different per VA API). - Returns - ------- - token : str - A bearer token. - """ - try: - if token_cache.has_token(va_api): - return token_cache.get_token(va_api) - logging.error( - "No token found, requesting a new one for VA %s api.", va_api.capitalize() - ) - token = oidc.login( - provider_config=oidc.config.ProviderConfig( - issuer=ISSUER, - authorization_endpoint=f"{AUTH_SERVER}/authorization", - token_endpoint=f"{AUTH_SERVER}/token", - ), - client_id=CLIENT_ID, - redirect_uri=REDIRECT, # update this later - scope=scope, - interactive=True, - ) - if token is None: - logging.error("Fetching token failed.") - return None - token_cache.tokens[va_api] = token.access_token - return token.access_token - except Exception as e: - logging.error(e) +from pyvet.token_scheduler import TokenScheduler def create_session() -> requests.Session: @@ -118,7 +39,7 @@ def create_session() -> requests.Session: current_session = create_session() -token_cache = TokenCache() +token_scheduler = TokenScheduler(session=current_session) if current_session.headers.get("apiKey") == "REPLACE ME": logging.error( diff --git a/pyvet/health/community_care/api.py b/pyvet/health/community_care/api.py index 2d9cf95..153c63c 100644 --- a/pyvet/health/community_care/api.py +++ b/pyvet/health/community_care/api.py @@ -8,9 +8,7 @@ from pyvet.client import ( current_session as session, ) -from pyvet.client import ( - get_bearer_token, -) +from pyvet.client import token_scheduler from pyvet.creds import API_URL from pyvet.json_alias import Json @@ -39,7 +37,7 @@ def get_eligibility( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="community_care", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="community_care", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None status_url = HEALTH_URL + "search" diff --git a/pyvet/health/patient_health/api.py b/pyvet/health/patient_health/api.py index 2563390..417f643 100644 --- a/pyvet/health/patient_health/api.py +++ b/pyvet/health/patient_health/api.py @@ -8,9 +8,7 @@ from pyvet.client import ( current_session as session, ) -from pyvet.client import ( - get_bearer_token, -) +from pyvet.client import token_scheduler from pyvet.creds import API_URL from pyvet.json_alias import Json @@ -59,7 +57,7 @@ def get_allergy_intolerance( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None allergy_url = HEALTH_URL + "AllergyIntolerance" @@ -92,7 +90,7 @@ def get_allergy_intolerance_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None allergy_url = HEALTH_URL + f"AllergyIntolerance/{resource_id}" @@ -140,10 +138,11 @@ def get_appointment( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None appointment_url = HEALTH_URL + "Appointment" + # session.headers["Accept"] = "application/fhir+json" params = { "patient": patient, "_id": resource_id, @@ -175,7 +174,7 @@ def get_appointment_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None appointment_url = HEALTH_URL + f"Appointment/{resource_id}" @@ -200,7 +199,7 @@ def get_binary_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None binary_url = HEALTH_URL + f"Binary/{resource_id}" @@ -256,7 +255,7 @@ def get_condition( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None condition_url = HEALTH_URL + "Condition" @@ -293,7 +292,7 @@ def get_condition_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None condition_url = HEALTH_URL + f"Condition/{resource_id}" @@ -338,7 +337,7 @@ def get_device( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None condition_url = HEALTH_URL + "Device" @@ -372,7 +371,7 @@ def get_device_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None device_url = HEALTH_URL + f"Device/{resource_id}" @@ -414,7 +413,7 @@ def get_device_request( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None device_request_url = HEALTH_URL + "DeviceRequest" @@ -447,7 +446,7 @@ def get_device_request_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None device_request_url = HEALTH_URL + f"DeviceRequest/{resource_id}" @@ -501,7 +500,7 @@ def get_diagnostic_report( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None diagnostic_report_url = HEALTH_URL + "DiagnosticReport" @@ -538,7 +537,7 @@ def get_diagnostic_report_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None diagnostic_report_url = HEALTH_URL + f"DiagnosticReport/{resource_id}" @@ -586,7 +585,7 @@ def get_document_reference( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None document_reference_url = HEALTH_URL + "DocumentReference" @@ -621,7 +620,7 @@ def get_document_reference_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None document_reference_url = HEALTH_URL + f"DocumentReference/{resource_id}" @@ -666,7 +665,7 @@ def get_encounter( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None encounter_url = HEALTH_URL + "Encounter" @@ -700,7 +699,7 @@ def get_encounter_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None encounter_url = HEALTH_URL + f"Encounter/{resource_id}" @@ -742,7 +741,7 @@ def get_immunization( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None immunization_url = HEALTH_URL + "Immunization" @@ -775,7 +774,7 @@ def get_immunization_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None immunization_url = HEALTH_URL + f"Immunization/{resource_id}" @@ -829,7 +828,7 @@ def get_location( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None location_url = HEALTH_URL + "Location" @@ -866,7 +865,7 @@ def get_location_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None location_url = HEALTH_URL + f"Location/{resource_id}" @@ -908,7 +907,7 @@ def get_medication( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None medication_url = HEALTH_URL + "Medication" @@ -941,7 +940,7 @@ def get_medication_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None medication_url = HEALTH_URL + f"Medication/{resource_id}" @@ -986,7 +985,7 @@ def get_medication_request( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None medication_request_url = HEALTH_URL + "MedicationRequest" @@ -1020,7 +1019,7 @@ def get_medication_request_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None medication_request_url = HEALTH_URL + f"MedicationRequest/{resource_id}" @@ -1071,7 +1070,7 @@ def get_observation( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None observation_url = HEALTH_URL + "Observation" @@ -1107,7 +1106,7 @@ def get_observation_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None observation_url = HEALTH_URL + f"Observation/{resource_id}" @@ -1161,7 +1160,7 @@ def get_organization( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None organization_url = HEALTH_URL + "Organization" @@ -1198,7 +1197,7 @@ def get_organization_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None organization_url = HEALTH_URL + f"Organization/{resource_id}" @@ -1234,7 +1233,7 @@ def get_patient( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None patient_url = HEALTH_URL + "Patient" @@ -1265,7 +1264,7 @@ def get_patient_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None patient_url = HEALTH_URL + f"Patient/{resource_id}" @@ -1313,7 +1312,7 @@ def get_practitioner( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None practitioner_url = HEALTH_URL + "Practitioner" @@ -1348,7 +1347,7 @@ def get_practitioner_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None practitioner_url = HEALTH_URL + f"Practitioner/{resource_id}" @@ -1390,7 +1389,7 @@ def get_practitioner_role( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None practitioner_role_url = HEALTH_URL + "PractitionerRole" @@ -1423,7 +1422,7 @@ def get_practitioner_role_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None practitioner_role_url = HEALTH_URL + f"PractitionerRole/{resource_id}" @@ -1468,7 +1467,7 @@ def get_procedure( """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None procedure_url = HEALTH_URL + "Procedure" @@ -1502,7 +1501,7 @@ def get_procedure_by_id(resource_id: str) -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None procedure_url = HEALTH_URL + f"Procedure/{resource_id}" @@ -1523,7 +1522,7 @@ def get_metadata() -> Json: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="patient", scope=HEALTH_SCOPE)}""" if session.headers.get("Authorization") is None: return None metadata_url = HEALTH_URL + "metadata" diff --git a/pyvet/token_scheduler.py b/pyvet/token_scheduler.py new file mode 100644 index 0000000..121297f --- /dev/null +++ b/pyvet/token_scheduler.py @@ -0,0 +1,210 @@ +"""The token scheduler module.""" +import logging + +import oidc_client as oidc +import requests + +from pyvet.creds import ( + AUTH_SERVER, + CLIENT_ID, + DEFAULT_SCOPE, + ISSUER, + REDIRECT, +) + + +class TokenScheduler: + """A simple token scheduler.""" + + def __init__(self, session: requests.Session | None = None): + self.tokens: dict[str, oidc.oauth.TokenResponse] = {} + self.current_session = session + + def has_token(self, va_api: str) -> bool: + """Check if a token exists for a VA API. + Parameters + ---------- + va_api : str + The VA API to check for a token. + Returns + ------- + has_token : bool + Whether or not a token exists for the VA API. + """ + has_token = va_api in self.tokens + return has_token + + def get_token(self, va_api: str) -> str | None: + """Get a token for a VA API. + Parameters + ---------- + va_api : str + The VA API to get a token for. + Returns + ------- + token : str + A bearer token. + """ + token = self.tokens.get(va_api) + token_verified = self.is_token_verified(token) + if token and not token_verified: + logging.error( + "Token invalid (expired or revoked) for %s, fetching a new one.", + va_api.capitalize(), + ) + new_token = self.fetch_and_set_token(va_api) + return new_token.access_token if new_token else None + return token.access_token if token else None + + def is_token_verified(self, token: oidc.oauth.TokenResponse) -> bool: + """Check if a token is expired. + Parameters + ---------- + token : oidc.oauth.TokenResponse + A token. + Returns + ------- + bool + Whether or not the token is expired. + """ + if token is None: + return False + instrospect_token = self.introspect_token(token.access_token) + if instrospect_token is None: + return False + return instrospect_token.get("active") is True + + def fetch_and_set_token(self, va_api: str) -> oidc.oauth.TokenResponse | None: + """Fetch a token using the refresh token for a client and set it. + Parameters + ---------- + va_api : str + The VA API to get a token for. + """ + token = self.tokens.get(va_api) + if token is None: + logging.error("No token found.") + return None + params = { + "grant_type": "refresh_token", + "refresh_token": token.refresh_token, + "client_id": CLIENT_ID, + "scope": token.scope, + } + try: + response = requests.post( + f"{AUTH_SERVER}/token", + params=params, + timeout=5, + ) + if response.status_code != 200: + logging.error("Token refresh failed.") + return None + try: + token = oidc.oauth.TokenResponse(**response.json()) + self.tokens[va_api] = token + return token + except TypeError as e: + logging.error(e) + return None + except requests.exceptions.RequestException as e: + logging.error(e) + return None + + def introspect_token(self, token: str) -> dict: + """Introspect a token to see if it is active, can be expired or revoked. + Parameters + ---------- + token : str + A bearer token. + """ + response = requests.post( + f"{AUTH_SERVER}/introspect", + headers=None, + data={ + "token": token, + "token_type_hint": "access_token", + "client_id": CLIENT_ID, + }, + timeout=5, + ) + + if response.status_code != 200: + logging.error("Token introspect failed.") + return {} + return response.json() + + def get_user_info(self, token: str) -> dict: + """Get user info from a token. + Parameters + ---------- + token : str + A bearer token. + """ + if self.current_session is None: + logging.error("No session found.") + return {} + self.current_session.headers["Authorization"] = f"""Bearer {token}""" + try: + response = self.current_session.get(f"{AUTH_SERVER}/userinfo") + if response.status_code != 200: + logging.error("User info request failed.") + return {} + return response.json() + except requests.exceptions.RequestException as e: + logging.error(e) + return {} + + def manage_tokens(self) -> dict: + """Manage tokens.""" + if self.current_session is None: + logging.error("No session found.") + return {} + try: + response = self.current_session.get(f"{AUTH_SERVER}/manage") + if response.status_code != 200: + logging.error("Token management request failed.") + return {} + return response.json() + except requests.exceptions.RequestException as e: + logging.error(e) + return {} + + def get_bearer_token(self, va_api: str, scope: str = DEFAULT_SCOPE) -> str | None: + """Get a bearer token from the pyvet Token Scheduler or the VA OIDC server. + Parameters + ---------- + va_api : str + The VA API to request a token for. + scope : str + A scope to request from the VA OIDC server (different per VA API). + Returns + ------- + token : str + A bearer token. + """ + try: + if self.has_token(va_api): + return self.get_token(va_api) + logging.error( + "No token found, requesting a new one for VA %s api.", + va_api.capitalize(), + ) + token = oidc.login( + provider_config=oidc.config.ProviderConfig( + issuer=ISSUER, + authorization_endpoint=f"{AUTH_SERVER}/authorization", + token_endpoint=f"{AUTH_SERVER}/token", + ), + client_id=CLIENT_ID, + redirect_uri=REDIRECT, # update this later + scope=scope, + interactive=True, + ) + if token is None: + logging.error("Fetching token failed.") + return None + self.tokens[va_api] = token + return token.access_token + except Exception as e: + logging.error(e) diff --git a/pyvet/veteran/verification/api.py b/pyvet/veteran/verification/api.py index 718a5f5..cf3079a 100644 --- a/pyvet/veteran/verification/api.py +++ b/pyvet/veteran/verification/api.py @@ -8,9 +8,7 @@ from pyvet.client import ( current_session as session, ) -from pyvet.client import ( - get_bearer_token, -) +from pyvet.client import token_scheduler from pyvet.creds import API_URL from pyvet.json_alias import Json @@ -25,9 +23,8 @@ def get_status() -> Json | None: r : json Response in json format. """ - session.headers[ - "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="veteran", scope=VERIFICATION_SCOPE)}""" + token = token_scheduler.get_bearer_token(va_api="veteran", scope=VERIFICATION_SCOPE) + session.headers["Authorization"] = f"""Bearer {token}""" if session.headers.get("Authorization") is None: return None status_url = VERIFICATION_URL + "status" @@ -48,7 +45,7 @@ def get_disability_rating() -> Json | None: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="veteran", scope=VERIFICATION_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="veteran", scope=VERIFICATION_SCOPE)}""" if session.headers.get("Authorization") is None: return None disability_rating_url = VERIFICATION_URL + "disability_rating" @@ -69,7 +66,7 @@ def get_service_history() -> Json | None: """ session.headers[ "Authorization" - ] = f"""Bearer {get_bearer_token(va_api="veteran", scope=VERIFICATION_SCOPE)}""" + ] = f"""Bearer {token_scheduler.get_bearer_token(va_api="veteran", scope=VERIFICATION_SCOPE)}""" if session.headers.get("Authorization") is None: return None service_history_url = VERIFICATION_URL + "service_history" diff --git a/tests/benefits/test_benefits_claims.py b/tests/benefits/test_benefits_claims.py index 303a87a..93c62d9 100644 --- a/tests/benefits/test_benefits_claims.py +++ b/tests/benefits/test_benefits_claims.py @@ -1,9 +1,12 @@ import unittest +from unittest.mock import patch + from requests import Session + from pyvet import creds from pyvet.benefits.claims.api import ( - get_claims, get_claim, + get_claims, # submit_526, # upload_526, # upload_supporting_doc_526, @@ -16,11 +19,9 @@ MOCK_CLAIM, MOCK_CLAIMS, MOCK_INTENT_TO_FILE_LAST_ACTIVE, - MOCK_POA_STATUS_ID, MOCK_POA_LAST_ACTIVE, + MOCK_POA_STATUS_ID, ) -from unittest.mock import patch - mock_headers = dict(apiKey=creds.API_KEY_HEADER.get("apiKey")) @@ -35,7 +36,7 @@ def setUp(self): creds.API_KEY_HEADER["Authorization"] = None @patch( - "pyvet.benefits.claims.api.get_bearer_token", + "pyvet.benefits.claims.api.token_scheduler.get_bearer_token", return_value="somerandomtoken", ) @patch.object(Session().headers, "get", return_value=None) @@ -68,7 +69,7 @@ def test_get_claims(self, mock_get, mock_auth, mock_token): creds.API_KEY_HEADER["Authorization"] = None @patch( - "pyvet.benefits.claims.api.get_bearer_token", + "pyvet.benefits.claims.api.token_scheduler.get_bearer_token", return_value="somerandomtoken", ) @patch.object(Session().headers, "get", return_value=None) @@ -101,7 +102,7 @@ def test_get_claim(self, mock_get, mock_auth, mock_token): ) @patch( - "pyvet.benefits.claims.api.get_bearer_token", + "pyvet.benefits.claims.api.token_scheduler.get_bearer_token", return_value="somerandomtoken", ) @patch.object(Session().headers, "get", return_value=None) @@ -136,7 +137,7 @@ def test_get_last_active_intent_to_file(self, mock_get, mock_auth, mock_token): ) @patch( - "pyvet.benefits.claims.api.get_bearer_token", + "pyvet.benefits.claims.api.token_scheduler.get_bearer_token", return_value="somerandomtoken", ) @patch.object(Session().headers, "get", return_value=None) @@ -171,7 +172,7 @@ def test_get_poa_status_by_id(self, mock_get, mock_auth, mock_token): ) @patch( - "pyvet.benefits.claims.api.get_bearer_token", + "pyvet.benefits.claims.api.token_scheduler.get_bearer_token", return_value="somerandomtoken", ) @patch.object(Session().headers, "get", return_value=None) diff --git a/tests/benefits/test_benefits_intake.py b/tests/benefits/test_benefits_intake.py index e2de353..a0a543e 100644 --- a/tests/benefits/test_benefits_intake.py +++ b/tests/benefits/test_benefits_intake.py @@ -1,15 +1,17 @@ import json import unittest +from unittest.mock import ANY, mock_open, patch + from requests import Session + from pyvet import creds from pyvet.benefits.intake.api import ( - create_path_to_upload_files, - upload_files, bulk_status_report, - get_uploaded_document, + create_path_to_upload_files, download_uploaded_document, + get_uploaded_document, + upload_files, ) -from unittest.mock import ANY, patch, mock_open mock_create_path = { "data": { diff --git a/tests/benefits/test_benefits_reference.py b/tests/benefits/test_benefits_reference.py index 975639d..071f33b 100644 --- a/tests/benefits/test_benefits_reference.py +++ b/tests/benefits/test_benefits_reference.py @@ -1,10 +1,13 @@ import unittest +from unittest.mock import patch + from requests import Session + from pyvet import creds from pyvet.benefits.reference.api import ( - get_disabilities, get_contention_types, get_countries, + get_disabilities, get_intake_sites, get_military_pay_types, get_service_branches, @@ -23,7 +26,6 @@ MOCK_STATES, MOCK_TREATMENT_CENTERS, ) -from unittest.mock import patch @patch.object(Session, "get", headers=creds.API_KEY_HEADER) diff --git a/tests/facilities/test_facilities.py b/tests/facilities/test_facilities.py index fd18337..2c09af6 100644 --- a/tests/facilities/test_facilities.py +++ b/tests/facilities/test_facilities.py @@ -1,5 +1,8 @@ import unittest +from unittest.mock import patch + from requests import Session + from pyvet import creds from pyvet.facilities.api import ( get_all, @@ -9,13 +12,12 @@ get_nearby, ) from tests.data.mock_facilities import ( - MOCK_FACILITY, MOCK_FACILITIES, + MOCK_FACILITY, MOCK_FACILITY_IDS, MOCK_NEARBY, MOCK_QUERY_JSON, ) -from unittest.mock import patch @patch.object(Session, "get", headers=creds.API_KEY_HEADER) diff --git a/tests/forms/test_forms.py b/tests/forms/test_forms.py index df7f7e6..4a8783f 100644 --- a/tests/forms/test_forms.py +++ b/tests/forms/test_forms.py @@ -1,10 +1,11 @@ import unittest +from unittest.mock import patch + from requests import Session + from pyvet import creds from pyvet.forms.api import get_form, get_forms - from tests.data.mock_forms_data import MOCK_FORM, MOCK_FORMS -from unittest.mock import patch @patch.object(Session, "get", headers=creds.API_KEY_HEADER) diff --git a/tests/health/test_community_care.py b/tests/health/test_community_care.py index a40f8fb..1e3680b 100644 --- a/tests/health/test_community_care.py +++ b/tests/health/test_community_care.py @@ -1,10 +1,11 @@ import unittest +from unittest.mock import patch + from requests import Session + from pyvet import creds from pyvet.health.community_care.api import get_eligibility -from unittest.mock import patch - mock_community_care = { "patientRequest": { "patientIcn": "011235813V213455", @@ -62,7 +63,7 @@ @patch( - "pyvet.health.community_care.api.get_bearer_token", + "pyvet.health.community_care.api.token_scheduler.get_bearer_token", return_value="somerandomtoken", ) @patch.object(Session().headers, "get", return_value=None) diff --git a/tests/health/test_health_provider_directory.py b/tests/health/test_health_provider_directory.py index 15dfee8..a908fa9 100644 --- a/tests/health/test_health_provider_directory.py +++ b/tests/health/test_health_provider_directory.py @@ -1,5 +1,8 @@ import unittest +from unittest.mock import patch + from requests import Session + from pyvet import creds from pyvet.health.provider.api import ( get_location, @@ -12,16 +15,15 @@ get_practitioner_role_by_id, ) from tests.data.mock_health_data import ( - MOCK_ORG, - MOCK_ORG_ID, MOCK_LOCATION, MOCK_LOCATION_ID, + MOCK_ORG, + MOCK_ORG_ID, MOCK_PRACTITIONER, MOCK_PRACTITIONER_ID, MOCK_PRACTITIONER_ROLE, MOCK_PRACTITIONER_ROLE_ID, ) -from unittest.mock import patch @patch.object(Session, "get", headers=creds.API_KEY_HEADER) diff --git a/tests/health/test_patient.py b/tests/health/test_patient.py index 57c85e3..077050e 100644 --- a/tests/health/test_patient.py +++ b/tests/health/test_patient.py @@ -1,7 +1,9 @@ import unittest +from unittest.mock import patch + from requests import Session -from pyvet import creds +from pyvet import creds from pyvet.health.patient_health.api import ( get_allergy_intolerance, get_allergy_intolerance_by_id, @@ -28,6 +30,7 @@ get_medication_by_id, get_medication_request, get_medication_request_by_id, + get_metadata, get_observation, get_observation_by_id, get_organization, @@ -40,9 +43,7 @@ get_practitioner_role_by_id, get_procedure, get_procedure_by_id, - get_metadata, ) - from tests.data.mock_patient_data import ( MOCK_ALLERGY_INTOLERANCE, MOCK_ALLERGY_INTOLERANCE_ID, @@ -69,6 +70,7 @@ MOCK_MEDICATION_ID, MOCK_MEDICATION_REQUEST, MOCK_MEDICATION_REQUEST_ID, + MOCK_METADATA, MOCK_OBSERVATION, MOCK_OBSERVATION_ID, MOCK_ORGANIZATION, @@ -81,14 +83,11 @@ MOCK_PRACTITIONER_ROLE_ID, MOCK_PROCEDURE, MOCK_PROCEDURE_ID, - MOCK_METADATA, ) -from unittest.mock import patch - @patch( - "pyvet.health.patient_health.api.get_bearer_token", + "pyvet.health.patient_health.api.token_scheduler.get_bearer_token", return_value="somerandomtoken", ) @patch.object(Session().headers, "get", return_value=None) diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 7ed2f25..86b4ba4 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -1,4 +1,5 @@ import unittest + from pyvet import creds diff --git a/tests/veteran/test_veteran_confirmation.py b/tests/veteran/test_veteran_confirmation.py index abbb3b5..ee199b4 100644 --- a/tests/veteran/test_veteran_confirmation.py +++ b/tests/veteran/test_veteran_confirmation.py @@ -1,9 +1,10 @@ import unittest -from pyvet import creds -from pyvet.veteran.confirmation.api import get_status +from unittest.mock import patch + from requests import Session -from unittest.mock import patch +from pyvet import creds +from pyvet.veteran.confirmation.api import get_status mock_confirmed = {"veteran_status": "confirmed"} diff --git a/tests/veteran/test_veteran_verification.py b/tests/veteran/test_veteran_verification.py index cde5915..7c3ee8a 100644 --- a/tests/veteran/test_veteran_verification.py +++ b/tests/veteran/test_veteran_verification.py @@ -1,13 +1,14 @@ import unittest +from unittest.mock import patch + +from requests import Session + from pyvet import creds from pyvet.veteran.verification.api import ( - get_status, get_disability_rating, get_service_history, + get_status, ) -from requests import Session - -from unittest.mock import patch mock_confirmed = {"veteran_status": "confirmed"} @@ -58,7 +59,7 @@ @patch( - "pyvet.veteran.verification.api.get_bearer_token", + "pyvet.veteran.verification.api.token_scheduler.get_bearer_token", return_value="somerandomtoken", ) @patch.object(Session().headers, "get", return_value=None)