From 46754f6b1cdf3fa2722f9d6f220cd4772ead1128 Mon Sep 17 00:00:00 2001 From: Marjorie Lucas <47004511+marjo-luc@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:22:56 -0700 Subject: [PATCH] secret management (#104) * wip * cleanup * updated get_secret endpoint to return value * added tests for secrets management * updated error handling | added logging * review comments --- maap/Secrets.py | 131 ++++++++++++++++++++++++++++++++++++++++ maap/maap.py | 4 +- maap/utils/endpoints.py | 2 + test/functional_test.py | 35 +++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 maap/Secrets.py diff --git a/maap/Secrets.py b/maap/Secrets.py new file mode 100644 index 0000000..51f75cf --- /dev/null +++ b/maap/Secrets.py @@ -0,0 +1,131 @@ +import requests +import logging +import json +from maap.utils import endpoints +from maap.utils import requests_utils +from maap.utils import endpoints + +logger = logging.getLogger(__name__) + +class Secrets: + """ + Functions used for member secrets API interfacing + """ + def __init__(self, member_endpoint, api_header): + self._api_header = api_header + self._members_endpoint = f"{member_endpoint}/{endpoints.MEMBERS_SECRETS}" + + + def get_secrets(self): + """ + Returns a list of secrets for a given user. + + Returns: + list: Returns a list of dicts containing secret names e.g. [{'secret_name': 'secret1'}, {'secret_name': 'secret2'}]. + """ + try: + response = requests.get( + url = self._members_endpoint, + headers=self._api_header + ) + logger.debug(f"Response from get_secrets request: {response.text}") + return json.loads(response.text) + except Exception as e: + raise(f"Error retrieving secrets: {e}") + + + def get_secret(self, secret_name): + """ + Returns secret value for provided secret name. + + Args: + secret_name (str, required): Secret name. + + Returns: + string: Secret value. + + Raises: + ValueError: If secret name is not provided. + """ + if secret_name is None: + raise ValueError("Secret name parameter cannot be None.") + + try: + response = requests.get( + url = f"{self._members_endpoint}/{secret_name}", + headers=self._api_header + ) + + # Return secret value directly for user ease-of-use + if response.ok: + response = response.json() + return response["secret_value"] + + logger.debug(f"Response from get_secret request: {response.text}") + return json.loads(response.text) + except Exception as e: + raise(f"Error retrieving secret: {e}") + + + def add_secret(self, secret_name=None, secret_value=None): + """ + Adds a secret. Secret name must be provided. Secret value may be null. + + Args: + secret_name (str, required): Secret name. + secret_value (str, optional): Secret value. + + Returns: + dict: Containing name and value of secret that was just added. + + Raises: + ValueError: If secret name or secret value is not provided. + """ + if secret_name is None or secret_value is None: + raise ValueError("Failed to add secret. Secret name and secret value must not be 'None'.") + + try: + response = requests.post( + url = self._members_endpoint, + headers=self._api_header, + data=json.dumps({"secret_name": secret_name, "secret_value": secret_value}) + ) + + logger.debug(f"Response from add_secret: {response.text}") + return json.loads(response.text) + except Exception as e: + raise(f"Error adding secret: {e}") + + + def delete_secret(self, secret_name=None): + """ + Deletes a secret. + + Args: + secret_name (str, required): Secret name. + + Returns: + dict: Containing response code and message indicating whether or not deletion was successful. + + Raises: + ValueError: If secret name is not provided. + """ + if secret_name is None: + raise ValueError("Failed to delete secret. Please provide secret name.") + + try: + response = requests.delete( + url = f"{self._members_endpoint}/{secret_name}", + headers=self._api_header + ) + + logger.debug(f"Response from delete_secret: {response.text}") + return json.loads(response.text) + except Exception as e: + raise(f"Error deleting secret: {e}") + + + + + + diff --git a/maap/maap.py b/maap/maap.py index 7b2858d..19514f1 100644 --- a/maap/maap.py +++ b/maap/maap.py @@ -17,6 +17,7 @@ from maap.utils import algorithm_utils from maap.Profile import Profile from maap.AWS import AWS +from maap.Secrets import Secrets from maap.dps.DpsHelper import DpsHelper from maap.utils import endpoints from maap.utils import job @@ -41,10 +42,11 @@ def __init__(self, maap_host=os.getenv('MAAP_API_HOST', 'api.maap-project.org')) self.config.workspace_bucket_credentials, self._get_api_header() ) + self.secrets = Secrets(self.config.member, self._get_api_header(content_type="application/json")) def _get_api_header(self, content_type=None): - api_header = {'Accept': content_type if content_type else self.config.content_type, 'token': self.config.maap_token} + api_header = {'Accept': content_type if content_type else self.config.content_type, 'token': self.config.maap_token, 'Content-Type': content_type if content_type else self.config.content_type} if os.environ.get("MAAP_PGT"): api_header['proxy-ticket'] = os.environ.get("MAAP_PGT") diff --git a/maap/utils/endpoints.py b/maap/utils/endpoints.py index 33bc816..8937f8e 100644 --- a/maap/utils/endpoints.py +++ b/maap/utils/endpoints.py @@ -3,3 +3,5 @@ DPS_JOB_DISMISS = "cancel" DPS_JOB_LIST = "list" CMR_ALGORITHM_DATA = "data" + +MEMBERS_SECRETS = "secrets" diff --git a/test/functional_test.py b/test/functional_test.py index 64a3580..de55154 100644 --- a/test/functional_test.py +++ b/test/functional_test.py @@ -108,6 +108,31 @@ def cancel_job(maap: MAAP, job_id): assert resp is not None assert 'Accepted' in str(resp) +@log_decorator +def add_secret(maap: MAAP, secret_name=None, secret_value=None): + resp = maap.secrets.add_secret(secret_name, secret_value) + print(resp) + assert resp is not None + +@log_decorator +def get_secrets(maap: MAAP): + resp = maap.secrets.get_secrets() + print(resp) + assert resp is not None + +@log_decorator +def get_secret(maap: MAAP, secret_name=None, secret_value=None): + resp = maap.secrets.get_secret(secret_name) + print(resp) + assert resp is not None + assert resp == secret_value + +@log_decorator +def delete_secret(maap: MAAP, secret_name=None): + resp = maap.secrets.delete_secret(secret_name) + print(resp) + assert resp is not None + def main(): if os.environ.get('MAAP_PGT') is None: @@ -118,9 +143,19 @@ def main(): # list_algorithms(maap) job = submit_job(maap, queue="maap-dps-sandbox") cancel_job(maap, job.id) + + # Test secrets management + secret_name = "test_secret" + secret_value = "test_value" + get_secrets(maap) + add_secret(maap, secret_name, secret_value) + get_secret(maap, secret_name, secret_value) + delete_secret(maap, secret_name) + # submit_job(maap, wait_for_completion=True) # delete_algorithm(maap, "maap_functional_test_algo:main") + if __name__ == '__main__': main()