Merge branch 'master' of ssh://git.crans.org/git/ldap into cerveaulent

This commit is contained in:
Daniel STAN 2013-01-15 18:52:41 +01:00
commit 2073f5055b
14 changed files with 887 additions and 42 deletions

View file

@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from lc_ldap import lc_ldap
__all__ = [lc_ldap]

View file

@ -3,9 +3,11 @@
# #
# ATTRIBUTS.PY-- Description des attributs ldap # ATTRIBUTS.PY-- Description des attributs ldap
# #
# Copyright (C) 2010 Cr@ns <roots@crans.org> # Copyright (C) 2010-2013 Cr@ns <roots@crans.org>
# Author: Antoine Durand-Gasselin <adg@crans.org> # Authors: Antoine Durand-Gasselin <adg@crans.org>
# All rights reserved. # Nicolas Dandrimont <olasd@crans.org>
# Valentin Samir <samir@crans.org>
# Vincent Le Gallic <legallic@crans.org>
# #
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met: # modification, are permitted provided that the following conditions are met:
@ -54,13 +56,13 @@ def attrify(val, attr, ldif, conn, ctxt_check = True):
return CRANS_ATTRIBUTES.get(attr, Attr)(val, ldif, conn, ctxt_check) return CRANS_ATTRIBUTES.get(attr, Attr)(val, ldif, conn, ctxt_check)
class Attr(object): class Attr(object):
"""La liste des droits qui suffisent à avoir le droit de modifier la valeur"""
legend = "Human-readable description of attribute" legend = "Human-readable description of attribute"
singlevalue = None singlevalue = None
optional = None optional = None
conn = None conn = None
can_modify = ['Nounou'] can_modify = ['Nounou']
"""La liste des droits qui suffisent à avoir le droit de modifier la valeur"""
can_view = ['Nounou', 'apprenti', 'self', 'parent', 'owner'] can_view = ['Nounou', 'apprenti', 'self', 'parent', 'owner']
"""Qui peut voir l'attribut. Par défaut, les nounous et les apprentis """Qui peut voir l'attribut. Par défaut, les nounous et les apprentis
@ -154,8 +156,7 @@ class objectClass(Attr):
'adherent', 'club', 'machine', 'machineCrans', 'adherent', 'club', 'machine', 'machineCrans',
'borneWifi', 'machineWifi', 'machineFixe', 'borneWifi', 'machineWifi', 'machineFixe',
'cransAccount', 'service', 'facture', 'freeMid' ]: 'cransAccount', 'service', 'facture', 'freeMid' ]:
print(val) raise ValueError("Pourquoi insérer un objectClass=%r ?" % val)
raise ValueError("Pourquoi insérer un objectClass=%s ?" % val)
else: else:
self.value = unicode(val) self.value = unicode(val)
@ -163,7 +164,7 @@ class objectClass(Attr):
class intAttr(Attr): class intAttr(Attr):
def parse_value(self, val, ldif): def parse_value(self, val, ldif):
if int(val) <= 0: if int(val) <= 0:
raise ValueError("Valeur entière invalide : %s" % val) raise ValueError("Valeur entière invalide : %r" % val)
self.value = int(val) self.value = int(val)
def __unicode__(self): def __unicode__(self):
@ -220,7 +221,7 @@ class tel(Attr):
def parse_value(self, val, ldif): def parse_value(self, val, ldif):
self.value = format_tel(val) self.value = format_tel(val)
if len(self.value) == 0: if len(self.value) == 0:
raise ValueError("Numéro de téléphone invalide ('%s')" % val) raise ValueError("Numéro de téléphone invalide (%r)" % val)
class yearAttr(intAttr): class yearAttr(intAttr):
@ -228,8 +229,8 @@ class yearAttr(intAttr):
optional= True optional= True
def parse_value(self, val, ldif): def parse_value(self, val, ldif):
if int(val) < 1998 or int(val) > config.ann_scol: if int(val) < 1998:
raise ValueError("Année invalide (%s)" % val) raise ValueError("Année invalide (%r)" % val)
self.value = int(val) self.value = int(val)
@ -255,7 +256,7 @@ class mailAlias(Attr):
def parse_value(self, val, ldif): def parse_value(self, val, ldif):
val = val.lower() val = val.lower()
if not re.match('[a-z][-_.0-9a-z]+', val): if not re.match('[a-z][-_.0-9a-z]+', val):
raise ValueError("Alias mail invalide (%s)" % val) raise ValueError("Alias mail invalide (%r)" % val)
self.value = val self.value = val
@ -297,7 +298,7 @@ class chbre(Attr):
self.value = val self.value = val
return return
else: else:
raise ValueError("Club devrait etre en XclN, pas en %s" % val) raise ValueError("Club devrait etre en XclN, pas en %r" % val)
if val in (u"EXT", u"????"): if val in (u"EXT", u"????"):
self.value = val self.value = val
@ -320,8 +321,8 @@ class droits(Attr):
def parse_value(self, val, ldif): def parse_value(self, val, ldif):
if val.lower() not in ['apprenti', 'nounou', 'cableur', 'tresorier', 'bureau', if val.lower() not in ['apprenti', 'nounou', 'cableur', 'tresorier', 'bureau',
'webmaster', 'webradio', 'imprimeur', 'multimachines', 'victime', 'moderateur']: 'webmaster', 'webradio', 'imprimeur', 'multimachines', 'victime', 'moderateur', 'nounours']:
raise ValueError("Ces droits n'existent pas ('%s')" % val) raise ValueError("Ces droits n'existent pas (%r)" % val)
if val.lower() == 'webmaster': if val.lower() == 'webmaster':
self.value = u'WebMaster' self.value = u'WebMaster'
else: else:
@ -336,7 +337,7 @@ class solde(Attr):
def parse_value(self, solde, ldif): def parse_value(self, solde, ldif):
# on évite les dépassements, sauf si on nous dit de ne pas vérifier # on évite les dépassements, sauf si on nous dit de ne pas vérifier
if self.ctxt_check and not (float(solde) >= config.impression.decouvert and float(solde) <= 1024.): if self.ctxt_check and not (float(solde) >= config.impression.decouvert and float(solde) <= 1024.):
raise ValueError("Solde invalide: %s" % solde) raise ValueError("Solde invalide: %r" % solde)
self.value = solde self.value = solde
class dnsAttr(Attr): class dnsAttr(Attr):
@ -346,7 +347,7 @@ class dnsAttr(Attr):
name, net = dns.split('.', 1) name, net = dns.split('.', 1)
if self.ctxt_check and (net not in ['crans.org', 'wifi.crans.org'] or if self.ctxt_check and (net not in ['crans.org', 'wifi.crans.org'] or
not re.match('[a-z][-_a-z0-9]+', name)): not re.match('[a-z][-_a-z0-9]+', name)):
raise ValueError("Nom d'hote invalide '%s'" % dns) raise ValueError("Nom d'hote invalide %r" % dns)
self.value = dns self.value = dns
@ -436,18 +437,18 @@ class portAttr(Attr):
a,b = port.split(":", 1) a,b = port.split(":", 1)
if a: if a:
if int(a) <0 or int(a)> 65535: if int(a) <0 or int(a)> 65535:
raise ValueError("Port invalide: %s" % port) raise ValueError("Port invalide: %r" % port)
else: else:
a = 0 a = 0
if b: if b:
if int(a) <0 or int(a)> 65535: if int(a) <0 or int(a)> 65535:
raise ValueError("Port invalide: %s" % port) raise ValueError("Port invalide: %r" % port)
else: else:
b = 65535 b = 65535
self.value = [int(a), int(b)] self.value = [int(a), int(b)]
else: else:
if int(port) <0 or int(port)> 65535: if int(port) <0 or int(port)> 65535:
raise ValueError("Port invalide: %s" % port) raise ValueError("Port invalide: %r" % port)
self.value = [int(port)] self.value = [int(port)]
def __unicode__(self): def __unicode__(self):
@ -504,7 +505,7 @@ class responsable(Attr):
self.value = property(self.get_respo) self.value = property(self.get_respo)
def __unicode__(self): def __unicode__(self):
return self.value.attrs['aid'][0].__unicode__() return self.__resp
class blacklist(Attr): class blacklist(Attr):
@ -516,11 +517,11 @@ class blacklist(Attr):
def parse_value(self, bl, ldif): def parse_value(self, bl, ldif):
bl_debut, bl_fin, bl_type, bl_comm = bl.split('$') bl_debut, bl_fin, bl_type, bl_comm = bl.split('$')
now = time.time() now = time.time()
self.value = { 'debut' : int (bl_debut), self.value = { 'debut' : bl_debut if bl_debut == '-' else int (bl_debut),
'fin' : bl_fin if bl_fin == '-' else int(bl_fin), 'fin' : bl_fin if bl_fin == '-' else int(bl_fin),
'type' : bl_type, 'type' : bl_type,
'comm' : bl_comm, 'comm' : bl_comm,
'actif' : int(bl_debut) < now and (bl_fin == '-' or int(bl_fin) > now) } 'actif' : (bl_debut == '-' or int(bl_debut) < now) and (bl_fin == '-' or int(bl_fin) > now) }
def is_actif(self): def is_actif(self):
return self.value['actif'] return self.value['actif']
@ -561,7 +562,7 @@ class charteMA(Attr):
def parse_value(self, signed, ldif): def parse_value(self, signed, ldif):
if signed.upper() not in ["TRUE", "FALSE"]: if signed.upper() not in ["TRUE", "FALSE"]:
raise ValueError("La charte MA est soit TRUE ou FALSE, pas %s" % signed) raise ValueError("La charte MA est soit TRUE ou FALSE, pas %r" % signed)
self.value = signed.upper() self.value = signed.upper()
class homeDirectory(Attr): class homeDirectory(Attr):
@ -574,7 +575,7 @@ class homeDirectory(Attr):
if uid.startswith('club-'): if uid.startswith('club-'):
uid = uid.split('-',1)[1] uid = uid.split('-',1)[1]
if home != u'/home/%s' % uid and home != u'/home/club/%s' % uid: if home != u'/home/%s' % uid and home != u'/home/club/%s' % uid:
raise ValueError("Le répertoire personnel n'est pas bon: %s (devrait être %s ou %s)" % raise ValueError("Le répertoire personnel n'est pas bon: %r (devrait être %r ou %r)" %
(home, '/home/%s' % ldif['uid'][0], '/home/club/%s' % ldif['uid'][0])) (home, '/home/%s' % ldif['uid'][0], '/home/club/%s' % ldif['uid'][0]))
self.value = home self.value = home
@ -611,7 +612,7 @@ class loginShell(Attr):
'/bin//zsh' '/bin//zsh'
''] '']
if self.ctxt_check and (shell not in shells): if self.ctxt_check and (shell not in shells):
raise ValueError("Shell %s invalide" % shell) raise ValueError("Shell %r invalide" % shell)
self.value = shell self.value = shell
class uidNumber(intAttr): class uidNumber(intAttr):

