Repository: SneaksAndData/adapta (language: python)

Issue:
[FEATURE] Add a function for creating orphan Vault token with specified lifetime
We would like to be able to issue Vault tokens for CLI clients what do not have access to browser. Thus a user should authenticate on a dev station through a browser, and then run a simple func to get a short-lived orphan Vault token for other purposes.


Discussion / hints:

Fix patch:
diff --git a/adapta/security/clients/__init__.py b/adapta/security/clients/__init__.py
index f75b4e65..8dba4f69 100644
--- a/adapta/security/clients/__init__.py
+++ b/adapta/security/clients/__init__.py
@@ -41,6 +41,13 @@
 except ImportError:
     pass
 
+try:
+    from adapta.security.clients.hashicorp_vault.token_client import (
+        HashicorpVaultTokenClient,
+    )
+except ImportError:
+    pass
+
 try:
     from adapta.security.clients._azure_client import AzureClient
 except ImportError:
diff --git a/adapta/security/clients/hashicorp_vault/token_client.py b/adapta/security/clients/hashicorp_vault/token_client.py
new file mode 100644
index 00000000..497bd637
--- /dev/null
+++ b/adapta/security/clients/hashicorp_vault/token_client.py
@@ -0,0 +1,59 @@
+"""
+ Hashicorp Vault implementation of AuthenticationClient.
+"""
+#  Copyright (c) 2023. ECCO Sneaks & Data
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+
+from typing import Optional
+
+import hvac
+
+from adapta.security.clients._base import AuthenticationClient
+from adapta.security.clients.hashicorp_vault.hashicorp_vault_client import (
+    HashicorpVaultClient,
+)
+
+
+class HashicorpVaultTokenClient(HashicorpVaultClient):
+    """
+    Hashicorp vault Credentials provider for K8S.
+    """
+
+    @staticmethod
+    def from_base_client(client: AuthenticationClient) -> Optional["HashicorpVaultTokenClient"]:
+        """
+         Safe casts AuthenticationClient to HashicorpVaultClient if type checks out.
+
+        :param client: AuthenticationClient
+        :return: HashicorpVaultKubernetesClient or None if type does not check out
+        """
+        if isinstance(client, HashicorpVaultTokenClient):
+            return client
+
+        return None
+
+    def __init__(self, vault_address: str, access_token: str):
+        """
+        Initialization logic for Kubernetes auth method
+        :param vault_address: Address of hashicorp vault instance
+        """
+        super().__init__(vault_address)
+        self._client = hvac.Client(url=self._vault_address, token=access_token)
+
+    def get_credentials(self):
+        self.get_access_token()
+
+    def get_access_token(self, scope: Optional[str] = None) -> str:
+        return self._client.token
diff --git a/adapta/storage/secrets/README.md b/adapta/storage/secrets/README.md
index a86bde14..373d5950 100644
--- a/adapta/storage/secrets/README.md
+++ b/adapta/storage/secrets/README.md
@@ -6,6 +6,7 @@ Supported API:
 
 ## Example usage
 
+### Azure 
 ```python
 from adapta.security.clients import AzureClient
 from adapta.storage.secrets.azure_secret_client import AzureSecretStorageClient
@@ -17,3 +18,37 @@ my_secret = azure_secrets.read_secret('my-keyvault', 'my-secret')
 
 print(my_secret)
 ```
+
+### Hashicorp vault
+```python
+from adapta.security.clients import HashicorpVaultOidcClient, HashicorpVaultTokenClient
+```
+Create a HashicorpVaultTokenClient Instance
+To read secrets from HashiCorpVault, you need to create a HashicorpVaultTokenClient instance. This
+instance requires the address of your HashiCorpVault instance and an access token fetched from HashicorpVaultOidcClient
+to authenticate the client. You must run this code on a machine that has access to a web browser:
+
+```python
+oidc_client = HashicorpVaultOidcClient(TEST_VAULT_ADDRESS)
+access_token = oidc_client.get_access_token()
+token_client = HashicorpVaultTokenClient(TEST_VAULT_ADDRESS, access_token)
+client = HashicorpSecretStorageClient(base_client=token_client)
+```
+
+Read Secrets from HashiCorpVault
+Once you have created a HashicorpVaultTokenClient instance,
+you can read secrets from HashiCorpVault using the read_secret() method. This method takes two arguments:
+the name of the secret engine and the path to the secret:
+
+```python
+secret = client.read_secret("secret", "test/secret/with/path")
+```
+The above code reads a secret from the "secret" engine located at "test/secret/with/path".
+
+Access Secret Values
+Once you have read a secret, you can access its values using the key-value pairs returned by the read_secret() method:
+
+```python
+print(secret["key"])
+```
+The above code prints the value of the "key" field in the secret.


Test patch:
diff --git a/tests/test_vault_client.py b/tests/test_vault_client.py
index 4403fac9..77881df8 100644
--- a/tests/test_vault_client.py
+++ b/tests/test_vault_client.py
@@ -17,7 +17,7 @@
 
 import pytest
 
-from adapta.security.clients import HashicorpVaultClient, HashicorpVaultOidcClient
+from adapta.security.clients import HashicorpVaultOidcClient, HashicorpVaultTokenClient
 from adapta.security.clients.hashicorp_vault.kubernetes_client import (
     HashicorpVaultKubernetesClient,
 )
@@ -30,14 +30,23 @@
 
 @pytest.mark.skip("Uses desktop browser to login, should be only run locally")
 def test_oidc_credentials():
-    client = HashicorpVaultClient(TEST_VAULT_ADDRESS)
+    client = HashicorpVaultOidcClient(TEST_VAULT_ADDRESS)
     credentials = client.get_credentials()
     assert credentials is not None
 
 
 @pytest.mark.skip("Uses desktop browser to login, should be only run locally")
 def test_oidc_auth():
-    client = HashicorpSecretStorageClient(base_client=HashicorpVaultClient(TEST_VAULT_ADDRESS))
+    client = HashicorpSecretStorageClient(base_client=HashicorpVaultOidcClient(TEST_VAULT_ADDRESS))
+    secret = client.read_secret("secret", "test/secret/with/path")
+    assert secret["key"] == "value"
+
+
+@pytest.mark.skip("Uses desktop browser to login, should be only run locally")
+def test_token_auth():
+    oidc_client = HashicorpVaultOidcClient(TEST_VAULT_ADDRESS)
+    token_client = HashicorpVaultTokenClient(TEST_VAULT_ADDRESS, oidc_client.get_access_token())
+    client = HashicorpSecretStorageClient(base_client=token_client)
     secret = client.read_secret("secret", "test/secret/with/path")
     assert secret["key"] == "value"