SVN synchronisation groupes LDAP : Différence entre versions
m |
(Aucune différence)
|
Version actuelle en date du 27 mai 2019 à 23:15
La gestion des droits utilisateurs se base sur les fichiers internes de SVN. La mise en place de l'authentification est décrite dans l'article SVN authentification LDAP. Or comment faire pour que les comptes déclarés dans l'annuaire LDAP soit automatiquement repris dans ces fichiers ?
Il n'y a malheureusement pas de solution miracle et il est nécessaire de compléter le fichier conf/authz
, pour une installation sur Ubuntu, pour donner la composition des groupes ainsi que leur droit d'accès.
Heureusement, une personne a bien voulu écrire un petit script pour faciliter la tache. Les deux pages suivantes ont été consultées pour la mise en place de cette synchronisation:
- http://www.thoughtspark.org/node/26
- https://bitbucket.org/jcscoobyrs/jw-tools/src/e3396390e99b/sync_ldap_groups_to_svn_authz/sync_ldap_groups_to_svn_authz.py
Sommaire
Votre avis
Current user rating: 92/100 (4 votes)
|
|
Script
Le script nécessite une connexion à l'annuaire LDAP. Etant écrit en python, il est nécessaire d'installer le package python-ldap
, avec la traditionnelle commande apt-get sous Ubuntu:
#sudo apt-get install python-ldap Lecture des listes de paquets... Fait Construction de l'arbre des dépendances Lecture des informations d'état... Fait Paquets suggérés : python-ldap-doc Les NOUVEAUX paquets suivants seront installés : python-ldap 0 mis à jour, 1 nouvellement installés, 0 à enlever et 0 non mis à jour. Il est nécessaire de prendre 87,7 ko dans les archives. Après cette opération, 434 ko d'espace disque supplémentaires seront utilisés. Réception de : 1 http://fr.archive.ubuntu.com/ubuntu/ oneiric/main python-ldap amd64 2.3.13-1 [87,7 kB] 87,7 ko réceptionnés en 0s (323 ko/s) Sélection du paquet python-ldap précédemment désélectionné. (Lecture de la base de données... 145590 fichiers et répertoires déjà installés.) Dépaquetage de python-ldap (à partir de .../python-ldap_2.3.13-1_amd64.deb) ... Paramétrage de python-ldap (2.3.13-1) ...
Source
Les sources originales sont:
#!/usr/bin/env python
#
# -*-python-*-
#
################################################################################
# License
################################################################################
# Copyright (c) 2006 Jeremy Whitlock. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
################################################################################
import ConfigParser, datetime, getpass, os, re, sys, tempfile
from optparse import OptionParser
try:
import ldap
except ImportError:
print("Unable to locate the 'ldap' module. Please install python-ldap. " \
"(http://python-ldap.sourceforge.net)")
sys.exit(1)
################################################################################
# Configuration Options
################################################################################
# This is the distinguished name used to bind to the LDAP server.
# [Example: CN=Jeremy Whitlock,OU=Users,DC=subversion,DC=thoughtspark,DC=org]
bind_dn = None
# This is the password for the user connecting to the LDAP server.
# [Example: pa55w0rd]
bind_password = None
# This is the fully-qualified url to the LDAP server.
# [Example: ldap://localhost:389]
url = None
# This is the distinguished name to where the group search will start.
# [Example: DC=subversion,DC=thoughtspark,DC=org]
base_dn = None
# This is the query/filter used to identify group objects.
# [Example: objectClass=group]
group_query = "objectClass=group"
# This is the attribute of the group object that stores the group memberships.
# [Example: member]
group_member_attribute = "member"
# This is the query/filter used to identify user objects.
# [Example: objectClass=user]
user_query = "objectClass=user"
# This is the attribute of the user object that stores the userid to be used in
# the authz file. [Example: cn]
userid_attribute = "cn"
# This is the CA certificate to use with SSL LDAP connection
cacert = None
# This is the fully-qualified path to the authz file to write to.
# [Example: /opt/svn/svn_authz.txt]
authz_path = None
################################################################################
# Runtime Options
################################################################################
# This indicates whether or not to output logging information
verbose = True
################################################################################
# Application Settings
################################################################################
application_name = "LDAP Groups to Subversion Authz Groups Bridge"
application_version = "1.0.3"
application_description = "The '%s' is a simple script that will query your " \
"directory server for group objects and create a " \
"representation of those groups in your Subversion " \
"authorization (authz) file." % application_name
################################################################################
# Business Logic
################################################################################
def bind():
"""This function will bind to the LDAP instance and return an ldapobject."""
if cacert:
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, cacert)
ldapobject = ldap.initialize(url)
ldapobject.bind(bind_dn, bind_password)
if verbose:
print("Successfully bound to %s..." % url)
return ldapobject
# bind()
def search_for_groups(ldapobject):
"""This function will search the LDAP directory for group definitions."""
groups = []
result_set = get_ldap_search_resultset(base_dn, group_query, ldapobject)
if (len(result_set) == 0):
if verbose:
print("The group_query %s did not return any results." % group_query)
return
for i in range(len(result_set)):
for entry in result_set[i]:
groups.append(entry)
if verbose:
print("%d groups found." % len(groups))
return groups
# search_for_groups()
def get_ldap_search_resultset(base_dn, group_query, ldapobject):
"""This function will return a query result set."""
result_set = []
result_id = ldapobject.search(base_dn, ldap.SCOPE_SUBTREE, group_query)
while 1:
result_type, result_data = ldapobject.result(result_id, 0)
if (result_type == ldap.RES_SEARCH_ENTRY):
result_set.append(result_data)
elif (result_type == ldap.RES_SEARCH_RESULT):
break
return result_set
# get_ldap_search_resultset()
def create_group_model(groups, ldapobject):
"""This function will take the list of groups created by search_for_groups()
and will create a group membership model for each group."""
memberships = []
groupmap = create_group_map(groups)
if groups:
for group in groups:
group_members = []
members = []
if group[1].has_key(group_member_attribute):
group_members = group[1][group_member_attribute]
# We need to check for if the member is a group and handle specially
for member in group_members:
try:
try:
user = get_ldap_search_resultset(member, user_query, ldapobject)
except:
#error means likely that member isn't a fully OID, so run the search again
user = get_ldap_search_resultset(base_dn, "(&(%s=%s)(%s))" % (userid_attribute, member, user_query), ldapobject)
if (len(user) == 1):
# The member is a user
attrs = user[0][0][1]
if (attrs.has_key(userid_attribute)):
members.append(attrs[userid_attribute][0])
else:
if verbose:
print("[WARNING]: %s does not have the %s attribute..." \
% (user[0][0][0], userid_attribute))
elif (len(user) > 1):
# Check to see if this member is really a group
try:
mg = get_ldap_search_resultset(member, group_query, ldapobject)
except:
#error means likely that member isn't a fully OID, so run the search again
mg = get_ldap_search_resultset(base_dn, "(&(%s=%s)(%s))" % (group_member_attribute, member, group_query), ldapobject)
if (len(mg) == 1):
# The member is a group
members.append("GROUP:" + get_dict_key_from_value(groupmap,
mg[0][0][0]))
else:
if verbose:
print("[WARNING]: %s is a member of %s but is neither a group " \
"or a user." % (member, group[1]['cn'][0]))
except ldap.LDAPError, error_message:
if verbose:
print("[WARNING]: %s object was not found..." % member)
memberships.append(members)
return (groups, memberships)
# create_group_model()
def get_dict_key_from_value(dict, value):
"""Returns the key of the dictionary entry with the matching value."""
for k, v in dict.iteritems():
if (v == value):
return k
return None
# get_dict_key_from_value()
def create_group_map(groups):
groupmap = {}
dups = {}
if groups:
for group in groups:
cn = simplify_name(group[1]['cn'][0])
if (not groupmap.has_key(cn)):
groupmap[cn] = group[0]
else:
if (not dups.has_key(cn)):
dups[cn] = 1
else:
index = dups[cn]
dups[cn] = (index + 1)
groupmap[cn + str(dups[cn])] = group[0]
return groupmap
# create_group_map()
def simplify_name(name):
"""Creates an authz simple group name."""
return re.sub("\W", "", name)
# simplify_name()
def print_group_model(groups, memberships):
"""This function will write the groups and their members to a file."""
if not groups:
return
now = datetime.datetime.now()
header_start = "### Start generated content: " + application_name +" ("
header_middle = now.strftime("%Y/%m/%d %H:%M:%S")
header_end = ") ###"
header = header_start + header_middle + header_end
footer = "### End generated content: " + application_name + " ###"
file = None
tmp_fd, tmp_authz_path = tempfile.mkstemp()
if ((authz_path != None) and (authz_path != "None")):
if (os.path.exists(authz_path)):
file = open(authz_path, 'r')
tmpfile = open(tmp_authz_path, 'w')
# Remove previous generated content
inside_content = False
for line in file.readlines():
if (inside_content):
if (line.find(footer) > -1):
inside_content = False
else:
if (line.find(header_start) > -1):
inside_content = True
else:
tmpfile.write(line)
file.close()
tmpfile.close()
if (os.path.exists(tmp_authz_path)):
cp = ConfigParser.ConfigParser()
cp.read(tmp_authz_path)
if (not cp.has_section("groups")):
tmpfile = open(tmp_authz_path, 'a')
tmpfile.write("[groups]\n")
tmpfile.close()
else:
tmpfile = open(tmp_authz_path, 'a')
tmpfile.write("[groups]\n")
tmpfile.close()
needs_new_line = False
tmpfile = open(tmp_authz_path, 'r')
if (tmpfile.readlines()[-1].strip() != ''):
needs_new_line = True
tmpfile.close()
tmpfile = open(tmp_authz_path, 'a')
if (needs_new_line):
tmpfile.write("\n")
tmpfile.write(header + "\n")
groupmap = create_group_map(groups)
if groups:
for i in range(len(groups)):
if (i != 0):
tmpfile.write("\n")
short_name = simplify_name(get_dict_key_from_value(groupmap, groups[i][0]))
tmpfile.write(short_name + " = ")
for j in range(len(memberships[i])):
if (j != 0):
tmpfile.write(", ")
if (memberships[i][j].find("GROUP:") == 0):
tmpfile.write(memberships[i][j].replace("GROUP:","@"))
else:
tmpfile.write(memberships[i][j])
generate_legend(tmpfile, groups)
tmpfile.write("\n" + footer)
tmpfile.close()
if authz_path:
if (os.path.exists(authz_path + ".bak")):
os.remove(authz_path + ".bak")
if (os.path.exists(authz_path)):
os.rename(authz_path, authz_path + ".bak")
os.rename(tmp_authz_path, authz_path)
else:
tmpfile = open(tmp_authz_path, 'r')
for line in tmpfile.readlines():
print(line)
tmpfile.close()
os.remove(tmp_authz_path)
# print_group_model()
def generate_legend(output, groups):
"""This function will generate, and write, the legend to file."""
if groups:
output.write("\n")
output.write("\n###########################################################" +
"#####################\n")
output.write("########### " + application_name +" (Legend) ##########\n")
output.write("###########################################################" +
"#####################\n")
groupmap = create_group_map(groups)
for group in groups:
short_name = simplify_name(get_dict_key_from_value(groupmap, group[0]))
output.write("### " + short_name + " = " + str(group[0]) + "\n")
output.write("###########################################################" +
"#####################\n")
# generate_legend()
def load_cli_properties(parser):
"""This function will set the local properties based on cli arguments."""
global bind_dn
global bind_password
global url
global base_dn
global group_query
global group_member_attribute
global user_query
global userid_attribute
global authz_path
global verbose
global cacert
(options, args) = parser.parse_args(args=None, values=None)
bind_dn = options.bind_dn
bind_password = options.bind_password
url = options.url
base_dn = options.base_dn
group_query = options.group_query
group_member_attribute = options.group_member_attribute
user_query = options.user_query
userid_attribute = options.userid_attribute
authz_path = options.authz_path
verbose = options.verbose
cacert = options.cacert
# load_cli_properties()
def create_cli_parser():
"""Creates an OptionParser and returns it."""
usage = "usage: %prog [options]"
parser = OptionParser(usage=usage, description=application_description)
parser.add_option("-d", "--bind-dn", dest="bind_dn",
help="The DN of the user to bind to the directory with")
parser.add_option("-p", "--bind-password", dest="bind_password",
help="The password for the user specified with the " \
"--bind-dn")
parser.add_option("-l", "--url", dest="url",
help="The url (scheme://hostname:port) for the directory " \
"server")
parser.add_option("-b", "--base-dn", dest="base_dn",
help="The DN at which to perform the recursive search")
parser.add_option("-g", "--group-query", dest="group_query",
default="objectClass=group",
help="The query/filter used to identify group objects. " \
"[Default: %default]")
parser.add_option("-m", "--group-member-attribute",
dest="group_member_attribute", default="member",
help="The attribute of the group object that stores the " \
"group memberships. [Default: %default]")
parser.add_option("-u", "--user-query", dest="user_query",
default="objectClass=user",
help="The query/filter used to identify user objects. " \
"[Default: %default]")
parser.add_option("-i", "--userid_attribute", dest="userid_attribute",
default="cn",
help="The attribute of the user object that stores the " \
"userid to be used in the authz file. " \
"[Default: %default]")
parser.add_option("-c", "--cacert-path", dest="cacert",
help="The path to the CA CERT to validate certificate")
parser.add_option("-z", "--authz-path", dest="authz_path",
help="The path to the authz file to update/create")
parser.add_option("-q", "--quiet", action="store_false", dest="verbose",
default="True", help="Suppress logging information")
return parser
# create_cli_parser()
def are_properties_set():
"""This function will perform a simple test to make sure none of the
properties are 'None'."""
if (bind_dn == None):
return False
if (url == None):
return False
if (base_dn == None):
return False
if (group_query == None):
return False
if (group_member_attribute == None):
return False
if (user_query == None):
return False
if (userid_attribute == None):
return False
# bind_password is not checked since if not passed, the user will be prompted
# authz_path is not checked since it can be 'None' signifying stdout output
return True
# are_properties_set()
def get_unset_properties():
"""This function returns a list of unset properties necessary to run."""
unset_properties = []
if (bind_dn == None):
unset_properties += ['bind-dn']
if (url == None):
unset_properties += ['url']
if (base_dn == None):
unset_properties += ['base-dn']
if (group_query == None):
unset_properties += ['group-query']
if (group_member_attribute == None):
unset_properties += ['group-member-attribute']
if (user_query == None):
unset_properties += ['user-query']
if (userid_attribute == None):
unset_properties += ['userid-attribute']
return unset_properties
# get_unset_properties()
def main():
"""This function is the entry point for this script."""
# Create the OptionParser
parser = create_cli_parser()
# Attempt to load properties from the command line if necessary
if not are_properties_set():
load_cli_properties(parser)
if not are_properties_set():
print("There is not enough information to proceed.")
for prop in get_unset_properties():
print("'%s' was not passed" % prop)
print("")
parser.print_help()
parser.exit()
# Allow user to type in password if missing
global bind_password
if bind_password == None:
bind_password = getpass.getpass("Please provide the bind DN password: ")
ldapobject = None
groups = None
memberships = None
try:
ldapobject = bind()
except ldap.LDAPError, error_message:
print("Could not connect to %s. Error: %s " % (url, error_message))
sys.exit(1)
try:
groups = search_for_groups(ldapobject)
except ldap.LDAPError, error_message:
print("Error performing search: %s " % error_message)
sys.exit(1)
if groups and len(groups) == 0:
print("There were no groups found with the group_query you supplied.")
sys.exit(0)
try:
memberships = create_group_model(groups, ldapobject)[1]
except ldap.LDAPError, error_message:
print("Error creating group model: %s" % error_message)
sys.exit(1)
print_group_model(groups, memberships)
# main()
if __name__ == "__main__":
main()
Utilisation
L'utilisation de ce script est assez simple. Plusieurs paramètres sont obligatoires, d'autre optionnels dans la ligne de commande:
Argument court | Argument long | Valeur |
---|---|---|
-h | --help | Affiche l'aide en ligne du script. |
-d | --bind-dn | Compte utilisateur utilisé pour se connecter à l'annuaire LDAP lors de la recherche des groupes. |
-p | --bind-password | Le mot de passe pour le compte d'accès à l'annuaire LDAP. |
-l | --url | L'URL pour accéder à l'annuaire, de type scheme://hostname:port. |
-b | --base-dn | Contient l'OU racine à partir de laquelle les recherches de groupe seront effectuées. |
-g | --group-query | Contient le filtre de recherche des groupes à synchroniser. |
-m | --group-member-attribute | Le nom de l'attribut contenant la référence aux utilisateurs.
La valeur par défaut member est opérationnel dans le cadre d'une synchronisation sur OpenLDAP. |
-u | --user-query | Requête permettant de rechercher les utilisateurs une fois identifié dans les groupes retournés par la recherche.
Le filtre par défaut est objectClass=user. Mais ceci n'est pas trop pertinent, la classe des utilisateurs n'étant pas forcément user. |
-i | --userid_attribute | L'attribut utilisé pour récupérer l'identifiant de la personne. Cette valeur sera placée dans la composition des gorupes au niveau du fichier authz de SVN.
La valeur par défaut est cn, mais cela va placer l'id complet de l'annuaire LDAP. |
-c | --cacert-path | Utiliser dans le cadre de connexion sur un annuaire sécurisé. Contient l'emplacement du certificat. |
-z | --authz-path | Permet de spécifier le fichier cible pour la génération de la composition des groupes.
Si il n'est pas fourni, le résultat est écrit dans la console. Il est alors possible de rediriger la console vers un fichier. |
-q | --quiet | Mode silence. |
L'exemple suivant permet de synchroniser les groupes avec les paramètres:
Paramètre | Valeur | Description |
---|---|---|
-d | cn=admin,dc=ejnserver,dc=fr | Compte administrateur pour interroger l'annuaire. |
-p | <MOT DE PASSE ADMIN> | Remplacer par le mot de passe du compte fourni pour l'argument d. |
-l | ldap://localhost:389 | URL d'accès à l'annuaire. Le script est exécuté sur le même serveur que l'annuaire. |
-b | ou=groups,dc=ejnserver,dc=fr | La recherche des groupes s'effectue sous l'OU ou=groups,dc=ejnserver,dc=fr. |
-g | (&(objectClass=groupOfNames)(cn=svn-repos1*)) | Seuls les groupes de class groupOfNames et dont le nom commence par svn-repos1 sont synchronisés. |
-u | (objectClass=*) | Le filtre de recherche sur les utilisateur est obligatoire. Si il n'est pas fourni aucun utilisateur ne sera synchronisé. Ce type de filtre permet de prendre en compte tous les utilisateurs référencés dans les groupes. En effet, chaque entrée a forcément un attribut de nom objectClass. |
-i | uid | La valeur de l'attribut uid sera utilisée pour identifier les utilisateurs. |
-z | /tmp/authz | Le résultat sera stocké dans le fichier /tmp/authz |
#python sync_ldap_groups_to_svn_authz.py \ -d "cn=admin,dc=ejnserver,dc=fr" \ -p "<MOT DE PASSE>" \ -l "ldap://localhost:389" \ -b "ou=groups,dc=ejnserver,dc=fr" \ -g "(&(objectClass=groupOfNames)(cn=svn-repos1*))" \ -u "(objectClass=*)" \ -i "uid" \ -z /tmp/authz
Le résultat, dans le fichier /tmp/authz est le suivant:
[groups]
### Start generated content: LDAP Groups to Subversion Authz Groups Bridge (2011/11/12 12:08:58) ###
svnrepos1contrib = etienne
svnrepos1reader = etienne
################################################################################
########### LDAP Groups to Subversion Authz Groups Bridge (Legend) ##########
################################################################################
### svnrespos1contrib = cn=svn-repos1-contrib,ou=groups,dc=ejnserver,dc=fr
### svnrepos1reader = cn=svn-repos1-reader,ou=groups,dc=ejnserver,dc=fr
################################################################################
### End generated content: LDAP Groups to Subversion Authz Groups Bridge ###
A noter les tirets dans les noms des groupes est supprimé au niveau des noms des groupes SVN.
Seule la section groups
est créé.
Mais dans le cadre de l'intégration dans un SVN, le fichier à créer ne doit pas être dans /tmp
, mais au niveau du fichier authz
de chacun des repositories. La commande sera alors du type, pour un repository repos1
:
#python sync_ldap_groups_to_svn_authz.py \ -d "cn=admin,dc=ejnserver,dc=fr" \ -p "<MOT DE PASSE>" \ -l "ldap://localhost:389" \ -b "ou=groups,dc=ejnserver,dc=fr" \ -g "(&(objectClass=groupOfNames)(cn=svn-repos1*))" \ -u "(objectClass=*)" \ -i "uid" \ -z /var/opt/svn/repos1/conf/authz
Si le fichier existe déjà, il sera complété.
Patch
Lors de l'exécution, un fichier temporaire est créé dans le répertoire /tmp
sur une machine Linux. Or il est fréquent que le répertoire /tmp
ne soit pas monté sur le même filesystem que l'emplacement de SVN, par exemple /var/opt/svn
sur une machine Ubuntu. Or lors de la copie du fichier temporaire vers le fichier cible une erreur se produit:
#./run_sync_ldap_groups.sh Successfully bound to ldap://localhost:389... 2 groups found. Traceback (most recent call last): File "sync_ldap_groups_to_svn_authz.py", line 572, in <module> main() File "sync_ldap_groups_to_svn_authz.py", line 567, in main print_group_model(groups, memberships) File "sync_ldap_groups_to_svn_authz.py", line 358, in print_group_model os.rename(tmp_authz_path, authz_path) OSError: [Errno 18] Invalid cross-device link
L'erreur provient de la commande os.rename(tmp_authz_path, authz_path)
dans la fonction print_group_model
.
def print_group_model(groups, memberships):
"""This function will write the groups and their members to a file."""
if not groups:
return
now = datetime.datetime.now()
header_start = "### Start generated content: " + application_name +" ("
header_middle = now.strftime("%Y/%m/%d %H:%M:%S")
header_end = ") ###"
header = header_start + header_middle + header_end
footer = "### End generated content: " + application_name + " ###"
file = None
tmp_fd, tmp_authz_path = tempfile.mkstemp()
if ((authz_path != None) and (authz_path != "None")):
if (os.path.exists(authz_path)):
file = open(authz_path, 'r')
tmpfile = open(tmp_authz_path, 'w')
# Remove previous generated content
inside_content = False
for line in file.readlines():
if (inside_content):
if (line.find(footer) > -1):
inside_content = False
else:
if (line.find(header_start) > -1):
inside_content = True
else:
tmpfile.write(line)
file.close()
tmpfile.close()
if (os.path.exists(tmp_authz_path)):
cp = ConfigParser.ConfigParser()
cp.read(tmp_authz_path)
if (not cp.has_section("groups")):
tmpfile = open(tmp_authz_path, 'a')
tmpfile.write("[groups]\n")
tmpfile.close()
else:
tmpfile = open(tmp_authz_path, 'a')
tmpfile.write("[groups]\n")
tmpfile.close()
needs_new_line = False
tmpfile = open(tmp_authz_path, 'r')
if (tmpfile.readlines()[-1].strip() != ''):
needs_new_line = True
tmpfile.close()
tmpfile = open(tmp_authz_path, 'a')
if (needs_new_line):
tmpfile.write("\n")
tmpfile.write(header + "\n")
groupmap = create_group_map(groups)
if groups:
for i in range(len(groups)):
if (i != 0):
tmpfile.write("\n")
short_name = simplify_name(get_dict_key_from_value(groupmap, groups[i][0]))
tmpfile.write(short_name + " = ")
for j in range(len(memberships[i])):
if (j != 0):
tmpfile.write(", ")
if (memberships[i][j].find("GROUP:") == 0):
tmpfile.write(memberships[i][j].replace("GROUP:","@"))
else:
tmpfile.write(memberships[i][j])
generate_legend(tmpfile, groups)
tmpfile.write("\n" + footer)
tmpfile.close()
if authz_path:
if (os.path.exists(authz_path + ".bak")):
os.remove(authz_path + ".bak")
if (os.path.exists(authz_path)):
os.rename(authz_path, authz_path + ".bak")
os.rename(tmp_authz_path, authz_path)
else:
tmpfile = open(tmp_authz_path, 'r')
for line in tmpfile.readlines():
print(line)
tmpfile.close()
os.remove(tmp_authz_path)
# print_group_model()
Diff
De nombreuses discussions sur ce problème (python) sont disponibles sur internet. Il est souvent question de modifier l'appel à la fonction os.rename
par shutil.move
. Un fichier diff est fourni à l'adresse suivante: https://bitbucket.org/jcscoobyrs/jw-tools/issue/4/osrename-fails-if-tmp-and-authz-are-on
Index: sync_ldap_groups_to_svn_authz.py
===================================================================
--- sync_ldap_groups_to_svn_authz.py (revision 26)
+++ sync_ldap_groups_to_svn_authz.py (working copy)
@@ -21,7 +21,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
################################################################################
-import ConfigParser, datetime, getpass, os, re, sys, tempfile
+import ConfigParser, datetime, getpass, os, re, sys, tempfile, shutil
from optparse import OptionParser
try:
@@ -358,7 +358,7 @@
if (os.path.exists(authz_path)):
os.rename(authz_path, authz_path + ".bak")
- os.rename(tmp_authz_path, authz_path)
+ shutil.move(tmp_authz_path, authz_path)
else:
tmpfile = open(tmp_authz_path, 'r')
L'exécution est alors réalisée avec succès:
#./run_sync_ldap_groups.sh Successfully bound to ldap://localhost:389... 2 groups found.
Source
Le script final est donc le suivant:
#!/usr/bin/env python
#
# -*-python-*-
#
################################################################################
# License
################################################################################
# Copyright (c) 2006 Jeremy Whitlock. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
################################################################################
import ConfigParser, datetime, getpass, os, re, sys, tempfile, shutil
from optparse import OptionParser
try:
import ldap
except ImportError:
print("Unable to locate the 'ldap' module. Please install python-ldap. " \
"(http://python-ldap.sourceforge.net)")
sys.exit(1)
################################################################################
# Configuration Options
################################################################################
# This is the distinguished name used to bind to the LDAP server.
# [Example: CN=Jeremy Whitlock,OU=Users,DC=subversion,DC=thoughtspark,DC=org]
bind_dn = None
# This is the password for the user connecting to the LDAP server.
# [Example: pa55w0rd]
bind_password = None
# This is the fully-qualified url to the LDAP server.
# [Example: ldap://localhost:389]
url = None
# This is the distinguished name to where the group search will start.
# [Example: DC=subversion,DC=thoughtspark,DC=org]
base_dn = None
# This is the query/filter used to identify group objects.
# [Example: objectClass=group]
group_query = "objectClass=group"
# This is the attribute of the group object that stores the group memberships.
# [Example: member]
group_member_attribute = "member"
# This is the query/filter used to identify user objects.
# [Example: objectClass=user]
user_query = "objectClass=user"
# This is the attribute of the user object that stores the userid to be used in
# the authz file. [Example: cn]
userid_attribute = "cn"
# This is the CA certificate to use with SSL LDAP connection
cacert = None
# This is the fully-qualified path to the authz file to write to.
# [Example: /opt/svn/svn_authz.txt]
authz_path = None
################################################################################
# Runtime Options
################################################################################
# This indicates whether or not to output logging information
verbose = True
################################################################################
# Application Settings
################################################################################
application_name = "LDAP Groups to Subversion Authz Groups Bridge"
application_version = "1.0.3"
application_description = "The '%s' is a simple script that will query your " \
"directory server for group objects and create a " \
"representation of those groups in your Subversion " \
"authorization (authz) file." % application_name
################################################################################
# Business Logic
################################################################################
def bind():
"""This function will bind to the LDAP instance and return an ldapobject."""
if cacert:
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, cacert)
ldapobject = ldap.initialize(url)
ldapobject.bind(bind_dn, bind_password)
if verbose:
print("Successfully bound to %s..." % url)
return ldapobject
# bind()
def search_for_groups(ldapobject):
"""This function will search the LDAP directory for group definitions."""
groups = []
result_set = get_ldap_search_resultset(base_dn, group_query, ldapobject)
if (len(result_set) == 0):
if verbose:
print("The group_query %s did not return any results." % group_query)
return
for i in range(len(result_set)):
for entry in result_set[i]:
groups.append(entry)
if verbose:
print("%d groups found." % len(groups))
return groups
# search_for_groups()
def get_ldap_search_resultset(base_dn, group_query, ldapobject):
"""This function will return a query result set."""
result_set = []
result_id = ldapobject.search(base_dn, ldap.SCOPE_SUBTREE, group_query)
while 1:
result_type, result_data = ldapobject.result(result_id, 0)
if (result_type == ldap.RES_SEARCH_ENTRY):
result_set.append(result_data)
elif (result_type == ldap.RES_SEARCH_RESULT):
break
return result_set
# get_ldap_search_resultset()
def create_group_model(groups, ldapobject):
"""This function will take the list of groups created by search_for_groups()
and will create a group membership model for each group."""
memberships = []
groupmap = create_group_map(groups)
if groups:
for group in groups:
group_members = []
members = []
if group[1].has_key(group_member_attribute):
group_members = group[1][group_member_attribute]
# We need to check for if the member is a group and handle specially
for member in group_members:
try:
try:
user = get_ldap_search_resultset(member, user_query, ldapobject)
except:
#error means likely that member isn't a fully OID, so run the search again
user = get_ldap_search_resultset(base_dn, "(&(%s=%s)(%s))" % (userid_attribute, member, user_query), ldapobject)
if (len(user) == 1):
# The member is a user
attrs = user[0][0][1]
if (attrs.has_key(userid_attribute)):
members.append(attrs[userid_attribute][0])
else:
if verbose:
print("[WARNING]: %s does not have the %s attribute..." \
% (user[0][0][0], userid_attribute))
elif (len(user) > 1):
# Check to see if this member is really a group
try:
mg = get_ldap_search_resultset(member, group_query, ldapobject)
except:
#error means likely that member isn't a fully OID, so run the search again
mg = get_ldap_search_resultset(base_dn, "(&(%s=%s)(%s))" % (group_member_attribute, member, group_query), ldapobject)
if (len(mg) == 1):
# The member is a group
members.append("GROUP:" + get_dict_key_from_value(groupmap,
mg[0][0][0]))
else:
if verbose:
print("[WARNING]: %s is a member of %s but is neither a group " \
"or a user." % (member, group[1]['cn'][0]))
except ldap.LDAPError, error_message:
if verbose:
print("[WARNING]: %s object was not found..." % member)
memberships.append(members)
return (groups, memberships)
# create_group_model()
def get_dict_key_from_value(dict, value):
"""Returns the key of the dictionary entry with the matching value."""
for k, v in dict.iteritems():
if (v == value):
return k
return None
# get_dict_key_from_value()
def create_group_map(groups):
groupmap = {}
dups = {}
if groups:
for group in groups:
cn = simplify_name(group[1]['cn'][0])
if (not groupmap.has_key(cn)):
groupmap[cn] = group[0]
else:
if (not dups.has_key(cn)):
dups[cn] = 1
else:
index = dups[cn]
dups[cn] = (index + 1)
groupmap[cn + str(dups[cn])] = group[0]
return groupmap
# create_group_map()
def simplify_name(name):
"""Creates an authz simple group name."""
return re.sub("\W", "", name)
# simplify_name()
def print_group_model(groups, memberships):
"""This function will write the groups and their members to a file."""
if not groups:
return
now = datetime.datetime.now()
header_start = "### Start generated content: " + application_name +" ("
header_middle = now.strftime("%Y/%m/%d %H:%M:%S")
header_end = ") ###"
header = header_start + header_middle + header_end
footer = "### End generated content: " + application_name + " ###"
file = None
tmp_fd, tmp_authz_path = tempfile.mkstemp()
if ((authz_path != None) and (authz_path != "None")):
if (os.path.exists(authz_path)):
file = open(authz_path, 'r')
tmpfile = open(tmp_authz_path, 'w')
# Remove previous generated content
inside_content = False
for line in file.readlines():
if (inside_content):
if (line.find(footer) > -1):
inside_content = False
else:
if (line.find(header_start) > -1):
inside_content = True
else:
tmpfile.write(line)
file.close()
tmpfile.close()
if (os.path.exists(tmp_authz_path)):
cp = ConfigParser.ConfigParser()
cp.read(tmp_authz_path)
if (not cp.has_section("groups")):
tmpfile = open(tmp_authz_path, 'a')
tmpfile.write("[groups]\n")
tmpfile.close()
else:
tmpfile = open(tmp_authz_path, 'a')
tmpfile.write("[groups]\n")
tmpfile.close()
needs_new_line = False
tmpfile = open(tmp_authz_path, 'r')
if (tmpfile.readlines()[-1].strip() != ''):
needs_new_line = True
tmpfile.close()
tmpfile = open(tmp_authz_path, 'a')
if (needs_new_line):
tmpfile.write("\n")
tmpfile.write(header + "\n")
groupmap = create_group_map(groups)
if groups:
for i in range(len(groups)):
if (i != 0):
tmpfile.write("\n")
short_name = simplify_name(get_dict_key_from_value(groupmap, groups[i][0]))
tmpfile.write(short_name + " = ")
for j in range(len(memberships[i])):
if (j != 0):
tmpfile.write(", ")
if (memberships[i][j].find("GROUP:") == 0):
tmpfile.write(memberships[i][j].replace("GROUP:","@"))
else:
tmpfile.write(memberships[i][j])
generate_legend(tmpfile, groups)
tmpfile.write("\n" + footer)
tmpfile.close()
if authz_path:
if (os.path.exists(authz_path + ".bak")):
os.remove(authz_path + ".bak")
if (os.path.exists(authz_path)):
os.rename(authz_path, authz_path + ".bak")
#os.rename(tmp_authz_path, authz_path)
shutil.move(tmp_authz_path, authz_path)
else:
tmpfile = open(tmp_authz_path, 'r')
for line in tmpfile.readlines():
print(line)
tmpfile.close()
os.remove(tmp_authz_path)
# print_group_model()
def generate_legend(output, groups):
"""This function will generate, and write, the legend to file."""
if groups:
output.write("\n")
output.write("\n###########################################################" +
"#####################\n")
output.write("########### " + application_name +" (Legend) ##########\n")
output.write("###########################################################" +
"#####################\n")
groupmap = create_group_map(groups)
for group in groups:
short_name = simplify_name(get_dict_key_from_value(groupmap, group[0]))
output.write("### " + short_name + " = " + str(group[0]) + "\n")
output.write("###########################################################" +
"#####################\n")
# generate_legend()
def load_cli_properties(parser):
"""This function will set the local properties based on cli arguments."""
global bind_dn
global bind_password
global url
global base_dn
global group_query
global group_member_attribute
global user_query
global userid_attribute
global authz_path
global verbose
global cacert
(options, args) = parser.parse_args(args=None, values=None)
bind_dn = options.bind_dn
bind_password = options.bind_password
url = options.url
base_dn = options.base_dn
group_query = options.group_query
group_member_attribute = options.group_member_attribute
user_query = options.user_query
userid_attribute = options.userid_attribute
authz_path = options.authz_path
verbose = options.verbose
cacert = options.cacert
# load_cli_properties()
def create_cli_parser():
"""Creates an OptionParser and returns it."""
usage = "usage: %prog [options]"
parser = OptionParser(usage=usage, description=application_description)
parser.add_option("-d", "--bind-dn", dest="bind_dn",
help="The DN of the user to bind to the directory with")
parser.add_option("-p", "--bind-password", dest="bind_password",
help="The password for the user specified with the " \
"--bind-dn")
parser.add_option("-l", "--url", dest="url",
help="The url (scheme://hostname:port) for the directory " \
"server")
parser.add_option("-b", "--base-dn", dest="base_dn",
help="The DN at which to perform the recursive search")
parser.add_option("-g", "--group-query", dest="group_query",
default="objectClass=group",
help="The query/filter used to identify group objects. " \
"[Default: %default]")
parser.add_option("-m", "--group-member-attribute",
dest="group_member_attribute", default="member",
help="The attribute of the group object that stores the " \
"group memberships. [Default: %default]")
parser.add_option("-u", "--user-query", dest="user_query",
default="objectClass=user",
help="The query/filter used to identify user objects. " \
"[Default: %default]")
parser.add_option("-i", "--userid_attribute", dest="userid_attribute",
default="cn",
help="The attribute of the user object that stores the " \
"userid to be used in the authz file. " \
"[Default: %default]")
parser.add_option("-c", "--cacert-path", dest="cacert",
help="The path to the CA CERT to validate certificate")
parser.add_option("-z", "--authz-path", dest="authz_path",
help="The path to the authz file to update/create")
parser.add_option("-q", "--quiet", action="store_false", dest="verbose",
default="True", help="Suppress logging information")
return parser
# create_cli_parser()
def are_properties_set():
"""This function will perform a simple test to make sure none of the
properties are 'None'."""
if (bind_dn == None):
return False
if (url == None):
return False
if (base_dn == None):
return False
if (group_query == None):
return False
if (group_member_attribute == None):
return False
if (user_query == None):
return False
if (userid_attribute == None):
return False
# bind_password is not checked since if not passed, the user will be prompted
# authz_path is not checked since it can be 'None' signifying stdout output
return True
# are_properties_set()
def get_unset_properties():
"""This function returns a list of unset properties necessary to run."""
unset_properties = []
if (bind_dn == None):
unset_properties += ['bind-dn']
if (url == None):
unset_properties += ['url']
if (base_dn == None):
unset_properties += ['base-dn']
if (group_query == None):
unset_properties += ['group-query']
if (group_member_attribute == None):
unset_properties += ['group-member-attribute']
if (user_query == None):
unset_properties += ['user-query']
if (userid_attribute == None):
unset_properties += ['userid-attribute']
return unset_properties
# get_unset_properties()
def main():
"""This function is the entry point for this script."""
# Create the OptionParser
parser = create_cli_parser()
# Attempt to load properties from the command line if necessary
if not are_properties_set():
load_cli_properties(parser)
if not are_properties_set():
print("There is not enough information to proceed.")
for prop in get_unset_properties():
print("'%s' was not passed" % prop)
print("")
parser.print_help()
parser.exit()
# Allow user to type in password if missing
global bind_password
if bind_password == None:
bind_password = getpass.getpass("Please provide the bind DN password: ")
ldapobject = None
groups = None
memberships = None
try:
ldapobject = bind()
except ldap.LDAPError, error_message:
print("Could not connect to %s. Error: %s " % (url, error_message))
sys.exit(1)
try:
groups = search_for_groups(ldapobject)
except ldap.LDAPError, error_message:
print("Error performing search: %s " % error_message)
sys.exit(1)
if groups and len(groups) == 0:
print("There were no groups found with the group_query you supplied.")
sys.exit(0)
try:
memberships = create_group_model(groups, ldapobject)[1]
except ldap.LDAPError, error_message:
print("Error creating group model: %s" % error_message)
sys.exit(1)
print_group_model(groups, memberships)
# main()
if __name__ == "__main__":
main()
Configuration
Non présence de la section groups
Comme vu, le script va écrire la section groups
dans le fichier authz
. Or si celle-ci est déjà présente, elle sera complétée. Il est donc important de vérifier le contenu du fichier original.
Par exemple, pour le fichier origine suivant:
### This file is an example authorization file for svnserve.
### Its format is identical to that of mod_authz_svn authorization
### files.
### As shown below each section defines authorizations for the path and
### (optional) repository specified by the section name.
### The authorizations follow. An authorization line can refer to:
### - a single user,
### - a group of users defined in a special [groups] section,
### - an alias defined in a special [aliases] section,
### - all authenticated users, using the '$authenticated' token,
### - only anonymous users, using the '$anonymous' token,
### - anyone, using the '*' wildcard.
###
### A match can be inverted by prefixing the rule with '~'. Rules can
### grant read ('r') access, read-write ('rw') access, or no access
### ('').
[aliases]
# joe = /C=XZ/ST=Dessert/L=Snake City/O=Snake Oil, Ltd./OU=Research Institute/CN=Joe Average
[groups]
# harry_and_sally = harry,sally
# harry_sally_and_joe = harry,sally,&joe
# [/foo/bar]
# harry = rw
# &joe = r
# * =
# [repository:/baz/fuz]
# @harry_and_sally = rw
# * = r
[/]
@svnrepos1contrib = rw
@svnrepos1reader = r
* =
La section groups
est déjà présente. Elle sera donc complétée et le résultat donnera la fichier suivant:
### This file is an example authorization file for svnserve.
### Its format is identical to that of mod_authz_svn authorization
### files.
### As shown below each section defines authorizations for the path and
### (optional) repository specified by the section name.
### The authorizations follow. An authorization line can refer to:
### - a single user,
### - a group of users defined in a special [groups] section,
### - an alias defined in a special [aliases] section,
### - all authenticated users, using the '$authenticated' token,
### - only anonymous users, using the '$anonymous' token,
### - anyone, using the '*' wildcard.
###
### A match can be inverted by prefixing the rule with '~'. Rules can
### grant read ('r') access, read-write ('rw') access, or no access
### ('').
[aliases]
# joe = /C=XZ/ST=Dessert/L=Snake City/O=Snake Oil, Ltd./OU=Research Institute/CN=Joe Average
[groups]
# harry_and_sally = harry,sally
# harry_sally_and_joe = harry,sally,&joe
# [/foo/bar]
# harry = rw
# &joe = r
# * =
# [repository:/baz/fuz]
# @harry_and_sally = rw
# * = r
[/]
@svnrepos1contrib = rw
@svnrepos1reader = r
* =
### Start generated content: LDAP Groups to Subversion Authz Groups Bridge (2011/11/12 13:48:54) ###
svnrepos1contrib = etienne
svnrepos1reader = etienne
################################################################################
########### LDAP Groups to Subversion Authz Groups Bridge (Legend) ##########
################################################################################
### svnrepos1contrib = cn=svn-repos1-contrib,ou=groups,dc=ejnserver,dc=fr
### svnrepos1reader = cn=svn-repos1-reader,ou=groups,dc=ejnserver,dc=fr
################################################################################
### End generated content: LDAP Groups to Subversion Authz Groups Bridge ###
Donc, les compositions des groupes ne sont pas situées au bon endroit. Il faut impérativement que cette section ne soit pas disponible dans le fichier original. La composition doit donc être:
### This file is an example authorization file for svnserve.
### Its format is identical to that of mod_authz_svn authorization
### files.
### As shown below each section defines authorizations for the path and
### (optional) repository specified by the section name.
### The authorizations follow. An authorization line can refer to:
### - a single user,
### - a group of users defined in a special [groups] section,
### - an alias defined in a special [aliases] section,
### - all authenticated users, using the '$authenticated' token,
### - only anonymous users, using the '$anonymous' token,
### - anyone, using the '*' wildcard.
###
### A match can be inverted by prefixing the rule with '~'. Rules can
### grant read ('r') access, read-write ('rw') access, or no access
### ('').
[aliases]
# joe = /C=XZ/ST=Dessert/L=Snake City/O=Snake Oil, Ltd./OU=Research Institute/CN=Joe Average
#[groups]
# harry_and_sally = harry,sally
# harry_sally_and_joe = harry,sally,&joe
# [/foo/bar]
# harry = rw
# &joe = r
# * =
# [repository:/baz/fuz]
# @harry_and_sally = rw
# * = r
[/]
@svnrepos1contrib = rw
@svnrepos1reader = r
* =
Droits du fichier
Lorsque le script est exécuté avec un compte root, les permissions sur ce fichier (pour une machine Ubuntu) sont les suivantes:
drw-r----- 2 www-data subversion 4096 2011-11-12 13:52 ./ drw-r----- 6 www-data subversion 4096 2011-10-22 18:35 ../ -rw------- 1 root subversion 1082 2011-11-12 13:52 authz -rw-r----- 1 root subversion 1130 2011-11-12 13:47 authz.bak -rw-r----- 1 www-data subversion 330 2011-10-22 18:38 passwd -rw-r----- 1 www-data subversion 2277 2011-10-22 18:41 svnserve.conf
Les droits ne sont donc pas suffisant et les programme, type WebSVN ou ViewVC, ne pourront pas lire les fichiers. Il est donc nécessaire de modifier le propriété et les droits d'accès avec un commande du type:
#chmod 640 /var/opt/svn/repos1/conf/authz #chown www-data:subversion /var/opt/svn/repos1/conf/authz
Planification
Cette étude permet donc d'avoir un outil qui va synchroniser les groupes LDAP dans les configurations SVN. Afin d'exécuter ce script périodiquement, tous les jours par exemple, il suffit de placer un script (ou un lien) dans la planification de anacron.
Dans le cadre de cette étude, le script python et le shell run_sync_ldap_groups.sh
ont été placés dans le répertoire /var/opt/ldap_svn
. Le shell permet d'exécuter le script python pour chacun des repository mis en place et nécessaitant une synchronisation. Pour les repository repos1
et repos2
, son contenu est le suivant:
#!/bin/sh
pwd="<MOT PASSE ADMIN>"
bindDn="cn=admin,dc=ejnserver,dc=fr"
ldapUrl="ldap://localhost:389"
baseDn="ou=groups,dc=ejnserver,dc=fr"
userQuery="(objectClass=*)"
userAttr="uid"
python /var/opt/ldap_svn/sync_ldap_groups_to_svn_authz.py \
-d "$bindDn" \
-p "$pwd" \
-l "$ldapUrl" \
-b "$baseDn" \
-g "(&(objectClass=groupOfNames)(cn=svn-repos1*))" \
-u "$userQuery" \
-i "$userAttr" \
-z /var/opt/svn/repos1/conf/authz
chmod 640 /var/opt/svn/repos1/conf/authz
chown www-data:subversion /var/opt/svn/repos1/conf/authz
python /var/opt/ldap_svn/sync_ldap_groups_to_svn_authz.py \
-d "$bindDn" \
-p "$pwd" \
-l "$ldapUrl" \
-b "$baseDn" \
-g "(&(objectClass=groupOfNames)(cn=svn-repos2*))" \
-u "$userQuery" \
-i "$userAttr" \
-z /var/opt/svn/repos2/conf/authz
chmod 640 /var/opt/svn/repos2/conf/authz
chown www-data:subversion /var/opt/svn/repos2/conf/authz
Pour la planification journalière, un simple lien est mis dans le répertoire /etc/cron.daily
:
#sudo ln -s /var/opt/ldap_svn/run_sync_ldap_groups.sh /etc/cron.daily/run_sync_ldap_groups