Compare commits

...

36 Commits

Author SHA1 Message Date
d42 ecdb350015 poor man's fingering 2020-02-26 20:23:38 +01:00
d42 cae82f7044 precommit config 2020-02-26 19:33:02 +01:00
d42 61fe751b75 application_add redirect_uris 2020-02-26 19:32:20 +01:00
d42 c4eaa19b1a blueprint path 2020-02-26 19:24:48 +01:00
d42 cb7d706169 authlib uses current_token instead of request.oauth 2020-02-26 19:23:56 +01:00
d42 26cad5ab55 token expires_in as int 2020-02-26 19:21:40 +01:00
d42 22514a4d6d testing 2020-02-26 19:21:21 +01:00
d42 b87379c66a logging 2020-02-26 18:23:57 +01:00
d42 ebd4e20602 delet trash 2020-02-26 02:02:35 +01:00
d42 d1edbf5377 yolo 2020-02-25 19:32:35 +01:00
d42 2732913acc enterprise testing framework 2020-02-25 05:59:49 +01:00
d42 2221009fca move comment 2020-02-25 05:58:51 +01:00
d42 9a0187805b fixme? 2020-02-25 05:58:37 +01:00
d42 230d3d1700 redirect_uris setter 2020-02-25 05:58:17 +01:00
d42 ff51c2916c requests-mock 2020-02-25 04:39:42 +01:00
d42 c27e7c31b6 uwu 2020-02-25 03:30:49 +01:00
d42 b5fe8dc0e4 move routes 2020-02-25 03:30:49 +01:00
d42 1c6c38670d openid-configuration 2020-02-25 03:30:49 +01:00
d42 36dab4dc13 pylint 2020-02-25 03:30:49 +01:00
d42 06b95b2336 crud? crud 2020-02-25 03:30:49 +01:00
d42 80a9cd6538 openid routes; imports 2020-02-25 03:30:49 +01:00
d42 0b399c1a95 missing url_for 2020-02-25 03:30:49 +01:00
d42 646adc18ad environs 2020-02-25 03:30:45 +01:00
d42 d8d07b7dbd openid-configuration 2020-02-25 02:40:22 +01:00
d42 14ee96ea4a typo 2020-02-25 02:29:31 +01:00
d42 83e3be8213 userinfo_endpoint 2020-02-25 02:29:17 +01:00
d42 f5d448db24 flask shell environment 2020-02-25 02:08:42 +01:00
d42 303b13b0f5 handle empty ldap fields 2020-02-24 21:32:19 +01:00
d42 4d20664450 import logout_user 2020-02-24 21:12:03 +01:00
d42 860ec7dbf2 remove trailing new lines 2020-02-24 21:12:03 +01:00
d42 76abcb5265 alembic 2020-02-24 21:12:03 +01:00
d42 446340c207 poetry is love; poetry is life 2020-02-24 21:12:03 +01:00
d42 c1006efefd very secure 2020-02-24 21:12:03 +01:00
d42 56e4aaa5e3 endpint 2020-02-24 21:11:57 +01:00
d42 696896d48e replace deprecated authlib.flask.oauth2 with authlib.integrations.flask_oauth2 2020-02-24 19:56:49 +01:00
d42 950809fe44 use functools.cached_property 2020-02-24 19:52:39 +01:00
24 changed files with 1240 additions and 167 deletions

