Compare commits
36 Commits
Author | SHA1 | Date |
---|---|---|
d42 | ecdb350015 | |
d42 | cae82f7044 | |
d42 | 61fe751b75 | |
d42 | c4eaa19b1a | |
d42 | cb7d706169 | |
d42 | 26cad5ab55 | |
d42 | 22514a4d6d | |
d42 | b87379c66a | |
d42 | ebd4e20602 | |
d42 | d1edbf5377 | |
d42 | 2732913acc | |
d42 | 2221009fca | |
d42 | 9a0187805b | |
d42 | 230d3d1700 | |
d42 | ff51c2916c | |
d42 | c27e7c31b6 | |
d42 | b5fe8dc0e4 | |
d42 | 1c6c38670d | |
d42 | 36dab4dc13 | |
d42 | 06b95b2336 | |
d42 | 80a9cd6538 | |
d42 | 0b399c1a95 | |
d42 | 646adc18ad | |
d42 | d8d07b7dbd | |
d42 | 14ee96ea4a | |
d42 | 83e3be8213 | |
d42 | f5d448db24 | |
d42 | 303b13b0f5 | |
d42 | 4d20664450 | |
d42 | 860ec7dbf2 | |
d42 | 76abcb5265 | |
d42 | 446340c207 | |
d42 | c1006efefd | |
d42 | 56e4aaa5e3 | |
d42 | 696896d48e | |
d42 | 950809fe44 |
|
@ -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]
|
|
@ -0,0 +1 @@
|
|||
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
|
@ -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
|
|
@ -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()
|
|
@ -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"}
|
|
@ -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 ###
|
|
@ -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"]
|
|
@ -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,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
|
|
@ -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()
|
|
@ -0,0 +1,2 @@
|
|||
def test_get_jwks(app, client):
|
||||
result = client.get("/jwks.json")
|
|
@ -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",
|
||||
)
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
]
|
|
@ -6,4 +6,3 @@ class LoginForm(FlaskForm):
|
|||
username = StringField("username", validators=[DataRequired()])
|
||||
password = PasswordField("password", validators=[DataRequired()])
|
||||
remember = BooleanField("remember me")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
from .routes import *
|
||||
from .routes_openid import *
|
|
@ -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",
|
||||
})
|
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
Loading…
Reference in New Issue