From 502c0350f648ec778f6e5c0fc14706bc3fab00fe Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Tue, 8 Mar 2022 14:25:56 -0600 Subject: [PATCH 01/19] fix missing legal tag issue --- tests/test_data/test_create_single_record.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_data/test_create_single_record.json b/tests/test_data/test_create_single_record.json index f160eb1..a41acbe 100644 --- a/tests/test_data/test_create_single_record.json +++ b/tests/test_data/test_create_single_record.json @@ -6,7 +6,7 @@ "Name": "Test Record 1" }, "legal": { - "legaltags": ["osdu-public-usa-dataset-1"], + "legaltags": ["osdu-public-usa-dataset"], "otherRelevantDataCountries": ["US"], "status": "compliant" }, @@ -23,7 +23,7 @@ "Name": "Test Record 2" }, "legal": { - "legaltags": ["osdu-public-usa-dataset-1"], + "legaltags": ["osdu-public-usa-dataset"], "otherRelevantDataCountries": ["US"], "status": "compliant" }, @@ -40,7 +40,7 @@ "Name": "Test Record 3" }, "legal": { - "legaltags": ["osdu-public-usa-dataset-1"], + "legaltags": ["osdu-public-usa-dataset"], "otherRelevantDataCountries": ["US"], "status": "compliant" }, From 892e28e6371ee08262ea98e496f7c37d58bc49e5 Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Fri, 11 Mar 2022 13:02:19 -0600 Subject: [PATCH 02/19] add update_token to simple client --- osdu/client/simple.py | 40 ++++++++++++++++++--- osdu/services/authentication.py | 63 +++++++++++++++++++++++++++++++++ tests/integration.py | 24 +++++++++++-- 3 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 osdu/services/authentication.py diff --git a/osdu/client/simple.py b/osdu/client/simple.py index 70cfe92..e0ebd3b 100644 --- a/osdu/client/simple.py +++ b/osdu/client/simple.py @@ -1,3 +1,4 @@ +import os from ._base import BaseOsduClient @@ -6,15 +7,46 @@ class SimpleOsduClient(BaseOsduClient): This client assumes you are obtaining a token yourself (e.g. via your application's login form or otheer mechanism. With this SimpleOsduClient, you simply provide that token. - With this simplicity, you are also then respnsible for reefreeshing the token as needed and - re-instantiating the client with the new token. + With this simplicity, you are also then respnsible for reefreeshing the token as needed either by manually + re-instantiating the client with the new token or by providing the authentication client id, secret, refresh token, and refresh url. """ + + @property + def client_id(self): + return self._client_id + + @property + def client_secret(self): + return self._client_secret + + @property + def refresh_url(self): + return self._refresh_url + + @property + def refresh_token(self): + return self._refresh_token + + @property + def access_token(self): + return self._access_token + + @access_token.setter + def access_token(self, val): + self._access_token = val + - def __init__(self, data_partition_id: str, access_token: str, api_url: str=None) -> None: + def __init__(self, data_partition_id: str, access_token: str=None, api_url: str=None, refresh_token: str=None, refresh_url: str=None) -> None: """ :param: access_token: The access token only (not including the 'Bearer ' prefix). :param: api_url: must be only the base URL, e.g. https://myapi.myregion.mydomain.com + :param: refresh_token: The refresh token only (not including the 'Bearer ' prefix). + :param: refresh_url: The authentication Url, typically a Cognito URL ending in "/token". """ super().__init__(data_partition_id, api_url) - self._access_token = access_token \ No newline at end of file + self._access_token = access_token + self._refresh_token = refresh_token or os.environ.get('OSDU_REFRESH_TOKEN') + self._refresh_url = refresh_url or os.environ.get('OSDU_REFRESH_URL') + self._client_id = os.environ.get('OSDU_CLIENTWITHSECRET_ID') + self._client_secret = os.environ.get('OSDU_CLIENTWITHSECRET_SECRET') \ No newline at end of file diff --git a/osdu/services/authentication.py b/osdu/services/authentication.py new file mode 100644 index 0000000..3bb7df9 --- /dev/null +++ b/osdu/services/authentication.py @@ -0,0 +1,63 @@ +""" Provides a simple Python interface to the OSDU Authentication API. +""" +import requests +from .base import BaseService +from time import time + +class AuthenticationService(BaseService): + + def update_token(client): + if(AuthenticationService._need_update_token(client)): + if (hasattr(client, "resource_prefix") and client.resource_prefix is not None): #service principal client + token = AuthenticationService._update_token_simple(client) + elif (hasattr(client, "profile") and client.profile is not None): #aws client + token = AuthenticationService._update_token_aws(client) + else: #simple client + token = AuthenticationService._update_token_simple(client) + else: + token = client.access_token + return token + + def _need_update_token(client): + return hasattr(client, "_token_expiration") and client._token_expiration < time() or client.access_token is None + + + #TODO add expiration time to return + def _update_token_simple(client) -> dict: + """Executes a query against the OSDU search service. + + :param client: the simple client being used. In order to refresh the token, the client must have: + client_id: corresponding to the clientwithsecret + client_secret: corresponding to the clientwithsecret + refresh_token + refresh_url + all are set via optional parameters in the SimpleClient constructor or environment variables + + :returns: dict containing 3 items: aggregations, results, totalCount + - id_token: not used + - access_token: used to access OSDU services + - expires_in: expiration time for the token + - token_type: not used (should be Bearer) + """ + data = {'grant_type': 'refresh_token', + 'client_id': client.client_id, + 'client_secret': client.client_secret, + 'refresh_token': client.refresh_token, + 'scope': 'openid email'} + response = requests.post(url=client.refresh_url,headers=AuthenticationService._auth_headers(), data=data) + response.raise_for_status() + + return response.json() + + def _update_token_aws(client) -> dict: + client.update_token() + return client._access_token + + def _update_token_service_principal(client) -> dict: + return client.update_token() + + def _auth_headers(): + return { + "Content-Type": "application/x-www-form-urlencoded", + "Accept-Encoding": "gzip, deflate, br" + } \ No newline at end of file diff --git a/tests/integration.py b/tests/integration.py index 9d7c221..864be55 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -1,7 +1,14 @@ -""" In order to run these tests, you must provide an appropriate `user` and `password`. The password -can be set locally by setting the environment variable OSDU_PASSWORD. If using +""" In order to run these tests, you must provide appropriate environment variables. If using VS Code, then you can set this in your local `.env` file in your workspace directory to easily switch between OSDU environments. +Most Integration tests require: + OSDU_USER + AWS_PROFILE +SimpleClient update_token integration test require: + OSDU_CLIENTWITHSECRET_ID + OSDU_CLIENTWITHSECRET_SECRET + OSDU_REFRESH_URL + OSDU_REFRESH_TOKEN """ import json import os @@ -14,6 +21,7 @@ AwsServicePrincipalOsduClient, SimpleOsduClient ) +from osdu.services.authentication import AuthenticationService load_dotenv(verbose=True, override=True) @@ -33,6 +41,18 @@ def test_endpoint_access(self): result = client.search.query(query)['results'] self.assertEqual(1, len(result)) + + def test_update_token(self): + query = { + "kind": f"*:*:*:*", + "limit": 1 + } + + client = SimpleOsduClient(data_partition) + update_token_response = AuthenticationService.update_token(client) + client.access_token = update_token_response['access_token'] + result = client.search.query(query)['results'] + self.assertEqual(1, len(result)) class TestAwsOsduClient(TestCase): From 30832d4a30e2f4f855df0ff418f0c149e424c842 Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Fri, 11 Mar 2022 15:37:49 -0600 Subject: [PATCH 03/19] add update_token with aws client --- osdu/client/aws.py | 12 +++++++++++- tests/integration.py | 13 +++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/osdu/client/aws.py b/osdu/client/aws.py index 266b7d1..a275fdc 100644 --- a/osdu/client/aws.py +++ b/osdu/client/aws.py @@ -1,4 +1,5 @@ import os +from time import time import boto3 from ._base import BaseOsduClient @@ -71,6 +72,15 @@ def get_tokens(self, password, secret_hash) -> None: AuthParameters=auth_params ) - + self._token_expiration = response['AuthenticationResult']["ExpiresIn"] + time() self._access_token = response['AuthenticationResult']['AccessToken'] self._refresh_token = response['AuthenticationResult']['RefreshToken'] + + # CHECK ABOUT: refresh can only be used if password is in environment variables. Or can we store the password securely? + def update_token(self): + password = os.environ.get('OSDU_PASSWORD') + if(password): + self.get_tokens(password, self._secret_hash) + password = None + return None # If we don't have a password, we can't refresh the token with the AWS client + diff --git a/tests/integration.py b/tests/integration.py index 864be55..3b76f52 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -49,10 +49,9 @@ def test_update_token(self): } client = SimpleOsduClient(data_partition) - update_token_response = AuthenticationService.update_token(client) - client.access_token = update_token_response['access_token'] - result = client.search.query(query)['results'] - self.assertEqual(1, len(result)) + updated_access_token = AuthenticationService.update_token(client) + self.assertIsNotNone(updated_access_token) + class TestAwsOsduClient(TestCase): @@ -61,6 +60,12 @@ def test_get_access_token(self): client = AwsOsduClient(data_partition) self.assertIsNotNone(client.access_token) + def test_update_token(self): + client = AwsOsduClient(data_partition) + client._token_expiration = 0 # change the token expiration so we force a refresh + updated_access_token = AuthenticationService.update_token(client) + self.assertIsNotNone(updated_access_token) + class TestAwsServicePrincipalOsduClient(TestCase): From 610281064fd45a4934204fa179eede41cd483ffc Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Fri, 11 Mar 2022 16:07:49 -0600 Subject: [PATCH 04/19] add service principal refresh token, and some small coding style fixes --- osdu/client/_service_principal_util.py | 3 ++- osdu/client/aws.py | 1 + osdu/client/aws_service_principal.py | 12 +++++++++++- osdu/services/authentication.py | 12 +++++------- tests/integration.py | 10 ++++++++++ 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/osdu/client/_service_principal_util.py b/osdu/client/_service_principal_util.py index 778ee85..cad0690 100644 --- a/osdu/client/_service_principal_util.py +++ b/osdu/client/_service_principal_util.py @@ -28,6 +28,7 @@ # - Updated formatting to be PEP8-compliant. # import base64 +from time import time import boto3 import requests import json @@ -116,4 +117,4 @@ def get_service_principal_token(self, resource_prefix): response = requests.post(url=token_url, headers=headers) - return json.loads(response.content.decode())['access_token'] + return json.loads(response.content.decode())['access_token'], json.loads(response.content.decode())['expires_in'] + time() diff --git a/osdu/client/aws.py b/osdu/client/aws.py index a275fdc..555a8db 100644 --- a/osdu/client/aws.py +++ b/osdu/client/aws.py @@ -82,5 +82,6 @@ def update_token(self): if(password): self.get_tokens(password, self._secret_hash) password = None + return self.access_token, self._token_expiration return None # If we don't have a password, we can't refresh the token with the AWS client diff --git a/osdu/client/aws_service_principal.py b/osdu/client/aws_service_principal.py index 4d12f67..8511a73 100644 --- a/osdu/client/aws_service_principal.py +++ b/osdu/client/aws_service_principal.py @@ -4,12 +4,22 @@ class AwsServicePrincipalOsduClient(BaseOsduClient): + @property + def resource_prefix(self): + return self._resource_prefix + def __init__(self, data_partition_id: str, resource_prefix: str, profile: str = None, region: str = None): self._sp_util = ServicePrincipalUtil( resource_prefix, profile=profile, region=region) self._resource_prefix = resource_prefix - self._access_token = self._get_tokens() + token_and_expiration = self._get_tokens() + self._access_token = token_and_expiration[0] + self._token_expiration = token_and_expiration[1] + super().__init__(data_partition_id, self._sp_util.api_url) def _get_tokens(self): return self._sp_util.get_service_principal_token(self._resource_prefix) + + def update_token(self): + return self._sp_util.get_service_principal_token(self._resource_prefix) diff --git a/osdu/services/authentication.py b/osdu/services/authentication.py index 3bb7df9..dd1b556 100644 --- a/osdu/services/authentication.py +++ b/osdu/services/authentication.py @@ -9,13 +9,13 @@ class AuthenticationService(BaseService): def update_token(client): if(AuthenticationService._need_update_token(client)): if (hasattr(client, "resource_prefix") and client.resource_prefix is not None): #service principal client - token = AuthenticationService._update_token_simple(client) + token = AuthenticationService._update_token_service_principal(client) elif (hasattr(client, "profile") and client.profile is not None): #aws client token = AuthenticationService._update_token_aws(client) else: #simple client token = AuthenticationService._update_token_simple(client) else: - token = client.access_token + token = client.access_token, client._token_expiration return token def _need_update_token(client): @@ -33,11 +33,9 @@ def _update_token_simple(client) -> dict: refresh_url all are set via optional parameters in the SimpleClient constructor or environment variables - :returns: dict containing 3 items: aggregations, results, totalCount - - id_token: not used + :returns: tuple containing 2 items: the access_token and it's expiration_time - access_token: used to access OSDU services - expires_in: expiration time for the token - - token_type: not used (should be Bearer) """ data = {'grant_type': 'refresh_token', 'client_id': client.client_id, @@ -47,11 +45,11 @@ def _update_token_simple(client) -> dict: response = requests.post(url=client.refresh_url,headers=AuthenticationService._auth_headers(), data=data) response.raise_for_status() - return response.json() + return response.json()["access_token"],response.json()["expires_in"] + time() def _update_token_aws(client) -> dict: client.update_token() - return client._access_token + return client._access_token, client._token_expiration def _update_token_service_principal(client) -> dict: return client.update_token() diff --git a/tests/integration.py b/tests/integration.py index 3b76f52..1b3639c 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -95,6 +95,16 @@ def test_endpoint_access(self): self.assertEqual(1, len(result)) + def test_update_token(self): + client = AwsServicePrincipalOsduClient(data_partition, + os.environ['OSDU_RESOURCE_PREFIX'], + profile=os.environ['AWS_PROFILE'], + region=os.environ['AWS_DEFAULT_REGION'] + ) + client._token_expiration = 0 # change the token expiration so we force a refresh + updated_access_token = AuthenticationService.update_token(client) + self.assertIsNotNone(updated_access_token) + class TestOsduServiceBase(TestCase): From 89604cabfc210292064b8680c31908324df0a0c6 Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Mon, 14 Mar 2022 13:33:43 -0500 Subject: [PATCH 05/19] check token expiry on every service call. Attempt update if expired --- osdu/services/authentication.py | 41 +++++++++++++++++++-------------- osdu/services/base.py | 7 ------ osdu/services/dataset.py | 11 +++++---- osdu/services/entitlements.py | 11 +++++---- osdu/services/search.py | 5 ++-- osdu/services/storage.py | 3 ++- 6 files changed, 41 insertions(+), 37 deletions(-) diff --git a/osdu/services/authentication.py b/osdu/services/authentication.py index dd1b556..6a604f4 100644 --- a/osdu/services/authentication.py +++ b/osdu/services/authentication.py @@ -7,6 +7,19 @@ class AuthenticationService(BaseService): def update_token(client): + """Determines if the current access token associated with the client has expired. + If the token is not expired, the current access_token will be returned, unchanged. + If the token has expired, this function will attempt to refresh it, update it on client, and return it. + For simple clients, refresh requires a OSDU_CLIENTWITHSECRET_ID, OSDU_CLIENTWITHSECRET_SECRET, REFRESH_TOKEN, and REFRESH_URL + For Service Principal clients, refresh requires a resource_prefix and AWS_PROFILE (same as initial auth) + For AWS clients, refresh requires OSDU_USER, OSDU_PASSWORD, AWS_PROFILE, and OSDU_CLIENT_ID + + :param client: client in use + + :returns: tuple containing 2 items: the new access token and it's expiration time + - access_token: used to access OSDU services + - expires_in: expiration time for the token + """ if(AuthenticationService._need_update_token(client)): if (hasattr(client, "resource_prefix") and client.resource_prefix is not None): #service principal client token = AuthenticationService._update_token_service_principal(client) @@ -15,28 +28,14 @@ def update_token(client): else: #simple client token = AuthenticationService._update_token_simple(client) else: - token = client.access_token, client._token_expiration + token = client.access_token, client._token_expiration if hasattr(client, "_token_expiration") else None return token def _need_update_token(client): return hasattr(client, "_token_expiration") and client._token_expiration < time() or client.access_token is None - #TODO add expiration time to return def _update_token_simple(client) -> dict: - """Executes a query against the OSDU search service. - - :param client: the simple client being used. In order to refresh the token, the client must have: - client_id: corresponding to the clientwithsecret - client_secret: corresponding to the clientwithsecret - refresh_token - refresh_url - all are set via optional parameters in the SimpleClient constructor or environment variables - - :returns: tuple containing 2 items: the access_token and it's expiration_time - - access_token: used to access OSDU services - - expires_in: expiration time for the token - """ data = {'grant_type': 'refresh_token', 'client_id': client.client_id, 'client_secret': client.client_secret, @@ -52,10 +51,18 @@ def _update_token_aws(client) -> dict: return client._access_token, client._token_expiration def _update_token_service_principal(client) -> dict: - return client.update_token() + client.update_token() + return client._access_token, client._token_expiration def _auth_headers(): return { "Content-Type": "application/x-www-form-urlencoded", - "Accept-Encoding": "gzip, deflate, br" + } + + def get_headers(client): + AuthenticationService.update_token(client) + return { + "Content-Type": "application/json", + "data-partition-id": client._data_partition_id, + "Authorization": "Bearer " + client.access_token } \ No newline at end of file diff --git a/osdu/services/base.py b/osdu/services/base.py index bfd626a..f71496c 100644 --- a/osdu/services/base.py +++ b/osdu/services/base.py @@ -3,10 +3,3 @@ class BaseService(): def __init__(self, client, service_name: str, service_version: int): self._client = client self._service_url = f'{self._client.api_url}/api/{service_name}/v{service_version}' - - def _headers(self): - return { - "Content-Type": "application/json", - "data-partition-id": self._client._data_partition_id, - "Authorization": "Bearer " + self._client.access_token - } diff --git a/osdu/services/dataset.py b/osdu/services/dataset.py index a3a3e26..28e61d6 100644 --- a/osdu/services/dataset.py +++ b/osdu/services/dataset.py @@ -3,6 +3,7 @@ from typing import List import requests from .base import BaseService +from .authentication import AuthenticationService class DatasetService(BaseService): @@ -17,7 +18,7 @@ def get_dataset_registry(self, registry_id: str): :returns: The API Response """ url = f'{self._service_url}/getDatasetRegistry?id={registry_id}' - response = requests.get(url=url, headers=self._headers()) + response = requests.get(url=url, headers=AuthenticationService.get_headers(self._client)) response.raise_for_status() return response.json() @@ -30,7 +31,7 @@ def get_dataset_registries(self, registry_ids: List[str]): """ url = f'{self._service_url}/getDatasetRegistry?' data = {'datasetRegistryIds': registry_ids} - response = requests.post(url=url, headers=self._headers(), json=data) + response = requests.post(url=url, headers=AuthenticationService.get_headers(self._client), json=data) response.raise_for_status() return response.json() @@ -42,7 +43,7 @@ def get_storage_instructions(self, kind_subtype: str): :returns: The API Response """ url = f'{self._service_url}/getStorageInstructions?kindSubType={kind_subtype}' - response = requests.get(url, headers=self._headers()) + response = requests.get(url, headers=AuthenticationService.get_headers(self._client)) response.raise_for_status() return response.json() @@ -55,7 +56,7 @@ def register_dataset(self, datasetRegistries: List[dict]): """ url = f'{self._service_url}/registerDataset' response = requests.put( - url, headers=self._headers(), json=datasetRegistries) + url, headers=AuthenticationService.get_headers(self._client), json=datasetRegistries) response.raise_for_status() return response.json() @@ -69,7 +70,7 @@ def get_retrieval_instructions(self, dataset_registry_ids: List[dict]): url = f'{self._service_url}/getRetrievalInstructions' data = {'datasetRegistryIds': dataset_registry_ids} response = requests.post( - url, headers=self._headers(), json=data) + url, headers=AuthenticationService.get_headers(self._client), json=data) response.raise_for_status() return response.json() diff --git a/osdu/services/entitlements.py b/osdu/services/entitlements.py index 36b9ad6..34e3c92 100644 --- a/osdu/services/entitlements.py +++ b/osdu/services/entitlements.py @@ -3,6 +3,7 @@ import json import requests from .base import BaseService +from .authentication import AuthenticationService class EntitlementsService(BaseService): @@ -22,7 +23,7 @@ def get_groups(self) -> dict: url = f'{self._service_url}/groups' query = {} - response = requests.get(url=url, headers=self._headers(), json=query) + response = requests.get(url=url, headers=AuthenticationService.get_headers(self._client), json=query) response.raise_for_status() return response.json() @@ -38,7 +39,7 @@ def get_group_members(self, groupEmail:str=None) -> dict: url = f'{self._service_url}/groups/' + groupEmail + '/members' query = '' - response = requests.get(url=url, headers=self._headers(), json=query) + response = requests.get(url=url, headers=AuthenticationService.get_headers(self._client), json=query) response.raise_for_status() return response.json() @@ -53,7 +54,7 @@ def add_group_member(self, groupEmail:str, query: dict) -> dict: """ url = f'{self._service_url}/groups/' + groupEmail + '/members' - response = requests.post(url=url, headers=self._headers(), json=query) + response = requests.post(url=url, headers=AuthenticationService.get_headers(self._client), json=query) response.raise_for_status() return response.json() @@ -68,7 +69,7 @@ def delete_group_member(self, groupEmail:str, query: dict) -> dict: """ url = f'{self._service_url}/groups/' + groupEmail + '/members' - response = requests.delete(url=url, headers=self._headers(), json=query) + response = requests.delete(url=url, headers=AuthenticationService.get_headers(self._client), json=query) response.raise_for_status() return response.json() @@ -88,6 +89,6 @@ def create_group(self, groupEmail:str, query: dict) -> dict: """ url = f'{self._service_url}/groups/' + groupEmail + '/members' - response = requests.delete(url=url, headers=self._headers(), json=query) + response = requests.delete(url=url, headers=AuthenticationService.get_headers(self._client), json=query) response.raise_for_status() return response.json() \ No newline at end of file diff --git a/osdu/services/search.py b/osdu/services/search.py index ff99722..bf38ecd 100644 --- a/osdu/services/search.py +++ b/osdu/services/search.py @@ -2,6 +2,7 @@ """ import requests from .base import BaseService +from .authentication import AuthenticationService class SearchService(BaseService): @@ -23,7 +24,7 @@ def query(self, query: dict) -> dict: query or the 1,000 record limit of the API """ url = f'{self._service_url}/query' - response = requests.post(url=url, headers=self._headers(), json=query) + response = requests.post(url=url, headers=AuthenticationService.get_headers(self._client), json=query) response.raise_for_status() return response.json() @@ -54,7 +55,7 @@ def query_with_paging(self, query: dict): query['cursor'] = cursor response = requests.post( - url=url, headers=self._headers(), json=query) + url=url, headers=AuthenticationService.get_headers(self._client), json=query) response.raise_for_status() response_values: dict = response.json() diff --git a/osdu/services/storage.py b/osdu/services/storage.py index 869e585..d6304d4 100644 --- a/osdu/services/storage.py +++ b/osdu/services/storage.py @@ -3,6 +3,7 @@ from typing import List import requests from .base import BaseService +from .authentication import AuthenticationService class StorageService(BaseService): @@ -84,7 +85,7 @@ def get_record_version(self, record_id: str, version: str): return response.json() def __execute_request(self, method: str, url: str, json=None): - headers = self._headers() + headers = AuthenticationService.get_headers(self._client) response = requests.request(method, url, headers=headers, json=json) response.raise_for_status() From b4a8c021e506f7ade7e2c9557a65d28f82d92bf1 Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Mon, 14 Mar 2022 15:09:04 -0500 Subject: [PATCH 06/19] add refresh token info in readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index c539a2f..c4f277d 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,18 @@ osdu_client = AwsOsduClient(data_partition, profile=profile) ``` +### Automatically re-authorizing the client +Each client will automatically attempt to re-authorize when its access token expires. In order for this re-authorization to succeed, you will need to supply the client with additional parameters (either through environment variables or in their consructor): +Simple Client: +1. OSDU_CLIENTWITHSECRET_ID +1. OSDU_CLIENTWITHSECRET_SECRET +1. REFRESH_TOKEN +1. REFRESH_URL +AWS Client: +1. OSDU_PASSWORD (in the environment variables, or somewhere else it can persist securely) +Service Principal: +N/A--this client can re-authorize with just the variables needed for it to instantiate + ### Using the client Below are just a few usage examples. See [integration tests](https://github.com/pariveda/osdupy/blob/master/tests/tests_integration.py) for more comprehensive usage examples. From 164432562e1bf664a294ff140d5a4a3c6ad2a70a Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Mon, 14 Mar 2022 15:12:06 -0500 Subject: [PATCH 07/19] comment formatting --- README.md | 2 ++ osdu/client/aws.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c4f277d..a46216a 100644 --- a/README.md +++ b/README.md @@ -197,8 +197,10 @@ Simple Client: 1. OSDU_CLIENTWITHSECRET_SECRET 1. REFRESH_TOKEN 1. REFRESH_URL + AWS Client: 1. OSDU_PASSWORD (in the environment variables, or somewhere else it can persist securely) + Service Principal: N/A--this client can re-authorize with just the variables needed for it to instantiate diff --git a/osdu/client/aws.py b/osdu/client/aws.py index 555a8db..920a36a 100644 --- a/osdu/client/aws.py +++ b/osdu/client/aws.py @@ -76,7 +76,7 @@ def get_tokens(self, password, secret_hash) -> None: self._access_token = response['AuthenticationResult']['AccessToken'] self._refresh_token = response['AuthenticationResult']['RefreshToken'] - # CHECK ABOUT: refresh can only be used if password is in environment variables. Or can we store the password securely? + # TODO: refresh can only be used if password is in environment variables. Is there another way to store the password securely? def update_token(self): password = os.environ.get('OSDU_PASSWORD') if(password): From 536df48d03785d1a4504f36912bfe4a5ef675a3a Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Mon, 14 Mar 2022 15:13:00 -0500 Subject: [PATCH 08/19] readme formatting --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a46216a..f1cf51c 100644 --- a/README.md +++ b/README.md @@ -192,16 +192,17 @@ osdu_client = AwsOsduClient(data_partition, ### Automatically re-authorizing the client Each client will automatically attempt to re-authorize when its access token expires. In order for this re-authorization to succeed, you will need to supply the client with additional parameters (either through environment variables or in their consructor): -Simple Client: + +#### Simple Client: 1. OSDU_CLIENTWITHSECRET_ID 1. OSDU_CLIENTWITHSECRET_SECRET 1. REFRESH_TOKEN 1. REFRESH_URL -AWS Client: +#### AWS Client: 1. OSDU_PASSWORD (in the environment variables, or somewhere else it can persist securely) -Service Principal: +#### Service Principal: N/A--this client can re-authorize with just the variables needed for it to instantiate ### Using the client From df7ce2735b37f359b70076fe7aebe54c7f29b0c4 Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Wed, 16 Mar 2022 10:37:53 -0500 Subject: [PATCH 09/19] move update_token functions from AuthenticationService to respective clients --- osdu/client/_base.py | 29 +++++++++++- osdu/client/aws.py | 4 +- osdu/client/aws_service_principal.py | 5 +- osdu/client/simple.py | 25 ++++++---- osdu/services/authentication.py | 68 ---------------------------- osdu/services/base.py | 8 ++++ osdu/services/dataset.py | 11 ++--- osdu/services/entitlements.py | 11 ++--- osdu/services/search.py | 5 +- osdu/services/storage.py | 3 +- tests/integration.py | 16 ++++--- tests/unit.py | 2 +- 12 files changed, 82 insertions(+), 105 deletions(-) delete mode 100644 osdu/services/authentication.py diff --git a/osdu/client/_base.py b/osdu/client/_base.py index fd613c8..b080a85 100644 --- a/osdu/client/_base.py +++ b/osdu/client/_base.py @@ -2,7 +2,7 @@ """ import os - +from time import time from ..services.search import SearchService from ..services.storage import StorageService from ..services.dataset import DatasetService @@ -13,6 +13,7 @@ class BaseOsduClient: @property def access_token(self): + self._ensure_valid_token() return self._access_token @property @@ -67,3 +68,29 @@ def __init__(self, data_partition_id, api_url: str = None): # TODO: Implement these services. # self.__legal = LegaService(self) + def _need_update_token(self): + return hasattr(self, "_token_expiration") and self._token_expiration < time() or self._access_token is None + + def _ensure_valid_token(self): + """Determines if the current access token associated with the client has expired. + If the token is not expired, the current access_token will be returned, unchanged. + If the token has expired, this function will attempt to refresh it, update it on client, and return it. + For simple clients, refresh requires a OSDU_CLIENTWITHSECRET_ID, OSDU_CLIENTWITHSECRET_SECRET, REFRESH_TOKEN, and REFRESH_URL + For Service Principal clients, refresh requires a resource_prefix and AWS_PROFILE (same as initial auth) + For AWS clients, refresh requires OSDU_USER, OSDU_PASSWORD, AWS_PROFILE, and OSDU_CLIENT_ID + + :param client: client in use + + :returns: tuple containing 2 items: the new access token and it's expiration time + - access_token: used to access OSDU services + - expires_in: expiration time for the token + """ + if(self._need_update_token()): + token = self._update_token() + else: + token = self._access_token, self._token_expiration if hasattr(self, "_token_expiration") else None + return token + + def _update_token(self): + pass #each client has their own update_token method + diff --git a/osdu/client/aws.py b/osdu/client/aws.py index 920a36a..0db4975 100644 --- a/osdu/client/aws.py +++ b/osdu/client/aws.py @@ -77,11 +77,11 @@ def get_tokens(self, password, secret_hash) -> None: self._refresh_token = response['AuthenticationResult']['RefreshToken'] # TODO: refresh can only be used if password is in environment variables. Is there another way to store the password securely? - def update_token(self): + def _update_token(self): password = os.environ.get('OSDU_PASSWORD') if(password): self.get_tokens(password, self._secret_hash) password = None - return self.access_token, self._token_expiration + return self._access_token, self._token_expiration return None # If we don't have a password, we can't refresh the token with the AWS client diff --git a/osdu/client/aws_service_principal.py b/osdu/client/aws_service_principal.py index 8511a73..8c5778b 100644 --- a/osdu/client/aws_service_principal.py +++ b/osdu/client/aws_service_principal.py @@ -21,5 +21,6 @@ def __init__(self, data_partition_id: str, resource_prefix: str, profile: str = def _get_tokens(self): return self._sp_util.get_service_principal_token(self._resource_prefix) - def update_token(self): - return self._sp_util.get_service_principal_token(self._resource_prefix) + def _update_token(self): + self._access_token, self._token_expiration = self._sp_util.get_service_principal_token(self._resource_prefix) + return self._access_token, self._token_expiration diff --git a/osdu/client/simple.py b/osdu/client/simple.py index e0ebd3b..6402bc3 100644 --- a/osdu/client/simple.py +++ b/osdu/client/simple.py @@ -1,4 +1,6 @@ import os +import requests +from time import time from ._base import BaseOsduClient @@ -27,13 +29,6 @@ def refresh_url(self): def refresh_token(self): return self._refresh_token - @property - def access_token(self): - return self._access_token - - @access_token.setter - def access_token(self, val): - self._access_token = val def __init__(self, data_partition_id: str, access_token: str=None, api_url: str=None, refresh_token: str=None, refresh_url: str=None) -> None: @@ -49,4 +44,18 @@ def __init__(self, data_partition_id: str, access_token: str=None, api_url: str= self._refresh_token = refresh_token or os.environ.get('OSDU_REFRESH_TOKEN') self._refresh_url = refresh_url or os.environ.get('OSDU_REFRESH_URL') self._client_id = os.environ.get('OSDU_CLIENTWITHSECRET_ID') - self._client_secret = os.environ.get('OSDU_CLIENTWITHSECRET_SECRET') \ No newline at end of file + self._client_secret = os.environ.get('OSDU_CLIENTWITHSECRET_SECRET') + + def _update_token(self) -> dict: + data = {'grant_type': 'refresh_token', + 'client_id': self._client_id, + 'client_secret': self._client_secret, + 'refresh_token': self._refresh_token, + 'scope': 'openid email'} + headers = {} + headers["Content-Type"] = "application/x-www-form-urlencoded" + response = requests.post(url=self._refresh_url,headers=headers, data=data) + response.raise_for_status() + self._access_token = response.json()["access_token"] + self._token_expiration = response.json()["expires_in"] + time() + return self._access_token, self._token_expiration \ No newline at end of file diff --git a/osdu/services/authentication.py b/osdu/services/authentication.py deleted file mode 100644 index 6a604f4..0000000 --- a/osdu/services/authentication.py +++ /dev/null @@ -1,68 +0,0 @@ -""" Provides a simple Python interface to the OSDU Authentication API. -""" -import requests -from .base import BaseService -from time import time - -class AuthenticationService(BaseService): - - def update_token(client): - """Determines if the current access token associated with the client has expired. - If the token is not expired, the current access_token will be returned, unchanged. - If the token has expired, this function will attempt to refresh it, update it on client, and return it. - For simple clients, refresh requires a OSDU_CLIENTWITHSECRET_ID, OSDU_CLIENTWITHSECRET_SECRET, REFRESH_TOKEN, and REFRESH_URL - For Service Principal clients, refresh requires a resource_prefix and AWS_PROFILE (same as initial auth) - For AWS clients, refresh requires OSDU_USER, OSDU_PASSWORD, AWS_PROFILE, and OSDU_CLIENT_ID - - :param client: client in use - - :returns: tuple containing 2 items: the new access token and it's expiration time - - access_token: used to access OSDU services - - expires_in: expiration time for the token - """ - if(AuthenticationService._need_update_token(client)): - if (hasattr(client, "resource_prefix") and client.resource_prefix is not None): #service principal client - token = AuthenticationService._update_token_service_principal(client) - elif (hasattr(client, "profile") and client.profile is not None): #aws client - token = AuthenticationService._update_token_aws(client) - else: #simple client - token = AuthenticationService._update_token_simple(client) - else: - token = client.access_token, client._token_expiration if hasattr(client, "_token_expiration") else None - return token - - def _need_update_token(client): - return hasattr(client, "_token_expiration") and client._token_expiration < time() or client.access_token is None - - - def _update_token_simple(client) -> dict: - data = {'grant_type': 'refresh_token', - 'client_id': client.client_id, - 'client_secret': client.client_secret, - 'refresh_token': client.refresh_token, - 'scope': 'openid email'} - response = requests.post(url=client.refresh_url,headers=AuthenticationService._auth_headers(), data=data) - response.raise_for_status() - - return response.json()["access_token"],response.json()["expires_in"] + time() - - def _update_token_aws(client) -> dict: - client.update_token() - return client._access_token, client._token_expiration - - def _update_token_service_principal(client) -> dict: - client.update_token() - return client._access_token, client._token_expiration - - def _auth_headers(): - return { - "Content-Type": "application/x-www-form-urlencoded", - } - - def get_headers(client): - AuthenticationService.update_token(client) - return { - "Content-Type": "application/json", - "data-partition-id": client._data_partition_id, - "Authorization": "Bearer " + client.access_token - } \ No newline at end of file diff --git a/osdu/services/base.py b/osdu/services/base.py index f71496c..dd91bff 100644 --- a/osdu/services/base.py +++ b/osdu/services/base.py @@ -3,3 +3,11 @@ class BaseService(): def __init__(self, client, service_name: str, service_version: int): self._client = client self._service_url = f'{self._client.api_url}/api/{service_name}/v{service_version}' + + + def get_headers(self): + return { + "Content-Type": "application/json", + "data-partition-id": self._client._data_partition_id, + "Authorization": "Bearer " + self._client.access_token + } \ No newline at end of file diff --git a/osdu/services/dataset.py b/osdu/services/dataset.py index 28e61d6..541ac2b 100644 --- a/osdu/services/dataset.py +++ b/osdu/services/dataset.py @@ -3,7 +3,6 @@ from typing import List import requests from .base import BaseService -from .authentication import AuthenticationService class DatasetService(BaseService): @@ -18,7 +17,7 @@ def get_dataset_registry(self, registry_id: str): :returns: The API Response """ url = f'{self._service_url}/getDatasetRegistry?id={registry_id}' - response = requests.get(url=url, headers=AuthenticationService.get_headers(self._client)) + response = requests.get(url=url, headers=self.get_headers()) response.raise_for_status() return response.json() @@ -31,7 +30,7 @@ def get_dataset_registries(self, registry_ids: List[str]): """ url = f'{self._service_url}/getDatasetRegistry?' data = {'datasetRegistryIds': registry_ids} - response = requests.post(url=url, headers=AuthenticationService.get_headers(self._client), json=data) + response = requests.post(url=url, headers=self.get_headers(), json=data) response.raise_for_status() return response.json() @@ -43,7 +42,7 @@ def get_storage_instructions(self, kind_subtype: str): :returns: The API Response """ url = f'{self._service_url}/getStorageInstructions?kindSubType={kind_subtype}' - response = requests.get(url, headers=AuthenticationService.get_headers(self._client)) + response = requests.get(url, headers=self.get_headers()) response.raise_for_status() return response.json() @@ -56,7 +55,7 @@ def register_dataset(self, datasetRegistries: List[dict]): """ url = f'{self._service_url}/registerDataset' response = requests.put( - url, headers=AuthenticationService.get_headers(self._client), json=datasetRegistries) + url, headers=self.get_headers(), json=datasetRegistries) response.raise_for_status() return response.json() @@ -70,7 +69,7 @@ def get_retrieval_instructions(self, dataset_registry_ids: List[dict]): url = f'{self._service_url}/getRetrievalInstructions' data = {'datasetRegistryIds': dataset_registry_ids} response = requests.post( - url, headers=AuthenticationService.get_headers(self._client), json=data) + url, headers=self.get_headers(), json=data) response.raise_for_status() return response.json() diff --git a/osdu/services/entitlements.py b/osdu/services/entitlements.py index 34e3c92..608e644 100644 --- a/osdu/services/entitlements.py +++ b/osdu/services/entitlements.py @@ -3,7 +3,6 @@ import json import requests from .base import BaseService -from .authentication import AuthenticationService class EntitlementsService(BaseService): @@ -23,7 +22,7 @@ def get_groups(self) -> dict: url = f'{self._service_url}/groups' query = {} - response = requests.get(url=url, headers=AuthenticationService.get_headers(self._client), json=query) + response = requests.get(url=url, headers=self.get_headers(), json=query) response.raise_for_status() return response.json() @@ -39,7 +38,7 @@ def get_group_members(self, groupEmail:str=None) -> dict: url = f'{self._service_url}/groups/' + groupEmail + '/members' query = '' - response = requests.get(url=url, headers=AuthenticationService.get_headers(self._client), json=query) + response = requests.get(url=url, headers=self.get_headers(), json=query) response.raise_for_status() return response.json() @@ -54,7 +53,7 @@ def add_group_member(self, groupEmail:str, query: dict) -> dict: """ url = f'{self._service_url}/groups/' + groupEmail + '/members' - response = requests.post(url=url, headers=AuthenticationService.get_headers(self._client), json=query) + response = requests.post(url=url, headers=self.get_headers(), json=query) response.raise_for_status() return response.json() @@ -69,7 +68,7 @@ def delete_group_member(self, groupEmail:str, query: dict) -> dict: """ url = f'{self._service_url}/groups/' + groupEmail + '/members' - response = requests.delete(url=url, headers=AuthenticationService.get_headers(self._client), json=query) + response = requests.delete(url=url, headers=self.get_headers(), json=query) response.raise_for_status() return response.json() @@ -89,6 +88,6 @@ def create_group(self, groupEmail:str, query: dict) -> dict: """ url = f'{self._service_url}/groups/' + groupEmail + '/members' - response = requests.delete(url=url, headers=AuthenticationService.get_headers(self._client), json=query) + response = requests.delete(url=url, headers=self.get_headers(), json=query) response.raise_for_status() return response.json() \ No newline at end of file diff --git a/osdu/services/search.py b/osdu/services/search.py index bf38ecd..928572b 100644 --- a/osdu/services/search.py +++ b/osdu/services/search.py @@ -2,7 +2,6 @@ """ import requests from .base import BaseService -from .authentication import AuthenticationService class SearchService(BaseService): @@ -24,7 +23,7 @@ def query(self, query: dict) -> dict: query or the 1,000 record limit of the API """ url = f'{self._service_url}/query' - response = requests.post(url=url, headers=AuthenticationService.get_headers(self._client), json=query) + response = requests.post(url=url, headers=self.get_headers(), json=query) response.raise_for_status() return response.json() @@ -55,7 +54,7 @@ def query_with_paging(self, query: dict): query['cursor'] = cursor response = requests.post( - url=url, headers=AuthenticationService.get_headers(self._client), json=query) + url=url, headers=self.get_headers(), json=query) response.raise_for_status() response_values: dict = response.json() diff --git a/osdu/services/storage.py b/osdu/services/storage.py index d6304d4..746fc58 100644 --- a/osdu/services/storage.py +++ b/osdu/services/storage.py @@ -3,7 +3,6 @@ from typing import List import requests from .base import BaseService -from .authentication import AuthenticationService class StorageService(BaseService): @@ -85,7 +84,7 @@ def get_record_version(self, record_id: str, version: str): return response.json() def __execute_request(self, method: str, url: str, json=None): - headers = AuthenticationService.get_headers(self._client) + headers = self.get_headers() response = requests.request(method, url, headers=headers, json=json) response.raise_for_status() diff --git a/tests/integration.py b/tests/integration.py index 1b3639c..7a8453f 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -21,7 +21,6 @@ AwsServicePrincipalOsduClient, SimpleOsduClient ) -from osdu.services.authentication import AuthenticationService load_dotenv(verbose=True, override=True) @@ -47,11 +46,12 @@ def test_update_token(self): "kind": f"*:*:*:*", "limit": 1 } - client = SimpleOsduClient(data_partition) - updated_access_token = AuthenticationService.update_token(client) + old_access_token = client._access_token + client._token_expiration = 0 + updated_access_token = client.access_token self.assertIsNotNone(updated_access_token) - + self.assertNotEqual(old_access_token,updated_access_token) class TestAwsOsduClient(TestCase): @@ -62,9 +62,11 @@ def test_get_access_token(self): def test_update_token(self): client = AwsOsduClient(data_partition) + old_access_token = client.access_token client._token_expiration = 0 # change the token expiration so we force a refresh - updated_access_token = AuthenticationService.update_token(client) + updated_access_token = client.access_token self.assertIsNotNone(updated_access_token) + self.assertNotEqual(old_access_token,updated_access_token) class TestAwsServicePrincipalOsduClient(TestCase): @@ -101,9 +103,11 @@ def test_update_token(self): profile=os.environ['AWS_PROFILE'], region=os.environ['AWS_DEFAULT_REGION'] ) + old_access_token = client.access_token client._token_expiration = 0 # change the token expiration so we force a refresh - updated_access_token = AuthenticationService.update_token(client) + updated_access_token = client.access_token self.assertIsNotNone(updated_access_token) + self.assertNotEqual(old_access_token,updated_access_token) class TestOsduServiceBase(TestCase): diff --git a/tests/unit.py b/tests/unit.py index 1e4c77b..de185c2 100644 --- a/tests/unit.py +++ b/tests/unit.py @@ -26,7 +26,7 @@ def test_initialize_aws_client_with_args(self, mock_b64decode, mock_session, moc self.assertIsNotNone(client) self.assertEqual(partition, client.data_partition_id) - self.assertIsNotNone(client.access_token) + self.assertIsNotNone(client._access_token) self.assertIsNotNone(client.api_url) From 6b8de66abe4aedf1ce9d3b426ed9d83fc3a43a9f Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Wed, 16 Mar 2022 10:56:13 -0500 Subject: [PATCH 10/19] code cleanup--remove unused property getter/setter and update comments --- osdu/client/simple.py | 23 +++-------------------- tests/integration.py | 2 +- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/osdu/client/simple.py b/osdu/client/simple.py index 6402bc3..44ef8d5 100644 --- a/osdu/client/simple.py +++ b/osdu/client/simple.py @@ -9,27 +9,10 @@ class SimpleOsduClient(BaseOsduClient): This client assumes you are obtaining a token yourself (e.g. via your application's login form or otheer mechanism. With this SimpleOsduClient, you simply provide that token. - With this simplicity, you are also then respnsible for reefreeshing the token as needed either by manually - re-instantiating the client with the new token or by providing the authentication client id, secret, refresh token, and refresh url. + With this simplicity, you are also then respnsible for refreshing the token as needed either by manually + re-instantiating the client with the new token or by providing the authentication client id, secret, refresh token, and refresh url + and allowing the client to attempt the refresh automatically. """ - - @property - def client_id(self): - return self._client_id - - @property - def client_secret(self): - return self._client_secret - - @property - def refresh_url(self): - return self._refresh_url - - @property - def refresh_token(self): - return self._refresh_token - - def __init__(self, data_partition_id: str, access_token: str=None, api_url: str=None, refresh_token: str=None, refresh_url: str=None) -> None: """ diff --git a/tests/integration.py b/tests/integration.py index 7a8453f..9bbec91 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -48,7 +48,7 @@ def test_update_token(self): } client = SimpleOsduClient(data_partition) old_access_token = client._access_token - client._token_expiration = 0 + client._token_expiration = 0 #change token expiration so we force an update updated_access_token = client.access_token self.assertIsNotNone(updated_access_token) self.assertNotEqual(old_access_token,updated_access_token) From 58cf62d19e9b1706e3a53eb1166b6a96512801a1 Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Wed, 16 Mar 2022 13:04:10 -0500 Subject: [PATCH 11/19] responding to coding-style PR comments --- osdu/client/_service_principal_util.py | 4 ++-- osdu/client/aws.py | 2 +- osdu/client/aws_service_principal.py | 4 +--- osdu/client/simple.py | 3 +++ osdu/services/base.py | 2 +- osdu/services/dataset.py | 10 +++++----- osdu/services/entitlements.py | 10 +++++----- osdu/services/search.py | 4 ++-- osdu/services/storage.py | 2 +- tests/unit.py | 5 +++-- 10 files changed, 24 insertions(+), 22 deletions(-) diff --git a/osdu/client/_service_principal_util.py b/osdu/client/_service_principal_util.py index cad0690..e947790 100644 --- a/osdu/client/_service_principal_util.py +++ b/osdu/client/_service_principal_util.py @@ -116,5 +116,5 @@ def get_service_principal_token(self, resource_prefix): token_url, client_id, aws_oauth_custom_scope) response = requests.post(url=token_url, headers=headers) - - return json.loads(response.content.decode())['access_token'], json.loads(response.content.decode())['expires_in'] + time() + response_json = json.loads(response.content.decode()) + return response_json['access_token'], response_json['expires_in'] + time() diff --git a/osdu/client/aws.py b/osdu/client/aws.py index 0db4975..2848721 100644 --- a/osdu/client/aws.py +++ b/osdu/client/aws.py @@ -72,7 +72,7 @@ def get_tokens(self, password, secret_hash) -> None: AuthParameters=auth_params ) - self._token_expiration = response['AuthenticationResult']["ExpiresIn"] + time() + self._token_expiration = response['AuthenticationResult']['ExpiresIn'] + time() self._access_token = response['AuthenticationResult']['AccessToken'] self._refresh_token = response['AuthenticationResult']['RefreshToken'] diff --git a/osdu/client/aws_service_principal.py b/osdu/client/aws_service_principal.py index 8c5778b..c16630c 100644 --- a/osdu/client/aws_service_principal.py +++ b/osdu/client/aws_service_principal.py @@ -12,9 +12,7 @@ def __init__(self, data_partition_id: str, resource_prefix: str, profile: str = self._sp_util = ServicePrincipalUtil( resource_prefix, profile=profile, region=region) self._resource_prefix = resource_prefix - token_and_expiration = self._get_tokens() - self._access_token = token_and_expiration[0] - self._token_expiration = token_and_expiration[1] + self._access_token,self._token_expiration = self._get_tokens() super().__init__(data_partition_id, self._sp_util.api_url) diff --git a/osdu/client/simple.py b/osdu/client/simple.py index 44ef8d5..0426413 100644 --- a/osdu/client/simple.py +++ b/osdu/client/simple.py @@ -30,6 +30,9 @@ def __init__(self, data_partition_id: str, access_token: str=None, api_url: str= self._client_secret = os.environ.get('OSDU_CLIENTWITHSECRET_SECRET') def _update_token(self) -> dict: + if not self._refresh_token or not self._refresh_url: + raise Exception('Expired or invalid access token. Both \'refresh_token\' and \'refresh_url\' must be set for token to be auto refreshed.') + data = {'grant_type': 'refresh_token', 'client_id': self._client_id, 'client_secret': self._client_secret, diff --git a/osdu/services/base.py b/osdu/services/base.py index dd91bff..d0077cd 100644 --- a/osdu/services/base.py +++ b/osdu/services/base.py @@ -5,7 +5,7 @@ def __init__(self, client, service_name: str, service_version: int): self._service_url = f'{self._client.api_url}/api/{service_name}/v{service_version}' - def get_headers(self): + def _headers(self): return { "Content-Type": "application/json", "data-partition-id": self._client._data_partition_id, diff --git a/osdu/services/dataset.py b/osdu/services/dataset.py index 541ac2b..a3a3e26 100644 --- a/osdu/services/dataset.py +++ b/osdu/services/dataset.py @@ -17,7 +17,7 @@ def get_dataset_registry(self, registry_id: str): :returns: The API Response """ url = f'{self._service_url}/getDatasetRegistry?id={registry_id}' - response = requests.get(url=url, headers=self.get_headers()) + response = requests.get(url=url, headers=self._headers()) response.raise_for_status() return response.json() @@ -30,7 +30,7 @@ def get_dataset_registries(self, registry_ids: List[str]): """ url = f'{self._service_url}/getDatasetRegistry?' data = {'datasetRegistryIds': registry_ids} - response = requests.post(url=url, headers=self.get_headers(), json=data) + response = requests.post(url=url, headers=self._headers(), json=data) response.raise_for_status() return response.json() @@ -42,7 +42,7 @@ def get_storage_instructions(self, kind_subtype: str): :returns: The API Response """ url = f'{self._service_url}/getStorageInstructions?kindSubType={kind_subtype}' - response = requests.get(url, headers=self.get_headers()) + response = requests.get(url, headers=self._headers()) response.raise_for_status() return response.json() @@ -55,7 +55,7 @@ def register_dataset(self, datasetRegistries: List[dict]): """ url = f'{self._service_url}/registerDataset' response = requests.put( - url, headers=self.get_headers(), json=datasetRegistries) + url, headers=self._headers(), json=datasetRegistries) response.raise_for_status() return response.json() @@ -69,7 +69,7 @@ def get_retrieval_instructions(self, dataset_registry_ids: List[dict]): url = f'{self._service_url}/getRetrievalInstructions' data = {'datasetRegistryIds': dataset_registry_ids} response = requests.post( - url, headers=self.get_headers(), json=data) + url, headers=self._headers(), json=data) response.raise_for_status() return response.json() diff --git a/osdu/services/entitlements.py b/osdu/services/entitlements.py index 608e644..36b9ad6 100644 --- a/osdu/services/entitlements.py +++ b/osdu/services/entitlements.py @@ -22,7 +22,7 @@ def get_groups(self) -> dict: url = f'{self._service_url}/groups' query = {} - response = requests.get(url=url, headers=self.get_headers(), json=query) + response = requests.get(url=url, headers=self._headers(), json=query) response.raise_for_status() return response.json() @@ -38,7 +38,7 @@ def get_group_members(self, groupEmail:str=None) -> dict: url = f'{self._service_url}/groups/' + groupEmail + '/members' query = '' - response = requests.get(url=url, headers=self.get_headers(), json=query) + response = requests.get(url=url, headers=self._headers(), json=query) response.raise_for_status() return response.json() @@ -53,7 +53,7 @@ def add_group_member(self, groupEmail:str, query: dict) -> dict: """ url = f'{self._service_url}/groups/' + groupEmail + '/members' - response = requests.post(url=url, headers=self.get_headers(), json=query) + response = requests.post(url=url, headers=self._headers(), json=query) response.raise_for_status() return response.json() @@ -68,7 +68,7 @@ def delete_group_member(self, groupEmail:str, query: dict) -> dict: """ url = f'{self._service_url}/groups/' + groupEmail + '/members' - response = requests.delete(url=url, headers=self.get_headers(), json=query) + response = requests.delete(url=url, headers=self._headers(), json=query) response.raise_for_status() return response.json() @@ -88,6 +88,6 @@ def create_group(self, groupEmail:str, query: dict) -> dict: """ url = f'{self._service_url}/groups/' + groupEmail + '/members' - response = requests.delete(url=url, headers=self.get_headers(), json=query) + response = requests.delete(url=url, headers=self._headers(), json=query) response.raise_for_status() return response.json() \ No newline at end of file diff --git a/osdu/services/search.py b/osdu/services/search.py index 928572b..ff99722 100644 --- a/osdu/services/search.py +++ b/osdu/services/search.py @@ -23,7 +23,7 @@ def query(self, query: dict) -> dict: query or the 1,000 record limit of the API """ url = f'{self._service_url}/query' - response = requests.post(url=url, headers=self.get_headers(), json=query) + response = requests.post(url=url, headers=self._headers(), json=query) response.raise_for_status() return response.json() @@ -54,7 +54,7 @@ def query_with_paging(self, query: dict): query['cursor'] = cursor response = requests.post( - url=url, headers=self.get_headers(), json=query) + url=url, headers=self._headers(), json=query) response.raise_for_status() response_values: dict = response.json() diff --git a/osdu/services/storage.py b/osdu/services/storage.py index 746fc58..869e585 100644 --- a/osdu/services/storage.py +++ b/osdu/services/storage.py @@ -84,7 +84,7 @@ def get_record_version(self, record_id: str, version: str): return response.json() def __execute_request(self, method: str, url: str, json=None): - headers = self.get_headers() + headers = self._headers() response = requests.request(method, url, headers=headers, json=json) response.raise_for_status() diff --git a/tests/unit.py b/tests/unit.py index de185c2..423261e 100644 --- a/tests/unit.py +++ b/tests/unit.py @@ -2,6 +2,7 @@ import hashlib import hmac from unittest import TestCase, mock +from time import time from osdu.client import ( AwsOsduClient, @@ -12,7 +13,7 @@ class TestAwsServicePrincipalOsduClient(TestCase): - @mock.patch('osdu.client._service_principal_util.ServicePrincipalUtil.get_service_principal_token') + @mock.patch('osdu.client._service_principal_util.ServicePrincipalUtil.get_service_principal_token', return_value=["testtoken",time()+ 999]) @mock.patch('boto3.Session') @mock.patch('base64.b64decode') def test_initialize_aws_client_with_args(self, mock_b64decode, mock_session, mock_sputil): @@ -26,7 +27,7 @@ def test_initialize_aws_client_with_args(self, mock_b64decode, mock_session, moc self.assertIsNotNone(client) self.assertEqual(partition, client.data_partition_id) - self.assertIsNotNone(client._access_token) + self.assertIsNotNone(client.access_token) self.assertIsNotNone(client.api_url) From a6206e9ec485e35beef88acd41d5a4b84be8a2ce Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Wed, 16 Mar 2022 13:30:36 -0500 Subject: [PATCH 12/19] add raise_for_status() check in SP client --- osdu/client/_service_principal_util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osdu/client/_service_principal_util.py b/osdu/client/_service_principal_util.py index e947790..5515969 100644 --- a/osdu/client/_service_principal_util.py +++ b/osdu/client/_service_principal_util.py @@ -114,7 +114,8 @@ def get_service_principal_token(self, resource_prefix): token_url = '{}?grant_type=client_credentials&client_id={}&scope={}'.format( token_url, client_id, aws_oauth_custom_scope) - + response = requests.post(url=token_url, headers=headers) + response.raise_for_status() response_json = json.loads(response.content.decode()) return response_json['access_token'], response_json['expires_in'] + time() From c1ea1ac778984baa8575e46e2e61fb0ec69a91b9 Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Wed, 16 Mar 2022 14:28:14 -0500 Subject: [PATCH 13/19] add log entry to service_principal_util --- osdu/client/_service_principal_util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osdu/client/_service_principal_util.py b/osdu/client/_service_principal_util.py index 5515969..0e6b64d 100644 --- a/osdu/client/_service_principal_util.py +++ b/osdu/client/_service_principal_util.py @@ -26,7 +26,9 @@ # - Refactored _get_secret method to fix UnboundLocalError for local variable 'secret'. # - Refactored _get_secret method to simplify try/except flow and to print secret_name on exception. # - Updated formatting to be PEP8-compliant. -# +# 2022-03-16 johnny.reichman@parivedasolutions.com +# - Updated to return the token expiration in addition to the token +# - Added a more descriptive exception check after the POST request import base64 from time import time import boto3 From 42bb1110b9cdb425ec8d7e7f3d111d396868650e Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Wed, 16 Mar 2022 14:47:43 -0500 Subject: [PATCH 14/19] raise exception for if aws client fails update due to no OSDU_PASSWORD --- osdu/client/aws.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osdu/client/aws.py b/osdu/client/aws.py index 2848721..27d516c 100644 --- a/osdu/client/aws.py +++ b/osdu/client/aws.py @@ -79,9 +79,11 @@ def get_tokens(self, password, secret_hash) -> None: # TODO: refresh can only be used if password is in environment variables. Is there another way to store the password securely? def _update_token(self): password = os.environ.get('OSDU_PASSWORD') + password = None if(password): self.get_tokens(password, self._secret_hash) password = None return self._access_token, self._token_expiration - return None # If we don't have a password, we can't refresh the token with the AWS client + else: + raise Exception('Expired or invalid access token. OSDU_PASSWORD env variable must be set for token to be auto refreshed.') From 01d2cb443c055f85ababddf44dca54ca2fb7c168 Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Thu, 17 Mar 2022 15:43:31 -0500 Subject: [PATCH 15/19] remove aws client password issue --- osdu/client/aws.py | 1 - 1 file changed, 1 deletion(-) diff --git a/osdu/client/aws.py b/osdu/client/aws.py index 27d516c..321258f 100644 --- a/osdu/client/aws.py +++ b/osdu/client/aws.py @@ -79,7 +79,6 @@ def get_tokens(self, password, secret_hash) -> None: # TODO: refresh can only be used if password is in environment variables. Is there another way to store the password securely? def _update_token(self): password = os.environ.get('OSDU_PASSWORD') - password = None if(password): self.get_tokens(password, self._secret_hash) password = None From 93308165181d8182b90f5d21f176b9b30cfb0c2b Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Mon, 28 Mar 2022 15:16:12 -0500 Subject: [PATCH 16/19] add legal service requests and tests --- osdu/client/_base.py | 8 +- osdu/services/legal.py | 91 +++++++++++++++++++++++ tests/integration.py | 56 ++++++++++++++ tests/test_data/test_create_legaltag.json | 16 ++++ tests/test_data/test_update_legaltag.json | 6 ++ 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 osdu/services/legal.py create mode 100644 tests/test_data/test_create_legaltag.json create mode 100644 tests/test_data/test_update_legaltag.json diff --git a/osdu/client/_base.py b/osdu/client/_base.py index b080a85..772eb3b 100644 --- a/osdu/client/_base.py +++ b/osdu/client/_base.py @@ -7,6 +7,7 @@ from ..services.storage import StorageService from ..services.dataset import DatasetService from ..services.entitlements import EntitlementsService +from ..services.legal import LegalService class BaseOsduClient: @@ -39,6 +40,10 @@ def delivery(self): @property def dataset(self): return self._dataset + + @property + def legal(self): + return self.__legal @property def data_partition_id(self): @@ -65,8 +70,7 @@ def __init__(self, data_partition_id, api_url: str = None): self._storage = StorageService(self) self._dataset = DatasetService(self) self._entitlements = EntitlementsService(self) - # TODO: Implement these services. - # self.__legal = LegaService(self) + self.__legal = LegalService(self) def _need_update_token(self): return hasattr(self, "_token_expiration") and self._token_expiration < time() or self._access_token is None diff --git a/osdu/services/legal.py b/osdu/services/legal.py new file mode 100644 index 0000000..498e4f6 --- /dev/null +++ b/osdu/services/legal.py @@ -0,0 +1,91 @@ +""" Provides a simple Python interface to the OSDU Legal API. +""" +from typing import List +import requests +from .base import BaseService + + +class LegalService(BaseService): + + def __init__(self, client): + super().__init__(client, service_name='legal', service_version=1) + + def get_legaltag(self, legaltag_name: str): + """Returns information about the given legaltag.""" + url = f'{self._service_url}/legaltags/{legaltag_name}' + response = self.__execute_request('get', url) + + return response.json() + + def create_legaltag(self, legaltag: dict): + """Create a new legaltag. """ + url = f'{self._service_url}/legaltags' + response = self.__execute_request('post', url, json=legaltag) + + return response.json() + + def delete_legaltag(self, legaltag_id: str) -> bool: + """Deletes the given legaltag. This operation cannot be reverted (except by re-creating the legaltag). + + :returns: True if legaltag deleted successfully. Otherwise False. + """ + url = f'{self._service_url}/legaltags/{legaltag_id}' + response = self.__execute_request('delete', url) + + return response.status_code == 204 + + def get_legaltags(self, isValid: bool = True): + """Fetches all matching legaltags. + + :param valid: Boolean to restrict results to only valid legaltags (true) or only invalid legal tags (false). Default is true + """ + url = f'{self._service_url}/legaltags/' + ('?valid=true' if isValid else '?valid=false') + response = self.__execute_request('get', url) + + return response.json() + + def update_legaltag(self, legaltag: dict): + """Updates a legaltag. Empty properties are ignored, not deleted. + + :param legaltag: dictionary of properties to add/change to an existing legaltag + """ + url = f'{self._service_url}/legaltags' + response = self.__execute_request('put', url, json=legaltag) + + return response.json() + + def batch_retrive_legaltags(self, legaltag_names: List[str]): + """Retrieves information about a list of legaltags + + :param legaltag_names: List of legaltag names to fetch information about + """ + url = f'{self._service_url}/legaltags:batchRetrieve' + payload = {'names': legaltag_names} + response = self.__execute_request('post', url, json=payload) + + return response.json() + + def validate_legaltags(self, legaltag_names: List[str]): + """Validates the given legaltags--returning a list of which legaltags are invalid. + + :param legaltag_names: List of legaltag names to validate + """ + url = f'{self._service_url}/legaltags:validate' + payload = {'names': legaltag_names} + response = self.__execute_request('post', url, json=payload) + + return response.json() + + def get_legaltag_properties(self): + """Fetch information about possible valuesn for legaltag properties""" + url = f'{self._service_url}/legaltags:properties' + response = self.__execute_request('get', url) + + return response.json() + + def __execute_request(self, method: str, url: str, json=None): + headers = self._headers() + response = requests.request(method, url, headers=headers, json=json) + response.raise_for_status() + + return response diff --git a/tests/integration.py b/tests/integration.py index 9bbec91..b6143ea 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -324,3 +324,59 @@ def tearDownClass(cls): for record_id in cls.test_records: cls.osdu.storage.purge_record(record_id) super().tearDownClass() + +class TestLegalService(TestOsduServiceBase): + + def test_get_legaltags(self): + result = self.osdu.legal.get_legaltags() + + self.assertTrue(len(result['legalTags']) > 0) + + def test_validate_legaltags(self): + legaltag_names = ["osdu-public-usa-dataset", "osdu-testing-legal-tag-plz-delete"] + result = self.osdu.legal.validate_legaltags(legaltag_names) + + self.assertIsNotNone(result['invalidLegalTags']) + + def test_get_legaltag_properties(self): + result = self.osdu.legal.get_legaltag_properties() + + self.assertIsNotNone(result['dataTypes']) + +class TestLegalService_WithSideEffects(TestOsduServiceBase): + + def test_001_create_legaltag(self): + test_data_file = 'tests/test_data/test_create_legaltag.json' + with open(test_data_file, 'r') as _file: + legaltag_to_store = json.load(_file) + + result = self.osdu.legal.create_legaltag(legaltag_to_store) + + self.assertIsNotNone(result["name"]) + + def test_002_get_legaltag(self): + legaltag = self.osdu.legal.get_legaltag("osdu-testing-legal-tag-plz-delete") + + self.assertIsNotNone(legaltag["name"]) + + def test_003_batch_retrieve_legaltag(self): + legaltag_names = ["osdu-public-usa-dataset", "osdu-testing-legal-tag-plz-delete"] + result = self.osdu.legal.batch_retrive_legaltags(legaltag_names) + + self.assertTrue(len(result['legalTags']) > 0) + + def test_004_update_legaltag(self): + test_data_file = 'tests/test_data/test_update_legaltag.json' + with open(test_data_file, 'r') as _file: + legaltag_to_store = json.load(_file) + + result = self.osdu.legal.update_legaltag(legaltag_to_store) + + self.assertEqual(result['description'], legaltag_to_store['description']) + + def test_005_delete_legaltag(self): + tag_was_deleted = self.osdu.legal.delete_legaltag("osdu-testing-legal-tag-plz-delete") + + self.assertTrue(tag_was_deleted) + + diff --git a/tests/test_data/test_create_legaltag.json b/tests/test_data/test_create_legaltag.json new file mode 100644 index 0000000..5a69d76 --- /dev/null +++ b/tests/test_data/test_create_legaltag.json @@ -0,0 +1,16 @@ +{ + "name": "osdu-testing-legal-tag-plz-delete", + "description": "Another default legal tag", + "properties": { + "countryOfOrigin": [ + "US" + ], + "contractId": "A1234", + "expirationDate": "2040-06-02", + "originator": "Default", + "dataType": "Public Domain Data", + "securityClassification": "Public", + "personalData": "No Personal Data", + "exportClassification": "EAR99" + } +} \ No newline at end of file diff --git a/tests/test_data/test_update_legaltag.json b/tests/test_data/test_update_legaltag.json new file mode 100644 index 0000000..e11cc01 --- /dev/null +++ b/tests/test_data/test_update_legaltag.json @@ -0,0 +1,6 @@ +{ + "name": "osdu-testing-legal-tag-plz-delete", + "contractId":"A1234", + "expirationDate":2222222222222, + "description": "new description" +} \ No newline at end of file From bf68415e3a75099b2468b84d535a75767fd8bf5e Mon Sep 17 00:00:00 2001 From: Johnny Reichman Date: Tue, 29 Mar 2022 14:06:09 -0500 Subject: [PATCH 17/19] move shared strings, variables to class constructor --- tests/integration.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/tests/integration.py b/tests/integration.py index b6143ea..a2dec4a 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -345,37 +345,40 @@ def test_get_legaltag_properties(self): class TestLegalService_WithSideEffects(TestOsduServiceBase): - def test_001_create_legaltag(self): - test_data_file = 'tests/test_data/test_create_legaltag.json' - with open(test_data_file, 'r') as _file: - legaltag_to_store = json.load(_file) + @classmethod + def setUpClass(cls): + super().setUpClass() + create_legaltag_data_file = 'tests/test_data/test_create_legaltag.json' + with open(create_legaltag_data_file, 'r') as _file: + cls.legaltag_to_create = json.load(_file) + + update_legaltag_data_file = 'tests/test_data/test_update_legaltag.json' + with open(update_legaltag_data_file, 'r') as _file: + cls.legaltag_to_update = json.load(_file) - result = self.osdu.legal.create_legaltag(legaltag_to_store) + def test_001_create_legaltag(self): + result = self.osdu.legal.create_legaltag(self.legaltag_to_create) self.assertIsNotNone(result["name"]) def test_002_get_legaltag(self): - legaltag = self.osdu.legal.get_legaltag("osdu-testing-legal-tag-plz-delete") + legaltag = self.osdu.legal.get_legaltag(self.legaltag_to_create['name']) self.assertIsNotNone(legaltag["name"]) def test_003_batch_retrieve_legaltag(self): - legaltag_names = ["osdu-public-usa-dataset", "osdu-testing-legal-tag-plz-delete"] + legaltag_names = ["osdu-public-usa-dataset", self.legaltag_to_create['name']] result = self.osdu.legal.batch_retrive_legaltags(legaltag_names) self.assertTrue(len(result['legalTags']) > 0) def test_004_update_legaltag(self): - test_data_file = 'tests/test_data/test_update_legaltag.json' - with open(test_data_file, 'r') as _file: - legaltag_to_store = json.load(_file) - - result = self.osdu.legal.update_legaltag(legaltag_to_store) + result = self.osdu.legal.update_legaltag(self.legaltag_to_update) - self.assertEqual(result['description'], legaltag_to_store['description']) + self.assertEqual(result['description'], self.legaltag_to_update['description']) def test_005_delete_legaltag(self): - tag_was_deleted = self.osdu.legal.delete_legaltag("osdu-testing-legal-tag-plz-delete") + tag_was_deleted = self.osdu.legal.delete_legaltag(self.legaltag_to_create['name']) self.assertTrue(tag_was_deleted) From b0d0ca62e5a4bdd2c3510e167a0bb2b4b1d28d83 Mon Sep 17 00:00:00 2001 From: Johnny Reichman <88909002+JohnnyReichmanPariveda@users.noreply.github.com> Date: Tue, 29 Mar 2022 14:19:12 -0500 Subject: [PATCH 18/19] update README--add legal service --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index f1cf51c..979f949 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,15 @@ For OSDU on AWS, this client is useful in the case where you may want to perform - add_group_member - delete_group_member - create_group +- [legal](osdu/services/legal.py) + - get_legaltag + - create_legaltag + - delete_legaltag + - get_legaltags + - update_legaltag + - batch_retrive_legaltags + - validate_legaltags + - get_legaltag_properties ## Installation From 48a313f3976eee8a9854d26c489d320c13797597 Mon Sep 17 00:00:00 2001 From: Johnny Reichman <88909002+JohnnyReichmanPariveda@users.noreply.github.com> Date: Tue, 29 Mar 2022 14:32:44 -0500 Subject: [PATCH 19/19] update comments --- osdu/services/legal.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osdu/services/legal.py b/osdu/services/legal.py index 498e4f6..28a0dc0 100644 --- a/osdu/services/legal.py +++ b/osdu/services/legal.py @@ -11,35 +11,42 @@ def __init__(self, client): super().__init__(client, service_name='legal', service_version=1) def get_legaltag(self, legaltag_name: str): - """Returns information about the given legaltag.""" + """Returns information about the given legaltag. + + param legaltag_name: the name of the legaltag of interest + """ url = f'{self._service_url}/legaltags/{legaltag_name}' response = self.__execute_request('get', url) return response.json() def create_legaltag(self, legaltag: dict): - """Create a new legaltag. """ + """Create a new legaltag. + + param legaltag: a JSON representation of a legaltag + """ url = f'{self._service_url}/legaltags' response = self.__execute_request('post', url, json=legaltag) return response.json() - def delete_legaltag(self, legaltag_id: str) -> bool: + def delete_legaltag(self, legaltag_name: str) -> bool: """Deletes the given legaltag. This operation cannot be reverted (except by re-creating the legaltag). + :param legaltag_name: the name of the legaltag to delete :returns: True if legaltag deleted successfully. Otherwise False. """ - url = f'{self._service_url}/legaltags/{legaltag_id}' + url = f'{self._service_url}/legaltags/{legaltag_name}' response = self.__execute_request('delete', url) return response.status_code == 204 - def get_legaltags(self, isValid: bool = True): + def get_legaltags(self, valid: bool = True): """Fetches all matching legaltags. :param valid: Boolean to restrict results to only valid legaltags (true) or only invalid legal tags (false). Default is true """ - url = f'{self._service_url}/legaltags/' + ('?valid=true' if isValid else '?valid=false') + url = f'{self._service_url}/legaltags/' + ('?valid=true' if valid else '?valid=false') response = self.__execute_request('get', url) return response.json() @@ -77,7 +84,7 @@ def validate_legaltags(self, legaltag_names: List[str]): return response.json() def get_legaltag_properties(self): - """Fetch information about possible valuesn for legaltag properties""" + """Fetch information about possible values for legaltag properties""" url = f'{self._service_url}/legaltags:properties' response = self.__execute_request('get', url)