View file

@ -41,6 +41,12 @@ def ip_of_mid(mid):
break break
else: else:
raise ValueError("Mid dans aucune plage: %d" % mid) raise ValueError("Mid dans aucune plage: %d" % mid)
if net == 'special':
try:
return netaddr.IPAddress(config.mid_machines_speciales[mid])
except KeyError:
return ValueError(u"Machine speciale inconnue: %d" % mid)
return netaddr.IPAddress(netaddr.IPNetwork(config.NETs[net][0]).first + mid - plage[0]) return netaddr.IPAddress(netaddr.IPNetwork(config.NETs[net][0]).first + mid - plage[0])

158
doc/Makefile Normal file
View file

@ -0,0 +1,158 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/LightweightCrans-LDAP.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/LightweightCrans-LDAP.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/LightweightCrans-LDAP"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/LightweightCrans-LDAP"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo
@echo "Coverage test finished. The results are in $(BUILDDIR)/coverage."

3
doc/README Normal file
View file

@ -0,0 +1,3 @@
Voir le fichier source/README.rst

17
doc/source/README.rst Normal file
View file

@ -0,0 +1,17 @@
Utilisation de Sphinx
=====================
La doc est généré à partir des fichiers dans :file:`source/`, et est écrite en
`reStructuredText`_.
.. _reStructuredText: http://sphinx-doc.org/rest.html
Les plugins ``autodoc`` et ``coverage`` sont activés, ils permettent
respectivement de récupérer les docstrings depuis le code source, et de voir ce
qui n'est pas documenté (cf. :file:`build/coverage/python.txt`).
Pour générer la doc, il y a un :file:`Makefile`. Les règles les plus utiles :
* :command:`make html` qui génère la documentation HTML dans :file:`build/html`,
* :command:`make coverage` qui actualise la liste du code non documenté.

