Skip to content
Commits on Source (3)
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]
language: python
sudo: required
dist: bionic
dist: focal
python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
- "3.9"
env:
- FLASK=1.1.2
- FLASK=2.0.2
- FLASK=1.1.4
- FLASK=1.0.4
- FLASK=0.12.5
install:
- pip install Flask==$FLASK
- pip install -r dev_requirements.txt
......
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
......
.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
Flask-SimpleLDAP
Flask-SimpleLDAP [![Build Status](https://app.travis-ci.com/alexferl/flask-simpleldap.svg?branch=master)](https://app.travis-ci.com/alexferl/flask-simpleldap)
================
[![Build Status](https://travis-ci.com/alexferl/flask-simpleldap.svg?branch=master)](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()
```
......
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
......
# 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'
}
......@@ -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}
......@@ -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
......@@ -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()
......@@ -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()
......@@ -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):
......
......@@ -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}
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"))
from flask_simpleldap import LDAP
ldap = LDAP()
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"
......@@ -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()
......@@ -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()
......@@ -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
......
Flask==1.1.1
mock==3.0.5 # for ci
Flask==2.0.2
mock==4.0.3 # for ci