Skip to content

Commit

Permalink
LDAP secret engine support (#1032) (#1033)
Browse files Browse the repository at this point in the history
* Initial commit for LDAP secrets engine
No dynamic role support yet

* Fix docs and linting issues

* Fix linting error

* Run tests with docker container so we don't need to install vault
Use LDAP server in docker as well
Configure Vault/LDAP with terraform
Added LDAP tests - not yet finished

* More tests

* Fix indentation

* Fix client not being available

* Various test fixes

* Reverting the changes prior to implementing unit tests

* Reverting the changes prior to implementing unit tests

* Reverting the changes prior to implementing unit tests

* Unit tests for LDAP secrets

* Reverting the changes prior to implementing unit tests

* Linting

* Fix newline?

* Fix newline?

* Fix linting

* Apply suggestions from code review

Documentation updates

* Update hvac/api/secrets_engines/ldap.py

remove unused args/kwargs

* nit: remove docs character

* remove use of arbitrary kwargs

* use example.com in tests

* add unit test for generate_static_credentials

---------

Co-authored-by: Brian Scholer <1260690+briantist@users.noreply.github.com>
  • Loading branch information
JordanStopford and briantist committed Apr 13, 2024
1 parent 781156f commit bedff7c
Show file tree
Hide file tree
Showing 5 changed files with 662 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/usage/secrets_engines/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Secrets Engines
database
gcp
identity
ldap
pki
kv
kv_v1
Expand Down
146 changes: 146 additions & 0 deletions docs/usage/secrets_engines/ldap.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
LDAP
================

.. contents::

Configure LDAP Secrets Secrets Engine
-------------------------------------

Configure the LDAP secrets engine to either manage service accounts or service account libraries.

Source reference: :py:meth:`hvac.api.secrets_engines.ldap.configure`

.. code:: python
import hvac
client = hvac.Client()
# Authenticate to Vault using client.auth.x
# Not all these settings may apply to your setup, refer to Vault
# documentation for context of what to use here
config_response = client.secrets.ldap.configure(
binddn='username@domain.fqdn', # A upn or DN can be used for this value, Vault resolves the user to a dn silently
bindpass='***********',
url='ldaps://domain.fqdn',
userdn='cn=Users,dn=domain,dn=fqdn',
upndomain='domain.fqdn',
userattr="cn",
schema="openldap"
)
print(config_response)
Read Config
-----------

Return the LDAP Secret Engine configuration.

Source reference: :py:meth:`hvac.api.secrets_engines.ldap.read_config`

.. code:: python
import hvac
client = hvac.Client()
# Authenticate to Vault using client.auth.x
config_response = client.secrets.ldap.read_config()
Create or Update Static Role
----------------------------

Create or Update a role which allows the retrieval and rotation of an LDAP account. Retrieve and rotate the actual credential via generate_static_credentials().

Source reference: :py:meth:`hvac.api.secrets_engines.ldap.create_or_update_static_role`

.. code:: python
import hvac
client = hvac.Client()
# Authenticate to Vault using client.auth.x
role_response = client.secrets.ldap.create_or_update_static_role(
name='hvac-role',
username='sql-service-account',
dn='cn=sql-service-account,dc=petshop,dc=com',
rotation_period="60s")
Read Static Role
----------------

Retrieve the role configuration which allows the retrieval and rotation of an LDAP account. Retrieve and rotate the actual credential via generate_static_credentials().

Source reference: :py:meth:`hvac.api.secrets_engines.ldap.read_static_role`

.. code:: python
import hvac
client = hvac.Client()
# Authenticate to Vault using client.auth.x
role_response = client.secrets.ldap.read_static_role(name='sql-service-account')
List Static Roles
-----------------

List all configured roles which allows the retrieval and rotation of an LDAP account. Retrieve and rotate the actual credential via generate_static_credentials().

Source reference: :py:meth:`hvac.api.secrets_engines.ldap.list_static_roles`

.. code:: python
import hvac
client = hvac.Client()
# Authenticate to Vault using client.auth.x
all_static_roles = client.secrets.ldap.list_static_roles()
Delete Static Role
------------------

Remove the role configuration which allows the retrieval and rotation of an LDAP account.

Passwords are not rotated upon deletion of a static role. The password should be manually rotated prior to deleting the role or revoking access to the static role.

Source reference: :py:meth:`hvac.api.secrets_engines.ldap.delete_static_role`

.. code:: python
import hvac
client = hvac.Client()
# Authenticate to Vault using client.auth.x
deletion_response = client.secrets.ldap.delete_static_role(name='sql-service-account')
Generate Static Credentials
---------------------------

Retrieve a service account password from LDAP. Return the previous password (if known). Vault shall rotate
the password before returning it, if it has breached its configured ttl.

Source reference: :py:meth:`hvac.api.secrets_engines.ldap.generate_static_credentials`

.. code:: python
import hvac
client = hvac.Client()
# Authenticate to Vault using client.auth.x
gen_creds_response = client.secrets.ldap.generate_static_credentials(
name='hvac-role',
)
print('Retrieved Service Account Password: {access} (Current) / {secret} (Old)'.format(
access=gen_creds_response['data']['current_password'],
secret=gen_creds_response['data']['old_password'],
))
3 changes: 3 additions & 0 deletions hvac/api/secrets_engines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from hvac.api.secrets_engines.kv import Kv
from hvac.api.secrets_engines.kv_v1 import KvV1
from hvac.api.secrets_engines.kv_v2 import KvV2
from hvac.api.secrets_engines.ldap import Ldap
from hvac.api.secrets_engines.pki import Pki
from hvac.api.secrets_engines.rabbitmq import RabbitMQ
from hvac.api.secrets_engines.ssh import Ssh
Expand All @@ -25,6 +26,7 @@
"Kv",
"KvV1",
"KvV2",
"Ldap",
"Pki",
"Transform",
"Transit",
Expand All @@ -45,6 +47,7 @@ class SecretsEngines(VaultApiCategory):
ActiveDirectory,
Identity,
Kv,
Ldap,
Pki,
Transform,
Transit,
Expand Down
193 changes: 193 additions & 0 deletions hvac/api/secrets_engines/ldap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#!/usr/bin/env python
"""LDAP methods module."""

from hvac import utils
from hvac.api.vault_api_base import VaultApiBase

DEFAULT_MOUNT_POINT = "ldap"


class Ldap(VaultApiBase):
"""LDAP Secrets Engine (API).
Reference: https://www.vaultproject.io/api/secret/ldap/index.html
"""

def configure(
self,
binddn=None,
bindpass=None,
url=None,
password_policy=None,
schema=None,
userdn=None,
userattr=None,
upndomain=None,
mount_point=DEFAULT_MOUNT_POINT,
):
"""Configure shared information for the ldap secrets engine.
Supported methods:
POST: /{mount_point}/config. Produces: 204 (empty body)
:param binddn: Distinguished name of object to bind when performing user and group search.
:type binddn: str | unicode
:param bindpass: Password to use along with binddn when performing user search.
:type bindpass: str | unicode
:param url: Base DN under which to perform user search.
:type url: str | unicode
:param userdn: Base DN under which to perform user search.
:type userdn: str | unicode
:param upndomain: userPrincipalDomain used to construct the UPN string for the authenticating user.
:type upndomain: str | unicode
:param password_policy: The name of the password policy to use to generate passwords.
: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 mount_point: The "path" the method/backend was mounted on.
:type mount_point: str | unicode
:return: The response of the request.
:rtype: requests.Response
"""
params = utils.remove_nones(
{
"binddn": binddn,
"bindpass": bindpass,
"url": url,
"userdn": userdn,
"userattr": userattr,
"upndomain": upndomain,
"password_policy": password_policy,
"schema": schema,
}
)

api_path = utils.format_url("/v1/{mount_point}/config", mount_point=mount_point)
return self._adapter.post(
url=api_path,
json=params,
)

def read_config(self, mount_point=DEFAULT_MOUNT_POINT):
"""Read the configured shared information for the ldap secrets engine.
Credentials will be omitted from returned data.
Supported methods:
GET: /{mount_point}/config. Produces: 200 application/json
:param mount_point: The "path" the method/backend was mounted on.
:type mount_point: str | unicode
:return: The JSON response of the request.
:rtype: dict
"""
api_path = utils.format_url("/v1/{mount_point}/config", mount_point=mount_point)
return self._adapter.get(
url=api_path,
)

def rotate_root(self, mount_point=DEFAULT_MOUNT_POINT):
"""Rotate the root password for the binddn entry used to manage the ldap secrets engine.
Supported methods:
POST: /{mount_point}/rotate root. Produces: 200 application/json
:param mount_point: The "path" the method/backend was mounted on.
:type mount_point: str | unicode
:return: The JSON response of the request.
:rtype: dict
"""
api_path = utils.format_url(
"/v1/{mount_point}/rotate-root", mount_point=mount_point
)
return self._adapter.post(url=api_path)

def create_or_update_static_role(
self,
name,
username=None,
dn=None,
rotation_period=None,
mount_point=DEFAULT_MOUNT_POINT,
):
"""This endpoint creates or updates the ldap static role definition.
:param name: Specifies the name of an existing static role against which to create this ldap credential.
:type name: str | unicode
:param username: The name of a pre-existing service account in LDAP that maps to this static role.
This value is required on create and cannot be updated.
:type username: str | unicode
:param dn: Distinguished name of the existing LDAP entry to manage password rotation for (takes precedence over username).
Optional but cannot be modified after creation.
:type dn: str | unicode
:param rotation_period: How often Vault should rotate the password.
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).
:type mount_point: str | unicode
:return: The response of the request.
:rtype: requests.Response
"""
api_path = utils.format_url("/v1/{}/static-role/{}", mount_point, name)
params = {"username": username, "rotation_period": rotation_period}
params.update(utils.remove_nones({"dn": dn}))
return self._adapter.post(
url=api_path,
json=params,
)

def read_static_role(self, name, mount_point=DEFAULT_MOUNT_POINT):
"""This endpoint queries for information about an ldap static role with the given name.
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).
:type mount_point: str | unicode
:return: The response of the request.
:rtype: requests.Response
"""
api_path = utils.format_url("/v1/{}/static-role/{}", mount_point, name)
return self._adapter.get(
url=api_path,
)

def list_static_roles(self, mount_point=DEFAULT_MOUNT_POINT):
"""This endpoint lists all existing static roles in the secrets engine.
:return: The response of the request.
:rtype: requests.Response
"""
api_path = utils.format_url("/v1/{}/static-role", mount_point)
return self._adapter.list(
url=api_path,
)

def delete_static_role(self, name, mount_point=DEFAULT_MOUNT_POINT):
"""This endpoint deletes an ldap static role with the given name.
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).
:type mount_point: str | unicode
:return: The response of the request.
:rtype: requests.Response
"""
api_path = utils.format_url("/v1/{}/static-role/{}", mount_point, name)
return self._adapter.delete(
url=api_path,
)

def generate_static_credentials(self, name, mount_point=DEFAULT_MOUNT_POINT):
"""This endpoint retrieves the previous and current LDAP password for
the associated account (or rotate if required)
: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).
:type mount_point: str | unicode
:return: The response of the request.
:rtype: requests.Response
"""
api_path = utils.format_url("/v1/{}/static-cred/{}", mount_point, name)
return self._adapter.get(
url=api_path,
)

0 comments on commit bedff7c

Please sign in to comment.