31
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,31 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.5.0
hooks:
- id: check-merge-conflict
- id: debug-statements
- id: flake8
args: [--max-line-length=120]
exclude: (tests/*|migrations/versions/*)
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/Lucas-C/pre-commit-hooks-bandit
rev: v1.0.4
hooks:
- id: python-bandit-vulnerability-check
args: [-l, --recursive, -x, tests, -s, B322]
files: .py$
- repo: https://github.com/Lucas-C/pre-commit-hooks-safety
rev: v1.1.0
hooks:
- id: python-safety-dependencies-check
- repo: local
hooks:
- id: tests
name: run tests
entry: pytest -v
language: system
types: [python]
stages: [push]

1
auth.cfg.test Normal file
View File

@ -0,0 +1 @@
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'

View File

@ -1,5 +1,4 @@
import random
import string
from secrets import token_hex
from website.app import create_app
from website.models import Client, db
@ -22,7 +21,7 @@ while not client_id:
while not confidential:
confidential = input("Is the client confidential? Say yes for web apps, no for mobile apps: [yn] ").strip()
while not redirect_uris:
redirect_uris = input("Whitespace-delimited redirect URIs: ").strip()
redirect_uris = input("Whitespace-delimited redirect URIs: ").strip().split()
if confidential.lower().startswith('y'):
confidential = True
@ -44,9 +43,9 @@ c = Client()
c.name = app_name
c.description = app_description
c.client_id = client_id
c.client_secret = ''.join([random.choice("0123456789abcdef") for _ in range(32)])
c.client_secret = token_hex(32)
c.is_confidential = confidential
c.redirect_uris_ = redirect_uris
c.redirect_uris = redirect_uris
db.session.add(c)
db.session.commit()

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

45
migrations/alembic.ini Normal file
View File

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

96
migrations/env.py Normal file
View File

@ -0,0 +1,96 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option(
'sqlalchemy.url', current_app.config.get(
'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,67 @@
"""initial migration
Revision ID: 745c334ab1cd
Revises:
Create Date: 2020-02-24 21:04:37.834777
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '745c334ab1cd'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('client',
sa.Column('name', sa.String(length=40), nullable=True),
sa.Column('description', sa.String(length=400), nullable=True),
sa.Column('client_id', sa.String(length=40), nullable=False),
sa.Column('client_secret', sa.String(length=55), nullable=False),
sa.Column('is_confidential', sa.Boolean(), nullable=True),
sa.Column('redirect_uris_', sa.Text(), nullable=True),
sa.Column('default_scopes_', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('client_id')
)
op.create_index(op.f('ix_client_client_secret'), 'client', ['client_secret'], unique=True)
op.create_table('grant',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user', sa.String(length=40), nullable=False),
sa.Column('client_id', sa.String(length=40), nullable=False),
sa.Column('code', sa.String(length=255), nullable=False),
sa.Column('redirect_uri', sa.String(length=255), nullable=True),
sa.Column('expires', sa.DateTime(), nullable=True),
sa.Column('_scopes', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['client.client_id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_grant_code'), 'grant', ['code'], unique=False)
op.create_table('token',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('client_id', sa.String(length=40), nullable=False),
sa.Column('user', sa.String(length=40), nullable=False),
sa.Column('token_type', sa.String(length=40), nullable=True),
sa.Column('access_token', sa.String(length=255), nullable=False),
sa.Column('refresh_token', sa.String(length=255), nullable=True),
sa.Column('expires', sa.DateTime(), nullable=True),
sa.Column('_scopes', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['client.client_id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('access_token')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('token')
op.drop_index(op.f('ix_grant_code'), table_name='grant')
op.drop_table('grant')
op.drop_index(op.f('ix_client_client_secret'), table_name='client')
op.drop_table('client')
# ### end Alembic commands ###

556
poetry.lock generated Normal file
View File

@ -0,0 +1,556 @@
[[package]]
category = "main"
description = "A database migration tool for SQLAlchemy."
name = "alembic"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.4.0"
[package.dependencies]
Mako = "*"
SQLAlchemy = ">=1.1.0"
python-dateutil = "*"
python-editor = ">=0.3"
[[package]]
category = "dev"
description = "An abstract syntax tree for Python with inference support."
name = "astroid"
optional = false
python-versions = ">=3.5.*"
version = "2.3.3"
[package.dependencies]
lazy-object-proxy = ">=1.4.0,<1.5.0"
six = ">=1.12,<2.0"
wrapt = ">=1.11.0,<1.12.0"
[[package]]
category = "dev"
description = "Atomic file writes."
marker = "sys_platform == \"win32\""
name = "atomicwrites"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.3.0"
[[package]]
category = "dev"
description = "Classes Without Boilerplate"
name = "attrs"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.3.0"
[[package]]
category = "main"
description = "The ultimate Python library in building OAuth and OpenID Connect servers."
name = "authlib"
optional = false
python-versions = "*"
version = "0.14.1"
[package.dependencies]
cryptography = "*"
[[package]]
category = "main"
description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
optional = false
python-versions = "*"
version = "2019.11.28"
[[package]]
category = "main"
description = "Foreign Function Interface for Python calling C code."
name = "cffi"
optional = false
python-versions = "*"
version = "1.14.0"
[package.dependencies]
pycparser = "*"
[[package]]
category = "main"
description = "Universal encoding detector for Python 2 and 3"
name = "chardet"
optional = false
python-versions = "*"
version = "3.0.4"
[[package]]
category = "main"
description = "Composable command line interface toolkit"
name = "click"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "7.0"
[[package]]
category = "dev"
description = "Cross-platform colored terminal text."
marker = "sys_platform == \"win32\""
name = "colorama"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.3"
[[package]]
category = "main"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
name = "cryptography"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "2.8"
[package.dependencies]
cffi = ">=1.8,<1.11.3 || >1.11.3"
six = ">=1.4.1"
[[package]]
category = "main"
description = "simplified environment variable parsing"
name = "environs"
optional = false
python-versions = ">=3.5"
version = "7.2.0"
[package.dependencies]
marshmallow = ">=2.7.0"
python-dotenv = "*"
[[package]]
category = "main"
description = "A simple framework for building complex web applications."
name = "flask"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.1.1"
[package.dependencies]
Jinja2 = ">=2.10.1"
Werkzeug = ">=0.15"
click = ">=5.1"
itsdangerous = ">=0.24"
[[package]]
category = "main"
description = "User session management for Flask"
name = "flask-login"
optional = false
python-versions = "*"
version = "0.5.0"
[package.dependencies]
Flask = "*"
[[package]]
category = "main"
description = "SQLAlchemy database migrations for Flask applications using Alembic"
name = "flask-migrate"
optional = false
python-versions = "*"
version = "2.5.2"
[package.dependencies]
Flask = ">=0.9"
Flask-SQLAlchemy = ">=1.0"
alembic = ">=0.7"
[[package]]
category = "main"
description = "Adds SQLAlchemy support to your Flask application."
name = "flask-sqlalchemy"
optional = false
python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*"
version = "2.4.1"
[package.dependencies]
Flask = ">=0.10"
SQLAlchemy = ">=0.8.0"
[[package]]
category = "main"
description = "Simple integration of Flask and WTForms."
name = "flask-wtf"
optional = false
python-versions = "*"
version = "0.14.3"
[package.dependencies]
Flask = "*"
WTForms = "*"
itsdangerous = "*"
[[package]]
category = "main"
description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.9"
[[package]]
category = "dev"
description = "A Python utility / library to sort Python imports."
name = "isort"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "4.3.21"
[[package]]
category = "main"
description = "Various helpers to pass data to untrusted environments and back."
name = "itsdangerous"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.1.0"
[[package]]
category = "main"
description = "A very fast and expressive template engine."
name = "jinja2"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.11.1"
[package.dependencies]
MarkupSafe = ">=0.23"
[[package]]
category = "dev"
description = "A fast and thorough lazy object proxy."
name = "lazy-object-proxy"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.4.3"
[[package]]
category = "main"
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
name = "mako"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.1.1"
[package.dependencies]
MarkupSafe = ">=0.9.2"
[[package]]
category = "main"
description = "Safely add untrusted strings to HTML/XML markup."
name = "markupsafe"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.1.1"
[[package]]
category = "main"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
name = "marshmallow"
optional = false
python-versions = ">=3.5"
version = "3.5.0"
[[package]]
category = "dev"
description = "McCabe checker, plugin for flake8"
name = "mccabe"
optional = false
python-versions = "*"
version = "0.6.1"
[[package]]
category = "dev"
description = "More routines for operating on iterables, beyond itertools"
name = "more-itertools"
optional = false
python-versions = ">=3.5"
version = "8.2.0"
[[package]]
category = "dev"
description = "Core utilities for Python packages"
name = "packaging"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.1"
[package.dependencies]
pyparsing = ">=2.0.2"
six = "*"
[[package]]
category = "dev"
description = "plugin and hook calling mechanisms for python"
name = "pluggy"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.13.1"
[[package]]
category = "dev"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.8.1"
[[package]]
category = "main"
description = "ASN.1 types and codecs"
name = "pyasn1"
optional = false
python-versions = "*"
version = "0.4.8"
[[package]]
category = "main"
description = "A collection of ASN.1-based protocols modules."
name = "pyasn1-modules"
optional = false
python-versions = "*"
version = "0.2.8"
[package.dependencies]
pyasn1 = ">=0.4.6,<0.5.0"
[[package]]
category = "main"
description = "C parser in Python"
name = "pycparser"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.19"
[[package]]
category = "dev"
description = "python code static checker"
name = "pylint"
optional = false
python-versions = ">=3.5.*"
version = "2.4.4"
[package.dependencies]
astroid = ">=2.3.0,<2.4"
colorama = "*"
isort = ">=4.2.5,<5"
mccabe = ">=0.6,<0.7"
[[package]]
category = "dev"
description = "Python parsing module"
name = "pyparsing"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "2.4.6"
[[package]]
category = "dev"
description = "pytest: simple powerful testing with Python"
name = "pytest"
optional = false
python-versions = ">=3.5"
version = "5.3.5"
[package.dependencies]
atomicwrites = ">=1.0"
attrs = ">=17.4.0"
colorama = "*"
more-itertools = ">=4.0.0"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.5.0"
wcwidth = "*"
[[package]]
category = "dev"
description = "A set of py.test fixtures to test Flask applications."
name = "pytest-flask"
optional = false
python-versions = "*"
version = "0.15.1"
[package.dependencies]
Flask = "*"
Werkzeug = ">=0.7"
pytest = [">=3.6", "*"]
[[package]]
category = "main"
description = "Extensions to the standard Python datetime module"
name = "python-dateutil"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
version = "2.8.1"
[package.dependencies]
six = ">=1.5"
[[package]]
category = "main"
description = "Add .env support to your django/flask apps in development and deployments"
name = "python-dotenv"
optional = false
python-versions = "*"
version = "0.11.0"
[[package]]
category = "main"
description = "Programmatically open an editor, capture the result."
name = "python-editor"
optional = false
python-versions = "*"
version = "1.0.4"
[[package]]
category = "main"
description = "Python modules for implementing LDAP clients"
name = "python-ldap"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "3.2.0"
[package.dependencies]
pyasn1 = ">=0.3.7"
pyasn1_modules = ">=0.1.5"
[[package]]
category = "main"
description = "Python HTTP for Humans."
name = "requests"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.23.0"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<4"
idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
[[package]]
category = "dev"
description = "Mock out responses from the requests package"
name = "requests-mock"
optional = false
python-versions = "*"
version = "1.7.0"
[package.dependencies]
requests = ">=2.3,<3"
six = "*"
[[package]]
category = "main"
description = "Python 2 and 3 compatibility utilities"
name = "six"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.14.0"
[[package]]
category = "main"
description = "Database Abstraction Library"
name = "sqlalchemy"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.3.13"
[[package]]
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.25.8"
[[package]]
category = "dev"
description = "Measures number of Terminal column cells of wide-character codes"
name = "wcwidth"
optional = false
python-versions = "*"
version = "0.1.8"
[[package]]
category = "main"
description = "The comprehensive WSGI web application library."
name = "werkzeug"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.0.0"
[[package]]
category = "dev"
description = "Module for decorators, wrappers and monkey patching."
name = "wrapt"
optional = false
python-versions = "*"
version = "1.11.2"
[[package]]
category = "main"
description = "A flexible forms validation and rendering library for Python web development."
name = "wtforms"
optional = false
python-versions = "*"
version = "2.2.1"
[metadata]
content-hash = "81051c91be0ba0400cc6ffbcaa3ba964a85c9d30898929ae9a112c7ba2036c00"
python-versions = "^3.8"
[metadata.hashes]
alembic = ["2df2519a5b002f881517693b95626905a39c5faf4b5a1f94de4f1441095d1d26"]
astroid = ["71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", "840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"]
atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"]
attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"]
authlib = ["89d55b14362f8acee450f9d153645e438e3a38be99b599190718c4406f575b05", "b6d3f59f609d352bff26dce2c7969cff7204213fae1c21742037b7aa8d7360a6"]
certifi = ["017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", "25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"]
cffi = ["001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", "00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", "028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", "14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", "1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", "2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", "2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", "337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", "399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", "3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", "3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", "62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", "66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", "675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", "7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", "8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", "8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", "95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", "99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", "b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", "c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", "c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", "cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", "cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", "cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", "e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", "e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", "f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"]
chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"]
click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"]
cryptography = ["02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", "1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", "369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", "3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", "44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", "4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", "58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", "6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", "7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", "73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", "7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", "90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", "971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", "a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", "b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", "b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", "d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", "de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", "df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", "ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", "fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"]
environs = ["2291ce502c9e61b8e208c8c9be4ac474e0f523c4dc23e0beb23118086e43b324", "44700c562fb6f783640f90c2225d9a80d85d24833b4dd02d20b8ff1c83901e47"]
flask = ["13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", "45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"]
flask-login = ["6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b", "7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"]
flask-migrate = ["6fb038be63d4c60727d5dfa5f581a6189af5b4e2925bc378697b4f0a40cfb4e1", "a96ff1875a49a40bd3e8ac04fce73fdb0870b9211e6168608cbafa4eb839d502"]
flask-sqlalchemy = ["0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327", "6974785d913666587949f7c2946f7001e4fa2cb2d19f4e69ead02e4b8f50b33d"]
flask-wtf = ["57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2", "d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"]
idna = ["7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"]
isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"]
itsdangerous = ["321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"]
jinja2 = ["93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", "b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"]
lazy-object-proxy = ["0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", "194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", "1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", "4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", "48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", "5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", "59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", "8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", "9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", "9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", "97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", "9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", "a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", "a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", "ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", "cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", "d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", "d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", "eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", "efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"]
mako = ["2984a6733e1d472796ceef37ad48c26f4a984bb18119bb2dbc37a44d8f6e75a4"]
markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"]
marshmallow = ["3a94945a7461f2ab4df9576e51c97d66bee2c86155d3d3933fab752b31effab8", "4b95c7735f93eb781dfdc4dded028108998cad759dda8dd9d4b5b4ac574cbf13"]
mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
more-itertools = ["5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", "b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"]
packaging = ["170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", "e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"]
pluggy = ["15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"]
py = ["5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", "c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"]
pyasn1 = ["014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", "03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", "0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", "08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", "39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", "5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", "6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", "78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", "7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", "99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", "aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", "e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", "fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"]
pyasn1-modules = ["0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", "0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", "15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", "426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", "65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", "905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", "a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", "a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", "b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", "c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", "cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", "f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", "fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"]
pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"]
pylint = ["3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", "886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"]
pyparsing = ["4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", "c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"]
pytest = ["0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d", "ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6"]
pytest-flask = ["9001f6128c5c4a0d243ce46c117f3691052828d2faf39ac151b8388657dce447", "cbd8c5b9f8f1b83e9c159ac4294964807c4934317a5fba181739ac15e1b823e6"]
python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"]
python-dotenv = ["8429f459fc041237d98c9ff32e1938e7e5535b5ff24388876315a098027c3a57", "ca9f3debf2262170d6f46571ce4d6ca1add60bb93b69c3a29dcb3d1a00a65c93"]
python-editor = ["1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", "51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", "5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", "c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", "ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"]
python-ldap = ["7d1c4b15375a533564aad3d3deade789221e450052b21ebb9720fb822eccdb8e"]
requests = ["43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", "b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"]
requests-mock = ["510df890afe08d36eca5bb16b4aa6308a6f85e3159ad3013bac8b9de7bd5a010", "88d3402dd8b3c69a9e4f9d3a73ad11b15920c6efd36bc27bf1f701cf4a8e4646"]
six = ["236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", "8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"]
sqlalchemy = ["64a7b71846db6423807e96820993fa12a03b89127d278290ca25c0b11ed7b4fb"]
urllib3 = ["2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", "87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"]
wcwidth = ["8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", "f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"]
werkzeug = ["169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096", "6dc65cf9091cf750012f56f2cad759fa9e879f511b5ff8685e456b4e3bf90d16"]
wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"]
wtforms = ["0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", "e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1"]

26
pyproject.toml Normal file
View File

@ -0,0 +1,26 @@
[tool.poetry]
name = "sso"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies]
python = "^3.8"
flask-login = "^0.5.0"
flask-sqlalchemy = "^2.4"
authlib = "^0.14.1"
flask-wtf = "^0.14.3"
python-ldap = "^3.2"
requests = "^2.23"
flask-migrate = "^2.5"
environs = "^7.2"
[tool.poetry.dev-dependencies]
pytest = "^5.3"
pytest-flask = "^0.15.1"
pylint = "^2.4"
requests-mock = "^1.7"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

0
tests/__init__.py Normal file
View File

75
tests/conftest.py Normal file
View File

@ -0,0 +1,75 @@
import os
os.environ['TESTING'] = 'yes'
from io import BytesIO
import pytest
from sqlalchemy.exc import IntegrityError
import requests
import requests_mock
from website.app import create_app
from website.models import db as _db
from website.ldap import LDAPUser
@pytest.fixture
def app():
app = create_app()
return app
@pytest.fixture
def loggeduser(app):
user = LDAPUser("alicehacker", {"uid": [b"alicehacker"]})
@app.login_manager.request_loader
def load_user(request):
return user
return user
@pytest.fixture
def db(app):
_db.app = app
with app.app_context():
_db.create_all()
yield _db
_db.session.close()
_db.drop_all()
@pytest.fixture
def auth_cls(app, client):
from authlib.integrations.requests_client import OAuth2Session
from authlib.oauth2.rfc7523 import ClientSecretJWT
def custom_matcher(request):
resp = client.open(
request.path_url,
headers=request.headers.items(),
data=request.text,
method=request.method,
url_scheme="https",
)
r = requests.Response()
r.status_code = resp.status_code
r.headers = dict(resp.headers)
r._content = resp.data
return r
adapter = requests_mock.Adapter()
adapter.add_matcher(custom_matcher)
def inner(*args, **kwargs):
session = OAuth2Session(*args, **kwargs)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
return inner

8
tests/test_discovery.py Normal file
View File

@ -0,0 +1,8 @@
from authlib.oidc.discovery import OpenIDProviderMetadata
def test_discovery(app, client):
result = client.get("/.well-known/openid-configuration")
metadata = OpenIDProviderMetadata(result.json)
metadata.validate()

2
tests/test_jwks.py Normal file
View File

@ -0,0 +1,2 @@
def test_get_jwks(app, client):
result = client.get("/jwks.json")

77
tests/test_oauth2.py Normal file
View File

@ -0,0 +1,77 @@
from typing import Type
from website.models import Client, Grant, Token
from authlib.oauth2.rfc7523 import ClientSecretJWT
from authlib.integrations.requests_client import OAuth2Session
def test_oauth2_invalid_redirect(
app, loggeduser, db, client, auth_cls: Type[OAuth2Session]
):
c = Client(
name="testapp",
description="testapp",
client_id="testapp",
client_secret="secret",
is_confidential=True,
scope="openid profile:read users:read",
)
c.redirect_uris = ["https://www.buziaczek.pl/a"]
c.save()
auth = auth_cls("testapp", "secret", scope="profile:write")
authorization_endpoint = "https://localhost:5000/oauth/authorize"
token_endpoint = "https://localhost:5000/oauth/token"
uri, state = auth.create_authorization_url(
authorization_endpoint, redirect_uri="https://www.buziaczek.pl/b"
)
resp = client.post(uri, data={"confirm": "yes"})
assert resp.status_code == 400
assert resp.json["error_description"] == 'Invalid "redirect_uri" in request.'
def test_oauth2_no_scope(app, loggeduser, db, client, auth_cls: Type[OAuth2Session]):
c = Client(
name="testapp",
description="testapp",
client_id="testapp",
client_secret="secret",
is_confidential=True,
scope="openid profile:read users:read",
)
c.redirect_uris = ["https://www.wykop.pl", "https://www.buziaczek.pl"]
c.save()
auth = auth_cls("testapp", "secret", scope="profile:write foo:bar")
authorization_endpoint = "https://localhost:5000/oauth/authorize"
token_endpoint = "https://localhost:5000/oauth/token"
uri, state = auth.create_authorization_url(authorization_endpoint)
resp = client.post(uri, data={"confirm": "yes"})
token = auth.fetch_token(token_endpoint, authorization_response=resp.location)
def test_oauth2_ok(app, loggeduser, db, client, auth_cls: Type[OAuth2Session]):
c = Client(
name="testapp",
description="testapp",
client_id="testapp",
client_secret="secret",
is_confidential=True,
scope="openid profile:read users:read",
)
c.redirect_uris = ["https://www.wykop.pl", "https://www.buziaczek.pl"]
c.save()
auth = auth_cls("testapp", "secret", scope="profile:read")
authorization_endpoint = "https://localhost:5000/oauth/authorize"
token_endpoint = "https://localhost:5000/oauth/token"
uri, state = auth.create_authorization_url(
authorization_endpoint, redirect_uri="https://www.wykop.pl"
)
resp = client.post(uri, data={"confirm": "yes"})
token = auth.fetch_token(
token_endpoint,
authorization_response=resp.location,
redirect_uri="https://www.wykop.pl",
)

View File

@ -2,8 +2,9 @@ import logging
from flask import Flask
from flask_login import LoginManager
from flask_migrate import Migrate
from .models import db
from . import models
from .routes import bp
from .ldap import LDAPUser
from .oauth2 import config_oauth
@ -11,23 +12,32 @@ from .oauth2 import config_oauth
def create_app():
app = Flask("auth")
app.config.from_object(__name__)
app.config.from_pyfile("auth.cfg")
app.config.from_object("website.config")
setup_app(app)
return app
def register_shellcontext(app):
"""Register shell context objects."""
def shell_context():
"""Shell context objects."""
return {"db": models.db, "models": models}
app.shell_context_processor(shell_context)
def setup_app(app):
logging.basicConfig(
level=logging.DEBUG,
format="[%(asctime)-15s] %(name)-10s %(levelname)7s: %(message)s",
)
db.init_app(app)
models.db.init_app(app)
migrate = Migrate(app, models.db)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "/login"
login_manager.user_loader(LDAPUser.by_login)
config_oauth(app)
app.register_blueprint(bp, url_prefix='')
register_shellcontext(app)

57
website/config.py Normal file
View File

@ -0,0 +1,57 @@
import os
from pathlib import Path
from environs import Env
from logging import getLogger
logger = getLogger(__file__)
def read_private_key_file(path):
with open(path) as f:
return f.read()
env = Env()
env.read_env("auth.cfg")
if 'TESTING' in os.environ:
test_path = Path(__file__).parents[1] / 'auth.cfg.test'
logger.warning("loading %s", test_path)
env.read_env(test_path, recurse=False, override=True)
STRIP_RE = env.str("STRIP_RE")
LDAP_URL = env.str("LDAP_URL")
DN_STRING = env.str("DN_STRING")
PEOPLE_BASEDN = env.str("PEOPLE_BASEDN")
UID_LDAP_FILTER = env.str("UID_LDAP_FILTER")
ISSUER_URL = env.str("ISSUER_URL", "https://arkhack.org")
LDAP_BIND_DN = env.str("LDAP_BIND_DN")
LDAP_BIND_PASSWORD = env.str("LDAP_BIND_PASSWORD")
SQLALCHEMY_DATABASE_URI = env.str("SQLALCHEMY_DATABASE_URI")
SQLALCHEMY_TRACK_MODIFICATIONS = env.bool("SQLALCHEMY_TRACK_MODIFICATIONS")
SECRET_KEY = env.str("SECRET_KEY")
JWT_CONFIG = {
'key': read_private_key_file('private.pem'),
'alg': 'RS512',
'iss': ISSUER_URL,
'exp': 3600
}
SCOPES_SUPPORTED = [
"profile:read",
"profile:write",
"password:write",
"users:read",
"openid",
]
RESPONSE_TYPES_SUPPORTED = [
"code",
"code id_token",
"id_token",
"token id_token",
]

View File

@ -6,4 +6,3 @@ class LoginForm(FlaskForm):
username = StringField("username", validators=[DataRequired()])
password = PasswordField("password", validators=[DataRequired()])
remember = BooleanField("remember me")

View File

@ -1,12 +1,15 @@
import logging
import re
from cached_property import cached_property
from functools import cached_property
from flask import current_app
import ldap
import requests
logger = logging.getLogger(__file__)
def check_credentials(username, password):
conn = ldap.initialize(current_app.config["LDAP_URL"])
conn.start_tls_s()
@ -14,8 +17,10 @@ def check_credentials(username, password):
conn.simple_bind_s(current_app.config["DN_STRING"] % username, password)
return True
except ldap.LDAPError:
logger.critical("ldap is ded")
return False
def _connect():
conn = ldap.initialize(current_app.config["LDAP_URL"])
conn.start_tls_s()
@ -25,13 +30,13 @@ def _connect():
class LDAPUser(object):
def __init__(self, username, ldap_data):
self.username = username
self.username = username # FIXME: argument or ldap_data?
self.is_authenticated = True
self.is_anonymous = False
self.username = ldap_data.get("uid", [None])[0].decode()
self.gecos = ldap_data.get("gecos", [None])[0].decode()
self.username = ldap_data.get("uid", [b''])[0].decode()
self.gecos = ldap_data.get("gecos", [b''])[0].decode()
self.mifare_hashes = [m.decode() for m in ldap_data.get("mifareIDHash", [])]
self.phone = ldap_data.get("mobile", [None])[0].decode()
self.phone = ldap_data.get("mobile", [b''])[0].decode()
self.personal_email = [m.decode() for m in ldap_data.get("mailRoutingAddress", [])]
@classmethod
@ -65,7 +70,7 @@ class LDAPUser(object):
r = requests.get(url.format(self.username))
return bool(r.json()["content"])
except Exception as e:
logging.error("When getting data from Kasownik: {}".format(e))
logger.error("When getting data from Kasownik: {}".format(e))
# Fail-safe.
return True
@ -76,10 +81,8 @@ class LDAPUser(object):
r = requests.get(url.format(self.username))
return "YES" in r.text
except Exception as e:
logging.error("When getting data from Capacifier: {}".format(e))
logger.error("When getting data from Capacifier: {}".format(e))
return False
def get_id(self):
return self.username

View File

@ -1,142 +1,111 @@
from datetime import datetime
from authlib.flask.oauth2.sqla import (
OAuth2ClientMixin,
OAuth2TokenMixin,
OIDCAuthorizationCodeMixin,
)
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Client(db.Model):
class CRUDMixin:
"""Mixin that adds convenience methods for CRUD (create, read, update, delete) operations."""
@classmethod
def create(cls, **kwargs):
"""Create a new record and save it the database."""
instance = cls(**kwargs)
return instance.save()
def update(self, commit=True, **kwargs):
"""Update specific fields of a record."""
for attr, value in kwargs.items():
setattr(self, attr, value)
if commit:
self.save()
return self
def save(self, commit=True):
"""Save the record."""
db.session.add(self)
if commit:
db.session.commit()
return self
def merge(self, commit=True):
db.session.merge(self)
if commit:
db.session.commit()
def delete(self, commit=True):
"""Remove the record from the database."""
db.session.delete(self)
return commit and db.session.commit()
class Model(CRUDMixin, db.Model):
"""Base model class that includes CRUD convenience methods."""
__abstract__ = True
class Client(Model, OAuth2ClientMixin):
# human readable name
name = db.Column(db.String(40))
# human readable description
description = db.Column(db.String(400))
client_id = db.Column(db.String(40), primary_key=True)
client_secret = db.Column(db.String(55), unique=True, index=True, nullable=False)
client_id = db.Column(db.String(48), primary_key=True)
client_secret = db.Column(db.String(120), unique=True, index=True, nullable=False)
# public or confidential
is_confidential = db.Column(db.Boolean)
redirect_uris_ = db.Column(db.Text)
default_scopes_ = db.Column(db.Text)
# TODO
# approved = db.Column(db.Boolean, default=False)
approved = True
@property
def client_type(self):
if self.is_confidential:
return "confidential"
return "public"
@property
def redirect_uris(self):
if self.redirect_uris_:
return self.redirect_uris_.split()
return []
@property
def default_redirect_uri(self):
return self.redirect_uris[0]
@property
def default_scopes(self):
if self.default_scopes_:
return self.default_scopes_.split()
return []
def check_response_type(self, response_type):
return True
def check_redirect_uri(self, redirect_uri):
return redirect_uri in self.redirect_uris
def check_requested_scopes(self, scopes):
return {
"profile:read",
"profile:write",
"password:write",
"users:read",
"openid",
}.issuperset(scopes)
def check_token_endpoint_auth_method(self, method):
allowed = ['client_secret_post', 'client_secret_basic']
allowed = ["client_secret_post", "client_secret_basic"]
if not self.is_confidential:
allowed.append('none')
allowed.append("none")
return method in allowed
def check_client_secret(self, secret):
return self.client_secret == secret
def check_grant_type(self, grant_type):
return grant_type in ['authorization_code']
def check_client_type(self, client_type):
return client_type == self.client_type
return grant_type in ["authorization_code"]
class Grant(db.Model):
class Grant(Model, OIDCAuthorizationCodeMixin):
id = db.Column(db.Integer, primary_key=True)
user = db.Column(db.String(40), nullable=False)
client_id = db.Column(
db.String(40), db.ForeignKey("client.client_id"), nullable=False
db.String(48), db.ForeignKey("client.client_id"), nullable=False
)
client = db.relationship("Client")
code = db.Column(db.String(255), index=True, nullable=False)
redirect_uri = db.Column(db.String(255))
expires = db.Column(db.DateTime)
_scopes = db.Column(db.Text)
expires_at = db.Column(db.DateTime)
def is_expired(self):
return self.expires < datetime.utcnow()
return self.expires_at < datetime.utcnow()
def get_redirect_uri(self):
return self.redirect_uri
def get_scope(self):
return self._scopes
class Token(db.Model):
class Token(Model, OAuth2TokenMixin):
id = db.Column(db.Integer, primary_key=True)
client_id = db.Column(
db.String(40), db.ForeignKey("client.client_id"), nullable=False
db.String(48), db.ForeignKey("client.client_id"), nullable=False
)
client = db.relationship("Client")
user = db.Column(db.String(40), nullable=False)
# currently only bearer is supported
token_type = db.Column(db.String(40))
access_token = db.Column(db.String(255), unique=True, nullable=False)
refresh_token = db.Column(db.String(255))
expires = db.Column(db.DateTime)
_scopes = db.Column(db.Text)
def delete(self):
db.session.delete(self)
db.session.commit()
return self
@property
def scopes(self):
if self._scopes:
return self._scopes.split()
return []
def is_expired(self):
return self.expires < datetime.utcnow()
def delete(self):
db.session.delete(self)
db.session.commit()

View File

@ -1,11 +1,10 @@
from datetime import datetime, timedelta
from authlib.flask.oauth2 import AuthorizationServer, ResourceProtector
from authlib.integrations.flask_oauth2 import AuthorizationServer, ResourceProtector, current_token # noqa: F401
from authlib.oauth2.rfc6749 import grants
from authlib.oidc.core import grants as oidgrants
# from authlib.oidc.core import grants as oidgrants
from authlib.oauth2.rfc6750 import BearerTokenValidator
from authlib.oauth2.rfc7009 import RevocationEndpoint
from werkzeug.security import gen_salt
from .models import db
from .models import Client, Grant, Token
@ -13,20 +12,16 @@ from .ldap import LDAPUser, check_credentials
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
def create_authorization_code(self, client, user, request):
code = gen_salt(48)
def save_authorization_code(self, code, request):
expires = datetime.utcnow() + timedelta(seconds=100)
item = Grant(
Grant(
code=code,
client_id=client.client_id,
client_id=request.client.client_id,
redirect_uri=request.redirect_uri,
_scopes=request.scope,
user=user.username,
expires=expires,
)
db.session.add(item)
db.session.commit()
return code
scope=request.scope,
user=request.user.username,
expires_at=expires
).save()
def parse_authorization_code(self, code, client):
item = Grant.query.filter_by(
@ -56,7 +51,7 @@ class RefreshTokenGrant(grants.RefreshTokenGrant):
return token
def authenticate_user(self, credential):
return LDAPUser.by_login(credentials.user_id)
return LDAPUser.by_login(credential.user_id)
def query_client(client_id):
@ -75,8 +70,8 @@ def save_token(token, request):
token_type=token['token_type'],
access_token=token.get('access_token'),
refresh_token=token.get('refresh_token'),
expires=datetime.utcnow() + timedelta(seconds=token['expires_in']),
_scopes=token['scope'],
expires_in=token['expires_in'],
scope=token['scope'], # TODO: what if 360 no scopes
)
item = Token(client_id=client.client_id, user=user_id, **t)
@ -98,7 +93,7 @@ class _BearerTokenValidator(BearerTokenValidator):
def request_invalid(self, request):
return False
def token_revoked(self,token):
def token_revoked(self, token):
return False

View File

@ -0,0 +1,2 @@
from .routes import *
from .routes_openid import *

View File

@ -1,14 +1,17 @@
from authlib.oauth2 import OAuth2Error
from flask import Blueprint, render_template, jsonify, request, flash, redirect, current_app
from flask_login import login_required, current_user, login_user
from flask import (
Blueprint, render_template, request, flash, redirect,
url_for, jsonify, abort
)
from flask_login import login_required, current_user, login_user, logout_user
from .forms import LoginForm
from .ldap import LDAPUser, check_credentials
from .oauth2 import authorization, require_oauth
from .models import Token
from ..forms import LoginForm
from ..ldap import LDAPUser, check_credentials
from ..oauth2 import authorization, require_oauth, current_token
from ..models import Token
bp = Blueprint(__name__, 'sso')
bp = Blueprint('website.routes', 'sso')
@bp.route("/")
@ -54,7 +57,7 @@ def logout():
@bp.route("/api/1/profile")
@require_oauth("profile:read")
def api_profile():
user = LDAPUser.by_login(request.oauth.user)
user = LDAPUser.by_login(current_token.user)
return jsonify(
email=user.email,
username=user.username,
@ -64,26 +67,6 @@ def api_profile():
)
# OpenIDConnect userinfo
@bp.route("/api/1/userinfo")
@require_oauth("profile:read")
def api_userinfo():
user = LDAPUser.by_login(request.oauth.user)
groups = []
if user.is_staff:
groups.append("staff")
return jsonify(
sub=user.username,
name=user.gecos,
email=user.email,
preferred_username=user.username,
nickname=user.username,
user_name=user.username,
user_id=user.username,
groups=groups,
)
@bp.route("/oauth/authorize", methods=["GET", "POST"])
@login_required
def authorize():
@ -99,7 +82,6 @@ def authorize():
grant_user = None
if request.form['confirm']:
grant_user = current_user
return authorization.create_authorization_response(grant_user=grant_user)
@ -113,7 +95,7 @@ def access_token():
def token_revoke(id):
token = Token.query.filter(Token.user == current_user.username, Token.id == id).first()
if not token:
flask.abort(404)
abort(404)
token.delete()
return redirect('/')
@ -121,13 +103,3 @@ def token_revoke(id):
@bp.route("/oauth/revoke", methods=["POST"])
def oauth_token_revoke():
return authorization.create_endpoint_response('revocation')
@bp.route("/.well-known/openid-configuration")
def oidc_configureation():
issuer = current_app.config['ISSUER_URL']
return jsonify({
"issuer": issuer,
"authorization_endpint": issuer + "/oauth/authorize",
"token_endpint": issuer + "/oauth/token",
"token_endpint": issuer + "/api/1/userinfo",
})

View File

@ -0,0 +1,58 @@
import re
from urllib.parse import urlparse
from flask import jsonify, request
from ..routes import bp
from ..oauth2 import require_oauth
from ..config import ISSUER_URL, RESPONSE_TYPES_SUPPORTED, SCOPES_SUPPORTED
from ..ldap import LDAPUser
# OpenIDConnect userinfo
@bp.route("/api/1/userinfo")
@require_oauth("profile:read")
def api_userinfo():
user = LDAPUser.by_login(request.oauth.user)
groups = []
if user.is_staff:
groups.append("staff")
return jsonify(
sub=user.username,
name=user.gecos,
email=user.email,
preferred_username=user.username,
nickname=user.username,
user_name=user.username,
user_id=user.username,
groups=groups,
)
@bp.route("/.well-known/openid-configuration")
def oidc_configuration():
return jsonify({
"issuer": ISSUER_URL,
"authorization_endpoint": f"{ISSUER_URL}/oauth/authorize",
"token_endpoint": f"{ISSUER_URL}/oauth/token",
"userinfo_endpoint": f"{ISSUER_URL}/api/1/userinfo",
"jwks_uri": f"{ISSUER_URL}/jwks.json",
"scopes_supported": SCOPES_SUPPORTED, # recommended
"response_types_supported": RESPONSE_TYPES_SUPPORTED,
"subject_types_supported": ["pairwise"],
"id_token_signing_alg_values_supported": ["RS256", "none"],
})
@bp.route("/.well-known/webfinger")
def webfinger():
ne = urlparse(ISSUER_URL).netloc
acct = re.search(r'([a-z]+)@{}'.format(ne), request.args['resource'])
return jsonify({
"subject": f"acct:{acct}",
"links":
[
{
"rel": "http://openid.net/specs/connect/1.0/issuer",
"href": ISSUER_URL
}
]
})