Skip to content

Commit

Permalink
Merge pull request #20 from OneIdentity/gaspar-master
Browse files Browse the repository at this point in the history
use new plugin sdk features
  • Loading branch information
gorosz committed Nov 5, 2020
2 parents 5cbc5ec + 502d188 commit 5c7c621
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 198 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
2.0.0 2020-08-05

Use new credential store base class features to make plugin code easier to maintain

1.1.2 2019-11-15

Give use_credential parameter a default value, as written in the default.cfg
Expand Down
4 changes: 2 additions & 2 deletions MANIFEST
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
api: 1.3
api: 1.6
type: credentialstore
name: SPS_hashicorp
description: One Identity Safeguard for Privileged Hashicorp Vault plugin
version: 1.1.2
version: 2.0.0
entry_point: main.py
author_name: One Identity PAM Integration Team
author_email: QDL.QBU-OI.RD.Safeguard.Integration@oneidentity.com
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[packages]

[dev-packages]
oneidentity-safeguard-sessions-plugin-sdk = "~=1.3.0"
oneidentity-safeguard-sessions-plugin-sdk = "~=1.6.0"

[requires]
python_version = "3.6"
104 changes: 25 additions & 79 deletions lib/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from safeguard.sessions.plugin.requests_tls import RequestsTLS
from safeguard.sessions.plugin.logging import get_logger
from safeguard.sessions.plugin import PluginSDKRuntimeError
from safeguard.sessions.plugin.endpoint_extractor import EndpointExtractor, EndpointException


logger = get_logger(__name__)
Expand All @@ -35,10 +36,6 @@ class VaultException(PluginSDKRuntimeError):
pass


class CredentialsCannotBeDeterminedException(PluginSDKRuntimeError):
pass


class InvalidConfigurationError(PluginSDKRuntimeError):
pass

Expand All @@ -63,6 +60,10 @@ def secret_retriever(self):
def session_factory(self):
return self.__requests_tls

@staticmethod
def token_header_from(token):
return {"X-Vault-Token": token}

def get_secret(self, key):
with self.__requests_tls.open_session() as session:
client_token = self.__authenticator.authenticate(session)
Expand All @@ -79,14 +80,16 @@ def _determine_vault_to_use(cls, requests_tls, vault_addresses, vault_port):
)
try:
logger.debug("Try to setup connection to Vault on address: {}".format(vault_address))
response = _invoke_http_method(session, vault_url + "/v1/sys/health", None, "get")
response = EndpointExtractor().invoke_http_method(session, vault_url + "/v1/sys/health", "get")
if response.status_code in cls.USABLE_HEALTH_RESPONSE_CODES:
logger.info(
'Using Vault {}; status="{}"'.format(
vault_url, cls.USABLE_HEALTH_RESPONSE_CODES[response.status_code],
)
)
break
except EndpointException as exc:
raise VaultException("Connection error: {}".format(exc))
except VaultException:
logger.error("Cannot connect to vault on the following address: {}".format(vault_url))
continue
Expand All @@ -95,7 +98,7 @@ def _determine_vault_to_use(cls, requests_tls, vault_addresses, vault_port):
return vault_url

