diff --git a/__init__.py b/__init__.py index e69de29..9cf1040 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from lc_ldap import lc_ldap +__all__ = [lc_ldap] diff --git a/attributs.py b/attributs.py index 8c4cbea..8be7a3e 100644 --- a/attributs.py +++ b/attributs.py @@ -3,9 +3,11 @@ # # ATTRIBUTS.PY-- Description des attributs ldap # -# Copyright (C) 2010 Cr@ns -# Author: Antoine Durand-Gasselin -# All rights reserved. +# Copyright (C) 2010-2013 Cr@ns +# Authors: Antoine Durand-Gasselin +# Nicolas Dandrimont +# Valentin Samir +# Vincent Le Gallic # # Redistribution and use in source and binary forms, with or without # 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) class Attr(object): + """La liste des droits qui suffisent à avoir le droit de modifier la valeur""" legend = "Human-readable description of attribute" singlevalue = None optional = None conn = None can_modify = ['Nounou'] - """La liste des droits qui suffisent à avoir le droit de modifier la valeur""" can_view = ['Nounou', 'apprenti', 'self', 'parent', 'owner'] """Qui peut voir l'attribut. Par défaut, les nounous et les apprentis @@ -154,8 +156,7 @@ class objectClass(Attr): 'adherent', 'club', 'machine', 'machineCrans', 'borneWifi', 'machineWifi', 'machineFixe', 'cransAccount', 'service', 'facture', 'freeMid' ]: - print(val) - raise ValueError("Pourquoi insérer un objectClass=%s ?" % val) + raise ValueError("Pourquoi insérer un objectClass=%r ?" % val) else: self.value = unicode(val) @@ -163,7 +164,7 @@ class objectClass(Attr): class intAttr(Attr): def parse_value(self, val, ldif): if int(val) <= 0: - raise ValueError("Valeur entière invalide : %s" % val) + raise ValueError("Valeur entière invalide : %r" % val) self.value = int(val) def __unicode__(self): @@ -220,7 +221,7 @@ class tel(Attr): def parse_value(self, val, ldif): self.value = format_tel(val) 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): @@ -228,8 +229,8 @@ class yearAttr(intAttr): optional= True def parse_value(self, val, ldif): - if int(val) < 1998 or int(val) > config.ann_scol: - raise ValueError("Année invalide (%s)" % val) + if int(val) < 1998: + raise ValueError("Année invalide (%r)" % val) self.value = int(val) @@ -255,7 +256,7 @@ class mailAlias(Attr): def parse_value(self, val, ldif): val = val.lower() 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 @@ -297,7 +298,7 @@ class chbre(Attr): self.value = val return 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"????"): self.value = val @@ -320,8 +321,8 @@ class droits(Attr): def parse_value(self, val, ldif): if val.lower() not in ['apprenti', 'nounou', 'cableur', 'tresorier', 'bureau', - 'webmaster', 'webradio', 'imprimeur', 'multimachines', 'victime', 'moderateur']: - raise ValueError("Ces droits n'existent pas ('%s')" % val) + 'webmaster', 'webradio', 'imprimeur', 'multimachines', 'victime', 'moderateur', 'nounours']: + raise ValueError("Ces droits n'existent pas (%r)" % val) if val.lower() == 'webmaster': self.value = u'WebMaster' else: @@ -336,7 +337,7 @@ class solde(Attr): def parse_value(self, solde, ldif): # 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.): - raise ValueError("Solde invalide: %s" % solde) + raise ValueError("Solde invalide: %r" % solde) self.value = solde class dnsAttr(Attr): @@ -346,7 +347,7 @@ class dnsAttr(Attr): name, net = dns.split('.', 1) if self.ctxt_check and (net not in ['crans.org', 'wifi.crans.org'] or 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 @@ -436,18 +437,18 @@ class portAttr(Attr): a,b = port.split(":", 1) if a: if int(a) <0 or int(a)> 65535: - raise ValueError("Port invalide: %s" % port) + raise ValueError("Port invalide: %r" % port) else: a = 0 if b: if int(a) <0 or int(a)> 65535: - raise ValueError("Port invalide: %s" % port) + raise ValueError("Port invalide: %r" % port) else: b = 65535 self.value = [int(a), int(b)] else: if int(port) <0 or int(port)> 65535: - raise ValueError("Port invalide: %s" % port) + raise ValueError("Port invalide: %r" % port) self.value = [int(port)] def __unicode__(self): @@ -504,7 +505,7 @@ class responsable(Attr): self.value = property(self.get_respo) def __unicode__(self): - return self.value.attrs['aid'][0].__unicode__() + return self.__resp class blacklist(Attr): @@ -516,11 +517,11 @@ class blacklist(Attr): def parse_value(self, bl, ldif): bl_debut, bl_fin, bl_type, bl_comm = bl.split('$') 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), 'type' : bl_type, '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): return self.value['actif'] @@ -561,7 +562,7 @@ class charteMA(Attr): def parse_value(self, signed, ldif): 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() class homeDirectory(Attr): @@ -574,7 +575,7 @@ class homeDirectory(Attr): if uid.startswith('club-'): uid = uid.split('-',1)[1] 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])) self.value = home @@ -611,7 +612,7 @@ class loginShell(Attr): '/bin//zsh' ''] if self.ctxt_check and (shell not in shells): - raise ValueError("Shell %s invalide" % shell) + raise ValueError("Shell %r invalide" % shell) self.value = shell class uidNumber(intAttr): diff --git a/crans_utils.py b/crans_utils.py index b4b498c..33c5620 100644 --- a/crans_utils.py +++ b/crans_utils.py @@ -41,6 +41,12 @@ def ip_of_mid(mid): break else: 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]) diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..fa18b24 --- /dev/null +++ b/doc/Makefile @@ -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 ' where 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." diff --git a/doc/README b/doc/README new file mode 100644 index 0000000..1bd398e --- /dev/null +++ b/doc/README @@ -0,0 +1,3 @@ + +Voir le fichier source/README.rst + diff --git a/doc/source/README.rst b/doc/source/README.rst new file mode 100644 index 0000000..d142efd --- /dev/null +++ b/doc/source/README.rst @@ -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é. + diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..a2d7439 --- /dev/null +++ b/doc/source/conf.py @@ -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 +# " v 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 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' diff --git a/doc/source/coverage.rst b/doc/source/coverage.rst new file mode 100644 index 0000000..d9c8274 --- /dev/null +++ b/doc/source/coverage.rst @@ -0,0 +1,9 @@ + +Objets non documentés +===================== + +Cette page liste les objets non documentés. + +.. include:: ../build/coverage/python.txt + :start-line: 2 + diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..2887011 --- /dev/null +++ b/doc/source/index.rst @@ -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` + diff --git a/doc/source/lc_ldap/attributs.rst b/doc/source/lc_ldap/attributs.rst new file mode 100644 index 0000000..093d760 --- /dev/null +++ b/doc/source/lc_ldap/attributs.rst @@ -0,0 +1,7 @@ + +lc_ldap.attributs -- Conteneurs pour les attributs LDAP +======================================================= + +.. automodule:: attributs + :members: + diff --git a/doc/source/lc_ldap/index.rst b/doc/source/lc_ldap/index.rst new file mode 100644 index 0000000..b85686d --- /dev/null +++ b/doc/source/lc_ldap/index.rst @@ -0,0 +1,7 @@ + +lc_ldap -- Module principal +=========================== + +.. automodule:: lc_ldap + :members: + diff --git a/doc/source/others.rst b/doc/source/others.rst new file mode 100644 index 0000000..de1e8ca --- /dev/null +++ b/doc/source/others.rst @@ -0,0 +1,11 @@ + +Annexes +======= + +.. toctree:: + :maxdepth: 2 + + README + coverage +.. var/coverage + diff --git a/filter.py b/filter.py new file mode 100644 index 0000000..8b0171f --- /dev/null +++ b/filter.py @@ -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] diff --git a/lc_ldap.py b/lc_ldap.py index 3041308..8d8a241 100644 --- a/lc_ldap.py +++ b/lc_ldap.py @@ -3,9 +3,13 @@ # # LC_LDAP.PY-- LightWeight CransLdap # -# Copyright (C) 2010 Cr@ns -# Author: Antoine Durand-Gasselin -# All rights reserved. +# Copyright (C) 2010-2013 Cr@ns +# Authors: Antoine Durand-Gasselin +# Nicolas Dandrimont +# Olivier Iffrig +# Valentin Samir +# Daniel Stan +# Vincent Le Gallic # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -32,6 +36,7 @@ from __future__ import with_statement import os, sys, ldap, re, netaddr, datetime, copy, time, random +import ldap.filter from ldap.modlist import addModlist, modifyModlist try: from Levenshtein import jaro @@ -43,13 +48,30 @@ import config, crans_utils from attributs import attrify, blacklist 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' 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 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): uldif = {} for attr, vals in ldif.items(): @@ -76,28 +98,42 @@ def cldif_to_ldif(cldif): def lc_ldap_test(): """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): """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, - 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 le dn associé à l'uid user, et effectue l'authentification avec ce dn et cred - 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) 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: - 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) if len(res) < 1: 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): """La fonction de recherche dans la base ldap qui renvoie un liste de CransLdapObjects. On utilise la feature de sizelimit de python ldap""" - res = self.search_ext_s(dn, scope, filterstr, sizelimit=sizelimit) - return [ new_cransldapobject(self, r[0], mode=mode) for r in res ] + ldap_res = self.search_ext_s(dn, scope, filterstr, sizelimit=sizelimit) + ret = [] + for dn, ldif in ldap_res: + ret.append(new_cransldapobject(self, dn, mode=mode)) + return ret def allMachinesAdherents(self): """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) # 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 if realm == 'fil': @@ -327,6 +366,7 @@ class CransLdapObject(object): self.attrs = ldif_to_cldif(self.attrs, conn, check_ctxt = False) if mode in ['w', 'rw']: ### 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] nldif = cldif_to_ldif(self.attrs) @@ -336,10 +376,27 @@ class CransLdapObject(object): if v in vals: vals.remove(v) 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) + 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): """Sauvegarde dans la base les modifications apportées à l'objet. 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 ? - 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) - 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 = '-'): u""" @@ -477,6 +544,13 @@ class proprio(CransLdapObject): super(proprio, self).__init__(conn, dn, mode, ldif) 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): u"""Renvoie si le propriétaire a payé pour l'année en cours""" if self.dn == base_dn: @@ -486,8 +560,15 @@ class proprio(CransLdapObject): for paiement in self['paiement']: if paiement.value == config.ann_scol: 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: 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"]): bool_carte = False try: @@ -496,10 +577,42 @@ class proprio(CransLdapObject): bool_carte = True except KeyError: 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_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): """Renvoie la liste des machines"""