From 10448cc8960537838c2cc18da0888dceb3912be6 Mon Sep 17 00:00:00 2001 From: admiralobvious <aferlandqc@gmail.com> Date: Sat, 9 Aug 2014 23:56:57 -0400 Subject: [PATCH] first code commit --- .gitignore | 56 ++++ LICENSE | 21 ++ README.md | 24 +- docs/Makefile | 177 ++++++++++++ docs/_themes/LICENSE | 37 +++ docs/_themes/README | 0 docs/_themes/flask/layout.html | 24 ++ docs/_themes/flask/relations.html | 19 ++ docs/_themes/flask/static/flasky.css_t | 0 docs/_themes/flask/theme.conf | 0 docs/_themes/flask_small/layout.html | 22 ++ docs/_themes/flask_small/static/flasky.css_t | 0 docs/_themes/flask_small/theme.conf | 0 docs/_themes/flask_theme_support.py | 86 ++++++ docs/conf.py | 288 +++++++++++++++++++ docs/index.rst | 92 ++++++ example/app.py | 63 ++++ flask_simpleldap/__init__.py | 257 +++++++++++++++++ requirements.txt | 2 + setup.py | 36 +++ 20 files changed, 1203 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 docs/Makefile create mode 100644 docs/_themes/LICENSE create mode 100644 docs/_themes/README create mode 100644 docs/_themes/flask/layout.html create mode 100644 docs/_themes/flask/relations.html create mode 100644 docs/_themes/flask/static/flasky.css_t create mode 100644 docs/_themes/flask/theme.conf create mode 100644 docs/_themes/flask_small/layout.html create mode 100644 docs/_themes/flask_small/static/flasky.css_t create mode 100644 docs/_themes/flask_small/theme.conf create mode 100644 docs/_themes/flask_theme_support.py create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 example/app.py create mode 100644 flask_simpleldap/__init__.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4738dc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cbfb58b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Alexandre Ferland + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index aef27d1..5811f48 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ -flask-simpleldap +Flask-SimpleLDAP ================ + +Flask-SimpleLDAP provides LDAP authentication for Flask. + +Quickstart +------- + + from flask import Flask + from flask.ext.simpleldap import LDAP + + app = Flask(__name__) + ldap = LDAP(app) + + app.config['LDAP_BASE_DN'] = 'OU=users,dc=example,dc=org' + app.config['LDAP_USERNAME'] = 'CN=user,OU=Users,DC=example,DC=org' + app.config['LDAP_PASSWORD'] = 'password' + + +Resources +--------- + +- [Documentation](http://flask-simpleldap.readthedocs.org/en/latest/) +- [PyPI](https://pypi.python.org/pypi/Flask-SimpleLDAP) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..372cedc --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @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 " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @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/Flask-SimpleLDAP.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-SimpleLDAP.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/Flask-SimpleLDAP" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-SimpleLDAP" + @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." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @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." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE new file mode 100644 index 0000000..8daab7e --- /dev/null +++ b/docs/_themes/LICENSE @@ -0,0 +1,37 @@ +Copyright (c) 2010 by Armin Ronacher. + +Some rights reserved. + +Redistribution and use in source and binary forms of the theme, with or +without modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +We kindly ask you to only use these themes in an unmodified manner just +for Flask and Flask-related products, not for unrelated projects. If you +like the visual style and want to use it for your own projects, please +consider making some larger changes to the themes (such as changing +font faces, sizes, colors or margins). + +THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/_themes/README b/docs/_themes/README new file mode 100644 index 0000000..e69de29 diff --git a/docs/_themes/flask/layout.html b/docs/_themes/flask/layout.html new file mode 100644 index 0000000..19c43fb --- /dev/null +++ b/docs/_themes/flask/layout.html @@ -0,0 +1,24 @@ +{%- extends "basic/layout.html" %} +{%- block extrahead %} + {{ super() }} + {% if theme_touch_icon %} + <link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" /> + {% endif %} + <meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9"> +{% endblock %} +{%- block relbar2 %}{% endblock %} +{% block header %} + {{ super() }} + {% if pagename == 'index' %} + <div class=indexwrapper> + {% endif %} +{% endblock %} +{%- block footer %} + <div class="footer"> + © Copyright {{ copyright }}. + Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>. + </div> + {% if pagename == 'index' %} + </div> + {% endif %} +{%- endblock %} diff --git a/docs/_themes/flask/relations.html b/docs/_themes/flask/relations.html new file mode 100644 index 0000000..3bbcde8 --- /dev/null +++ b/docs/_themes/flask/relations.html @@ -0,0 +1,19 @@ +<h3>Related Topics</h3> +<ul> + <li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul> + {%- for parent in parents %} + <li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul> + {%- endfor %} + {%- if prev %} + <li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter') + }}">{{ prev.title }}</a></li> + {%- endif %} + {%- if next %} + <li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter') + }}">{{ next.title }}</a></li> + {%- endif %} + {%- for parent in parents %} + </ul></li> + {%- endfor %} + </ul></li> +</ul> diff --git a/docs/_themes/flask/static/flasky.css_t b/docs/_themes/flask/static/flasky.css_t new file mode 100644 index 0000000..e69de29 diff --git a/docs/_themes/flask/theme.conf b/docs/_themes/flask/theme.conf new file mode 100644 index 0000000..e69de29 diff --git a/docs/_themes/flask_small/layout.html b/docs/_themes/flask_small/layout.html new file mode 100644 index 0000000..aa1716a --- /dev/null +++ b/docs/_themes/flask_small/layout.html @@ -0,0 +1,22 @@ +{% extends "basic/layout.html" %} +{% block header %} + {{ super() }} + {% if pagename == 'index' %} + <div class=indexwrapper> + {% endif %} +{% endblock %} +{% block footer %} + {% if pagename == 'index' %} + </div> + {% endif %} +{% endblock %} +{# do not display relbars #} +{% block relbar1 %}{% endblock %} +{% block relbar2 %} + {% if theme_github_fork %} + <a href="http://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;" + src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a> + {% endif %} +{% endblock %} +{% block sidebar1 %}{% endblock %} +{% block sidebar2 %}{% endblock %} diff --git a/docs/_themes/flask_small/static/flasky.css_t b/docs/_themes/flask_small/static/flasky.css_t new file mode 100644 index 0000000..e69de29 diff --git a/docs/_themes/flask_small/theme.conf b/docs/_themes/flask_small/theme.conf new file mode 100644 index 0000000..e69de29 diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py new file mode 100644 index 0000000..33f4744 --- /dev/null +++ b/docs/_themes/flask_theme_support.py @@ -0,0 +1,86 @@ +# flasky extensions. flasky pygments style based on tango style +from pygments.style import Style +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Whitespace, Punctuation, Other, Literal + + +class FlaskyStyle(Style): + background_color = "#f8f8f8" + default_style = "" + + styles = { + # No corresponding class for the following: + #Text: "", # class: '' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#000000", # class 'x' + + Comment: "italic #8f5902", # class: 'c' + Comment.Preproc: "noitalic", # class: 'cp' + + Keyword: "bold #004461", # class: 'k' + Keyword.Constant: "bold #004461", # class: 'kc' + Keyword.Declaration: "bold #004461", # class: 'kd' + Keyword.Namespace: "bold #004461", # class: 'kn' + Keyword.Pseudo: "bold #004461", # class: 'kp' + Keyword.Reserved: "bold #004461", # class: 'kr' + Keyword.Type: "bold #004461", # class: 'kt' + + Operator: "#582800", # class: 'o' + Operator.Word: "bold #004461", # class: 'ow' - like keywords + + Punctuation: "bold #000000", # class: 'p' + + # because special names such as Name.Class, Name.Function, etc. + # are not recognized as such later in the parsing, we choose them + # to look the same as ordinary variables. + Name: "#000000", # class: 'n' + Name.Attribute: "#c4a000", # class: 'na' - to be revised + Name.Builtin: "#004461", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#000000", # class: 'nc' - to be revised + Name.Constant: "#000000", # class: 'no' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "bold #cc0000", # class: 'ne' + Name.Function: "#000000", # class: 'nf' + Name.Property: "#000000", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#000000", # class: 'nn' - to be revised + Name.Other: "#000000", # class: 'nx' + Name.Tag: "bold #004461", # class: 'nt' - like a keyword + Name.Variable: "#000000", # class: 'nv' - to be revised + Name.Variable.Class: "#000000", # class: 'vc' - to be revised + Name.Variable.Global: "#000000", # class: 'vg' - to be revised + Name.Variable.Instance: "#000000", # class: 'vi' - to be revised + + Number: "#990000", # class: 'm' + + Literal: "#000000", # class: 'l' + Literal.Date: "#000000", # class: 'ld' + + String: "#4e9a06", # class: 's' + String.Backtick: "#4e9a06", # class: 'sb' + String.Char: "#4e9a06", # class: 'sc' + String.Doc: "italic #8f5902", # class: 'sd' - like a comment + String.Double: "#4e9a06", # class: 's2' + String.Escape: "#4e9a06", # class: 'se' + String.Heredoc: "#4e9a06", # class: 'sh' + String.Interpol: "#4e9a06", # class: 'si' + String.Other: "#4e9a06", # class: 'sx' + String.Regex: "#4e9a06", # class: 'sr' + String.Single: "#4e9a06", # class: 's1' + String.Symbol: "#4e9a06", # class: 'ss' + + Generic: "#000000", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #000000", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "bold #000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' + Generic.Strong: "bold #000000", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' + } diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..5662aca --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# +# Flask-SimpleLDAP documentation build configuration file, created by +# sphinx-quickstart on Sat Aug 9 19:44:30 2014. +# +# 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 +import os +from mock import Mock as MagicMock + + +sys.path.insert(0, os.path.abspath('..')) + +class Mock(MagicMock): + @classmethod + def __getattr__(cls, name): + return Mock() + + +MOCK_MODULES = ['ldap'] +sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) + +# 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('.')) + +# -- 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.intersphinx', + ] + +# 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'Flask-SimpleLDAP' +copyright = u'2014, Alexandre Ferland' + +# 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.0' +# The full version, including alpha/beta/rc tags. +release = '0.1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# 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 = ['_build'] + +# 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 = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- 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 = 'flask_small' + +# 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. +sys.path.append(os.path.abspath('_themes')) +html_theme_path = ['_themes'] + +# custom settings for flask theme +html_theme_options = { + 'index_logo': '', #TODO + 'github_fork': 'admiralobvious/flask-simpleldap', + } + +# 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'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# 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 = 'Flask-SimpleLDAPdoc' + + +# -- 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, or own class]). +latex_documents = [ + ('index', 'Flask-SimpleLDAP.tex', u'Flask-SimpleLDAP Documentation', + u'Alexandre Ferland', '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', 'flask-simpleldap', u'Flask-SimpleLDAP Documentation', + [u'Alexandre Ferland'], 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', 'Flask-SimpleLDAP', u'Flask-SimpleLDAP Documentation', + u'Alexandre Ferland', 'Flask-SimpleLDAP', 'One line description of project.', + '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' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'http://docs.python.org/': None, + 'flask': ('http://flask.pocoo.org/docs/', None) +} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..15e09b8 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,92 @@ +.. Flask-SimpleLDAP documentation master file, created by + sphinx-quickstart on Sat Aug 9 19:44:30 2014. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Flask-SimpleLDAP's documentation! +============================================ + +Flask-SimpleLDAP provides LDAP authentication for Flask. + + +Quickstart +---------- + +First, install Flask-SimpleLDAP: + +.. code-block:: bash + + $ pip install flask-simpleldap + +Flask-SimpleLDAP depends, and will install for you, recent versions of Flask +(0.9 or later) and Python-LDAP. Flask-SimpleLDAP is compatible +with and tested on Python 2.6 and 2.7. + +Next, add a :class:`~flask_simpleldap.LDAP` to your code and at least the three +required configuration options: + +.. code-block:: python + + from flask import Flask + from flask.ext.simpleldap import LDAP + + app = Flask(__name__) + ldap = LDAP(app) + + app.config['LDAP_BASE_DN'] = 'dc=company,dc=org' + app.config['LDAP_USERNAME'] = 'cn=ldap_user,ou=users,dc=company,dc=org' + app.config['LDAP_PASSWORD'] = 'password' + + +Configuration +------------- + +:class:`~flask_simpleldap.LDAP` understands the following configuration +directives: + +============================ =================================================== +``LDAP_HOST`` The host name or IP address of your LDAP server. + Default: 'localhost'. +``LDAP_PORT`` The port number of your LDAP server. Default: 389. +``LDAP_SCHEMA`` The LDAP schema to use between 'ldap' and 'ldaps'. + Default: 'ldap'. +``LDAP_USERNAME`` **Required**: The user name used to bind. +``LDAP_PASSWORD`` **Required**: The password used to bind. +``LDAP_TIMEOUT`` How long (seconds) a connection can take to be opened + before timing out. Default: 10. +``LDAP_USE_SSL`` Set to ``True`` if your server uses SSL. + Default: ``False``. +``LDAP_USE_TLS`` Set to ``True`` if your server uses TLS. + Default: ``False``. +``LDAP_REQUIRE_CERT`` Set to ``True`` if your server requires a certificate. + Default: ``False``. +``LDAP_CERT_PATH`` Path to the certificate if ``LDAP_REQUIRE_CERT`` is + ``True``. +``LDAP_BASE_DN`` **Required**: The distinguished name to use as the search base. +``LDAP_OBJECTS_DN`` The field to use as the objects' distinguished name. + Default: 'distinguishedName'. +``LDAP_USER_FIELDS`` ``list`` of fields to return when searching for a user's + object details. Default: ``list`` (all). +``LDAP_USER_OBJECT_FILTER`` The filter to use when searching for a user object. + Default: '(&(objectclass=Person)(userPrincipalName={}))' +``LDAP_USER_GROUPS_FIELD`` The field to return when searching for a user's + groups. Default: 'memberOf'. +``LDAP_GROUP_FIELDS`` ``list`` of fields to return when searching for a group's + object details. Default: ``list`` (all). +``LDAP_GROUP_OBJECT_FILTER`` The filter to use when searching for a group object. + Default: '(&(objectclass=Group)(userPrincipalName={}))' +``LDAP_GROUP_MEMBERS_FIELD`` The field to return when searching for a group's members. + Default: 'member' +``LDAP_LOGIN_VIEW`` The view to redirect to when a user needs to log-in. + Default: 'login'. +============================ =================================================== + + +API +=== + +Classes +------- + +.. autoclass:: flask_simpleldap.LDAP + :members: diff --git a/example/app.py b/example/app.py new file mode 100644 index 0000000..cbda3c6 --- /dev/null +++ b/example/app.py @@ -0,0 +1,63 @@ +from flask import Flask, g, request, session, redirect, url_for +from flask.ext.simpleldap import LDAP + +app = Flask(__name__) +app.secret_key = 'dev key' +app.debug = True + +app.config['LDAP_HOST'] = 'ldap.example.org' +app.config['LDAP_BASE_DN'] = 'OU=users,dc=example,dc=org' +app.config['LDAP_USERNAME'] = 'CN=user,OU=Users,DC=example,DC=org' +app.config['LDAP_PASSWORD'] = 'password' + +ldap = LDAP(app) + +@app.before_request +def before_request(): + g.user = None + if 'user_id' in session: + # This is where you'd query your database to get the user info. + g.user = {} + # Create a global with the LDAP groups the user is a member of. + g.ldap_groups = ldap.get_user_groups(user=session['user_id']) + + +@app.route('/') +@ldap.login_required +def index(): + return 'Successfully logged in!' + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if g.user: + return redirect(url_for('index')) + if request.method == 'POST': + user = request.form['user'] + passwd = request.form['passwd'] + test = ldap.bind_user(user, passwd) + if test is None: + return 'Invalid credentials' + else: + session['user_id'] = request.form['user'] + return redirect('/') + return """<form action="" method="post"> + user: <input name="user"><br> + password:<input type="password" name="passwd"><br> + <input type="submit" value="Submit"></form>""" + + +@app.route('/group') +@ldap.group_required(groups=['Web Developers', 'QA']) +def group(): + return 'Group restricted page' + + +@app.route('/logout') +def logout(): + session.pop('user_id', None) + return redirect(url_for('index')) + + +if __name__ == '__main__': + app.run() diff --git a/flask_simpleldap/__init__.py b/flask_simpleldap/__init__.py new file mode 100644 index 0000000..0fd122b --- /dev/null +++ b/flask_simpleldap/__init__.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +__all__ = ['LDAP'] + +from functools import wraps +import re + +import ldap +from flask import abort, current_app, g, redirect, url_for + +try: + from flask import _app_ctx_stack as stack +except ImportError: + from flask import _request_ctx_stack as stack + + +class LDAPException(RuntimeError): + message = None + + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message + + def __unicode__(self): + return self.message + + +class LDAP(object): + def __init__(self, app=None): + self.app = app + if app is not None: + self.init_app(app) + + @staticmethod + def init_app(app): + """Initialize the `app` for use with this :class:`~LDAP`. This is + called automatically if `app` is passed to :meth:`~LDAP.__init__`. + + :param flask.Flask app: the application to configure for use with + this :class:`~LDAP` + """ + + app.config.setdefault('LDAP_HOST', 'localhost') + app.config.setdefault('LDAP_PORT', 389) + app.config.setdefault('LDAP_SCHEMA', 'ldap') + app.config.setdefault('LDAP_USERNAME', None) + app.config.setdefault('LDAP_PASSWORD', None) + app.config.setdefault('LDAP_TIMEOUT', 10) + app.config.setdefault('LDAP_USE_SSL', False) + app.config.setdefault('LDAP_USE_TLS', False) + app.config.setdefault('LDAP_REQUIRE_CERT', False) + app.config.setdefault('LDAP_CERT_PATH', '/path/to/cert') + app.config.setdefault('LDAP_BASE_DN', None) + app.config.setdefault('LDAP_OBJECTS_DN', 'distinguishedName') + app.config.setdefault('LDAP_USER_FIELDS', []) + app.config.setdefault('LDAP_USER_OBJECT_FILTER', + '(&(objectclass=Person)(userPrincipalName={0}))') + app.config.setdefault('LDAP_USER_GROUPS_FIELD', 'memberOf') + app.config.setdefault('LDAP_GROUP_FIELDS', []) + app.config.setdefault('LDAP_GROUP_OBJECT_FILTER', + '(&(objectclass=Group)(userPrincipalName={0}))') + app.config.setdefault('LDAP_GROUP_MEMBERS_FIELD', 'member') + app.config.setdefault('LDAP_LOGIN_VIEW', 'login') + + if app.config['LDAP_USE_SSL'] or app.config['LDAP_USE_TLS']: + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, + ldap.OPT_X_TLS_NEVER) + + if app.config['LDAP_REQUIRE_CERT']: + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, + ldap.OPT_X_TLS_DEMAND, + (ldap.OPT_X_TLS_CACERTFILE, + current_app.config['LDAP_CERT_PATH'])) + + for option in ['USERNAME', 'PASSWORD', 'BASE_DN']: + if app.config['LDAP_{0}'.format(option)] is None: + raise LDAPException('LDAP_{0} cannot be None!'.format(option)) + + @property + def initialize(self): + """Initialize a connection to the LDAP server. + + :return: LDAP connection object. + """ + + try: + conn = ldap.initialize('{0}://{1}:{2}'.format( + current_app.config['LDAP_SCHEMA'], + current_app.config['LDAP_HOST'], + current_app.config['LDAP_PORT'])) + conn.set_option(ldap.OPT_NETWORK_TIMEOUT, + current_app.config['LDAP_TIMEOUT']) + conn.protocol_version = ldap.VERSION3 + if current_app.config['LDAP_USE_TLS']: + conn.start_tls_s() + return conn + except ldap.LDAPError as e: + raise LDAPException(self.error(e)) + + @property + def bind(self): + """Attempts to bind to the LDAP server using the credentials of the + service account. + + :return: Bound LDAP connection object if successful or ``None`` if + unsuccessful. + """ + + conn = self.initialize + try: + conn.simple_bind_s(current_app.config['LDAP_USERNAME'], + current_app.config['LDAP_PASSWORD'].encode('utf-8')) + return conn + except ldap.LDAPError as e: + raise LDAPException(self.error(e)) + + def bind_user(self, username, password): + """Attempts to bind a user to the LDAP server using the credentials + supplied. + + :param str username: The username to attempt to bind with. + :param str password: The password of the username we're attempting to + bind with. + :return: Returns ``True`` if successful or ``None`` if the credentials + are invalid. + """ + + user_dn = self.get_object_details(user=username, dn_only=True) + if user_dn is None: + return + try: + conn = self.initialize + conn.simple_bind_s(user_dn, password) + return True + except ldap.LDAPError: + return + + def get_object_details(self, user=None, group=None, dn_only=False): + """Returns a ``dict`` with the object's (user or group) details. + + :param str user: Username of the user object you want details for. + :param str group: Name of the group object you want details for. + :param bool dn_only: If we should only retrieve the object's + distinguished name or not. Default: ``False``. + """ + + query = None + fields = None + if user is not None: + if not dn_only: + fields = current_app.config['LDAP_USER_FIELDS'] + query = current_app.config['LDAP_USER_OBJECT_FILTER'].format(user) + elif group is not None: + if not dn_only: + fields = current_app.config['LDAP_GROUP_FIELDS'] + query = current_app.config['LDAP_GROUP_OBJECT_FILTER'].format(group) + conn = self.bind + try: + records = conn.search_s(current_app.config['LDAP_BASE_DN'], + ldap.SCOPE_SUBTREE, query, fields) + conn.unbind_s() + result = {} + if records: + if dn_only: + if current_app.config['LDAP_OBJECTS_DN'] in records[0][1]: + dn = records[0][1][current_app.config['LDAP_OBJECTS_DN']] + return dn[0] + for k, v in records[0][1].items(): + result[k] = v + return result + except ldap.LDAPError as e: + raise LDAPException(self.error(e)) + + def get_user_groups(self, user): + """Returns a ``list`` with the user's groups or ``None`` if + unsuccessful. + + :param str user: User we want groups for. + """ + + conn = self.bind + try: + records = conn.search_s(current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, + current_app.config['LDAP_USER_OBJECT_FILTER'].format(user), + [current_app.config['LDAP_USER_GROUPS_FIELD']]) + conn.unbind_s() + if records: + if current_app.config['LDAP_USER_GROUPS_FIELD'] in records[0][1]: + groups = records[0][1][current_app.config['LDAP_USER_GROUPS_FIELD']] + result = [re.findall('cn=(.*?),', group)[0] for group in groups] + return result + except ldap.LDAPError as e: + raise LDAPException(self.error(e)) + + def get_group_members(self, group): + """Returns a ``list`` with the group's members or ``None`` if + unsuccessful. + + :param str group: + """ + + conn = self.bind + try: + records = conn.search_s(current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, + current_app.config['LDAP_GROUP_OBJECT_FILTER'].format(group), + [current_app.config['LDAP_GROUP_MEMBERS_FIELD']]) + conn.unbind_s() + if records: + if current_app.config['LDAP_GROUP_MEMBERS_FIELD'] in records[0][1]: + members = records[0][1][current_app.config['LDAP_GROUP_MEMBERS_FIELD']] + return members + except ldap.LDAPError as e: + raise LDAPException(self.error(e)) + + @staticmethod + def error(e): + if 'desc' in dict(e.message): + return dict(e.message)['desc'] + else: + return e[1] + + @staticmethod + def login_required(func): + """Used to decorate a view function to require LDAP login but does NOT + require membership from a specific group. + + :param func: The view function to decorate. + """ + + @wraps(func) + def wrapped(*args, **kwargs): + if g.user is None: + return redirect(url_for(current_app.config['LDAP_LOGIN_VIEW'])) + return func(*args, **kwargs) + return wrapped + + @staticmethod + def group_required(groups=None): + """Used to decorate a view function to require LDAP login AND membership + from one of the groups within the groups list. + + :param list groups: List of groups that should be able to access the view + function. + """ + + def wrapper(func): + @wraps(func) + def wrapped(*args, **kwargs): + if g.user is None: + return redirect(url_for(current_app.config['LDAP_LOGIN_VIEW'])) + match = [group for group in groups if group in g.ldap_groups] + if not match: + abort(401) + return func(*args, **kwargs) + return wrapped + return wrapper diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f616212 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==0.10.1 +mock==1.0.1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..69ade3a --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +""" +Flask-SimpleLDAP +---------------- + +LDAP authentication extension for Flask +""" +from setuptools import setup + + +setup( + name='Flask-SimpleLDAP', + version='0.1.0', + url='https://github.com/admiralobvious/flask-simpleldap', + license='MIT', + author='Alexandre Ferland', + author_email='aferlandqc@gmail.com', + description='LDAP authentication extension for Flask', + long_description=__doc__, + packages=['flask_simpleldap'], + zip_safe=False, + include_package_data=True, + platforms='any', + install_requires=[ + 'Flask>=0.9', + 'python-ldap' + ], + classifiers=[ + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Topic :: Software Development :: Libraries :: Python Modules' + ] +) -- GitLab