Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LDAP secret engine support (#1032) #1033

Merged
merged 28 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5d526ba
Initial commit for LDAP secrets engine
JordanStopford Aug 23, 2023
1b5aaad
Fix docs and linting issues
JordanStopford Aug 29, 2023
682d5eb
Fix linting error
JordanStopford Sep 4, 2023
df51d22
Run tests with docker container so we don't need to install vault
JordanStopford Mar 13, 2024
2dc7358
More tests
JordanStopford Mar 13, 2024
d1f5fd2
Merge branch 'main' into ldap-secrets
JordanStopford Mar 13, 2024
922bc96
Fix indentation
JordanStopford Mar 13, 2024
72258d3
Fix client not being available
JordanStopford Mar 13, 2024
1161ea4
Various test fixes
JordanStopford Mar 13, 2024
3adf09f
Merge branch 'hvac:main' into ldap-secrets
JordanStopford Mar 20, 2024
0feed5c
Reverting the changes prior to implementing unit tests
JordanStopford Mar 20, 2024
deb7d5b
Reverting the changes prior to implementing unit tests
JordanStopford Mar 20, 2024
c6221ae
Reverting the changes prior to implementing unit tests
JordanStopford Mar 20, 2024
a6a2602
Unit tests for LDAP secrets
JordanStopford Mar 20, 2024
f8e56cd
Reverting the changes prior to implementing unit tests
JordanStopford Mar 20, 2024
cda852f
Linting
JordanStopford Mar 20, 2024
0dc0cc4
Fix newline?
JordanStopford Mar 20, 2024
776db59
Fix newline?
JordanStopford Mar 20, 2024
777f961
Fix linting
JordanStopford Mar 20, 2024
476f6bc
Merge branch 'hvac:main' into ldap-secrets
JordanStopford Mar 22, 2024
a53faba
Apply suggestions from code review
briantist Apr 13, 2024
6a5d830
Update hvac/api/secrets_engines/ldap.py
briantist Apr 13, 2024
bd4b2a9
nit: remove docs character
briantist Apr 13, 2024
c705eb9
remove use of arbitrary kwargs
briantist Apr 13, 2024
d07671a
use example.com in tests
briantist Apr 13, 2024
cbe3f83
add unit test for generate_static_credentials
briantist Apr 13, 2024
f199003
Merge branch 'main' into pr/JordanStopford/1033
briantist Apr 13, 2024
d166125
Merge branch 'main' into pr/JordanStopford/1033
briantist Apr 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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='[email protected]', # A upn or DN can be used for this value, Vault resolves the user to a dn silently
bindpass='***********',
url='ldaps:https://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,
)
Loading
Loading