243
doc/source/conf.py Normal file
View file

@ -0,0 +1,243 @@
# -*- coding: utf-8 -*-
#
# Lightweight Crans-LDAP documentation build configuration file, created by
# sphinx-quickstart on Tue Jan 8 12:43:48 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('../../'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Lightweight Crans-LDAP'
copyright = u'2013, Olivier Iffrig'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
language = 'fr'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'lc_ldap_doc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'lc_ldap_doc.tex', u'Lightweight Crans-LDAP Documentation',
u'Olivier Iffrig', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'lc_ldap', u'Lightweight Crans-LDAP Documentation',
[u'Olivier Iffrig'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'lc_ldap', u'Lightweight Crans-LDAP Documentation',
u'Olivier Iffrig', 'lc_ldap', 'Binding LDAP du Cr@ns',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'

9
doc/source/coverage.rst Normal file
View file

@ -0,0 +1,9 @@
Objets non documentés
=====================
Cette page liste les objets non documentés.
.. include:: ../build/coverage/python.txt
:start-line: 2

21
doc/source/index.rst Normal file
View file

@ -0,0 +1,21 @@
lc_ldap -- Lightweight Crans-LDAP
=================================
Table des matières :
.. toctree::
:maxdepth: 2
lc_ldap/index
lc_ldap/attributs
others
Index
=====
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View file

@ -0,0 +1,7 @@
lc_ldap.attributs -- Conteneurs pour les attributs LDAP
=======================================================
.. automodule:: attributs
:members:

View file

@ -0,0 +1,7 @@
lc_ldap -- Module principal
===========================
.. automodule:: lc_ldap
:members:

11
doc/source/others.rst Normal file
View file

@ -0,0 +1,11 @@
Annexes
=======
.. toctree::
:maxdepth: 2
README
coverage
.. var/coverage

244
filter.py Normal file
View file

@ -0,0 +1,244 @@
#/usr/bin/env python
# -*- coding: utf8 -*-
def human_to_ldap(filtre):
"""
Transforme les filtres "human readables" en
filtres respectant la syntaxe LDAP.
"""
# Piles, quand on croise une parenthèse ouvrante
# on change de pile
stacks = {0:''}
# Position dans la pile
pos = 0
# operateur \in {&, |, &|, }
operateur = ""
# La pile externe, qu'on merge à la pile en cours quand c'est utile.
ext_stack = ""
# Élement de la forme paiement=2012 ou retour de la pile
# supérieure (quand on croise une parenthèse fermante, la
# pile est dumpée dans argument)
# Est systématiquement dumpé dans ext_stack quand on croise
# un opérateur.
argument = ""
# Y a-t-il un ! dans la salle ?
neg = False
# Quand on quitte une pile parenthésée, on veut savoir quel était
# l'opérateur actif avant, pour que le comportement de la fonction
# soit défini.
anciens_operateurs = []
for char in filtre:
if char == "(":
# Une nouvelle stack ne démarre que si le dernier argument a été dumpé
# dans l'ext stack (si ce n'est pas le cas quand on respecte la syntaxe
# des filtres, la fonction est mal codée, donc on plante).
if argument != "":
raise ValueError('Argument entammé non terminé !')
# Un dumpe ext_stack dans la stack en cours.
stacks[pos] += "%s" % (ext_stack)
# On augmente le conteur de la stack
pos = pos + 1
# On (ré)?initialise la stack
stacks[pos] = ''
# On stocke l'opérateur en cours dans la stack inférieure dans
# une liste.
anciens_operateurs.append(operateur)
# On flush operateur
operateur = ""
# On flush ext_stack, l'environnement est enfin propre pour
# bosser dans la nouvelle pile.
ext_stack = ""
elif char == ")":
# Cas classique
if operateur == "|":
ext_stack += "(%s)" % argument
# Moins classique, &| est un opérateur servant à dire qu'on
# a déjà croisé un | dans la stack en cours, le cas échéant,
# celui-ci doit être pris en compte, pour respecter la
# priorité du &. Dans ce cas seulement, une parenthèse
# est ouverte et non fermée (cf elif char == '&' cas
# operateur == "|"), on la ferme ici.
elif operateur == "&|":
argument += ")"
ext_stack += "(%s)" % argument
# Classique
elif operateur == "&":
ext_stack += "(%s)" % argument
# Pas d'opérateur, pas de parenthèse superflue.
else:
ext_stack += "%s" % argument
# On passe la stack en argument, les parenthèses
# l'encadrant seront placées d'un qu'un opérateur
# sera croisé.
argument = "%s%s" % (stacks[pos], ext_stack)
# Ménage
stacks[pos] = ""
ext_stack = ""
pos = pos - 1
# Retour à l'opérateur de la stack précédente.
operateur = anciens_operateurs.pop()
elif char == "|":
if not argument:
raise ValueError('Aucun argument')
# Ce cas permet d'éviter une répétition de la forme :
# (|(a)(|(b)(c))), quand on est déjà dans un ou, on
# rajoute juste l'argument suivant sans remettre de
# symbole.
if operateur == "|":
# neg est True si on a croisé un ! dans la chaîne.
# À améliorer pour qu'il ne marche que pour !=
if neg:
argument = "!(%s)" % argument
neg = False
# Ajout à la stack externe de l'argument
ext_stack += "(%s)" % argument
argument = ""
elif operateur == "&":
if neg:
argument = "!(%s)" % argument
neg = False
ext_stack += "(%s)" % argument
# | prend le relais sur &, on dumpe ext_stack et on commence une nouvelle
# chaîne qui sera ajoutée à droite. Le ou va tout à gauche de la stack
# en cours, pour s'appliquer sur tout son contenu.
stacks[pos] = "%s(%s%s)" % (char, stacks[pos], ext_stack)
ext_stack = ""
argument = ""
operateur = "|"
# C'est un & dans un |, donc on ferme juste la chaîne
# des &, d'où la parenthèse fermante en trop.
elif operateur == "&|":
if neg:
argument = "!(%s)" % argument
neg = False
ext_stack += "(%s)" % argument
argument = ""
stacks[pos] = "%s%s)" % (stacks[pos], ext_stack)
ext_stack = ""
operateur = "|"
# Pas encore d'opérateur annoncé
elif operateur == "":
if neg:
argument = "!(%s)" % argument
neg = False
ext_stack += "%s" % argument
argument = ""
# ouverture
stacks[pos] = "%s(%s%s)" % (char, stacks[pos], ext_stack)
ext_stack = ""
operateur = "|"
else:
raise TypeError('Erreur d\'opérateur.')
elif char == "&":
if not argument:
raise ValueError('Aucun argument')
if operateur == "&":
if neg:
argument = "!(%s)" % argument
neg = False
ext_stack += "(%s)" % argument
argument = ""
# Le cas spécial, on ouvre un & après un |, donc pour respecter
# la priorité de &, on démarre une nouvelle chaîne, mais dans
# l'ext_stack (contrairement à char == '|', operateur == "&")
elif operateur == "|":
if neg:
argument = "!(%s)" % argument
neg = False
ext_stack += "(%s(%s)" % (char, argument)
argument = ""
operateur = "&|"
# On était déjà dans un &|...
elif operateur == "&|":
if neg:
argument = "!(%s)" % argument
neg = False
ext_stack += "(%s)" % argument
argument = ""
# Comme ci-dessus
elif operateur == "":
if neg:
argument = "!(%s)" % argument
neg = False
ext_stack += "%s" % argument
argument = ""
stacks[pos] = "%s(%s%s)" % (char, stacks[pos], ext_stack)
ext_stack = ""
operateur = "&"
else:
raise TypeError('Erreur d\'opérateur.')
elif char == "!":
neg = True
# Remplissage d'argument
else:
argument += char
# Décommenter pour débug.
# En modifiant un peu, encore plus utile pour savoir ce qu'il
# fait à chaque étape !
#print stacks
#print pos, argument, ext_stack, operateur
if pos > 0:
raise Exception("Tu ne sais pas parenthéser, crétin.")
else:
# Comme parenthèse fermante.
if neg:
argument = "!(%s)" % argument
if operateur == "&|":
argument += ')'
ext_stack += "(%s)" % argument
argument = ""
stacks[pos] = "(%s%s)" % (stacks[pos], ext_stack)
ext_stack = ""
# On retourne la pile de plus haut niveau
return stacks[0]

View file

@ -3,9 +3,13 @@
# #
# LC_LDAP.PY-- LightWeight CransLdap # LC_LDAP.PY-- LightWeight CransLdap
# #
# Copyright (C) 2010 Cr@ns <roots@crans.org> # Copyright (C) 2010-2013 Cr@ns <roots@crans.org>
# Author: Antoine Durand-Gasselin <adg@crans.org> # Authors: Antoine Durand-Gasselin <adg@crans.org>
# All rights reserved. # Nicolas Dandrimont <olasd@crans.org>
# Olivier Iffrig <iffrig@crans.org>
# Valentin Samir <samir@crans.org>
# Daniel Stan <dstan@crans.org>
# Vincent Le Gallic <legallic@crans.org>
# #
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met: # modification, are permitted provided that the following conditions are met:
@ -32,6 +36,7 @@
from __future__ import with_statement from __future__ import with_statement
import os, sys, ldap, re, netaddr, datetime, copy, time, random import os, sys, ldap, re, netaddr, datetime, copy, time, random
import ldap.filter
from ldap.modlist import addModlist, modifyModlist from ldap.modlist import addModlist, modifyModlist
try: try:
from Levenshtein import jaro from Levenshtein import jaro
@ -43,13 +48,30 @@ import config, crans_utils
from attributs import attrify, blacklist from attributs import attrify, blacklist
from ldap_locks import CransLock from ldap_locks import CransLock
uri = 'ldapi:///' #'ldap://ldap.adm.crans.org/' uri = 'ldap://ldap.adm.crans.org/'
base_dn = 'ou=data,dc=crans,dc=org' base_dn = 'ou=data,dc=crans,dc=org'
log_dn = "cn=log" log_dn = "cn=log"
# Pour enregistrer dans l'historique, on a besoin de savoir qui exécute le script
# Si le script a été exécuté via un sudo, la variable SUDO_USER (l'utilisateur qui a effectué le sudo)
# est plus pertinente que USER (qui sera root)
current_user = os.getenv("SUDO_USER") or os.getenv("USER")
# Quand on a besoin du fichier de secrets
def import_secrets():
if not "/etc/crans/secrets/" in sys.path:
sys.path.append("/etc/crans/secrets/")
import secrets
return secrets
# Champs à ignorer dans l'historique # Champs à ignorer dans l'historique
HIST_IGNORE_FIELDS = ["modifiersName", "entryCSN", "modifyTimestamp", "historique"] HIST_IGNORE_FIELDS = ["modifiersName", "entryCSN", "modifyTimestamp", "historique"]
def escape(chaine):
"""Renvoie une chaîne échapée pour pouvoir la mettre en toute sécurité
dans une requête ldap."""
return ldap.filter.escape_filter_chars(chaine)
def ldif_to_uldif(ldif): def ldif_to_uldif(ldif):
uldif = {} uldif = {}
for attr, vals in ldif.items(): for attr, vals in ldif.items():
@ -76,28 +98,42 @@ def cldif_to_ldif(cldif):
def lc_ldap_test(): def lc_ldap_test():
"""Binding LDAP à la base de tests""" """Binding LDAP à la base de tests"""
return lc_ldap(dn='cn=admin,dc=crans,dc=org', cred='75bdb64f32') return lc_ldap(uri='ldap://vo.adm.crans.org',dn='cn=admin,dc=crans,dc=org', cred='75bdb64f32')
def lc_ldap_admin():
"""Binding LDAP à la vraie base, en admin.
Possible seulement si on peut lire secrets.py"""
secrets = import_secrets()
return lc_ldap(uri='ldap://ldap.adm.crans.org/', dn=secrets.ldap_auth_dn, cred=secrets.ldap_password)
class lc_ldap(ldap.ldapobject.LDAPObject): class lc_ldap(ldap.ldapobject.LDAPObject):
"""Connexion à la base ldap crans, chaque instance représente une connexion """Connexion à la base ldap crans, chaque instance représente une connexion
""" """
def __init__(self, dn=None, user=None, cred=None, uri=uri): def __init__(self, dn=None, user=None, cred=None, uri=uri, test=False):
"""Initialise la connexion ldap, """Initialise la connexion ldap,
- En authentifiant avec dn et cred s'ils sont précisés - En authentifiant avec dn et cred s'ils sont précisés
- Si dn n'est pas précisé, mais que user est précisé, récupère - Si dn n'est pas précisé, mais que user est précisé, récupère
le dn associé à l'uid user, et effectue l'authentification le dn associé à l'uid user, et effectue l'authentification
avec ce dn et cred avec ce dn et cred
- Sinon effectue une authentification anonyme - Sinon effectue une authentification anonyme
Si test est à True, on se connecte à la base de test sur vo.
""" """
if test:
uri = "ldapi:///"
ldap.ldapobject.LDAPObject.__init__(self, uri) ldap.ldapobject.LDAPObject.__init__(self, uri)
if user and not re.match('[a-z_][a-z0-9_-]*', user): if user and not re.match('[a-z_][a-z0-9_-]*', user):
raise ValueError('Invalid user name: %s' % user) raise ValueError('Invalid user name: %r' % user)
# Si un username, on récupère le dn associé # Si un username, on récupère le dn associé
if user and not dn: if user and not dn:
self.simple_bind_s(base_dn) if test:
# …en anonyme si on se connecte à la base de test
self.simple_bind_s(base_dn)
else:
# …sinon, en se connectant en readonly (on récupère les identifiants dans secrets.py)
secrets = import_secrets()
self.simple_bind_s(who=secrets.ldap_readonly_auth_dn, cred=secrets.ldap_readonly_password)
res = self.search_s(base_dn, 1, 'uid=%s' % user) res = self.search_s(base_dn, 1, 'uid=%s' % user)
if len(res) < 1: if len(res) < 1:
raise ldap.INVALID_CREDENTIALS({'desc': 'No such user: %s' % user }) raise ldap.INVALID_CREDENTIALS({'desc': 'No such user: %s' % user })
@ -120,8 +156,11 @@ class lc_ldap(ldap.ldapobject.LDAPObject):
def search(self, filterstr='(objectClass=*)', mode='ro', dn= base_dn, scope= 2, sizelimit=400): def search(self, filterstr='(objectClass=*)', mode='ro', dn= base_dn, scope= 2, sizelimit=400):
"""La fonction de recherche dans la base ldap qui renvoie un liste de """La fonction de recherche dans la base ldap qui renvoie un liste de
CransLdapObjects. On utilise la feature de sizelimit de python ldap""" CransLdapObjects. On utilise la feature de sizelimit de python ldap"""
res = self.search_ext_s(dn, scope, filterstr, sizelimit=sizelimit) ldap_res = self.search_ext_s(dn, scope, filterstr, sizelimit=sizelimit)
return [ new_cransldapobject(self, r[0], mode=mode) for r in res ] ret = []
for dn, ldif in ldap_res:
ret.append(new_cransldapobject(self, dn, mode=mode))
return ret
def allMachinesAdherents(self): def allMachinesAdherents(self):
"""Renvoie la liste de toutes les machines et de tous les adherents """Renvoie la liste de toutes les machines et de tous les adherents
@ -192,7 +231,7 @@ class lc_ldap(ldap.ldapobject.LDAPObject):
assert isinstance(owner, adherent) or isinstance(owner, club) assert isinstance(owner, adherent) or isinstance(owner, club)
# XXX - Vérifier les droits # XXX - Vérifier les droits
else: raise ValueError("Realm inconnu: %s" % realm) else: raise ValueError("Realm inconnu: %r" % realm)
# On récupère la plage des mids # On récupère la plage des mids
if realm == 'fil': if realm == 'fil':
@ -327,6 +366,7 @@ class CransLdapObject(object):
self.attrs = ldif_to_cldif(self.attrs, conn, check_ctxt = False) self.attrs = ldif_to_cldif(self.attrs, conn, check_ctxt = False)
if mode in ['w', 'rw']: if mode in ['w', 'rw']:
### Vérification que `λv. str(Attr(v))` est bien une projection ### Vérification que `λv. str(Attr(v))` est bien une projection
### C'est-à-dire que si on str(Attr(str(Attr(v)))) on retombe sur str(Attr(v))
oldif = res[0][1] oldif = res[0][1]
nldif = cldif_to_ldif(self.attrs) nldif = cldif_to_ldif(self.attrs)
@ -336,10 +376,27 @@ class CransLdapObject(object):
if v in vals: if v in vals:
vals.remove(v) vals.remove(v)
nvals = [nldif[attr][v.index(v)] for v in vals ] nvals = [nldif[attr][v.index(v)] for v in vals ]
raise EnvironmentError("λv. str(Attr(v)) n'est peut-être pas une projection:", attr, nvals, vals) raise EnvironmentError("λv. str(Attr(v)) n'est peut-être pas une projection (ie non idempotente):", attr, nvals, vals)
self._modifs = ldif_to_cldif(ldif_to_uldif(res[0][1]), conn, check_ctxt = False) self._modifs = ldif_to_cldif(ldif_to_uldif(res[0][1]), conn, check_ctxt = False)
def _get_fields(self):
"""Renvoie la liste des champs LDAP de l'objet"""
return self.ofields + self.xfields + self.ufields + self.mfields
fields = property(_get_fields)
def history_add(self, login, chain):
"""Ajoute une ligne à l'historique de l'objet.
###ATTENTION : C'est un kludge pour pouvoir continuer à faire "comme avant",
### mais on devrait tout recoder pour utiliser l'historique LDAP"""
assert isinstance(login, str) or isinstance(login, unicode)
assert isinstance(chain, unicode)
new_line = u"%s, %s : %s" % (time.strftime("%d/%m/%Y %H:%M"), login, chain)
# Attention, le __setitem__ est surchargé, mais pas .append sur l'historique
self["historique"] = self["historique"] + [new_line]
def save(self): def save(self):
"""Sauvegarde dans la base les modifications apportées à l'objet. """Sauvegarde dans la base les modifications apportées à l'objet.
Interne: Vérifie que self._modifs contient des valeurs correctes et Interne: Vérifie que self._modifs contient des valeurs correctes et
@ -445,8 +502,18 @@ class CransLdapObject(object):
- Proposer de filtrer les blacklistes avec un arg supplémentaire ? - Proposer de filtrer les blacklistes avec un arg supplémentaire ?
- Vérifier les blacklistes des machines pour les adhérents ? - Vérifier les blacklistes des machines pour les adhérents ?
""" """
blacklist_liste=[]
# blacklistes virtuelle si on est un adhérent pour carte étudiant et chambre invalides
if self.__class__.__name__ == "adherent" and self.paiement_ok():
if not config.periode_transitoire and config.bl_carte_et_actif and not self.carte_ok() and not self.sursis_carte():
bl = blacklist(u'%s$%s$%s$%s' % ('-', '-', 'carte_etudiant', ''), {}, self.conn, False)
blacklist_liste.append(bl)
if self['chbre'][0].value == '????':
bl = blacklist(u'%s$%s$%s$%s' % ('-', '-', 'chambre_invalide', ''), {}, self.conn, False)
blacklist_liste.append(bl)
attrs = (self.attrs if self.mode not in ["w", "rw"] else self._modifs) attrs = (self.attrs if self.mode not in ["w", "rw"] else self._modifs)
return filter((lambda bl: bl.is_actif()), attrs.get("blacklist",[])) blacklist_liste.extend(filter((lambda bl: bl.is_actif()), attrs.get("blacklist",[])))
return blacklist_liste
def blacklist(self, sanction, commentaire, debut="now", fin = '-'): def blacklist(self, sanction, commentaire, debut="now", fin = '-'):
u""" u"""
@ -477,6 +544,13 @@ class proprio(CransLdapObject):
super(proprio, self).__init__(conn, dn, mode, ldif) super(proprio, self).__init__(conn, dn, mode, ldif)
self._machines = machines self._machines = machines
def sursis_carte(self):
for h in self['historique'][::-1]:
x=re.match("(.*),.* : .*(paiement\+%s|inscription).*" % config.ann_scol,h.value)
if x != None:
return ((time.time()-time.mktime(time.strptime(x.group(1),'%d/%m/%Y %H:%M')))<=config.sursis_carte)
return False
def paiement_ok(self): def paiement_ok(self):
u"""Renvoie si le propriétaire a payé pour l'année en cours""" u"""Renvoie si le propriétaire a payé pour l'année en cours"""
if self.dn == base_dn: if self.dn == base_dn:
@ -486,8 +560,15 @@ class proprio(CransLdapObject):
for paiement in self['paiement']: for paiement in self['paiement']:
if paiement.value == config.ann_scol: if paiement.value == config.ann_scol:
bool_paiement = True bool_paiement = True
break
# Pour la période transitoire année précédente ok
if config.periode_transitoire and paiement.value == config.ann_scol -1:
bool_paiement = True
break
except KeyError: except KeyError:
pass pass
# Doit-on bloquer en cas de manque de la carte d'etudiant ?
# (si période transitoire on ne bloque dans aucun cas)
if config.bl_carte_et_definitif and not 'club' in map(lambda x:x.value,self["objectClass"]): if config.bl_carte_et_definitif and not 'club' in map(lambda x:x.value,self["objectClass"]):
bool_carte = False bool_carte = False
try: try:
@ -496,10 +577,42 @@ class proprio(CransLdapObject):
bool_carte = True bool_carte = True
except KeyError: except KeyError:
pass pass
# Si inscrit depuis moins de config.sursis_carte, on laisse un sursis
if not bool_carte and self.sursis_carte():
bool_carte = True
return bool_carte and bool_paiement return bool_carte and bool_paiement
return bool_paiement return bool_paiement
def carte_ok(self):
u"""Renvoie si le propriétaire a donné sa carte pour l'année en cours"""
if not self.dn == base_dn and config.bl_carte_et_actif and not 'club' in map(lambda x:x.value,self["objectClass"]):
bool_carte = False
try:
for carte in self['carteEtudiant']:
if carte.value == config.ann_scol:
bool_carte = True
except KeyError:
pass
return bool_carte
return True
def update_solde(self, diff, comment=u"", login=None):
"""Modifie le solde du proprio. diff peut être négatif ou positif."""
login = login or current_user
assert isinstance(diff, int) or isinstance(diff, float)
assert isinstance(comment, unicode)
solde = float(self["solde"][0].value)
new_solde = solde + diff
# On vérifie qu'on ne dépasse par le découvert autorisé
if new_solde < config.impression.decouvert:
raise ValueError(u"Solde minimal atteint, opération non effectuée.")
transaction = u"credit" if diff >=0 else u"debit"
new_solde = u"%.2f" % new_solde
self.history_add(login, u"%s %.2f Euros [%s]" % (transaction, abs(diff), comment))
self["solde"] = new_solde
def machines(self): def machines(self):
"""Renvoie la liste des machines""" """Renvoie la liste des machines"""