From 9bfb036d2438579625888c5af0b09cdaf12f4287 Mon Sep 17 00:00:00 2001 From: alexferl <me@alexferl.com> Date: Sun, 13 Feb 2022 14:40:22 -0500 Subject: [PATCH] update docs and add black --- .pre-commit-config.yaml | 15 + LICENSE | 2 +- Makefile | 47 ++++ README.md | 67 ++--- dev_requirements.txt | 2 + docs/_themes/flask_theme_support.py | 147 +++++----- docs/conf.py | 169 ++++++----- docs/index.rst | 74 +++-- examples/basic_auth/app.py | 16 +- examples/basic_auth/app_oldap.py | 38 +-- examples/blueprints/blueprints/app.py | 9 +- examples/blueprints/blueprints/config.py | 14 +- examples/blueprints/blueprints/core/views.py | 36 +-- examples/blueprints/blueprints/extensions.py | 1 + examples/blueprints/blueprints/foo/views.py | 8 +- examples/groups/app.py | 52 ++-- examples/groups/app_oldap.py | 72 ++--- flask_simpleldap/__init__.py | 281 ++++++++++--------- requirements.txt | 4 +- setup.py | 50 ++-- 20 files changed, 623 insertions(+), 481 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 Makefile diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c74020e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + args: ['--fix=lf'] + description: Forces to replace line ending by the UNIX 'lf' character. +- repo: https://github.com/psf/black + rev: 22.1.0 + hooks: + - id: black + language_version: python3 + args: [-t, py310] diff --git a/LICENSE b/LICENSE index 89f8850..60d8589 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019 Alexandre Ferland +Copyright (c) 2022 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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..16ef367 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +.PHONY: help dev clean update test lint pre-commit + +VENV_NAME?=venv +VENV_ACTIVATE=. $(VENV_NAME)/bin/activate +PYTHON=${VENV_NAME}/bin/python3 + +.DEFAULT: help +help: + @echo "make dev" + @echo " prepare development environment, use only once" + @echo "make clean" + @echo " delete development environment" + @echo "make update" + @echo " update dependencies" + @echo "make test" + @echo " run tests" + @echo "make lint" + @echo " run black" + @echo "make pre-commit" + @echo " run pre-commit hooks" + +dev: + make venv + +venv: $(VENV_NAME)/bin/activate +$(VENV_NAME)/bin/activate: + test -d $(VENV_NAME) || virtualenv -p python3 $(VENV_NAME) + ${PYTHON} -m pip install -U pip + ${PYTHON} -m pip install -r dev_requirements.txt + $(VENV_NAME)/bin/pre-commit install + touch $(VENV_NAME)/bin/activate + +clean: + rm -rf venv + +update: + ${PYTHON} -m pip install -U -r dev_requirements.txt + $(VENV_NAME)/bin/pre-commit install + +test: venv + ${PYTHON} -m pytest + +lint: venv + $(VENV_NAME)/bin/black -t py310 --exclude $(VENV_NAME) . + +pre-commit: venv + $(VENV_NAME)/bin/pre-commit diff --git a/README.md b/README.md index fc09040..126c350 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ -Flask-SimpleLDAP +Flask-SimpleLDAP [](https://app.travis-ci.com/alexferl/flask-simpleldap) ================ -[](https://travis-ci.com/alexferl/flask-simpleldap) - Flask-SimpleLDAP provides LDAP authentication for Flask. -Flask-SimpleLDAP is compatible with and tested on Python 3.5, 3.6 and 3.7. +Flask-SimpleLDAP is compatible with and tested on Python 3.7+. Quickstart ---------- First, install Flask-SimpleLDAP: - - $ pip install flask-simpleldap - + +```shell +pip install flask-simpleldap +``` + + Flask-SimpleLDAP depends, and will install for you, recent versions of Flask (0.12.4 or later) and [python-ldap](https://python-ldap.org/). Please consult the [python-ldap installation instructions](https://www.python-ldap.org/en/latest/installing.html) if you get an error during installation. @@ -27,19 +28,19 @@ from flask import Flask, g from flask_simpleldap import LDAP app = Flask(__name__) -#app.config['LDAP_HOST'] = 'ldap.example.org' # defaults to localhost -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' +# app.config["LDAP_HOST"] = "ldap.example.org" # defaults to localhost +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.route('/') +@app.route("/") @ldap.basic_auth_required def index(): - return 'Welcome, {0}!'.format(g.ldap_username) + return "Welcome, {0}!".format(g.ldap_username) -if __name__ == '__main__': +if __name__ == "__main__": app.run() ``` @@ -54,8 +55,8 @@ Once you get the basic example working, check out the more complex ones: * [examples/groups](examples/groups) demostrates using: * `@ldap.login_required` for form/cookie-based auth, instead of basic HTTP authentication. * `@ldap.group_required()` to restrict access to pages based on the user's LDAP groups. -* [examples/blueprints](examples/blueprints) implements the same functionality, but uses Flask's -[application factories](http://flask.pocoo.org/docs/patterns/appfactories/) +* [examples/blueprints](examples/blueprints) implements the same functionality, but uses Flask's +[application factories](http://flask.pocoo.org/docs/patterns/appfactories/) and [blueprints](http://flask.pocoo.org/docs/blueprints/). @@ -63,7 +64,7 @@ OpenLDAP -------- Add the ``LDAP`` instance to your code and depending on your OpenLDAP -configuration, add the following at least LDAP_USER_OBJECT_FILTER and +configuration, add the following at least LDAP_USER_OBJECT_FILTER and LDAP_USER_OBJECT_FILTER. ```python @@ -73,31 +74,31 @@ from flask_simpleldap import LDAP app = Flask(__name__) # Base -app.config['LDAP_REALM_NAME'] = 'OpenLDAP Authentication' -app.config['LDAP_HOST'] = 'openldap.example.org' -app.config['LDAP_BASE_DN'] = 'dc=users,dc=openldap,dc=org' -app.config['LDAP_USERNAME'] = 'cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org' -app.config['LDAP_PASSWORD'] = 'password' +app.config["LDAP_REALM_NAME"] = "OpenLDAP Authentication" +app.config["LDAP_HOST"] = "openldap.example.org" +app.config["LDAP_BASE_DN"] = "dc=users,dc=openldap,dc=org" +app.config["LDAP_USERNAME"] = "cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org" +app.config["LDAP_PASSWORD"] = "password" -# OpenLDAP -app.config['LDAP_OBJECTS_DN'] = 'dn' -app.config['LDAP_OPENLDAP'] = True -app.config['LDAP_USER_OBJECT_FILTER'] = '(&(objectclass=inetOrgPerson)(uid=%s))' +# OpenLDAP +app.config["LDAP_OBJECTS_DN"] = "dn" +app.config["LDAP_OPENLDAP"] = True +app.config["LDAP_USER_OBJECT_FILTER"] = "(&(objectclass=inetOrgPerson)(uid=%s))" # Groups -app.config['LDAP_GROUP_MEMBERS_FIELD'] = "uniquemember" -app.config['LDAP_GROUP_OBJECT_FILTER'] = "(&(objectclass=groupOfUniqueNames)(cn=%s))" -app.config['LDAP_GROUP_MEMBER_FILTER'] = "(&(cn=*)(objectclass=groupOfUniqueNames)(uniquemember=%s))" -app.config['LDAP_GROUP_MEMBER_FILTER_FIELD'] = "cn" +app.config["LDAP_GROUP_MEMBERS_FIELD"] = "uniquemember" +app.config["LDAP_GROUP_OBJECT_FILTER"] = "(&(objectclass=groupOfUniqueNames)(cn=%s))" +app.config["LDAP_GROUP_MEMBER_FILTER"] = "(&(cn=*)(objectclass=groupOfUniqueNames)(uniquemember=%s))" +app.config["LDAP_GROUP_MEMBER_FILTER_FIELD"] = "cn" ldap = LDAP(app) -@app.route('/') +@app.route("/") @ldap.basic_auth_required def index(): - return 'Welcome, {0}!'.format(g.ldap_username) + return "Welcome, {0}!".format(g.ldap_username) -if __name__ == '__main__': +if __name__ == "__main__": app.run() ``` diff --git a/dev_requirements.txt b/dev_requirements.txt index 0c49739..32cd95d 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,3 +1,5 @@ +black==22.1.0 +pre-commit==2.17.0 python-ldap==3.2.0 # here instead of requirements.txt so rtfd can build Sphinx==2.1.2 diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py index 33f4744..0dcf53b 100644 --- a/docs/_themes/flask_theme_support.py +++ b/docs/_themes/flask_theme_support.py @@ -1,7 +1,19 @@ # 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 +from pygments.token import ( + Keyword, + Name, + Comment, + String, + Error, + Number, + Operator, + Generic, + Whitespace, + Punctuation, + Other, + Literal, +) class FlaskyStyle(Style): @@ -10,77 +22,68 @@ class FlaskyStyle(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' - + # 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' + 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 index 8f32b24..47beca9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,214 +20,218 @@ from mock import Mock as MagicMock class Mock(MagicMock): @classmethod def __getattr__(cls, name): - return Mock() + return Mock() -MOCK_MODULES = ['ldap'] + +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('..')) +sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# 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', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Flask-SimpleLDAP' -copyright = u'2019, Alexandre Ferland' +project = "Flask-SimpleLDAP" +copyright = "2022, 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 = '1.4.0' +version = "1.4.0" # The full version, including alpha/beta/rc tags. -release = '1.4.0' +release = "1.4.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# 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'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# 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 +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output ---------------------------------------------- -sys.path.append(os.path.abspath('_themes')) -html_theme_path = ['_themes'] -html_theme = 'flask_small' +sys.path.append(os.path.abspath("_themes")) +html_theme_path = ["_themes"] +html_theme = "flask_small" html_theme_options = { - 'index_logo': '', #TODO - 'github_fork': 'admiralobvious/flask-simpleldap', - } + "index_logo": "", # TODO + "github_fork": "alexferl/flask-simpleldap", +} # The name for this set of Sphinx documents. If None, it defaults to # "<project> v<release> documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# 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 +# 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 +# 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'] +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 = [] +# 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' +# 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 +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = 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 = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'Flask-SimpleLDAPdoc' +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': '', + # 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'), + ( + "index", + "Flask-SimpleLDAP.tex", + "Flask-SimpleLDAP Documentation", + "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 +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -235,12 +239,17 @@ latex_documents = [ # 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) + ( + "index", + "flask-simpleldap", + "Flask-SimpleLDAP Documentation", + ["Alexandre Ferland"], + 1, + ) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -249,23 +258,29 @@ man_pages = [ # (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'), + ( + "index", + "Flask-SimpleLDAP", + "Flask-SimpleLDAP Documentation", + "Alexandre Ferland", + "Flask-SimpleLDAP", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {"http://docs.python.org/": None} diff --git a/docs/index.rst b/docs/index.rst index 2ab3e2c..24b2345 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,37 +14,38 @@ Quickstart First, install Flask-SimpleLDAP: -.. code-block:: bash + .. code-block:: bash - $ pip install flask-simpleldap + pip install flask-simpleldap Flask-SimpleLDAP depends, and will install for you, recent versions of Flask (0.12.4 or later) and pyldap. Flask-SimpleLDAP is compatible -with and tested on Python 3.5, 3.6 and 3.7. +with and tested on Python 3.7+. Next, add a :class:`~flask_simpleldap.LDAP` to your code and at least the three required configuration options: -.. code-block:: python + .. code-block:: python - from flask import Flask - from flask_simpleldap import LDAP + from flask import Flask + from flask_simpleldap import LDAP - app = Flask(__name__) - 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' + app = Flask(__name__) + # app.config["LDAP_HOST"] = "ldap.example.org" # defaults to localhost + 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) + ldap = LDAP(app) - @app.route('/ldap') - @ldap.login_required - def ldap_protected(): - return 'Success!' - - - if __name__ == '__main__': - app.run() + @app.route("/ldap") + @ldap.login_required + def ldap_protected(): + return "Success!" + + + if __name__ == "__main__": + app.run() Configuration @@ -122,40 +123,62 @@ History Changes: +- 1.4.0 July 16, 2019 + + - This release drops support for `Python 2.7 <https://pythonclock.org/>`_. If you're still on Python 2.7, you can use `v1.3.3 <https://github.com/alexferl/flask-simpleldap/releases/tag/v1.3.3>`_. + + - Fixes: + + - `#62 <https://github.com/admiralobvious/flask-simpleldap/issues/62>`_ get_object_details returning None + - 1.3.0 July 14, 2019 + - Thanks to the contributors, this release fixes issues related to bind_user and fixes some issues related to filtering. - `#51 <https://github.com/admiralobvious/flask-simpleldap/pull/51>`_ Referral chasing crash - `#54 <https://github.com/admiralobvious/flask-simpleldap/pull/54>`_ Fixes #44 - Error in bind_user method, also fixes #60 and #61 - `#56 <https://github.com/admiralobvious/flask-simpleldap/pull/56>`_ OpenLDAP section has Incorrect LDAP_GROUP_OBJECT_FILTER - `#57 <https://github.com/admiralobvious/flask-simpleldap/pull/57>`_ next vaule: Priority use request.full_path - `#59 <https://github.com/admiralobvious/flask-simpleldap/pull/59>`_ get_object_details to take query_filter and fallback to LDAP_USER_OBJECT_FILTER or LDAP_GROUP_OBJECT_FILTER + + - `#51 <https://github.com/admiralobvious/flask-simpleldap/pull/51>`_ Referral chasing crash + + - `#54 <https://github.com/admiralobvious/flask-simpleldap/pull/54>`_ Fixes #44 - Error in bind_user method, also fixes #60 and #61 + + - `#56 <https://github.com/admiralobvious/flask-simpleldap/pull/56>`_ OpenLDAP section has Incorrect LDAP_GROUP_OBJECT_FILTER + + - `#57 <https://github.com/admiralobvious/flask-simpleldap/pull/57>`_ next vaule: Priority use request.full_path + + - `#59 <https://github.com/admiralobvious/flask-simpleldap/pull/59>`_ get_object_details to take query_filter and fallback to LDAP_USER_OBJECT_FILTER or LDAP_GROUP_OBJECT_FILTER - 1.2.0 September 26, 2017 + - Changed get_group_members() and get_user_groups() returning strings instead of bytes in PY3. - 1.1.2 July 17, 2017 + - Merge GitHub PR `#30 <https://github.com/admiralobvious/flask-simpleldap/pull/30>`_, Fix for python3 - Fix decoding bytes in PY3 for @ldap.group_required. - 1.1.1 April 10, 2017 + - Merge GitHub pull `#26 <https://github.com/admiralobvious/flask-simpleldap/pull/26>`_, Fix set_option call to LDAP for SSL CERT - 1.1.0 June 7, 2016 + - Add the ability the pass any valid pyldap config options via the LDAP_CUSTOM_OPTIONS configuration directive. - 1.0.1 June 5, 2016 + - Fix ldap filter import. - 1.0.0 June 4, 2016 + - Python 3.x support. Switched from python-ldap to pyldap which is a fork with Python 3.x support. - 0.4.0: September 5, 2015 + - Added support for OpenLDAP directories. Thanks to `@jm66 <https://github.com/jm66>`_ on GitHub. - 0.3.0: January 21, 2015 + - Fix Github issue `#10 <https://github.com/admiralobvious/flask-simpleldap/issues/10>`_, Redirect users back to the page they originally requested after authenticating @@ -163,14 +186,17 @@ Changes: Only trust .bind_user() with a non-empty password - 0.2.0: December 7, 2014 + - Added HTTP Basic Authentication. Thanks to `@OptiverTimAll <https://github.com/optivertimall>`_ on GitHub. - Fix GitHub issue `#4 <https://github.com/admiralobvious/flask-simpleldap/issues/4>`_, User or group queries are vulnerable to LDAP injection. Make sure you update your filters to use '%s' instead of the old '{}'! - 0.1.1: September 6, 2014 + - Fix GitHub issue `#3 <https://github.com/admiralobvious/flask-simpleldap/issues/3>`_, Not compatible with uppercase distinguished names. - 0.1: August 9, 2014 + - Initial Release diff --git a/examples/basic_auth/app.py b/examples/basic_auth/app.py index e9e745f..2cff66f 100644 --- a/examples/basic_auth/app.py +++ b/examples/basic_auth/app.py @@ -2,17 +2,19 @@ from flask import Flask, g from flask_simpleldap import LDAP app = Flask(__name__) -#app.config['LDAP_HOST'] = 'ldap.example.org' # defaults to localhost -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' +# app.config['LDAP_HOST'] = 'ldap.example.org' # defaults to localhost +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.route('/') + +@app.route("/") @ldap.basic_auth_required def index(): - return 'Welcome, {0}!'.format(g.ldap_username) + return "Welcome, {0}!".format(g.ldap_username) + -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/examples/basic_auth/app_oldap.py b/examples/basic_auth/app_oldap.py index 99aa789..89edb07 100644 --- a/examples/basic_auth/app_oldap.py +++ b/examples/basic_auth/app_oldap.py @@ -4,31 +4,35 @@ from flask_simpleldap import LDAP app = Flask(__name__) # Base -app.config['LDAP_REALM_NAME'] = 'OpenLDAP Authentication' -app.config['LDAP_HOST'] = 'openldap.example.org' -app.config['LDAP_BASE_DN'] = 'dc=users,dc=openldap,dc=org' -app.config['LDAP_USERNAME'] = 'cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org' -app.config['LDAP_PASSWORD'] = 'password' +app.config["LDAP_REALM_NAME"] = "OpenLDAP Authentication" +app.config["LDAP_HOST"] = "openldap.example.org" +app.config["LDAP_BASE_DN"] = "dc=users,dc=openldap,dc=org" +app.config["LDAP_USERNAME"] = "cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org" +app.config["LDAP_PASSWORD"] = "password" # OpenLDAP -app.config['LDAP_OPENLDAP'] = True -app.config['LDAP_OBJECTS_DN'] = 'dn' -app.config['LDAP_USER_OBJECT_FILTER'] = '(&(objectclass=inetOrgPerson)(uid=%s))' +app.config["LDAP_OPENLDAP"] = True +app.config["LDAP_OBJECTS_DN"] = "dn" +app.config["LDAP_USER_OBJECT_FILTER"] = "(&(objectclass=inetOrgPerson)(uid=%s))" # Groups configuration -app.config['LDAP_GROUP_MEMBERS_FIELD'] = 'uniquemember' -app.config['LDAP_GROUP_OBJECT_FILTER'] = '(&(objectclass=groupOfUniqueNames)(cn=%s))' -app.config['LDAP_GROUPS_OBJECT_FILTER'] = 'objectclass=groupOfUniqueNames' -app.config['LDAP_GROUP_FIELDS'] = ['cn', 'entryDN', 'member', 'description'] -app.config['LDAP_GROUP_MEMBER_FILTER'] = '(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))' -app.config['LDAP_GROUP_MEMBER_FILTER_FIELD'] = "cn" +app.config["LDAP_GROUP_MEMBERS_FIELD"] = "uniquemember" +app.config["LDAP_GROUP_OBJECT_FILTER"] = "(&(objectclass=groupOfUniqueNames)(cn=%s))" +app.config["LDAP_GROUPS_OBJECT_FILTER"] = "objectclass=groupOfUniqueNames" +app.config["LDAP_GROUP_FIELDS"] = ["cn", "entryDN", "member", "description"] +app.config[ + "LDAP_GROUP_MEMBER_FILTER" +] = "(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))" +app.config["LDAP_GROUP_MEMBER_FILTER_FIELD"] = "cn" ldap = LDAP(app) -@app.route('/') + +@app.route("/") @ldap.basic_auth_required def index(): - return 'Welcome, {0}!'.format(g.ldap_username) + return "Welcome, {0}!".format(g.ldap_username) + -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/examples/blueprints/blueprints/app.py b/examples/blueprints/blueprints/app.py index 13e5652..506b02c 100644 --- a/examples/blueprints/blueprints/app.py +++ b/examples/blueprints/blueprints/app.py @@ -5,10 +5,7 @@ from .extensions import ldap from .core import core from .foo import foo -DEFAULT_BLUEPRINTS = ( - core, - foo -) +DEFAULT_BLUEPRINTS = (core, foo) def create_app(config=None, app_name=None, blueprints=None): @@ -33,11 +30,11 @@ def register_hooks(app): @app.before_request def before_request(): g.user = None - if 'user_id' in session: + 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']) + g.ldap_groups = ldap.get_user_groups(user=session["user_id"]) def register_blueprints(app, blueprints): diff --git a/examples/blueprints/blueprints/config.py b/examples/blueprints/blueprints/config.py index 8156f29..8c0a5f3 100644 --- a/examples/blueprints/blueprints/config.py +++ b/examples/blueprints/blueprints/config.py @@ -2,14 +2,14 @@ import ldap class BaseConfig(object): - PROJECT = 'foo' - SECRET_KEY = 'dev key' + PROJECT = "foo" + SECRET_KEY = "dev key" DEBUG = True # LDAP - LDAP_HOST = 'ldap.example.org' - LDAP_BASE_DN = 'OU=users,dc=example,dc=org' - LDAP_USERNAME = 'CN=user,OU=Users,DC=example,DC=org' - LDAP_PASSWORD = 'password' - LDAP_LOGIN_VIEW = 'core.login' + LDAP_HOST = "ldap.example.org" + LDAP_BASE_DN = "OU=users,dc=example,dc=org" + LDAP_USERNAME = "CN=user,OU=Users,DC=example,DC=org" + LDAP_PASSWORD = "password" + LDAP_LOGIN_VIEW = "core.login" LDAP_CUSTOM_OPTIONS = {ldap.OPT_REFERRALS: 0} diff --git a/examples/blueprints/blueprints/core/views.py b/examples/blueprints/blueprints/core/views.py index cbb4dd5..e812bf7 100644 --- a/examples/blueprints/blueprints/core/views.py +++ b/examples/blueprints/blueprints/core/views.py @@ -1,41 +1,41 @@ from flask import Blueprint, g, request, session, redirect, url_for from ..extensions import ldap -core = Blueprint('core', __name__) +core = Blueprint("core", __name__) -@core.route('/') +@core.route("/") @ldap.login_required def index(): - return 'Successfully logged in!' + return "Successfully logged in!" -@core.route('/login', methods=['GET', 'POST']) +@core.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'] + 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 or passwd == '': - return 'Invalid credentials' + if test is None or passwd == "": + return "Invalid credentials" else: - session['user_id'] = request.form['user'] - return redirect('/') + 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>""" -@core.route('/group') -@ldap.group_required(groups=['Web Developers', 'QA']) +@core.route("/group") +@ldap.group_required(groups=["Web Developers", "QA"]) def group(): - return 'Group restricted page' + return "Group restricted page" -@core.route('/logout') +@core.route("/logout") def logout(): - session.pop('user_id', None) - return redirect(url_for('index')) + session.pop("user_id", None) + return redirect(url_for("index")) diff --git a/examples/blueprints/blueprints/extensions.py b/examples/blueprints/blueprints/extensions.py index 9549889..df61de2 100644 --- a/examples/blueprints/blueprints/extensions.py +++ b/examples/blueprints/blueprints/extensions.py @@ -1,2 +1,3 @@ from flask_simpleldap import LDAP + ldap = LDAP() diff --git a/examples/blueprints/blueprints/foo/views.py b/examples/blueprints/blueprints/foo/views.py index 4150518..c5f86d0 100644 --- a/examples/blueprints/blueprints/foo/views.py +++ b/examples/blueprints/blueprints/foo/views.py @@ -1,10 +1,10 @@ from flask import Blueprint from ..extensions import ldap -foo = Blueprint('foo', __name__, url_prefix='/foo') +foo = Blueprint("foo", __name__, url_prefix="/foo") -@foo.route('/group') -@ldap.group_required(groups=['Web Developers', 'QA']) +@foo.route("/group") +@ldap.group_required(groups=["Web Developers", "QA"]) def group(): - return 'Group restricted page in foo module' + return "Group restricted page in foo module" diff --git a/examples/groups/app.py b/examples/groups/app.py index 747d606..0ea5632 100644 --- a/examples/groups/app.py +++ b/examples/groups/app.py @@ -3,14 +3,14 @@ from flask import Flask, g, request, session, redirect, url_for from flask_simpleldap import LDAP app = Flask(__name__) -app.secret_key = 'dev key' +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' -app.config['LDAP_CUSTOM_OPTIONS'] = {l.OPT_REFERRALS: 0} +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" +app.config["LDAP_CUSTOM_OPTIONS"] = {l.OPT_REFERRALS: 0} ldap = LDAP(app) @@ -18,49 +18,49 @@ ldap = LDAP(app) @app.before_request def before_request(): g.user = None - if 'user_id' in session: + 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']) + g.ldap_groups = ldap.get_user_groups(user=session["user_id"]) -@app.route('/') +@app.route("/") @ldap.login_required def index(): - return 'Successfully logged in!' + return "Successfully logged in!" -@app.route('/login', methods=['GET', 'POST']) +@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'] + 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 or passwd == '': - return 'Invalid credentials' + if test is None or passwd == "": + return "Invalid credentials" else: - session['user_id'] = request.form['user'] - return redirect('/') + 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']) +@app.route("/group") +@ldap.group_required(groups=["Web Developers", "QA"]) def group(): - return 'Group restricted page' + return "Group restricted page" -@app.route('/logout') +@app.route("/logout") def logout(): - session.pop('user_id', None) - return redirect(url_for('index')) + session.pop("user_id", None) + return redirect(url_for("index")) -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/examples/groups/app_oldap.py b/examples/groups/app_oldap.py index 36677f1..4fd547a 100644 --- a/examples/groups/app_oldap.py +++ b/examples/groups/app_oldap.py @@ -2,25 +2,27 @@ from flask import Flask, g, request, session, redirect, url_for from flask_simpleldap import LDAP app = Flask(__name__) -app.secret_key = 'dev key' +app.secret_key = "dev key" app.debug = True -app.config['LDAP_OPENLDAP'] = True -app.config['LDAP_OBJECTS_DN'] = 'dn' -app.config['LDAP_REALM_NAME'] = 'OpenLDAP Authentication' -app.config['LDAP_HOST'] = 'openldap.example.org' -app.config['LDAP_BASE_DN'] = 'dc=users,dc=openldap,dc=org' -app.config['LDAP_USERNAME'] = 'cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org' -app.config['LDAP_PASSWORD'] = 'password' -app.config['LDAP_USER_OBJECT_FILTER'] = '(&(objectclass=inetOrgPerson)(uid=%s))' +app.config["LDAP_OPENLDAP"] = True +app.config["LDAP_OBJECTS_DN"] = "dn" +app.config["LDAP_REALM_NAME"] = "OpenLDAP Authentication" +app.config["LDAP_HOST"] = "openldap.example.org" +app.config["LDAP_BASE_DN"] = "dc=users,dc=openldap,dc=org" +app.config["LDAP_USERNAME"] = "cn=user,ou=servauth-users,dc=users,dc=openldap,dc=org" +app.config["LDAP_PASSWORD"] = "password" +app.config["LDAP_USER_OBJECT_FILTER"] = "(&(objectclass=inetOrgPerson)(uid=%s))" # Group configuration -app.config['LDAP_GROUP_MEMBERS_FIELD'] = 'uniquemember' -app.config['LDAP_GROUP_OBJECT_FILTER'] = '(&(objectclass=groupOfUniqueNames)(cn=%s))' -app.config['LDAP_GROUPS_OBJECT_FILTER'] = 'objectclass=groupOfUniqueNames' -app.config['LDAP_GROUP_FIELDS'] = ['cn', 'entryDN', 'member', 'description'] -app.config['LDAP_GROUP_MEMBER_FILTER'] = '(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))' -app.config['LDAP_GROUP_MEMBER_FILTER_FIELD'] = "cn" +app.config["LDAP_GROUP_MEMBERS_FIELD"] = "uniquemember" +app.config["LDAP_GROUP_OBJECT_FILTER"] = "(&(objectclass=groupOfUniqueNames)(cn=%s))" +app.config["LDAP_GROUPS_OBJECT_FILTER"] = "objectclass=groupOfUniqueNames" +app.config["LDAP_GROUP_FIELDS"] = ["cn", "entryDN", "member", "description"] +app.config[ + "LDAP_GROUP_MEMBER_FILTER" +] = "(&(cn=*)(objectclass=groupOfUniqueNames)(member=%s))" +app.config["LDAP_GROUP_MEMBER_FILTER_FIELD"] = "cn" ldap = LDAP(app) @@ -28,49 +30,49 @@ ldap = LDAP(app) @app.before_request def before_request(): g.user = None - if 'user_id' in session: + 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']) + g.ldap_groups = ldap.get_user_groups(user=session["user_id"]) -@app.route('/') +@app.route("/") @ldap.login_required def index(): - return 'Successfully logged in!' + return "Successfully logged in!" -@app.route('/login', methods=['GET', 'POST']) +@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'] + 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 or passwd == '': - return 'Invalid credentials' + if test is None or passwd == "": + return "Invalid credentials" else: - session['user_id'] = request.form['user'] - return redirect('/') + 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']) +@app.route("/group") +@ldap.group_required(groups=["web-developers"]) def group(): - return 'Group restricted page' + return "Group restricted page" -@app.route('/logout') +@app.route("/logout") def logout(): - session.pop('user_id', None) - return redirect(url_for('index')) + session.pop("user_id", None) + return redirect(url_for("index")) -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask_simpleldap/__init__.py b/flask_simpleldap/__init__.py index 7eff2c8..1d3867d 100644 --- a/flask_simpleldap/__init__.py +++ b/flask_simpleldap/__init__.py @@ -2,10 +2,9 @@ import re from functools import wraps import ldap from ldap import filter as ldap_filter -from flask import abort, current_app, g, make_response, redirect, url_for, \ - request +from flask import abort, current_app, g, make_response, redirect, url_for, request -__all__ = ['LDAP'] +__all__ = ["LDAP"] class LDAPException(RuntimeError): @@ -32,51 +31,50 @@ class LDAP(object): :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=%s))') - app.config.setdefault('LDAP_USER_GROUPS_FIELD', 'memberOf') - app.config.setdefault('LDAP_GROUP_FIELDS', []) - app.config.setdefault('LDAP_GROUPS_OBJECT_FILTER', 'objectclass=Group') - app.config.setdefault('LDAP_GROUP_OBJECT_FILTER', - '(&(objectclass=Group)(userPrincipalName=%s))') - app.config.setdefault('LDAP_GROUP_MEMBERS_FIELD', 'member') - app.config.setdefault('LDAP_LOGIN_VIEW', 'login') - app.config.setdefault('LDAP_REALM_NAME', 'LDAP authentication') - app.config.setdefault('LDAP_OPENLDAP', False) - app.config.setdefault('LDAP_GROUP_MEMBER_FILTER', '*') - app.config.setdefault('LDAP_GROUP_MEMBER_FILTER_FIELD', '*') - app.config.setdefault('LDAP_CUSTOM_OPTIONS', None) - - 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.set_option(ldap.OPT_X_TLS_CACERTFILE, - 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)) + 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=%s))" + ) + app.config.setdefault("LDAP_USER_GROUPS_FIELD", "memberOf") + app.config.setdefault("LDAP_GROUP_FIELDS", []) + app.config.setdefault("LDAP_GROUPS_OBJECT_FILTER", "objectclass=Group") + app.config.setdefault( + "LDAP_GROUP_OBJECT_FILTER", "(&(objectclass=Group)(userPrincipalName=%s))" + ) + app.config.setdefault("LDAP_GROUP_MEMBERS_FIELD", "member") + app.config.setdefault("LDAP_LOGIN_VIEW", "login") + app.config.setdefault("LDAP_REALM_NAME", "LDAP authentication") + app.config.setdefault("LDAP_OPENLDAP", False) + app.config.setdefault("LDAP_GROUP_MEMBER_FILTER", "*") + app.config.setdefault("LDAP_GROUP_MEMBER_FILTER_FIELD", "*") + app.config.setdefault("LDAP_CUSTOM_OPTIONS", None) + + 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.set_option(ldap.OPT_X_TLS_CACERTFILE, 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)) @staticmethod def _set_custom_options(conn): - options = current_app.config['LDAP_CUSTOM_OPTIONS'] + options = current_app.config["LDAP_CUSTOM_OPTIONS"] if options: for k, v in options.items(): conn.set_option(k, v) @@ -90,15 +88,19 @@ class LDAP(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 = 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 = self._set_custom_options(conn) conn.protocol_version = ldap.VERSION3 - if current_app.config['LDAP_USE_TLS']: + if current_app.config["LDAP_USE_TLS"]: conn.start_tls_s() return conn except ldap.LDAPError as e: @@ -116,8 +118,8 @@ class LDAP(object): conn = self.initialize try: conn.simple_bind_s( - current_app.config['LDAP_USERNAME'], - current_app.config['LDAP_PASSWORD']) + current_app.config["LDAP_USERNAME"], current_app.config["LDAP_PASSWORD"] + ) return conn except ldap.LDAPError as e: raise LDAPException(self.error(e.args)) @@ -148,15 +150,17 @@ class LDAP(object): return try: conn = self.initialize - _user_dn = user_dn.decode('utf-8') \ - if isinstance(user_dn, bytes) else user_dn + _user_dn = ( + user_dn.decode("utf-8") if isinstance(user_dn, bytes) else user_dn + ) conn.simple_bind_s(_user_dn, password) return True except ldap.LDAPError: return - def get_object_details(self, user=None, group=None, query_filter=None, - dn_only=False): + def get_object_details( + self, user=None, group=None, query_filter=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. @@ -169,33 +173,35 @@ class LDAP(object): fields = None if user is not None: if not dn_only: - fields = current_app.config['LDAP_USER_FIELDS'] - query_filter = query_filter or \ - current_app.config['LDAP_USER_OBJECT_FILTER'] + fields = current_app.config["LDAP_USER_FIELDS"] + query_filter = query_filter or current_app.config["LDAP_USER_OBJECT_FILTER"] query = ldap_filter.filter_format(query_filter, (user,)) elif group is not None: if not dn_only: - fields = current_app.config['LDAP_GROUP_FIELDS'] - query_filter = query_filter or \ - current_app.config['LDAP_GROUP_OBJECT_FILTER'] + fields = current_app.config["LDAP_GROUP_FIELDS"] + query_filter = ( + query_filter or current_app.config["LDAP_GROUP_OBJECT_FILTER"] + ) query = ldap_filter.filter_format(query_filter, (group,)) conn = self.bind try: - records = conn.search_s(current_app.config['LDAP_BASE_DN'], - ldap.SCOPE_SUBTREE, query, fields) + records = conn.search_s( + current_app.config["LDAP_BASE_DN"], ldap.SCOPE_SUBTREE, query, fields + ) conn.unbind_s() result = {} - if records and\ - records[0][0] is not None and isinstance(records[0][1], dict): + if ( + records + and records[0][0] is not None + and isinstance(records[0][1], dict) + ): if dn_only: - if current_app.config['LDAP_OPENLDAP']: + if current_app.config["LDAP_OPENLDAP"]: if records: return records[0][0] else: - if current_app.config['LDAP_OBJECTS_DN'] \ - in records[0][1]: - dn = records[0][1][ - current_app.config['LDAP_OBJECTS_DN']] + 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 list(records[0][1].items()): result[k] = v @@ -217,17 +223,21 @@ class LDAP(object): """ conn = self.bind try: - fields = fields or current_app.config['LDAP_GROUP_FIELDS'] - if current_app.config['LDAP_OPENLDAP']: + fields = fields or current_app.config["LDAP_GROUP_FIELDS"] + if current_app.config["LDAP_OPENLDAP"]: records = conn.search_s( - current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, - current_app.config['LDAP_GROUPS_OBJECT_FILTER'], - fields) + current_app.config["LDAP_BASE_DN"], + ldap.SCOPE_SUBTREE, + current_app.config["LDAP_GROUPS_OBJECT_FILTER"], + fields, + ) else: records = conn.search_s( - current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, - current_app.config['LDAP_GROUPS_OBJECT_FILTER'], - fields) + current_app.config["LDAP_BASE_DN"], + ldap.SCOPE_SUBTREE, + current_app.config["LDAP_GROUPS_OBJECT_FILTER"], + fields, + ) conn.unbind_s() if records: if dn_only: @@ -248,42 +258,51 @@ class LDAP(object): conn = self.bind try: - if current_app.config['LDAP_OPENLDAP']: - fields = \ - [str(current_app.config['LDAP_GROUP_MEMBER_FILTER_FIELD'])] + if current_app.config["LDAP_OPENLDAP"]: + fields = [str(current_app.config["LDAP_GROUP_MEMBER_FILTER_FIELD"])] records = conn.search_s( - current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, + current_app.config["LDAP_BASE_DN"], + ldap.SCOPE_SUBTREE, ldap_filter.filter_format( - current_app.config['LDAP_GROUP_MEMBER_FILTER'], - (self.get_object_details(user, dn_only=True),)), - fields) + current_app.config["LDAP_GROUP_MEMBER_FILTER"], + (self.get_object_details(user, dn_only=True),), + ), + fields, + ) else: records = conn.search_s( - current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, + current_app.config["LDAP_BASE_DN"], + ldap.SCOPE_SUBTREE, ldap_filter.filter_format( - current_app.config['LDAP_USER_OBJECT_FILTER'], - (user,)), - [current_app.config['LDAP_USER_GROUPS_FIELD']]) + current_app.config["LDAP_USER_OBJECT_FILTER"], (user,) + ), + [current_app.config["LDAP_USER_GROUPS_FIELD"]], + ) conn.unbind_s() if records: - if current_app.config['LDAP_OPENLDAP']: - group_member_filter = \ - current_app.config['LDAP_GROUP_MEMBER_FILTER_FIELD'] + if current_app.config["LDAP_OPENLDAP"]: + group_member_filter = current_app.config[ + "LDAP_GROUP_MEMBER_FILTER_FIELD" + ] record_list = [record[1] for record in records] record_dicts = [ - record for record in record_list if isinstance(record, dict)] - groups = [item.get([group_member_filter][0])[0] - for item in record_dicts] + record for record in record_list if isinstance(record, dict) + ] + groups = [ + item.get([group_member_filter][0])[0] for item in record_dicts + ] return groups else: - if current_app.config['LDAP_USER_GROUPS_FIELD'] in \ - records[0][1]: + 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(b'(?:cn=|CN=)(.*?),', group)[0] - for group in groups] - result = [r.decode('utf-8') for r in result] + current_app.config["LDAP_USER_GROUPS_FIELD"] + ] + result = [ + re.findall(b"(?:cn=|CN=)(.*?),", group)[0] + for group in groups + ] + result = [r.decode("utf-8") for r in result] return result except ldap.LDAPError as e: raise LDAPException(self.error(e.args)) @@ -298,17 +317,20 @@ class LDAP(object): conn = self.bind try: records = conn.search_s( - current_app.config['LDAP_BASE_DN'], ldap.SCOPE_SUBTREE, + current_app.config["LDAP_BASE_DN"], + ldap.SCOPE_SUBTREE, ldap_filter.filter_format( - current_app.config['LDAP_GROUP_OBJECT_FILTER'], (group,)), - [current_app.config['LDAP_GROUP_MEMBERS_FIELD']]) + current_app.config["LDAP_GROUP_OBJECT_FILTER"], (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]: + if current_app.config["LDAP_GROUP_MEMBERS_FIELD"] in records[0][1]: members = records[0][1][ - current_app.config['LDAP_GROUP_MEMBERS_FIELD']] - members = [m.decode('utf-8') for m in members] + current_app.config["LDAP_GROUP_MEMBERS_FIELD"] + ] + members = [m.decode("utf-8") for m in members] return members except ldap.LDAPError as e: raise LDAPException(self.error(e.args)) @@ -316,8 +338,8 @@ class LDAP(object): @staticmethod def error(e): e = e[0] - if 'desc' in e: - return e['desc'] + if "desc" in e: + return e["desc"] else: return e @@ -337,12 +359,12 @@ class LDAP(object): @wraps(func) def wrapped(*args, **kwargs): if g.user is None: - next_path=request.full_path or request.path - if next_path == '/?': - return redirect( - url_for(current_app.config['LDAP_LOGIN_VIEW'])) - return redirect(url_for(current_app.config['LDAP_LOGIN_VIEW'], - next=next_path)) + next_path = request.full_path or request.path + if next_path == "/?": + return redirect(url_for(current_app.config["LDAP_LOGIN_VIEW"])) + return redirect( + url_for(current_app.config["LDAP_LOGIN_VIEW"], next=next_path) + ) return func(*args, **kwargs) return wrapped @@ -367,8 +389,11 @@ class LDAP(object): def wrapped(*args, **kwargs): if g.user is None: return redirect( - url_for(current_app.config['LDAP_LOGIN_VIEW'], - next=request.full_path or request.path)) + url_for( + current_app.config["LDAP_LOGIN_VIEW"], + next=request.full_path or request.path, + ) + ) match = [group for group in groups if group in g.ldap_groups] if not match: abort(403) @@ -395,9 +420,8 @@ class LDAP(object): """ def make_auth_required_response(): - response = make_response('Unauthorized', 401) - response.www_authenticate.set_basic( - current_app.config['LDAP_REALM_NAME']) + response = make_response("Unauthorized", 401) + response.www_authenticate.set_basic(current_app.config["LDAP_REALM_NAME"]) return response @wraps(func) @@ -412,13 +436,14 @@ class LDAP(object): # with an empty password, even if you supply a non-anonymous user # ID, causing .bind_user() to return True. Therefore, only accept # non-empty passwords. - if req_username in ['', None] or req_password in ['', None]: - current_app.logger.debug('Got a request without auth data') + if req_username in ["", None] or req_password in ["", None]: + current_app.logger.debug("Got a request without auth data") return make_auth_required_response() if not self.bind_user(req_username, req_password): - current_app.logger.debug('User {0!r} gave wrong ' - 'password'.format(req_username)) + current_app.logger.debug( + "User {0!r} gave wrong " "password".format(req_username) + ) return make_auth_required_response() g.ldap_username = req_username diff --git a/requirements.txt b/requirements.txt index 88522a1..b78790e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -Flask==1.1.1 -mock==3.0.5 # for ci +Flask==2.0.2 +mock==4.0.3 # for ci diff --git a/setup.py b/setup.py index 84c3c61..3897a38 100644 --- a/setup.py +++ b/setup.py @@ -6,33 +6,35 @@ LDAP authentication extension for Flask """ from setuptools import setup +from pathlib import Path + +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text() + setup( - name='Flask-SimpleLDAP', - version='1.4.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'], + name="Flask-SimpleLDAP", + version="1.4.0", + url="https://github.com/alexferl/flask-simpleldap", + license="MIT", + author="Alexandre Ferland", + author_email="me@alexferl.com", + description="LDAP authentication extension for Flask", + long_description=long_description, + long_description_content_type="text/markdown", + packages=["flask_simpleldap"], zip_safe=False, include_package_data=True, - platforms='any', - install_requires=[ - 'Flask>=0.12.4', - 'python-ldap>=3.0.0' - ], + platforms="any", + install_requires=["Flask>=0.12.4", "python-ldap>=3.0.0"], classifiers=[ - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Topic :: Software Development :: Libraries :: Python Modules' - ] + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Software Development :: Libraries :: Python Modules", + ], ) - -- GitLab