ldap2pg¶
À propos
Mainteneur | Dalibo |
---|---|
License | PostgreSQL |
Site | labs.dalibo.com/ldap2pg/ |
Documentation | ldap2pg.readthedocs.io |
Sources | github.com/dalibo/ldap2pg/ |
Compatibilité | PostgreSQL 9.5 et supérieur |
Page rédigée pour ldap2pg 6.1.
ldap2pg est un outils en ligne de commande pour provisionner les rôles et les privilèges dans PostgreSQL. ldap2pg sait interroger un serveur LDAP pour provisionner les rôles à partir d’un annuaire d’entreprise. La synchronisation est réalisée en trois étapes : recherche de l’état voulue, inspection de PostgreSQL, génération des requêtes nécessaires.
Fonctionnalités¶
- Configurable à partir d’un fichier YAML expressif et souple.
- Crée, modifie et supprime les rôles dans PostgreSQL.
- Crée des rôles statiques à partir du YAML.
- Gère l’héritage des rôles (groupe).
- Accorde et révoque des privilèges.
- Mode audit.
- Traces chaque recherche LDAP sous forme de commande ldaputils.
- Traces chaque requêtes SQL.
Installation¶
ldap2pg fournit des paquets dans le dépôt Dalibo Labs YUM et APT. Écrit en Go, ldap2pg est un binaire unique et portable.
Configuration¶
pg_hba.conf
ldap2pg ne configure pas le fichier pg_hba.conf
.
Une fois l’authentification effective, passer à ldap2pg pour provisionner les rôles.
La configuration de ldap2pg se divise en trois parties:
- accès PostgreSQL.
- accès LDAP.
- règles de synchronisation.
Pour faciliter le partage du fichier de configuration entre différents environnements, l’accès à PostgreSQL et l’annuaire est découplé de la configuration de la synchronisation.
De plus, pour la rédaction de la configuration, il est recommandé de configurer l’environnement pour que psql et ldaputils accèdent aux serveurs. Si psql et ldaputils ont accès à leur serveurs respectifs, alors ldap2pg est configuré.
ldap2pg requiert un fichier de configuration au format YAML : ldap2pg.yml
.
Fichier ldap2pg.yml complet.
version: 6
# Inspection de PostgreSQL.
postgres:
databases_query: [nominal] # (1)!
# Profils de privilèges
privileges:
lecture:
- __connect__
- __select_on_tables__
- __select_on_sequences__
- __usage_on_schemas__
- __usage_on_types__
écriture:
- __temporary__
- __all_on_tables__
- __all_on_sequences__
création:
- __create_on_schemas__
# Règles de synchronisation
rules:
- description: "Définition statique des groupes et privilèges."
roles:
- names:
- lecteurs
options: NOLOGIN
- name: éditeurs
# Accorder la lecture par héritage.
parent: lecteurs
options: NOLOGIN
- name: propriétaires
# Accorder la lecture et l'écriture par héritage.
parent: éditeurs
options: NOLOGIN
grant:
- privilege: lecture
role: lecteurs
schemas: nominal # (2)!
- privilege: écriture
role: éditeurs
- privilege: création
role: propriétaires
- description: "Rechercher dans l'annuaire les utilisateurs lecteurs, éditeurs et propriétaires."
ldapsearch:
base: ou=groupes,dc=bridoulou,dc=fr
filter: "
(|
(cn=propriétaires)
(cn=lecteurs)
(cn=éditeurs)
)
"
role:
name: '{member.cn}'
options: LOGIN
parent: "{cn}"
- Restriction à une base de donnée arbitraire.
- Restriction à un seul schémas.
Synchronisation¶
Par défaut, ldap2pg ne modifie pas l’instance PostgreSQL.
Les traces affichent simplement les opérations prévues.
Utiliser l’option --real
pour effectivement synchroniser PostgreSQL.
Exécution de ldap2pg
$ ldap2pg
10:40:22 INFO Starting ldap2pg version=v6.1 runtime=go1.21.0 commit=549ba0c8
10:40:22 WARN Running a prerelease! Use at your own risks!
10:40:22 INFO Using YAML configuration file. path=./ldap2pg.yml
10:40:22 INFO Running as unprivileged user. user=ldap2pg super=false server="PostgreSQL 15.4" cluster=ldap2pg-dev database=nominal
10:40:22 INFO Connected to LDAP directory. uri=ldaps://ldap.ldap2pg.docker authzid="dn:cn=ldap2pg,dc=bridoulou,dc=fr"
10:40:22 INFO Setup static roles and grants.
10:40:22 INFO Search LDAP to create lecteurs, éditeurs and propriétaires.
10:40:22 WARN Dry run. Postgres instance will be untouched.
10:40:22 CHANGE Would Inherit role for management. role=lecteurs database=nominal
10:40:22 CHANGE Would Alter options. role=lecteurs options=NOLOGIN database=nominal
10:40:22 CHANGE Would Set role comment. role=lecteurs current="" wanted="Managed by ldap2pg" database=nominal
10:40:22 CHANGE Would Create role. role=éditeurs parents=[lecteurs] database=nominal
10:40:22 CHANGE Would Set role comment. role=éditeurs database=nominal
10:40:22 CHANGE Would Inherit role for management. role=éditeurs database=nominal
10:40:22 CHANGE Would Inherit role for management. role=propriétaires database=nominal
10:40:22 CHANGE Would Grant missing parents. role=propriétaires parents=[éditeurs] database=nominal
10:40:22 CHANGE Would Set role comment. role=propriétaires current="" wanted="Managed by ldap2pg" database=nominal
10:40:22 CHANGE Would Create role. role=alain parents=[lecteurs] database=nominal
10:40:22 CHANGE Would Set role comment. role=alain database=nominal
10:40:22 CHANGE Would Inherit role for management. role=alain database=nominal
10:40:22 CHANGE Would Inherit role for management. role=alizée database=nominal
10:40:22 CHANGE Would Alter options. role=alizée options="LOGIN CONNECTION LIMIT -1" database=nominal
10:40:22 CHANGE Would Grant missing parents. role=alizée parents=[éditeurs] database=nominal
10:40:22 CHANGE Would Revoke spurious parents. role=alizée parents=[propriétaires] database=nominal
10:40:22 CHANGE Would Set role comment. role=alizée current="" wanted="Managed by ldap2pg" database=nominal
10:40:22 CHANGE Would Create role. role=charles parents=[éditeurs] database=nominal
10:40:22 CHANGE Would Set role comment. role=charles database=nominal
10:40:22 CHANGE Would Inherit role for management. role=charles database=nominal
10:40:22 CHANGE Would Inherit role for management. role=alter database=nominal
10:40:22 CHANGE Would Alter options. role=alter options=LOGIN database=nominal
10:40:22 CHANGE Would Grant missing parents. role=alter parents=[propriétaires] database=nominal
10:40:22 CHANGE Would Set role comment. role=alter current="" wanted="Managed by ldap2pg" database=nominal
10:40:22 CHANGE Would Create role. role=corinne parents=[lecteurs] database=nominal
10:40:22 CHANGE Would Set role comment. role=corinne database=nominal
10:40:22 CHANGE Would Inherit role for management. role=corinne database=nominal
10:40:22 CHANGE Would Create role. role=clothilde parents=[propriétaires] database=nominal
10:40:22 CHANGE Would Set role comment. role=clothilde database=nominal
10:40:22 CHANGE Would Inherit role for management. role=clothilde database=nominal
10:40:22 CHANGE Would Terminate running sessions. role=daniel database=nominal
10:40:22 CHANGE Would Allow current user to reassign objects. role=daniel parent=ldap2pg database=nominal
10:40:22 CHANGE Would Reassign objects and purge ACL. role=daniel owner=nominal database=nominal
10:40:22 CHANGE Would Drop role. role=daniel database=nominal
10:40:22 CHANGE Would Allow current user to reassign objects. role=local_parent parent=ldap2pg database=nominal
10:40:22 CHANGE Would Reassign objects and purge ACL. role=local_parent owner=nominal database=nominal
10:40:22 CHANGE Would Drop role. role=local_parent database=nominal
10:40:22 CHANGE Would Allow current user to reassign objects. role=nicolas parent=ldap2pg database=nominal
10:40:22 CHANGE Would Reassign objects and purge ACL. role=nicolas owner=nominal database=nominal
10:40:22 CHANGE Would Drop role. role=nicolas database=nominal
10:40:22 CHANGE Would Allow current user to reassign objects. role="domitille with space" parent=ldap2pg database=nominal
10:40:22 CHANGE Would Reassign objects and purge ACL. role="domitille with space" owner=nominal database=nominal
10:40:22 CHANGE Would Drop role. role="domitille with space" database=nominal
10:40:22 CHANGE Would Allow current user to reassign objects. role=ldap_roles parent=ldap2pg database=nominal
10:40:22 CHANGE Would Reassign objects and purge ACL. role=ldap_roles owner=nominal database=nominal
10:40:22 CHANGE Would Drop role. role=ldap_roles database=nominal
10:40:22 CHANGE Would Grant privilege. grant="SELECT ON ALL SEQUENCES IN SCHEMA nominal.nominal TO lecteurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="SELECT ON ALL SEQUENCES IN SCHEMA nominal.nominal TO éditeurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="UPDATE ON ALL SEQUENCES IN SCHEMA nominal.nominal TO éditeurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="USAGE ON ALL SEQUENCES IN SCHEMA nominal.nominal TO éditeurs" database=nominal
10:40:22 CHANGE Would Revoke privilege. grant="PARTIAL UPDATE ON ALL TABLES IN SCHEMA nominal.nominal TO lecteurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="SELECT ON ALL TABLES IN SCHEMA nominal.nominal TO lecteurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="DELETE ON ALL TABLES IN SCHEMA nominal.nominal TO éditeurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="INSERT ON ALL TABLES IN SCHEMA nominal.nominal TO éditeurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="REFERENCES ON ALL TABLES IN SCHEMA nominal.nominal TO éditeurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="SELECT ON ALL TABLES IN SCHEMA nominal.nominal TO éditeurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="TRIGGER ON ALL TABLES IN SCHEMA nominal.nominal TO éditeurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="TRUNCATE ON ALL TABLES IN SCHEMA nominal.nominal TO éditeurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="UPDATE ON ALL TABLES IN SCHEMA nominal.nominal TO éditeurs" database=nominal
10:40:22 CHANGE Would Revoke privilege. grant="CONNECT ON DATABASE nominal TO public" database=""
10:40:22 CHANGE Would Revoke privilege. grant="TEMPORARY ON DATABASE nominal TO public" database=""
10:40:22 CHANGE Would Grant privilege. grant="CONNECT ON DATABASE nominal TO lecteurs" database=""
10:40:22 CHANGE Would Grant privilege. grant="TEMPORARY ON DATABASE nominal TO éditeurs" database=""
10:40:22 CHANGE Would Revoke privilege. grant="USAGE ON SCHEMA nominal.public TO public" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="USAGE ON SCHEMA nominal.nominal TO lecteurs" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="CREATE ON SCHEMA nominal.nominal TO propriétaires" database=nominal
10:40:22 CHANGE Would Grant privilege. grant="CREATE ON SCHEMA nominal.public TO propriétaires" database=nominal
10:40:22 INFO All default privileges configured. database=nominal
10:40:22 INFO Comparison complete. searches=1 roles=9 queries=83 grants=30
10:40:22 INFO Use --real option to apply changes.
10:40:22 INFO Done. elapsed=83.257071ms mempeak=1.6MiB ldap=865.347µs inspect=19.119837ms sync=0s
$
Exécution¶
Pour synchroniser régulièrement, planifier une tâche dans cron pour exécuter ldap2pg. La fréquence dépend de plusieurs paramètres.
- l’impact de la synchronisation sur l’instance PostgreSQL et l’annuaire.
- le délai minimum attendu entre une modification dans l’annuaire et son application.
# Synchroniser les rôles et les privilèges de PostgreSQL tous les quarts d'heure.
*/15 * * * * ldap2pg --real --config /var/lib/postgresql/ldap2pg.yml
ldap2pg chargera le fichier .env
et le fichier ldaprc
du dossier /var/lib/postgresql
.
Mode audit¶
ldap2pg permet de vérifier qu’une instance PostgreSQL est synchronisée avec l’option --check
.
En cas de désynchronisation de l’instance PostgreSQL, ldap2pg retourne un code d’erreur.
L’instance PostgreSQL sera intacte.
Habilitations dynamiques¶
Si la configuration de synchronisation dépend de données d’habilitations dans un service, un fichier CSV, une table dans la base de donnée ou autre sources, rédiger un script pour charger les habilitations et générer la configuration ldap2pg.yml dynamiquement. Un script Python chargeant ces données et les injectant dans un template Jinja2 fait bien l’affaire.
#!/usr/bin/python3
#
# Script pour récupérer des données externes
# et les injecter dans un template de configuration ldap2pg.
import jinja2
import os
import sys
import contextlib
# Cas simple: lit l'application de la machine depuis une variable d'environement
data = dict(
app=os.environ['APP], # ERP, CRM, etc.
env=os.environ['ENVIRONMENT'], # PROD, PREPROD, DEV
)
env = jinja2.Environment(loader=jinja2.FileSystemLoader(os.getcwd()),autoescape=False,undefined=jinja2.StrictUndefined,trim_blocks=True)
template = env.get_template(sys.argv[1])
print(template.render(**data))
# Template de configuration ldap2pg
version: 6
privileges:
reader:
- __connect__
- __usage_on_schemas__
- __select_on_tables__
- __select_on_sequences__
writer:
- reader
- __delete_on_all_tables__
- __insert_on_all_tables__
- __update_on_all_tables__
- __usage_on_all_sequences__
rules:
- roles:
- name: dev
options: NOLOGIN
grant:
- role: dev
{% if env == 'PROD' %}
privilege: reader
{% else %}
privilege: writer
{% end %}
- ldapsearch:
base: ou=Users,dc=bridoulou,dc=fr
filter: "'(memberOf:1.2.840.113556.1.4.1941:=cn=app_{{ app }},ou=groups,dc=bridoulou,dc=fr)'"
roles:
- name: "{sAMAccountName}"
parent: dev
options: LOGIN
Rediriger la sortie du script en entrée de ldap2pg
et indiquer à ldap2pg de lire la configuration depuis l’entrée standard avec --config=-
.
Aller plus loin¶
Pour aller plus loin, consulter la référence de la configuration dans la documentation publique du projet. Le projet fournit également quelques guides pour des cas courants dans un cahier de recette.