From 60683383f99e02752697601a80964fd5f1f5a8d9 Mon Sep 17 00:00:00 2001 From: Mario Weigel Date: Fri, 19 Apr 2024 00:31:46 +1200 Subject: [PATCH] LDAP secrets engine enhancements (#1163) * Add additional mount configuration parameters * Add method to rotate static role credentials manually * Update documentation --- docs/usage/secrets_engines/ldap.rst | 35 +++++++++++++ hvac/api/secrets_engines/ldap.py | 51 +++++++++++++++++-- .../api/secrets_engines/test_ldap.py | 33 ++++++++++++ 3 files changed, 115 insertions(+), 4 deletions(-) diff --git a/docs/usage/secrets_engines/ldap.rst b/docs/usage/secrets_engines/ldap.rst index f3e830f0..5e670473 100644 --- a/docs/usage/secrets_engines/ldap.rst +++ b/docs/usage/secrets_engines/ldap.rst @@ -49,6 +49,23 @@ Source reference: :py:meth:`hvac.api.secrets_engines.ldap.read_config` config_response = client.secrets.ldap.read_config() +Rotate Root +--------------------------- + +Rotate the password for the binddn entry used to manage LDAP. This generated password will only be known to Vault and will not be retrievable once rotated. + +Source reference: :py:meth:`hvac.api.secrets_engines.ldap.rotate_root` + +.. code:: python + + import hvac + client = hvac.Client() + + # Authenticate to Vault using client.auth.x + + rotate_response = client.secrets.ldap.rotate_root() + + Create or Update Static Role ---------------------------- @@ -122,6 +139,7 @@ Source reference: :py:meth:`hvac.api.secrets_engines.ldap.delete_static_role` deletion_response = client.secrets.ldap.delete_static_role(name='sql-service-account') + Generate Static Credentials --------------------------- @@ -144,3 +162,20 @@ Source reference: :py:meth:`hvac.api.secrets_engines.ldap.generate_static_creden access=gen_creds_response['data']['current_password'], secret=gen_creds_response['data']['old_password'], )) + + +Rotate Static Credentials +--------------------------- + +Manually rotate the password of an existing role. + +Source reference: :py:meth:`hvac.api.secrets_engines.ldap.rotate_static_credentials` + +.. code:: python + + import hvac + client = hvac.Client() + + # Authenticate to Vault using client.auth.x + + rotate_response = client.secrets.ldap.rotate_static_credentials(name='hvac-role') \ No newline at end of file diff --git a/hvac/api/secrets_engines/ldap.py b/hvac/api/secrets_engines/ldap.py index 369a8db5..1d8c133c 100644 --- a/hvac/api/secrets_engines/ldap.py +++ b/hvac/api/secrets_engines/ldap.py @@ -22,6 +22,13 @@ def configure( userdn=None, userattr=None, upndomain=None, + connection_timeout=None, + request_timeout=None, + starttls=None, + insecure_tls=None, + certificate=None, + client_tls_cert=None, + client_tls_key=None, mount_point=DEFAULT_MOUNT_POINT, ): """Configure shared information for the ldap secrets engine. @@ -43,6 +50,20 @@ def configure( :type password_policy: str | unicode :param schema: The LDAP schema to use when storing entry passwords. Valid schemas include ``openldap``, ``ad``, and ``racf``. :type schema: str | unicode + :param connection_timeout: Timeout, in seconds, when attempting to connect to the LDAP server before trying the next URL in the configuration. + :type connection_timeout: int | str + :param request_timeout: Timeout, in seconds, for the connection when making requests against the server before returning back an error. + :type request_timeout: int | str + :param starttls: If true, issues a StartTLS command after establishing an unencrypted connection. + :type starttls: bool + :param insecure_tls: If true, skips LDAP server SSL certificate verification - insecure, use with caution! + :type insecure_tls: bool + :param certificate: CA certificate to use when verifying LDAP server certificate, must be x509 PEM encoded. + :type certificate: str | unicode + :param client_tls_cert: Client certificate to provide to the LDAP server, must be x509 PEM encoded. + :type client_tls_cert: str | unicode + :param client_tls_key: Client key to provide to the LDAP server, must be x509 PEM encoded. + :type client_tls_key: str | unicode :param mount_point: The "path" the method/backend was mounted on. :type mount_point: str | unicode :return: The response of the request. @@ -58,6 +79,13 @@ def configure( "upndomain": upndomain, "password_policy": password_policy, "schema": schema, + "connection_timeout": connection_timeout, + "request_timeout": request_timeout, + "starttls": starttls, + "insecure_tls": insecure_tls, + "certificate": certificate, + "client_tls_cert": client_tls_cert, + "client_tls_key": client_tls_key, } ) @@ -123,7 +151,7 @@ def create_or_update_static_role( This is provided as a string duration with a time suffix like "30s" or "1h" or as seconds. If not provided, the default Vault rotation_period is used. :type rotation_period: str | unicode - :param mount_point: Specifies the place where the secrets engine will be accessible (default: ad). + :param mount_point: The "path" the method/backend was mounted on. :type mount_point: str | unicode :return: The response of the request. :rtype: requests.Response @@ -141,7 +169,7 @@ def read_static_role(self, name, mount_point=DEFAULT_MOUNT_POINT): If no role exists with that name, a 404 is returned. :param name: Specifies the name of the static role to query. :type name: str | unicode - :param mount_point: Specifies the place where the secrets engine will be accessible (default: ad). + :param mount_point: The "path" the method/backend was mounted on. :type mount_point: str | unicode :return: The response of the request. :rtype: requests.Response @@ -166,7 +194,7 @@ def delete_static_role(self, name, mount_point=DEFAULT_MOUNT_POINT): Even if the role does not exist, this endpoint will still return a successful response. :param name: Specifies the name of the role to delete. :type name: str | unicode - :param mount_point: Specifies the place where the secrets engine will be accessible (default: ad). + :param mount_point: The "path" the method/backend was mounted on. :type mount_point: str | unicode :return: The response of the request. :rtype: requests.Response @@ -182,7 +210,7 @@ def generate_static_credentials(self, name, mount_point=DEFAULT_MOUNT_POINT): :param name: Specifies the name of the static role to request credentials from. :type name: str | unicode - :param mount_point: Specifies the place where the secrets engine will be accessible (default: ad). + :param mount_point: The "path" the method/backend was mounted on. :type mount_point: str | unicode :return: The response of the request. :rtype: requests.Response @@ -191,3 +219,18 @@ def generate_static_credentials(self, name, mount_point=DEFAULT_MOUNT_POINT): return self._adapter.get( url=api_path, ) + + def rotate_static_credentials(self, name, mount_point=DEFAULT_MOUNT_POINT): + """This endpoint rotates the password of an existing static role. + + :param name: Specifies the name of the static role to rotate credentials for. + :type name: str | unicode + :param mount_point: The "path" the method/backend was mounted on. + :type mount_point: str | unicode + :return: The response of the request. + :rtype: requests.Response + """ + api_path = utils.format_url("/v1/{}/rotate-role/{}", mount_point, name) + return self._adapter.post( + url=api_path, + ) diff --git a/tests/unit_tests/api/secrets_engines/test_ldap.py b/tests/unit_tests/api/secrets_engines/test_ldap.py index 512ea9cb..cf39e4af 100644 --- a/tests/unit_tests/api/secrets_engines/test_ldap.py +++ b/tests/unit_tests/api/secrets_engines/test_ldap.py @@ -37,6 +37,10 @@ def test_configure(self, test_label, mount_point, requests_mocker): userattr=None, schema=None, userdn="ou=users,dc=example,dc=com", + connection_timeout="60s", + request_timeout="30s", + starttls=False, + insecure_tls=False, ) self.assertEqual( @@ -244,6 +248,35 @@ def test_generate_static_credentials( second=response, ) + @parameterized.expand( + [ + ("default mount point", DEFAULT_MOUNT_POINT), + ("custom mount point", "other-ldap-tree"), + ] + ) + @requests_mock.Mocker() + def test_rotate_static_credentials(self, test_label, mount_point, requests_mocker): + expected_status_code = 204 + role_name = "hvac" + mock_url = "http://localhost:8200/v1/{mount_point}/rotate-role/{name}".format( + mount_point=mount_point, + name=role_name, + ) + requests_mocker.register_uri( + method="POST", + url=mock_url, + status_code=expected_status_code, + ) + ldap = Ldap(adapter=JSONAdapter()) + response = ldap.rotate_static_credentials( + name=role_name, + mount_point=mount_point, + ) + self.assertEqual( + first=expected_status_code, + second=response.status_code, + ) + @parameterized.expand( [ ("default mount point", DEFAULT_MOUNT_POINT),