@classmethod
def create_client(cls, config, gw_user=None, gw_password=None, secrets_path=None):
def create_client(cls, config, auth_username, auth_password, secrets_path=None):
requests_tls = RequestsTLS.from_config(config)
vault_addresses = list(map(lambda va: va.strip(), config.get("hashicorp", "address", required=True).split(",")))
vault_port = config.getint("hashicorp", "port", default=8200)
Expand All @@ -110,27 +113,19 @@ def create_client(cls, config, gw_user=None, gw_password=None, secrets_path=None
else:
raise InvalidConfigurationError("No valid secrets engine can be determined based on config")

authenticator = AuthenticatorFactory.create_authenticator(config, vault_url, gw_user, gw_password)
authenticator = AuthenticatorFactory.create_authenticator(config, vault_url, auth_username, auth_password)
return cls(requests_tls, authenticator, secret_retriever)


class AuthenticatorFactory:
@classmethod
def create_authenticator(cls, config, vault_url, gw_user=None, gw_password=None):
def create_authenticator(cls, config, vault_url, auth_username, auth_password):
auth_method = config.getienum(
"hashicorp", "authentication_method", ("ldap", "userpass", "approle"), required=True
)
logger.debug("Authenticating to vault with method: {}".format(auth_method))
if auth_method in ("ldap", "userpass"):
username = cls.credential_from_config(config, "username") or gw_user
password = cls.credential_from_config(config, "password") or gw_password
if username and password:
authenticator = PasswordTypeAuthenticator(vault_url, username, password, auth_method)
else:
raise CredentialsCannotBeDeterminedException(
"Cannot determine credentials for Vault",
variables={"username": username, "password": "<password>" if password else "N/A"},
)
authenticator = PasswordTypeAuthenticator(vault_url, auth_username, auth_password, auth_method)
elif auth_method == "approle":
role = config.get("approle-authentication", "role", required=True)
vault_token = config.get("approle-authentication", "vault_token")
Expand All @@ -139,14 +134,6 @@ def create_authenticator(cls, config, vault_url, gw_user=None, gw_password=None)
raise InvalidConfigurationError("No valid authentication method can be determined based on config")
return authenticator

@staticmethod
def credential_from_config(config, option_name):
return (
config.get("hashicorp", option_name, required=True)
if config.getienum("hashicorp", "use_credential", ("explicit", "gateway"), default="gateway") == "explicit"
else None
)


class SecretRetriever(abc.ABC):
@abc.abstractmethod
Expand All @@ -160,11 +147,11 @@ def __init__(self, vault_url, secrets_path):
self.__secrets_path = secrets_path

def retrieve_secret(self, session, key, client_token):
secret = _extract_data_from_endpoint(
secret = EndpointExtractor().extract_data_from_endpoint(
session,
endpoint_url=self.__vault_url + "/v1/" + self.__secrets_path,
data_path="data." + key,
token=client_token,
headers=Client.token_header_from(client_token),
method="get",
)
return secret
Expand All @@ -191,25 +178,26 @@ def authentication_backend(self):
return "approle"

def authenticate(self, session):
role_id = _extract_data_from_endpoint(
endpoint_extractor = EndpointExtractor(self.__vault_url)
role_id = endpoint_extractor.extract_data_from_endpoint(
session,
endpoint_url=self.__vault_url + "/v1/auth/approle/role/" + self.__role + "/role-id",
endpoint_url="/v1/auth/approle/role/" + self.__role + "/role-id",
data_path="data.role_id",
token=self.__vault_token,
headers=Client.token_header_from(self.__vault_token),
method="get",
)
secret_id = _extract_data_from_endpoint(
secret_id = endpoint_extractor.extract_data_from_endpoint(
session,
endpoint_url=self.__vault_url + "/v1/auth/approle/role/" + self.__role + "/secret-id",
endpoint_url="/v1/auth/approle/role/" + self.__role + "/secret-id",
data_path="data.secret_id",
token=self.__vault_token,
headers=Client.token_header_from(self.__vault_token),
method="post",
)
client_token = _extract_data_from_endpoint(
client_token = endpoint_extractor.extract_data_from_endpoint(
session,
endpoint_url=self.__vault_url + "/v1/auth/approle/login",
endpoint_url="/v1/auth/approle/login",
data_path="auth.client_token",
token=self.__vault_token,
headers=Client.token_header_from(self.__vault_token),
method="post",
data={"role_id": role_id, "secret_id": secret_id},
)
Expand All @@ -233,53 +221,11 @@ def __calculate_endpoint(self):
def authenticate(self, session):
logger.debug("Performing Userpass authentication for user {}".format(self.__username))
endpoint = self.__calculate_endpoint()
client_token = _extract_data_from_endpoint(
client_token = EndpointExtractor().extract_data_from_endpoint(
session,
endpoint_url=endpoint,
data_path="auth.client_token",
token=None,
method="post",
data={"password": self.__password},
)
return client_token


def _extract_data_from_endpoint(session, endpoint_url, data_path, token, method, data=None):
response = _invoke_http_method(session, endpoint_url, token, method, data)
if response.ok:

def follow_path(d, path):
return d if not path else follow_path(d[path[0]], path[1:])

# not logging the response as it may contain sensitive data
logger.debug("Got 200 OK response from endpoint: {}".format(endpoint_url))
try:
return follow_path(json.loads(response.text), data_path.split("."))
except (KeyError, TypeError):
raise VaultException("Vault response did not contain the information", dict(data_path=data_path))
else:
raise VaultException(
"Vault response status not ok",
dict(
status_code=response.status_code,
status_text=responses.get(response.status_code, "UNKNOWN"),
endpoint_url=endpoint_url,
raw_response=response.text,
),
)


def _invoke_http_method(session, endpoint_url, token, method, data=None):
if token:
headers = {"X-Vault-Token": token}
else:
headers = {}
logger.debug('Sending http request to Hashicorp Vault, endpoint_url="{}", method="{}"'.format(endpoint_url, method))
try:
return (
session.get(endpoint_url, headers=headers)
if method == "get"
else session.post(endpoint_url, headers=headers, data=json.dumps(data) if data else None)
)
except requests.exceptions.ConnectionError as exc:
raise VaultException("Connection error: {}".format(exc))
16 changes: 4 additions & 12 deletions lib/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Plugin(CredentialStorePlugin):
}

def __init__(self, configuration):
super().__init__(configuration)
super().__init__(configuration, configuration_section="hashicorp")

def do_get_password_list(self):
try:
Expand All @@ -46,8 +46,8 @@ def do_get_password_list(self):

vault_client = Client.create_client(
self.plugin_configuration,
self.connection.gateway_username,
self.connection.gateway_password,
self.authentication_username,
self.authentication_password,
secret_path,
)
password = vault_client.get_secret(secret_field)
Expand All @@ -57,16 +57,8 @@ def do_get_password_list(self):
return None

def do_get_private_key_list(self):
def determine_keytype(key):
if key.startswith("-----BEGIN RSA PRIVATE KEY-----"):
return "ssh-rsa"
elif key.startswith("-----BEGIN DSA PRIVATE KEY-----"):
return "ssh-dss"
else:
self.logger.error("Unsupported key type")

def get_supported_key(key):
return list(filter(lambda key_pair: key_pair[0], [(determine_keytype(key), key)]))
return list(filter(lambda key_pair: key_pair[0], [(self.determine_key_type(key), key)]))

try:
secret_path, secret_field = self.secret_path_and_field(
Expand Down
23 changes: 23 additions & 0 deletions lib/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,26 @@ def _make_config(auth_method, secrets_path=site_parameters["secrets_path"], extr
)

return _make_config


@pytest.fixture
def generate_params():
def _params(cookie={}, session_cookie={}, server_username=None, protocol=None):
return {
"cookie": cookie,
"session_cookie": session_cookie,
"session_id": "example-1",
"client_ip": "1.1.1.1",
"client_hostname": None,
"gateway_domain": None,
"gateway_username": "wsmith",
"gateway_password": "",
"gateway_groups": None,
"server_username": server_username,
"server_ip": "1.2.3.4",
"server_port": 22,
"server_hostname": None,
"server_domain": None,
"protocol": protocol,
}
return _params

0 comments on commit 5c7c621

Please sign in to comment.