diff --git a/check_migrations.py b/check_migrations.py index 643f30fbf1eb413dcd86deb8add7da1148f0984b..1a9773ea68c6e6939525b5845bd1b2ce739f6a5e 100755 --- a/check_migrations.py +++ b/check_migrations.py @@ -7,15 +7,17 @@ import datetime import flask_migrate from uffd import create_app, db -from uffd.user.models import User, Group -from uffd.mfa.models import RecoveryCodeMethod, TOTPMethod, WebauthnMethod -from uffd.role.models import Role, RoleGroup -from uffd.signup.models import Signup -from uffd.invite.models import Invite, InviteGrant, InviteSignup -from uffd.session.models import DeviceLoginConfirmation -from uffd.service.models import Service -from uffd.oauth2.models import OAuth2Client, OAuth2LogoutURI, OAuth2Grant, OAuth2Token, OAuth2DeviceLoginInitiation -from uffd.selfservice.models import PasswordToken, MailToken +from uffd.models import ( + User, Group, + RecoveryCodeMethod, TOTPMethod, WebauthnMethod, + Role, RoleGroup, + Signup, + Invite, InviteGrant, InviteSignup, + DeviceLoginConfirmation, + Service, + OAuth2Client, OAuth2LogoutURI, OAuth2Grant, OAuth2Token, OAuth2DeviceLoginInitiation, + PasswordToken, MailToken, +) def run_test(dburi, revision): config = { diff --git a/tests/test_api.py b/tests/test_api.py index 7e86379a7fa9261aa792140843cee1ae1c64da98..46c6fefba59f42f15040e7cec0aebc6cd09faa8f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2,12 +2,10 @@ import base64 from flask import url_for -from uffd.api.views import apikey_required -from uffd.api.models import APIClient -from uffd.service.models import Service -from uffd.user.models import User, remailer +from uffd.models import APIClient, Service, User, remailer from uffd.password_hash import PlaintextPasswordHash from uffd.database import db +from uffd.views.api import apikey_required from utils import UffdTestCase, db_flush def basic_auth(username, password): diff --git a/tests/test_csrf.py b/tests/test_csrf.py index 1486beb74b7a5ce827cdae2a1bba168e4a990454..6475d9699ad5eb6351f48e35b8aa000f5698a65b 100644 --- a/tests/test_csrf.py +++ b/tests/test_csrf.py @@ -2,7 +2,7 @@ import unittest from flask import Flask, Blueprint, session, url_for -from uffd.csrf import csrf_bp, csrf_protect +from uffd.csrf import bp as csrf_bp, csrf_protect uid_counter = 0 diff --git a/tests/test_invite.py b/tests/test_invite.py index 0408ca6d9cf0c192a47166234a30ab7cb65ce705..295103208ecd1f1436e4066e7fadd5fe55c2f226 100644 --- a/tests/test_invite.py +++ b/tests/test_invite.py @@ -4,14 +4,9 @@ import time from flask import url_for, session, current_app -# These imports are required, because otherwise we get circular imports?! -from uffd import user - from uffd import create_app, db -from uffd.invite.models import Invite, InviteGrant, InviteSignup -from uffd.user.models import User, Group -from uffd.role.models import Role, RoleGroup -from uffd.session.views import login_get_user +from uffd.models import Invite, InviteGrant, InviteSignup, User, Group, Role, RoleGroup +from uffd.views.session import login_get_user from utils import dump, UffdTestCase, db_flush diff --git a/tests/test_mail.py b/tests/test_mail.py index ab8abe5b96ee86303d1295e8c2429076ab212a66..f794486b465f984264bd71a0c7116c04c35f58d7 100644 --- a/tests/test_mail.py +++ b/tests/test_mail.py @@ -4,11 +4,8 @@ import unittest from flask import url_for, session -# These imports are required, because otherwise we get circular imports?! -from uffd import user - -from uffd.mail.models import Mail from uffd import create_app, db +from uffd.models import Mail from utils import dump, UffdTestCase, db_flush diff --git a/tests/test_mfa.py b/tests/test_mfa.py index 05a61351bfeafd295282e034bcc73bdbd9406f0c..ff32a105fc9028a4f0c2ba77bfa7fe27d3590486 100644 --- a/tests/test_mfa.py +++ b/tests/test_mfa.py @@ -4,13 +4,9 @@ import time from flask import url_for, session, request -# These imports are required, because otherwise we get circular imports?! -from uffd import user - -from uffd.user.models import User -from uffd.role.models import Role, RoleGroup -from uffd.mfa.models import MFAMethod, MFAType, RecoveryCodeMethod, TOTPMethod, WebauthnMethod, _hotp from uffd import create_app, db +from uffd.models import User, Role, RoleGroup, MFAMethod, MFAType, RecoveryCodeMethod, TOTPMethod, WebauthnMethod +from uffd.models.mfa import _hotp from utils import dump, UffdTestCase, db_flush @@ -26,7 +22,7 @@ class TestMfaPrimitives(unittest.TestCase): def get_fido2_test_cred(self): try: - from uffd.mfa.fido2_compat import AttestedCredentialData + from uffd.fido2_compat import AttestedCredentialData except ImportError: self.skipTest('fido2 could not be imported') # Example public key from webauthn spec 6.5.1.1 diff --git a/tests/test_oauth2.py b/tests/test_oauth2.py index 9021240c3789e46c6483671e6c6df6425ff9b36d..cd612b4a02d96472b0eca35351c7defa6ffb4688 100644 --- a/tests/test_oauth2.py +++ b/tests/test_oauth2.py @@ -3,15 +3,9 @@ from urllib.parse import urlparse, parse_qs from flask import url_for, session -# These imports are required, because otherwise we get circular imports?! -from uffd import user - -from uffd.user.models import User, remailer -from uffd.password_hash import PlaintextPasswordHash -from uffd.session.models import DeviceLoginConfirmation -from uffd.service.models import Service -from uffd.oauth2.models import OAuth2Client, OAuth2DeviceLoginInitiation from uffd import create_app, db +from uffd.password_hash import PlaintextPasswordHash +from uffd.models import User, remailer, DeviceLoginConfirmation, Service, OAuth2Client, OAuth2DeviceLoginInitiation from utils import dump, UffdTestCase diff --git a/tests/test_ratelimit.py b/tests/test_ratelimit.py index f0bf393c10ceb169a30984c2dc220d29a1868671..a10903f4055ecaa93c472d6c12fdb0333cd4e6e7 100644 --- a/tests/test_ratelimit.py +++ b/tests/test_ratelimit.py @@ -2,7 +2,7 @@ import time from flask import Flask, Blueprint, session, url_for -from uffd.ratelimit import get_addrkey, format_delay, Ratelimit, RatelimitEvent +from uffd.models.ratelimit import get_addrkey, format_delay, Ratelimit, RatelimitEvent from utils import UffdTestCase diff --git a/tests/test_role.py b/tests/test_role.py index 6fe4125a63253c42195e8d41edebbde8305e29ba..7061d3936abd65d949fc01c0f8b5064a1ab0f47c 100644 --- a/tests/test_role.py +++ b/tests/test_role.py @@ -4,13 +4,9 @@ import unittest from flask import url_for, session -# These imports are required, because otherwise we get circular imports?! -from uffd import user - -from uffd.user.models import User, Group -from uffd.role.models import flatten_recursive, Role, RoleGroup -from uffd.mfa.models import TOTPMethod from uffd import create_app, db +from uffd.models import User, Group, Role, RoleGroup, TOTPMethod +from uffd.models.role import flatten_recursive from utils import dump, UffdTestCase diff --git a/tests/test_rolemod.py b/tests/test_rolemod.py index beb4721974574dd0d5708bca8b42ae4c31f84275..2950c83c43dba8801a524969e725397dff551d76 100644 --- a/tests/test_rolemod.py +++ b/tests/test_rolemod.py @@ -1,8 +1,7 @@ from flask import url_for -from uffd.user.models import User, Group -from uffd.role.models import Role, RoleGroup from uffd.database import db +from uffd.models import User, Group, Role, RoleGroup from utils import dump, UffdTestCase diff --git a/tests/test_selfservice.py b/tests/test_selfservice.py index c6724ef4016919fb3d03680a4fed5514d699220e..cc2b0a86a3341ab62d5a6e9deebabbde8bf46e97 100644 --- a/tests/test_selfservice.py +++ b/tests/test_selfservice.py @@ -3,13 +3,8 @@ import unittest from flask import url_for, request -# These imports are required, because otherwise we get circular imports?! -from uffd import user - -from uffd.selfservice.models import MailToken, PasswordToken -from uffd.user.models import User -from uffd.role.models import Role, RoleGroup from uffd import create_app, db +from uffd.models import MailToken, PasswordToken, User, Role, RoleGroup from utils import dump, UffdTestCase diff --git a/tests/test_services.py b/tests/test_services.py index efd71898001d796523f9d1733c6846ee5233f696..09cc802e5ae888e509e83b492a31371439d78275 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -3,9 +3,6 @@ import unittest from flask import url_for -# These imports are required, because otherwise we get circular imports?! -from uffd import user - from utils import dump, UffdTestCase class TestServices(UffdTestCase): diff --git a/tests/test_session.py b/tests/test_session.py index 173adfd13d669e550c71f62e78b7ff0b55a055c1..bad3b887499b6c9169809a39bcc049a921320629 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -3,16 +3,10 @@ import unittest from flask import url_for, request -# These imports are required, because otherwise we get circular imports?! -from uffd import user - -from uffd.session.views import login_required -from uffd.session.models import DeviceLoginConfirmation -from uffd.service.models import Service -from uffd.oauth2.models import OAuth2Client, OAuth2DeviceLoginInitiation -from uffd.user.models import User -from uffd.password_hash import PlaintextPasswordHash from uffd import create_app, db +from uffd.password_hash import PlaintextPasswordHash +from uffd.models import DeviceLoginConfirmation, Service, OAuth2Client, OAuth2DeviceLoginInitiation, User +from uffd.views.session import login_required from utils import dump, UffdTestCase, db_flush diff --git a/tests/test_signup.py b/tests/test_signup.py index c202a5019a5fa369cba952194bbad85f248678ef..5979da3bc0f066a1fde981081deabbe67c76ac11 100644 --- a/tests/test_signup.py +++ b/tests/test_signup.py @@ -4,14 +4,9 @@ import time from flask import url_for, session, request -# These imports are required, because otherwise we get circular imports?! -from uffd import user - from uffd import create_app, db -from uffd.signup.models import Signup -from uffd.user.models import User -from uffd.role.models import Role, RoleGroup -from uffd.session.views import login_get_user +from uffd.models import Signup, User, Role, RoleGroup +from uffd.views.session import login_get_user from utils import dump, UffdTestCase, db_flush diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 8f146d5ac8aa3e8c95283657478bc71396328c5b..6c297b1b9b1a73ee0cf1a6b4a073fcaa7d3bcd97 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -12,7 +12,7 @@ class TestCleanupTask(unittest.TestCase): app.debug = True app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' db = SQLAlchemy(app) - cleanup_task = CleanupTask(app, db) + cleanup_task = CleanupTask() @cleanup_task.delete_by_attribute('delete_me') class TestModel(db.Model): @@ -30,7 +30,10 @@ class TestCleanupTask(unittest.TestCase): db.session.expire_all() self.assertEqual(TestModel.query.count(), 5) - app.test_cli_runner().invoke(args=['cleanup']) + with app.test_request_context(): + cleanup_task.run() + db.session.commit() + db.session.expire_all() with app.test_request_context(): self.assertEqual(TestModel.query.count(), 2) diff --git a/tests/test_user.py b/tests/test_user.py index 0990a74f0bdb1f54e49eef14bafb19892bafe51b..0152a0583aec2a4c3a66affb0fcb5d9a9dd33580 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -4,13 +4,8 @@ import unittest from flask import url_for, session import sqlalchemy -# These imports are required, because otherwise we get circular imports?! -from uffd import user - -from uffd.user.models import User, remailer, RemailerAddress, Group -from uffd.role.models import Role, RoleGroup -from uffd.service.models import Service from uffd import create_app, db +from uffd.models import User, remailer, RemailerAddress, Group, Role, RoleGroup, Service from utils import dump, UffdTestCase diff --git a/tests/utils.py b/tests/utils.py index 6a2e2cb801ec9850fd25b2f42cd5707159a9cd5e..c4017b22f14f93392775406379571be52b6554aa 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,8 +6,7 @@ import unittest from flask import request, url_for from uffd import create_app, db -from uffd.user.models import User, Group -from uffd.mail.models import Mail +from uffd.models import User, Group, Mail def dump(basename, resp): basename = basename.replace('.', '_').replace('/', '_') diff --git a/uffd/__init__.py b/uffd/__init__.py index f9a0df33e96e06dab6102eba856d039e4c136b25..6f48d7a02a0ea4db062a7820232d92e195cd8b6f 100644 --- a/uffd/__init__.py +++ b/uffd/__init__.py @@ -4,24 +4,14 @@ import sys from flask import Flask, redirect, url_for, request, render_template from flask_babel import Babel -from werkzeug.routing import IntegerConverter -from werkzeug.serving import make_ssl_devcert -try: - from werkzeug.middleware.profiler import ProfilerMiddleware -except ImportError: - from werkzeug.contrib.profiler import ProfilerMiddleware -from werkzeug.exceptions import InternalServerError, Forbidden +from werkzeug.exceptions import Forbidden from flask_migrate import Migrate -from uffd.database import db, SQLAlchemyJSON, customize_db_engine -from uffd.tasks import cleanup_task -from uffd.template_helper import register_template_helper -from uffd.navbar import setup_navbar -from uffd.secure_redirect import secure_local_redirect -from uffd import user, selfservice, role, mail, session, csrf, mfa, oauth2, service, signup, rolemod, invite, api -from uffd.user.models import User, Group -from uffd.role.models import Role, RoleGroup -from uffd.mail.models import Mail +from .database import db, SQLAlchemyJSON, customize_db_engine +from .template_helper import register_template_helper +from .navbar import setup_navbar +from .csrf import bp as csrf_bp +from . import models, views, commands def load_config_file(app, path, silent=False): if not os.path.exists(path): @@ -75,6 +65,11 @@ def create_app(test_config=None): # pylint: disable=too-many-locals,too-many-sta positions += ["rolemod", "invite", "user", "group", "role", "mail"] setup_navbar(app, positions) + app.register_blueprint(csrf_bp) + + views.init_app(app) + commands.init_app(app) + # We never want to fail here, but at a file access that doesn't work. # We might only have read access to app.instance_path try: @@ -87,51 +82,10 @@ def create_app(test_config=None): # pylint: disable=too-many-locals,too-many-sta with app.app_context(): customize_db_engine(db.engine) - cleanup_task.init_app(app, db) - - for module in [user, selfservice, role, mail, session, csrf, mfa, oauth2, service, rolemod, api, signup, invite]: - for bp in module.bp: - app.register_blueprint(bp) - @app.shell_context_processor def push_request_context(): #pylint: disable=unused-variable app.test_request_context().push() # LDAP ORM requires request context - return {'db': db, 'User': User, 'Group': Group, 'Role': Role, 'Mail': Mail} - - @app.errorhandler(403) - def handle_403(error): - return render_template('403.html', description=error.description if error.description != Forbidden.description else None), 403 - - @app.route("/") - def index(): #pylint: disable=unused-variable - if app.config['DEFAULT_PAGE_SERVICES']: - return redirect(url_for('service.overview')) - return redirect(url_for('selfservice.index')) - - @app.route('/lang', methods=['POST']) - def setlang(): #pylint: disable=unused-variable - resp = secure_local_redirect(request.values.get('ref', '/')) - if 'lang' in request.values: - resp.set_cookie('language', request.values['lang']) - return resp - - @app.cli.command("gendevcert", help='Generates a self-signed TLS certificate for development') - def gendevcert(): #pylint: disable=unused-variable - if os.path.exists('devcert.crt') or os.path.exists('devcert.key'): - print('Refusing to overwrite existing "devcert.crt"/"devcert.key" file!') - return - make_ssl_devcert('devcert') - print('Certificate written to "devcert.crt", private key to "devcert.key".') - print('Run `flask run --cert devcert.crt --key devcert.key` to use it.') - - @app.cli.command("profile", help='Runs app with profiler') - def profile(): #pylint: disable=unused-variable - # app.run() is silently ignored if executed from commands. We really want - # to do this, so we overwrite the check by overwriting the environment - # variable. - os.environ['FLASK_RUN_FROM_CLI'] = 'false' - app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30]) - app.run(debug=True) + return {'db': db} | {name: getattr(models, name) for name in models.__all__} babel = Babel(app) diff --git a/uffd/api/__init__.py b/uffd/api/__init__.py deleted file mode 100644 index 656390049779db11f3fbd9a16498218b18982068..0000000000000000000000000000000000000000 --- a/uffd/api/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .views import bp as _bp - -bp = [_bp] diff --git a/uffd/commands/__init__.py b/uffd/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e3ad7436096922115fb7129f0684251e73ac5a74 --- /dev/null +++ b/uffd/commands/__init__.py @@ -0,0 +1,16 @@ +from .user import user_command +from .group import group_command +from .role import role_command +from .profile import profile_command +from .gendevcert import gendevcert_command +from .cleanup import cleanup_command +from .roles_update_all import roles_update_all_command + +def init_app(app): + app.cli.add_command(user_command) + app.cli.add_command(group_command) + app.cli.add_command(role_command) + app.cli.add_command(gendevcert_command) + app.cli.add_command(profile_command) + app.cli.add_command(cleanup_command) + app.cli.add_command(roles_update_all_command) diff --git a/uffd/commands/cleanup.py b/uffd/commands/cleanup.py new file mode 100644 index 0000000000000000000000000000000000000000..925dda274c6298d9c12d52ac8f2255edf962ae2d --- /dev/null +++ b/uffd/commands/cleanup.py @@ -0,0 +1,11 @@ +import click +from flask.cli import with_appcontext + +from uffd.tasks import cleanup_task +from uffd.database import db + +@click.command('cleanup', help='Cleanup expired data') +@with_appcontext +def cleanup_command(): + cleanup_task.run() + db.session.commit() diff --git a/uffd/commands/gendevcert.py b/uffd/commands/gendevcert.py new file mode 100644 index 0000000000000000000000000000000000000000..a536768ff80da3db3d6722a3940aaf1f88072983 --- /dev/null +++ b/uffd/commands/gendevcert.py @@ -0,0 +1,13 @@ +import os + +import click +from werkzeug.serving import make_ssl_devcert + +@click.command("gendevcert", help='Generates a self-signed TLS certificate for development') +def gendevcert_command(): #pylint: disable=unused-variable + if os.path.exists('devcert.crt') or os.path.exists('devcert.key'): + print('Refusing to overwrite existing "devcert.crt"/"devcert.key" file!') + return + make_ssl_devcert('devcert') + print('Certificate written to "devcert.crt", private key to "devcert.key".') + print('Run `flask run --cert devcert.crt --key devcert.key` to use it.') diff --git a/uffd/user/cli_group.py b/uffd/commands/group.py similarity index 78% rename from uffd/user/cli_group.py rename to uffd/commands/group.py index 0dd80e539290132d93a7c400c67bd9e8564fe4a8..a98e23b3f9bac00591cdb4d0c62e3e12a3b1584b 100644 --- a/uffd/user/cli_group.py +++ b/uffd/commands/group.py @@ -1,26 +1,21 @@ -from flask import Blueprint, current_app +from flask import current_app from flask.cli import AppGroup from sqlalchemy.exc import IntegrityError import click from uffd.database import db -from .models import Group +from uffd.models import Group -bp = Blueprint('group_cli', __name__) -group_cli = AppGroup('group', help='Manage groups') +group_command = AppGroup('group', help='Manage groups') -@bp.record -def add_cli_commands(state): - state.app.cli.add_command(group_cli) - -@group_cli.command(help='List names of all groups') +@group_command.command(help='List names of all groups') def list(): with current_app.test_request_context(): for group in Group.query: click.echo(group.name) -@group_cli.command(help='Show details of group') +@group_command.command(help='Show details of group') @click.argument('name') def show(name): with current_app.test_request_context(): @@ -32,7 +27,7 @@ def show(name): click.echo(f'Description: {group.description}') click.echo(f'Members: {", ".join([user.loginname for user in group.members])}') -@group_cli.command(help='Create new group') +@group_command.command(help='Create new group') @click.argument('name') @click.option('--description', default='', help='Set description text. Empty per default.') def create(name, description): @@ -46,7 +41,7 @@ def create(name, description): except IntegrityError as ex: raise click.ClickException(f'Group creation failed: {ex}') -@group_cli.command(help='Update group attributes') +@group_command.command(help='Update group attributes') @click.argument('name') @click.option('--description', help='Set description text.') def update(name, description): @@ -58,7 +53,7 @@ def update(name, description): group.description = description db.session.commit() -@group_cli.command(help='Delete group') +@group_command.command(help='Delete group') @click.argument('name') def delete(name): with current_app.test_request_context(): diff --git a/uffd/commands/profile.py b/uffd/commands/profile.py new file mode 100644 index 0000000000000000000000000000000000000000..38d8de55136f3bc7e48a978acc4da2e73e61eab7 --- /dev/null +++ b/uffd/commands/profile.py @@ -0,0 +1,19 @@ +import os + +from flask import current_app +from flask.cli import with_appcontext +import click +try: + from werkzeug.middleware.profiler import ProfilerMiddleware +except ImportError: + from werkzeug.contrib.profiler import ProfilerMiddleware + +@click.command("profile", help='Runs app with profiler') +@with_appcontext +def profile_command(): #pylint: disable=unused-variable + # app.run() is silently ignored if executed from commands. We really want + # to do this, so we overwrite the check by overwriting the environment + # variable. + os.environ['FLASK_RUN_FROM_CLI'] = 'false' + current_app.wsgi_app = ProfilerMiddleware(current_app.wsgi_app, restrictions=[30]) + current_app.run(debug=True) diff --git a/uffd/role/cli.py b/uffd/commands/role.py similarity index 92% rename from uffd/role/cli.py rename to uffd/commands/role.py index 6e94f5f1a78c3a1ada626d503c844746e12c56cb..584aaa0d3ec8da7ad4459827ea29ff81e6849c4f 100644 --- a/uffd/role/cli.py +++ b/uffd/commands/role.py @@ -1,19 +1,12 @@ -from flask import Blueprint, current_app +from flask import current_app from flask.cli import AppGroup from sqlalchemy.exc import IntegrityError import click from uffd.database import db -from uffd.user.models import Group +from uffd.models import Group, Role, RoleGroup -from .models import Role, RoleGroup - -bp = Blueprint('role_cli', __name__) -role_cli = AppGroup('role', help='Manage roles') - -@bp.record -def add_cli_commands(state): - state.app.cli.add_command(role_cli) +role_command = AppGroup('role', help='Manage roles') # pylint: disable=too-many-arguments,too-many-locals @@ -57,13 +50,13 @@ def update_attrs(role, description=None, default=None, raise click.ClickException(f'Role {role_name} not found') role.included_roles.remove(_role) -@role_cli.command(help='List names of all roles') +@role_command.command(help='List names of all roles') def list(): with current_app.test_request_context(): for role in Role.query: click.echo(role.name) -@role_cli.command(help='Show details of group') +@role_command.command(help='Show details of group') @click.argument('name') def show(name): with current_app.test_request_context(): @@ -80,7 +73,7 @@ def show(name): click.echo(f'Direct members: {", ".join(sorted([user.loginname for user in role.members]))}') click.echo(f'Effective members: {", ".join(sorted([user.loginname for user in role.members_effective]))}') -@role_cli.command(help='Create new role') +@role_command.command(help='Create new role') @click.argument('name') @click.option('--description', default='', help='Set description text.') @click.option('--default/--no-default', default=False, help='Mark role as default or not. Non-service users are auto-added to default roles.') @@ -99,7 +92,7 @@ def create(name, description, default, moderator_group, add_group, add_role): except IntegrityError as ex: raise click.ClickException(f'Role creation failed: {ex}') -@role_cli.command(help='Update role attributes') +@role_command.command(help='Update role attributes') @click.argument('name') @click.option('--description', help='Set description text.') @click.option('--default/--no-default', default=None, help='Mark role as default or not. Non-service users are auto-added to default roles.') @@ -126,7 +119,7 @@ def update(name, description, default, moderator_group, no_moderator_group, role.update_member_groups() db.session.commit() -@role_cli.command(help='Delete role') +@role_command.command(help='Delete role') @click.argument('name') def delete(name): with current_app.test_request_context(): diff --git a/uffd/commands/roles_update_all.py b/uffd/commands/roles_update_all.py new file mode 100644 index 0000000000000000000000000000000000000000..3ad676e12c745fc01e6ae6cad00f7509980310a8 --- /dev/null +++ b/uffd/commands/roles_update_all.py @@ -0,0 +1,30 @@ +import sys + +from flask import current_app +from flask.cli import with_appcontext +import click + +from uffd.database import db +from uffd.models import User + +@click.command('roles-update-all', help='Update group memberships for all users based on their roles') +@click.option('--check-only', is_flag=True) +@with_appcontext +def roles_update_all_command(check_only): #pylint: disable=unused-variable + consistent = True + with current_app.test_request_context(): + for user in User.query.all(): + groups_added, groups_removed = user.update_groups() + if groups_added: + consistent = False + print('Adding groups [%s] to user %s'%(', '.join([group.name for group in groups_added]), user.loginname)) + if groups_removed: + consistent = False + print('Removing groups [%s] from user %s'%(', '.join([group.name for group in groups_removed]), user.loginname)) + if not check_only: + db.session.commit() + if check_only and not consistent: + print('No changes were made because --check-only is set') + print() + print('Error: Groups are not consistent with roles in database') + sys.exit(1) diff --git a/uffd/user/cli_user.py b/uffd/commands/user.py similarity index 90% rename from uffd/user/cli_user.py rename to uffd/commands/user.py index 490af1cf2a84441ad6d2fff7e24e94b43a8e76e1..5b82eec677016caf71c071ed4c63e811b3fe4484 100644 --- a/uffd/user/cli_user.py +++ b/uffd/commands/user.py @@ -1,19 +1,12 @@ -from flask import Blueprint, current_app +from flask import current_app from flask.cli import AppGroup from sqlalchemy.exc import IntegrityError import click -from uffd.role.models import Role from uffd.database import db +from uffd.models import User, Role -from .models import User - -bp = Blueprint('user_cli', __name__) -user_cli = AppGroup('user', help='Manage users') - -@bp.record -def add_cli_commands(state): - state.app.cli.add_command(user_cli) +user_command = AppGroup('user', help='Manage users') # pylint: disable=too-many-arguments @@ -42,13 +35,13 @@ def update_attrs(user, mail=None, displayname=None, password=None, role.members.remove(user) user.update_groups() -@user_cli.command(help='List login names of all users') +@user_command.command(help='List login names of all users') def list(): with current_app.test_request_context(): for user in User.query: click.echo(user.loginname) -@user_cli.command(help='Show details of user') +@user_command.command(help='Show details of user') @click.argument('loginname') def show(loginname): with current_app.test_request_context(): @@ -62,7 +55,7 @@ def show(loginname): click.echo(f'Roles: {", ".join([role.name for role in user.roles])}') click.echo(f'Groups: {", ".join([group.name for group in user.groups])}') -@user_cli.command(help='Create new user') +@user_command.command(help='Create new user') @click.argument('loginname') @click.option('--mail', required=True, metavar='EMAIL_ADDRESS', help='E-Mail address') @click.option('--displayname', help='Set display name. Defaults to login name.') @@ -86,7 +79,7 @@ def create(loginname, mail, displayname, service, password, prompt_password, add except IntegrityError as ex: raise click.ClickException(f'User creation failed: {ex}') -@user_cli.command(help='Update user attributes and roles') +@user_command.command(help='Update user attributes and roles') @click.argument('loginname') @click.option('--mail', metavar='EMAIL_ADDRESS', help='Set e-mail address.') @click.option('--displayname', help='Set display name.') @@ -103,7 +96,7 @@ def update(loginname, mail, displayname, password, prompt_password, clear_roles, update_attrs(user, mail, displayname, password, prompt_password, clear_roles, add_role, remove_role) db.session.commit() -@user_cli.command(help='Delete user') +@user_command.command(help='Delete user') @click.argument('loginname') def delete(loginname): with current_app.test_request_context(): diff --git a/uffd/csrf/csrf.py b/uffd/csrf.py similarity index 100% rename from uffd/csrf/csrf.py rename to uffd/csrf.py diff --git a/uffd/csrf/__init__.py b/uffd/csrf/__init__.py deleted file mode 100644 index 601eb5f16ebb0cd6d4a8bbeae30d261ebf584f21..0000000000000000000000000000000000000000 --- a/uffd/csrf/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .csrf import bp as csrf_bp, csrf_protect - -bp = [csrf_bp] diff --git a/uffd/mfa/fido2_compat.py b/uffd/fido2_compat.py similarity index 100% rename from uffd/mfa/fido2_compat.py rename to uffd/fido2_compat.py diff --git a/uffd/invite/__init__.py b/uffd/invite/__init__.py deleted file mode 100644 index 656390049779db11f3fbd9a16498218b18982068..0000000000000000000000000000000000000000 --- a/uffd/invite/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .views import bp as _bp - -bp = [_bp] diff --git a/uffd/mail/__init__.py b/uffd/mail/__init__.py deleted file mode 100644 index 671578662f91c82cb987ffe679c1f102dc493d1f..0000000000000000000000000000000000000000 --- a/uffd/mail/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .views import bp as bp_ui - -bp = [bp_ui] diff --git a/uffd/mfa/__init__.py b/uffd/mfa/__init__.py deleted file mode 100644 index 656390049779db11f3fbd9a16498218b18982068..0000000000000000000000000000000000000000 --- a/uffd/mfa/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .views import bp as _bp - -bp = [_bp] diff --git a/uffd/models/__init__.py b/uffd/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2734866ec39d5c7eef3f19e56952a07def7ac6bb --- /dev/null +++ b/uffd/models/__init__.py @@ -0,0 +1,27 @@ +from .api import APIClient +from .invite import Invite, InviteGrant, InviteSignup +from .mail import Mail, MailReceiveAddress, MailDestinationAddress +from .mfa import MFAType, MFAMethod, RecoveryCodeMethod, TOTPMethod, WebauthnMethod +from .oauth2 import OAuth2Client, OAuth2RedirectURI, OAuth2LogoutURI, OAuth2Grant, OAuth2Token, OAuth2DeviceLoginInitiation +from .role import Role, RoleGroup, RoleGroupMap +from .selfservice import PasswordToken, MailToken +from .service import Service, get_services +from .session import DeviceLoginType, DeviceLoginInitiation, DeviceLoginConfirmation +from .signup import Signup +from .user import User, Group, RemailerAddress, remailer +from .ratelimit import RatelimitEvent, Ratelimit, HostRatelimit, host_ratelimit, format_delay + +__all__ = [ + 'APIClient', + 'Invite', 'InviteGrant', 'InviteSignup', + 'Mail', 'MailReceiveAddress', 'MailDestinationAddress', + 'MFAType', 'MFAMethod', 'RecoveryCodeMethod', 'TOTPMethod', 'WebauthnMethod', + 'OAuth2Client', 'OAuth2RedirectURI', 'OAuth2LogoutURI', 'OAuth2Grant', 'OAuth2Token', 'OAuth2DeviceLoginInitiation', + 'Role', 'RoleGroup', 'RoleGroupMap', + 'PasswordToken', 'MailToken', + 'Service', 'get_services', + 'DeviceLoginType', 'DeviceLoginInitiation', 'DeviceLoginConfirmation', + 'Signup', + 'User', 'Group', 'RemailerAddress', 'remailer', + 'RatelimitEvent', 'Ratelimit', 'HostRatelimit', 'host_ratelimit', 'format_delay', +] diff --git a/uffd/api/models.py b/uffd/models/api.py similarity index 100% rename from uffd/api/models.py rename to uffd/models/api.py diff --git a/uffd/invite/models.py b/uffd/models/invite.py similarity index 99% rename from uffd/invite/models.py rename to uffd/models/invite.py index f3c970040ea1f225efde39d1238499e7e148c715..12018d35ca11089c70e31da611de5d3b5060727f 100644 --- a/uffd/invite/models.py +++ b/uffd/models/invite.py @@ -5,9 +5,9 @@ from flask import current_app from sqlalchemy import Column, String, Integer, ForeignKey, DateTime, Boolean from sqlalchemy.orm import relationship -from uffd.database import db -from uffd.signup.models import Signup from uffd.utils import token_urlfriendly +from uffd.database import db +from .signup import Signup invite_roles = db.Table('invite_roles', Column('invite_id', Integer(), ForeignKey('invite.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True), diff --git a/uffd/mail/models.py b/uffd/models/mail.py similarity index 100% rename from uffd/mail/models.py rename to uffd/models/mail.py diff --git a/uffd/mfa/models.py b/uffd/models/mfa.py similarity index 97% rename from uffd/mfa/models.py rename to uffd/models/mfa.py index 1e117ac64842ce5619d640a33f6ffdc8fbca079d..7db21b5cd3f97ffad9da32c4f41d931ce1b26180 100644 --- a/uffd/mfa/models.py +++ b/uffd/models/mfa.py @@ -16,7 +16,7 @@ from sqlalchemy import Column, Integer, Enum, String, DateTime, Text, ForeignKey from sqlalchemy.orm import relationship, backref from uffd.database import db -from uffd.user.models import User +from .user import User User.mfa_enabled = property(lambda user: bool(user.mfa_totp_methods or user.mfa_webauthn_methods)) @@ -141,7 +141,7 @@ class WebauthnMethod(MFAMethod): @property def cred(self): - from .fido2_compat import AttestedCredentialData #pylint: disable=import-outside-toplevel + from uffd.fido2_compat import AttestedCredentialData #pylint: disable=import-outside-toplevel return AttestedCredentialData(base64.b64decode(self._cred)) @cred.setter diff --git a/uffd/oauth2/models.py b/uffd/models/oauth2.py similarity index 98% rename from uffd/oauth2/models.py rename to uffd/models/oauth2.py index 6ed91097900bda7b42f6620336ab66f47c075cb7..dd0df00f3b068c8db4529c13a347431574e0c30d 100644 --- a/uffd/oauth2/models.py +++ b/uffd/models/oauth2.py @@ -9,7 +9,7 @@ from sqlalchemy.ext.associationproxy import association_proxy from uffd.database import db, CommaSeparatedList from uffd.tasks import cleanup_task from uffd.password_hash import PasswordHashAttribute, HighEntropyPasswordHash -from uffd.session.models import DeviceLoginInitiation, DeviceLoginType +from .session import DeviceLoginInitiation, DeviceLoginType class OAuth2Client(db.Model): __tablename__ = 'oauth2client' diff --git a/uffd/ratelimit.py b/uffd/models/ratelimit.py similarity index 100% rename from uffd/ratelimit.py rename to uffd/models/ratelimit.py index 5604299d983a0e076e107dc2f9e1f46e5f14cfb3..cd370956b0e9fa980ff09e5e4d5faca614c83a52 100644 --- a/uffd/ratelimit.py +++ b/uffd/models/ratelimit.py @@ -7,8 +7,8 @@ from flask_babel import gettext as _ from sqlalchemy import Column, Integer, String, DateTime from sqlalchemy.ext.hybrid import hybrid_property -from uffd.database import db from uffd.tasks import cleanup_task +from uffd.database import db @cleanup_task.delete_by_attribute('expired') class RatelimitEvent(db.Model): diff --git a/uffd/role/models.py b/uffd/models/role.py similarity index 99% rename from uffd/role/models.py rename to uffd/models/role.py index 8bd6fcfcbac5f9ff289f9c6ff845dff232a4304e..27111b8c0524a7c6b335989b5a91b24b9a439e2d 100644 --- a/uffd/role/models.py +++ b/uffd/models/role.py @@ -3,7 +3,7 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm.collections import MappedCollection, collection from uffd.database import db -from uffd.user.models import User +from .user import User class RoleGroup(db.Model): __tablename__ = 'role_groups' diff --git a/uffd/selfservice/models.py b/uffd/models/selfservice.py similarity index 100% rename from uffd/selfservice/models.py rename to uffd/models/selfservice.py index 4ac327bd597230040c78cc3c020f990853fa7819..bb1484579e70b27f948d9b55638f25c124ef018c 100644 --- a/uffd/selfservice/models.py +++ b/uffd/models/selfservice.py @@ -5,8 +5,8 @@ from sqlalchemy.orm import relationship from sqlalchemy.ext.hybrid import hybrid_property from uffd.database import db -from uffd.tasks import cleanup_task from uffd.utils import token_urlfriendly +from uffd.tasks import cleanup_task @cleanup_task.delete_by_attribute('expired') class PasswordToken(db.Model): diff --git a/uffd/service/models.py b/uffd/models/service.py similarity index 100% rename from uffd/service/models.py rename to uffd/models/service.py diff --git a/uffd/session/models.py b/uffd/models/session.py similarity index 100% rename from uffd/session/models.py rename to uffd/models/session.py index 4b2099e8a5c88780778146b686ab1b40f068026e..6b72912b08c0b7a068a46ad3e0932bd055f6d2c3 100644 --- a/uffd/session/models.py +++ b/uffd/models/session.py @@ -7,8 +7,8 @@ from sqlalchemy.orm import relationship from sqlalchemy.ext.hybrid import hybrid_property from uffd.database import db -from uffd.tasks import cleanup_task from uffd.utils import token_typeable +from uffd.tasks import cleanup_task # Device login provides a convenient and secure way to log into SSO-enabled # services on a secondary device without entering the user password or diff --git a/uffd/signup/models.py b/uffd/models/signup.py similarity index 99% rename from uffd/signup/models.py rename to uffd/models/signup.py index 073d5eabdc59c3969bb867e83b212fd79691afd9..204edbc416d1c0b8c571499f12cb1a50454b6c5a 100644 --- a/uffd/signup/models.py +++ b/uffd/models/signup.py @@ -5,11 +5,11 @@ from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey from sqlalchemy.orm import relationship, backref from sqlalchemy.ext.hybrid import hybrid_property -from uffd.database import db from uffd.tasks import cleanup_task -from uffd.user.models import User from uffd.utils import token_urlfriendly from uffd.password_hash import PasswordHashAttribute, LowEntropyPasswordHash +from uffd.database import db +from .user import User @cleanup_task.delete_by_attribute('expired_and_not_completed') class Signup(db.Model): diff --git a/uffd/user/models.py b/uffd/models/user.py similarity index 99% rename from uffd/user/models.py rename to uffd/models/user.py index 8ff69508929b1714087c940b25a01871c85a7bf5..a91c9fe75f5f5f8e616da3c2dd2f12b9c680c1f6 100644 --- a/uffd/user/models.py +++ b/uffd/models/user.py @@ -172,7 +172,7 @@ class Remailer: def parse_address(self, address): # With a top-level import we get circular import problems # pylint: disable=import-outside-toplevel - from uffd.service.models import Service + from .service import Service if '@' not in address: return None local_part, domain = address.rsplit('@', 1) diff --git a/uffd/oauth2/__init__.py b/uffd/oauth2/__init__.py deleted file mode 100644 index 656390049779db11f3fbd9a16498218b18982068..0000000000000000000000000000000000000000 --- a/uffd/oauth2/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .views import bp as _bp - -bp = [_bp] diff --git a/uffd/role/__init__.py b/uffd/role/__init__.py deleted file mode 100644 index 550874c08599da42cff0a0caf7c4d2c04e085400..0000000000000000000000000000000000000000 --- a/uffd/role/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .views import bp as bp_ui -from .cli import bp as bp_cli - -bp = [bp_ui, bp_cli] diff --git a/uffd/rolemod/__init__.py b/uffd/rolemod/__init__.py deleted file mode 100644 index 671578662f91c82cb987ffe679c1f102dc493d1f..0000000000000000000000000000000000000000 --- a/uffd/rolemod/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .views import bp as bp_ui - -bp = [bp_ui] diff --git a/uffd/selfservice/__init__.py b/uffd/selfservice/__init__.py deleted file mode 100644 index 69a1df974cdf718d8d51c2672d3d43611b3bbe03..0000000000000000000000000000000000000000 --- a/uffd/selfservice/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .views import bp as bp_ui, send_passwordreset - -bp = [bp_ui] diff --git a/uffd/service/__init__.py b/uffd/service/__init__.py deleted file mode 100644 index 656390049779db11f3fbd9a16498218b18982068..0000000000000000000000000000000000000000 --- a/uffd/service/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .views import bp as _bp - -bp = [_bp] diff --git a/uffd/session/__init__.py b/uffd/session/__init__.py deleted file mode 100644 index 0e571f3a6aec00dcb8dd2d7eff8788441b735f3d..0000000000000000000000000000000000000000 --- a/uffd/session/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .views import bp as bp_ui, login_required, set_session - -bp = [bp_ui] diff --git a/uffd/signup/__init__.py b/uffd/signup/__init__.py deleted file mode 100644 index 656390049779db11f3fbd9a16498218b18982068..0000000000000000000000000000000000000000 --- a/uffd/signup/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .views import bp as _bp - -bp = [_bp] diff --git a/uffd/tasks.py b/uffd/tasks.py index 4ae6d380f9d221b54d8c57ce2ade602c4e065eb8..85f7b412002942ca1a412efa897d72584ee79b04 100644 --- a/uffd/tasks.py +++ b/uffd/tasks.py @@ -1,14 +1,6 @@ class CleanupTask: - def __init__(self, *init_args): + def __init__(self): self.handlers = [] - if init_args: - self.init_app(*init_args) - - def init_app(self, app, db): - @app.cli.command('cleanup', help='Cleanup expired data') - def cleanup(): #pylint: disable=unused-variable - self.run() - db.session.commit() def handler(self, func): self.handlers.append(func) diff --git a/uffd/user/templates/group/list.html b/uffd/templates/group/list.html similarity index 100% rename from uffd/user/templates/group/list.html rename to uffd/templates/group/list.html diff --git a/uffd/user/templates/group/show.html b/uffd/templates/group/show.html similarity index 100% rename from uffd/user/templates/group/show.html rename to uffd/templates/group/show.html diff --git a/uffd/invite/templates/invite/list.html b/uffd/templates/invite/list.html similarity index 100% rename from uffd/invite/templates/invite/list.html rename to uffd/templates/invite/list.html diff --git a/uffd/invite/templates/invite/new.html b/uffd/templates/invite/new.html similarity index 100% rename from uffd/invite/templates/invite/new.html rename to uffd/templates/invite/new.html diff --git a/uffd/invite/templates/invite/use.html b/uffd/templates/invite/use.html similarity index 100% rename from uffd/invite/templates/invite/use.html rename to uffd/templates/invite/use.html diff --git a/uffd/mail/templates/mail/list.html b/uffd/templates/mail/list.html similarity index 100% rename from uffd/mail/templates/mail/list.html rename to uffd/templates/mail/list.html diff --git a/uffd/mail/templates/mail/show.html b/uffd/templates/mail/show.html similarity index 100% rename from uffd/mail/templates/mail/show.html rename to uffd/templates/mail/show.html diff --git a/uffd/mfa/templates/mfa/auth.html b/uffd/templates/mfa/auth.html similarity index 100% rename from uffd/mfa/templates/mfa/auth.html rename to uffd/templates/mfa/auth.html diff --git a/uffd/mfa/templates/mfa/disable.html b/uffd/templates/mfa/disable.html similarity index 100% rename from uffd/mfa/templates/mfa/disable.html rename to uffd/templates/mfa/disable.html diff --git a/uffd/mfa/templates/mfa/setup.html b/uffd/templates/mfa/setup.html similarity index 100% rename from uffd/mfa/templates/mfa/setup.html rename to uffd/templates/mfa/setup.html diff --git a/uffd/mfa/templates/mfa/setup_recovery.html b/uffd/templates/mfa/setup_recovery.html similarity index 100% rename from uffd/mfa/templates/mfa/setup_recovery.html rename to uffd/templates/mfa/setup_recovery.html diff --git a/uffd/mfa/templates/mfa/setup_totp.html b/uffd/templates/mfa/setup_totp.html similarity index 100% rename from uffd/mfa/templates/mfa/setup_totp.html rename to uffd/templates/mfa/setup_totp.html diff --git a/uffd/oauth2/templates/oauth2/error.html b/uffd/templates/oauth2/error.html similarity index 100% rename from uffd/oauth2/templates/oauth2/error.html rename to uffd/templates/oauth2/error.html diff --git a/uffd/oauth2/templates/oauth2/logout.html b/uffd/templates/oauth2/logout.html similarity index 100% rename from uffd/oauth2/templates/oauth2/logout.html rename to uffd/templates/oauth2/logout.html diff --git a/uffd/role/templates/role/list.html b/uffd/templates/role/list.html similarity index 100% rename from uffd/role/templates/role/list.html rename to uffd/templates/role/list.html diff --git a/uffd/role/templates/role/show.html b/uffd/templates/role/show.html similarity index 100% rename from uffd/role/templates/role/show.html rename to uffd/templates/role/show.html diff --git a/uffd/rolemod/templates/rolemod/list.html b/uffd/templates/rolemod/list.html similarity index 100% rename from uffd/rolemod/templates/rolemod/list.html rename to uffd/templates/rolemod/list.html diff --git a/uffd/rolemod/templates/rolemod/show.html b/uffd/templates/rolemod/show.html similarity index 100% rename from uffd/rolemod/templates/rolemod/show.html rename to uffd/templates/rolemod/show.html diff --git a/uffd/selfservice/templates/selfservice/forgot_password.html b/uffd/templates/selfservice/forgot_password.html similarity index 100% rename from uffd/selfservice/templates/selfservice/forgot_password.html rename to uffd/templates/selfservice/forgot_password.html diff --git a/uffd/selfservice/templates/selfservice/mailverification.mail.txt b/uffd/templates/selfservice/mailverification.mail.txt similarity index 100% rename from uffd/selfservice/templates/selfservice/mailverification.mail.txt rename to uffd/templates/selfservice/mailverification.mail.txt diff --git a/uffd/selfservice/templates/selfservice/newuser.mail.txt b/uffd/templates/selfservice/newuser.mail.txt similarity index 100% rename from uffd/selfservice/templates/selfservice/newuser.mail.txt rename to uffd/templates/selfservice/newuser.mail.txt diff --git a/uffd/selfservice/templates/selfservice/passwordreset.mail.txt b/uffd/templates/selfservice/passwordreset.mail.txt similarity index 100% rename from uffd/selfservice/templates/selfservice/passwordreset.mail.txt rename to uffd/templates/selfservice/passwordreset.mail.txt diff --git a/uffd/selfservice/templates/selfservice/self.html b/uffd/templates/selfservice/self.html similarity index 100% rename from uffd/selfservice/templates/selfservice/self.html rename to uffd/templates/selfservice/self.html diff --git a/uffd/selfservice/templates/selfservice/set_password.html b/uffd/templates/selfservice/set_password.html similarity index 100% rename from uffd/selfservice/templates/selfservice/set_password.html rename to uffd/templates/selfservice/set_password.html diff --git a/uffd/service/templates/service/api.html b/uffd/templates/service/api.html similarity index 100% rename from uffd/service/templates/service/api.html rename to uffd/templates/service/api.html diff --git a/uffd/service/templates/service/index.html b/uffd/templates/service/index.html similarity index 100% rename from uffd/service/templates/service/index.html rename to uffd/templates/service/index.html diff --git a/uffd/service/templates/service/oauth2.html b/uffd/templates/service/oauth2.html similarity index 100% rename from uffd/service/templates/service/oauth2.html rename to uffd/templates/service/oauth2.html diff --git a/uffd/service/templates/service/overview.html b/uffd/templates/service/overview.html similarity index 100% rename from uffd/service/templates/service/overview.html rename to uffd/templates/service/overview.html diff --git a/uffd/service/templates/service/show.html b/uffd/templates/service/show.html similarity index 100% rename from uffd/service/templates/service/show.html rename to uffd/templates/service/show.html diff --git a/uffd/session/templates/session/deviceauth.html b/uffd/templates/session/deviceauth.html similarity index 100% rename from uffd/session/templates/session/deviceauth.html rename to uffd/templates/session/deviceauth.html diff --git a/uffd/session/templates/session/devicelogin.html b/uffd/templates/session/devicelogin.html similarity index 100% rename from uffd/session/templates/session/devicelogin.html rename to uffd/templates/session/devicelogin.html diff --git a/uffd/session/templates/session/login.html b/uffd/templates/session/login.html similarity index 100% rename from uffd/session/templates/session/login.html rename to uffd/templates/session/login.html diff --git a/uffd/signup/templates/signup/confirm.html b/uffd/templates/signup/confirm.html similarity index 100% rename from uffd/signup/templates/signup/confirm.html rename to uffd/templates/signup/confirm.html diff --git a/uffd/signup/templates/signup/mail.txt b/uffd/templates/signup/mail.txt similarity index 100% rename from uffd/signup/templates/signup/mail.txt rename to uffd/templates/signup/mail.txt diff --git a/uffd/signup/templates/signup/start.html b/uffd/templates/signup/start.html similarity index 100% rename from uffd/signup/templates/signup/start.html rename to uffd/templates/signup/start.html diff --git a/uffd/signup/templates/signup/submitted.html b/uffd/templates/signup/submitted.html similarity index 100% rename from uffd/signup/templates/signup/submitted.html rename to uffd/templates/signup/submitted.html diff --git a/uffd/user/templates/user/list.html b/uffd/templates/user/list.html similarity index 100% rename from uffd/user/templates/user/list.html rename to uffd/templates/user/list.html diff --git a/uffd/user/templates/user/show.html b/uffd/templates/user/show.html similarity index 100% rename from uffd/user/templates/user/show.html rename to uffd/templates/user/show.html diff --git a/uffd/user/__init__.py b/uffd/user/__init__.py deleted file mode 100644 index 17a20ec5bc904b2bf507b0e3f9aa922767a2452e..0000000000000000000000000000000000000000 --- a/uffd/user/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .views_user import bp as bp_user -from .cli_user import bp as bp_cli_user -from .views_group import bp as bp_group -from .cli_group import bp as bp_cli_group - -bp = [bp_user, bp_group, bp_cli_user, bp_cli_group] diff --git a/uffd/views/__init__.py b/uffd/views/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4f5bdb08fdb0ae7a282d4e57df70e1b84f39e45f --- /dev/null +++ b/uffd/views/__init__.py @@ -0,0 +1,38 @@ +from flask import redirect, url_for, request, render_template +from werkzeug.exceptions import Forbidden + +from uffd.secure_redirect import secure_local_redirect + +from . import session, selfservice, signup, mfa, oauth2, user, group, service, role, invite, api, mail, rolemod + +def init_app(app): + @app.errorhandler(403) + def handle_403(error): + return render_template('403.html', description=error.description if error.description != Forbidden.description else None), 403 + + @app.route("/") + def index(): #pylint: disable=unused-variable + if app.config['DEFAULT_PAGE_SERVICES']: + return redirect(url_for('service.overview')) + return redirect(url_for('selfservice.index')) + + @app.route('/lang', methods=['POST']) + def setlang(): #pylint: disable=unused-variable + resp = secure_local_redirect(request.values.get('ref', '/')) + if 'lang' in request.values: + resp.set_cookie('language', request.values['lang']) + return resp + + app.register_blueprint(session.bp) + app.register_blueprint(selfservice.bp) + app.register_blueprint(signup.bp) + app.register_blueprint(mfa.bp) + app.register_blueprint(oauth2.bp) + app.register_blueprint(user.bp) + app.register_blueprint(group.bp) + app.register_blueprint(service.bp) + app.register_blueprint(role.bp) + app.register_blueprint(invite.bp) + app.register_blueprint(api.bp) + app.register_blueprint(mail.bp) + app.register_blueprint(rolemod.bp) diff --git a/uffd/api/views.py b/uffd/views/api.py similarity index 95% rename from uffd/api/views.py rename to uffd/views/api.py index 33e7122ab2726e219955b03873a7834744fb4d0d..11d59db408c2e00f3302ae74b0c24ffd80e42803 100644 --- a/uffd/api/views.py +++ b/uffd/views/api.py @@ -2,11 +2,9 @@ import functools from flask import Blueprint, jsonify, request, abort -from uffd.user.models import User, remailer, Group -from uffd.mail.models import Mail, MailReceiveAddress, MailDestinationAddress -from uffd.api.models import APIClient -from uffd.session.views import login_get_user, login_ratelimit from uffd.database import db +from uffd.models import User, remailer, Group, Mail, MailReceiveAddress, MailDestinationAddress, APIClient +from .session import login_get_user, login_ratelimit bp = Blueprint('api', __name__, template_folder='templates', url_prefix='/api/v1/') diff --git a/uffd/user/views_group.py b/uffd/views/group.py similarity index 96% rename from uffd/user/views_group.py rename to uffd/views/group.py index 63d281f487aacbff8e7aa1c00f5d75926b73c5a2..d70751ef58127036ea722ccbb96cc842bcb41665 100644 --- a/uffd/user/views_group.py +++ b/uffd/views/group.py @@ -4,10 +4,9 @@ import sqlalchemy from uffd.navbar import register_navbar from uffd.csrf import csrf_protect -from uffd.session import login_required from uffd.database import db - -from .models import Group +from uffd.models import Group +from .session import login_required bp = Blueprint("group", __name__, template_folder='templates', url_prefix='/group/') diff --git a/uffd/invite/views.py b/uffd/views/invite.py similarity index 95% rename from uffd/invite/views.py rename to uffd/views/invite.py index 7e8074c7b1f414f937de8c56feb5df4f01e8e823..99b0675ed57ac571d90b38b13a097f170e5b1738 100644 --- a/uffd/invite/views.py +++ b/uffd/views/invite.py @@ -6,16 +6,13 @@ from flask_babel import gettext as _, lazy_gettext import sqlalchemy from uffd.csrf import csrf_protect -from uffd.database import db -from uffd.session import login_required -from uffd.role.models import Role -from uffd.invite.models import Invite, InviteSignup, InviteGrant -from uffd.user.models import User, Group from uffd.sendmail import sendmail from uffd.navbar import register_navbar -from uffd.ratelimit import host_ratelimit, format_delay -from uffd.signup.views import signup_ratelimit -from uffd.selfservice.views import selfservice_acl_check +from uffd.database import db +from uffd.models import Role, User, Group, Invite, InviteSignup, InviteGrant, host_ratelimit, format_delay +from .session import login_required +from .signup import signup_ratelimit +from .selfservice import selfservice_acl_check bp = Blueprint('invite', __name__, template_folder='templates', url_prefix='/invite/') diff --git a/uffd/mail/views.py b/uffd/views/mail.py similarity index 96% rename from uffd/mail/views.py rename to uffd/views/mail.py index 30981efa38f617eae29ca71d9b8b7dee4b023c6b..7bd2504907e2e7068a87128e5acc230e9d328787 100644 --- a/uffd/mail/views.py +++ b/uffd/views/mail.py @@ -4,9 +4,8 @@ from flask_babel import gettext as _, lazy_gettext from uffd.navbar import register_navbar from uffd.csrf import csrf_protect from uffd.database import db -from uffd.session import login_required - -from uffd.mail.models import Mail +from uffd.models import Mail +from .session import login_required bp = Blueprint("mail", __name__, template_folder='templates', url_prefix='/mail/') diff --git a/uffd/mfa/views.py b/uffd/views/mfa.py similarity index 96% rename from uffd/mfa/views.py rename to uffd/views/mfa.py index bae2464942113ab7636d998e1d6f6d2e85a6dc5f..7471bef285048d31add7d0bf4bfb8a1883292af3 100644 --- a/uffd/mfa/views.py +++ b/uffd/views/mfa.py @@ -4,13 +4,11 @@ import urllib.parse from flask import Blueprint, render_template, session, request, redirect, url_for, flash, current_app, abort from flask_babel import gettext as _ -from uffd.database import db -from uffd.mfa.models import MFAMethod, TOTPMethod, WebauthnMethod, RecoveryCodeMethod -from uffd.session.views import login_required, login_required_pre_mfa, set_request_user -from uffd.user.models import User from uffd.csrf import csrf_protect from uffd.secure_redirect import secure_local_redirect -from uffd.ratelimit import Ratelimit, format_delay +from uffd.database import db +from uffd.models import MFAMethod, TOTPMethod, WebauthnMethod, RecoveryCodeMethod, User, Ratelimit, format_delay +from .session import login_required, login_required_pre_mfa, set_request_user bp = Blueprint('mfa', __name__, template_folder='templates', url_prefix='/mfa/') @@ -101,7 +99,7 @@ def delete_totp(id): #pylint: disable=redefined-builtin # WebAuthn support is optional because fido2 has a pretty unstable # interface and might be difficult to install with the correct version try: - from .fido2_compat import * # pylint: disable=wildcard-import,unused-wildcard-import + from uffd.fido2_compat import * # pylint: disable=wildcard-import,unused-wildcard-import WEBAUTHN_SUPPORTED = True except ImportError as err: warn(_('2FA WebAuthn support disabled because import of the fido2 module failed (%s)')%err) diff --git a/uffd/oauth2/views.py b/uffd/views/oauth2.py similarity index 98% rename from uffd/oauth2/views.py rename to uffd/views/oauth2.py index e53bf7ccd28872d16216ea70a9d80f69be06e539..d9219cab5c02c6ffae05fc9f4eceb51d596fbfe5 100644 --- a/uffd/oauth2/views.py +++ b/uffd/views/oauth2.py @@ -7,11 +7,9 @@ import oauthlib.oauth2 from flask_babel import gettext as _ from sqlalchemy.exc import IntegrityError -from uffd.ratelimit import host_ratelimit, format_delay -from uffd.database import db from uffd.secure_redirect import secure_local_redirect -from uffd.session.models import DeviceLoginConfirmation -from .models import OAuth2Client, OAuth2Grant, OAuth2Token, OAuth2DeviceLoginInitiation +from uffd.database import db +from uffd.models import DeviceLoginConfirmation, OAuth2Client, OAuth2Grant, OAuth2Token, OAuth2DeviceLoginInitiation, host_ratelimit, format_delay class UffdRequestValidator(oauthlib.oauth2.RequestValidator): # Argument "oauthreq" is named "request" in superclass but this clashes with flask's "request" object diff --git a/uffd/role/views.py b/uffd/views/role.py similarity index 76% rename from uffd/role/views.py rename to uffd/views/role.py index ce99dafcca86bb2832e9b813ae713aeecca31491..153c2cda47834384c0343d8db5873c695c7240ba 100644 --- a/uffd/role/views.py +++ b/uffd/views/role.py @@ -1,41 +1,14 @@ -import sys - from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app from flask_babel import gettext as _, lazy_gettext -import click from uffd.navbar import register_navbar from uffd.csrf import csrf_protect -from uffd.role.models import Role, RoleGroup -from uffd.user.models import User, Group -from uffd.session import login_required from uffd.database import db +from uffd.models import Role, RoleGroup, Group +from .session import login_required bp = Blueprint("role", __name__, template_folder='templates', url_prefix='/role/') -@bp.record -def add_cli_commands(state): - @state.app.cli.command('roles-update-all', help='Update group memberships for all users based on their roles') - @click.option('--check-only', is_flag=True) - def roles_update_all(check_only): #pylint: disable=unused-variable - consistent = True - with current_app.test_request_context(): - for user in User.query.all(): - groups_added, groups_removed = user.update_groups() - if groups_added: - consistent = False - print('Adding groups [%s] to user %s'%(', '.join([group.name for group in groups_added]), user.loginname)) - if groups_removed: - consistent = False - print('Removing groups [%s] from user %s'%(', '.join([group.name for group in groups_removed]), user.loginname)) - if not check_only: - db.session.commit() - if check_only and not consistent: - print('No changes were made because --check-only is set') - print() - print('Error: LDAP groups are not consistent with roles in database') - sys.exit(1) - def role_acl_check(): return request.user and request.user.is_in_group(current_app.config['ACL_ADMIN_GROUP']) diff --git a/uffd/rolemod/views.py b/uffd/views/rolemod.py similarity index 94% rename from uffd/rolemod/views.py rename to uffd/views/rolemod.py index d1c520b72db617f98b3db1ca0264c9c6ebcec6ba..61dcdbffcfe8cf92095376e25e78f8a7e63c2f95 100644 --- a/uffd/rolemod/views.py +++ b/uffd/views/rolemod.py @@ -3,10 +3,9 @@ from flask_babel import gettext as _, lazy_gettext from uffd.navbar import register_navbar from uffd.csrf import csrf_protect -from uffd.role.models import Role -from uffd.user.models import User, Group -from uffd.session import login_required from uffd.database import db +from uffd.models import Role, User, Group +from .session import login_required bp = Blueprint('rolemod', __name__, template_folder='templates', url_prefix='/rolemod/') diff --git a/uffd/selfservice/views.py b/uffd/views/selfservice.py similarity index 96% rename from uffd/selfservice/views.py rename to uffd/views/selfservice.py index 3103c118acaf64ac723b2e9234e08d810bc6a708..fee71b8db93a74f3a8a00693dad9ff2c11596c68 100644 --- a/uffd/selfservice/views.py +++ b/uffd/views/selfservice.py @@ -5,13 +5,10 @@ from flask_babel import gettext as _, lazy_gettext from uffd.navbar import register_navbar from uffd.csrf import csrf_protect -from uffd.user.models import User -from uffd.session import login_required -from uffd.selfservice.models import PasswordToken, MailToken from uffd.sendmail import sendmail -from uffd.role.models import Role from uffd.database import db -from uffd.ratelimit import host_ratelimit, Ratelimit, format_delay +from uffd.models import User, PasswordToken, MailToken, Role, host_ratelimit, Ratelimit, format_delay +from .session import login_required bp = Blueprint("selfservice", __name__, template_folder='templates', url_prefix='/self/') diff --git a/uffd/service/views.py b/uffd/views/service.py similarity index 96% rename from uffd/service/views.py rename to uffd/views/service.py index edcb1c32c33bc9b1ed04e2b8bfe1e44d74280687..dbedf510577ba3a0cb6b161dafaeb0dfd446a659 100644 --- a/uffd/service/views.py +++ b/uffd/views/service.py @@ -5,12 +5,10 @@ from flask_babel import lazy_gettext from uffd.navbar import register_navbar from uffd.csrf import csrf_protect -from uffd.session import login_required -from uffd.service.models import Service, get_services -from uffd.user.models import Group -from uffd.oauth2.models import OAuth2Client, OAuth2LogoutURI -from uffd.api.models import APIClient from uffd.database import db +from uffd.models import Service, get_services, Group, OAuth2Client, OAuth2LogoutURI, APIClient + +from .session import login_required bp = Blueprint('service', __name__, template_folder='templates') diff --git a/uffd/session/views.py b/uffd/views/session.py similarity index 97% rename from uffd/session/views.py rename to uffd/views/session.py index 690397bc10736f925d1c634c83a50790d3f25f19..3f6a6187452e79193b715953608f2d1bfa58c1d7 100644 --- a/uffd/session/views.py +++ b/uffd/views/session.py @@ -8,9 +8,7 @@ from flask_babel import gettext as _ from uffd.database import db from uffd.csrf import csrf_protect from uffd.secure_redirect import secure_local_redirect -from uffd.user.models import User -from uffd.ratelimit import Ratelimit, host_ratelimit, format_delay -from uffd.session.models import DeviceLoginInitiation, DeviceLoginConfirmation +from uffd.models import User, DeviceLoginInitiation, DeviceLoginConfirmation, Ratelimit, host_ratelimit, format_delay bp = Blueprint("session", __name__, template_folder='templates', url_prefix='/') diff --git a/uffd/signup/views.py b/uffd/views/signup.py similarity index 96% rename from uffd/signup/views.py rename to uffd/views/signup.py index 752cc587fc283ec42daefea3dd36e0e6748fc5d9..edbd3dcb5b073a2683a516816959273334414f0a 100644 --- a/uffd/signup/views.py +++ b/uffd/views/signup.py @@ -4,12 +4,10 @@ import secrets from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify from flask_babel import gettext as _ -from uffd.database import db -from uffd.session import set_session -from uffd.user.models import User from uffd.sendmail import sendmail -from uffd.signup.models import Signup -from uffd.ratelimit import Ratelimit, host_ratelimit, format_delay +from uffd.database import db +from uffd.models import User, Signup, Ratelimit, host_ratelimit, format_delay +from .session import set_session bp = Blueprint('signup', __name__, template_folder='templates', url_prefix='/signup/') diff --git a/uffd/user/views_user.py b/uffd/views/user.py similarity index 96% rename from uffd/user/views_user.py rename to uffd/views/user.py index d9a8800b3738b40882babef1ce3f2203ad6627d7..3ecf45d0e1a4942ebefb4dfa69ba41b8db288015 100644 --- a/uffd/user/views_user.py +++ b/uffd/views/user.py @@ -7,12 +7,10 @@ from sqlalchemy.exc import IntegrityError from uffd.navbar import register_navbar from uffd.csrf import csrf_protect -from uffd.selfservice import send_passwordreset -from uffd.session import login_required -from uffd.role.models import Role from uffd.database import db - -from .models import User, remailer +from uffd.models import User, remailer, Role +from .selfservice import send_passwordreset +from .session import login_required bp = Blueprint("user", __name__, template_folder='templates', url_prefix='/user/')