From 5fdccfe3e6567bebbcdad79bf4518237ec40676b Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Tue, 13 May 2014 12:17:26 -0400 Subject: [PATCH] Add an alembic migration for the full initial database with the data. Switch LDAP to using bind and creating a federated login entry. Add LDAP support to the registry and index endpoints. Add a username transliteration and suggestion mechanism. Switch the database and model to require a manual initialization call. --- app.py | 8 +- auth/auth.py | 4 +- data/database.py | 12 +- data/migrations/env.py | 5 +- .../5a07499ce53f_set_up_initial_database.py | 607 ++++++++++++++++++ data/model/legacy.py | 69 +- data/users.py | 66 +- endpoints/index.py | 4 +- initdb.py | 1 + requirements-nover.txt | 1 + test/data/test.db | Bin 200704 -> 200704 bytes util/validation.py | 37 +- 12 files changed, 739 insertions(+), 75 deletions(-) create mode 100644 data/migrations/versions/5a07499ce53f_set_up_initial_database.py diff --git a/app.py b/app.py index b943f62d1..a27d7f058 100644 --- a/app.py +++ b/app.py @@ -9,6 +9,8 @@ from flask.ext.mail import Mail import features from storage import Storage +from data import model +from data import database from data.userfiles import Userfiles from data.users import UserAuthentication from util.analytics import Analytics @@ -47,6 +49,8 @@ userfiles = Userfiles(app) analytics = Analytics(app) billing = Billing(app) sentry = Sentry(app) +authentication = UserAuthentication(app) -from data import model -authentication = UserAuthentication(app, model) +database.configure(app.config) +model.config.app_config = app.config +model.config.store = storage diff --git a/auth/auth.py b/auth/auth.py index ac78102a4..0e5457a36 100644 --- a/auth/auth.py +++ b/auth/auth.py @@ -11,7 +11,7 @@ import scopes from data import model from data.model import oauth -from app import app +from app import app, authentication from permissions import QuayDeferredPermissionUser from auth_context import (set_authenticated_user, set_validated_token, set_authenticated_user_deferred, set_validated_oauth_token) @@ -108,7 +108,7 @@ def process_basic_auth(auth): logger.debug('Invalid robot or password for robot: %s' % credentials[0]) else: - authenticated = model.verify_user(credentials[0], credentials[1]) + authenticated = authentication.verify_user(credentials[0], credentials[1]) if authenticated: logger.debug('Successfully validated user: %s' % authenticated.username) diff --git a/data/database.py b/data/database.py index c8fa6cf25..5fd8abbf9 100644 --- a/data/database.py +++ b/data/database.py @@ -9,19 +9,19 @@ from playhouse.pool import PooledMySQLDatabase from sqlalchemy.engine.url import make_url from urlparse import urlparse -from app import app - logger = logging.getLogger(__name__) SCHEME_DRIVERS = { 'mysql': PooledMySQLDatabase, + 'mysql+pymysql': PooledMySQLDatabase, 'sqlite': SqliteDatabase, } +db = Proxy() -def generate_db(config_object): +def configure(config_object): db_kwargs = dict(config_object['DB_CONNECTION_ARGS']) parsed_url = make_url(config_object['DB_URI']) @@ -34,10 +34,8 @@ def generate_db(config_object): if parsed_url.password: db_kwargs['passwd'] = parsed_url.password - return SCHEME_DRIVERS[parsed_url.drivername](parsed_url.database, **db_kwargs) - - -db = generate_db(app.config) + real_db = SCHEME_DRIVERS[parsed_url.drivername](parsed_url.database, **db_kwargs) + db.initialize(real_db) def random_string_generator(length=16): diff --git a/data/migrations/env.py b/data/migrations/env.py index 65f00819f..5b1564b50 100644 --- a/data/migrations/env.py +++ b/data/migrations/env.py @@ -2,6 +2,7 @@ from __future__ import with_statement from alembic import context from sqlalchemy import engine_from_config, pool from logging.config import fileConfig +from urllib import unquote from data.database import all_models from app import app @@ -10,7 +11,7 @@ from data.model.sqlalchemybridge import gen_sqlalchemy_metadata # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -config.set_main_option('sqlalchemy.url', app.config['DB_URI']) +config.set_main_option('sqlalchemy.url', unquote(app.config['DB_URI'])) # Interpret the config file for Python logging. # This line sets up loggers basically. @@ -39,7 +40,7 @@ def run_migrations_offline(): script output. """ - url = app.config['DB_CONNECTION'] + url = unquote(app.config['DB_URI']) context.configure(url=url, target_metadata=target_metadata) with context.begin_transaction(): diff --git a/data/migrations/versions/5a07499ce53f_set_up_initial_database.py b/data/migrations/versions/5a07499ce53f_set_up_initial_database.py new file mode 100644 index 000000000..3c4a8de5d --- /dev/null +++ b/data/migrations/versions/5a07499ce53f_set_up_initial_database.py @@ -0,0 +1,607 @@ +"""Set up initial database + +Revision ID: 5a07499ce53f +Revises: None +Create Date: 2014-05-13 11:26:51.808426 + +""" + +# revision identifiers, used by Alembic. +revision = '5a07499ce53f' +down_revision = None + +from alembic import op +from data.model.sqlalchemybridge import gen_sqlalchemy_metadata +from data.database import all_models +import sqlalchemy as sa + + +def upgrade(): + schema = gen_sqlalchemy_metadata(all_models) + + ### commands auto generated by Alembic - please adjust! ### + op.create_table('loginservice', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('loginservice_name', 'loginservice', ['name'], unique=True) + + op.bulk_insert(schema.tables['loginservice'], + [ + {'id':1, 'name':'github'}, + {'id':2, 'name':'quayrobot'}, + {'id':3, 'name':'ldap'}, + ]) + + op.create_table('imagestorage', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', sa.String(length=255), nullable=False), + sa.Column('checksum', sa.String(length=255), nullable=True), + sa.Column('created', sa.DateTime(), nullable=True), + sa.Column('comment', sa.Text(), nullable=True), + sa.Column('command', sa.Text(), nullable=True), + sa.Column('image_size', sa.BigInteger(), nullable=True), + sa.Column('uploading', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('queueitem', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('queue_name', sa.String(length=1024), nullable=False), + sa.Column('body', sa.Text(), nullable=False), + sa.Column('available_after', sa.DateTime(), nullable=False), + sa.Column('available', sa.Boolean(), nullable=False), + sa.Column('processing_expires', sa.DateTime(), nullable=True), + sa.Column('retries_remaining', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('queueitem_available', 'queueitem', ['available'], unique=False) + op.create_index('queueitem_available_after', 'queueitem', ['available_after'], unique=False) + op.create_index('queueitem_processing_expires', 'queueitem', ['processing_expires'], unique=False) + op.create_index('queueitem_queue_name', 'queueitem', ['queue_name'], unique=False) + op.create_table('role', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('role_name', 'role', ['name'], unique=False) + + op.bulk_insert(schema.tables['role'], + [ + {'id':1, 'name':'admin'}, + {'id':2, 'name':'write'}, + {'id':3, 'name':'read'}, + ]) + + op.create_table('logentrykind', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('logentrykind_name', 'logentrykind', ['name'], unique=False) + + op.bulk_insert(schema.tables['logentrykind'], + [ + {'id':1, 'name':'account_change_plan'}, + {'id':2, 'name':'account_change_cc'}, + {'id':3, 'name':'account_change_password'}, + {'id':4, 'name':'account_convert'}, + + {'id':5, 'name':'create_robot'}, + {'id':6, 'name':'delete_robot'}, + + {'id':7, 'name':'create_repo'}, + {'id':8, 'name':'push_repo'}, + {'id':9, 'name':'pull_repo'}, + {'id':10, 'name':'delete_repo'}, + {'id':11, 'name':'create_tag'}, + {'id':12, 'name':'move_tag'}, + {'id':13, 'name':'delete_tag'}, + {'id':14, 'name':'add_repo_permission'}, + {'id':15, 'name':'change_repo_permission'}, + {'id':16, 'name':'delete_repo_permission'}, + {'id':17, 'name':'change_repo_visibility'}, + {'id':18, 'name':'add_repo_accesstoken'}, + {'id':19, 'name':'delete_repo_accesstoken'}, + {'id':20, 'name':'add_repo_webhook'}, + {'id':21, 'name':'delete_repo_webhook'}, + {'id':22, 'name':'set_repo_description'}, + + {'id':23, 'name':'build_dockerfile'}, + + {'id':24, 'name':'org_create_team'}, + {'id':25, 'name':'org_delete_team'}, + {'id':26, 'name':'org_add_team_member'}, + {'id':27, 'name':'org_remove_team_member'}, + {'id':28, 'name':'org_set_team_description'}, + {'id':29, 'name':'org_set_team_role'}, + + {'id':30, 'name':'create_prototype_permission'}, + {'id':31, 'name':'modify_prototype_permission'}, + {'id':32, 'name':'delete_prototype_permission'}, + + {'id':33, 'name':'setup_repo_trigger'}, + {'id':34, 'name':'delete_repo_trigger'}, + + {'id':35, 'name':'create_application'}, + {'id':36, 'name':'update_application'}, + {'id':37, 'name':'delete_application'}, + {'id':38, 'name':'reset_application_client_secret'}, + ]) + + op.create_table('notificationkind', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('notificationkind_name', 'notificationkind', ['name'], unique=False) + + op.bulk_insert(schema.tables['notificationkind'], + [ + {'id':1, 'name':'password_required'}, + {'id':2, 'name':'over_private_usage'}, + ]) + + op.create_table('teamrole', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('teamrole_name', 'teamrole', ['name'], unique=False) + + op.bulk_insert(schema.tables['teamrole'], + [ + {'id':1, 'name':'admin'}, + {'id':2, 'name':'creator'}, + {'id':3, 'name':'member'}, + ]) + + op.create_table('visibility', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('visibility_name', 'visibility', ['name'], unique=False) + + op.bulk_insert(schema.tables['visibility'], + [ + {'id':1, 'name':'public'}, + {'id':2, 'name':'private'}, + ]) + + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=255), nullable=False), + sa.Column('password_hash', sa.String(length=255), nullable=True), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('verified', sa.Boolean(), nullable=False), + sa.Column('stripe_id', sa.String(length=255), nullable=True), + sa.Column('organization', sa.Boolean(), nullable=False), + sa.Column('robot', sa.Boolean(), nullable=False), + sa.Column('invoice_email', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('user_email', 'user', ['email'], unique=True) + op.create_index('user_organization', 'user', ['organization'], unique=False) + op.create_index('user_robot', 'user', ['robot'], unique=False) + op.create_index('user_stripe_id', 'user', ['stripe_id'], unique=False) + op.create_index('user_username', 'user', ['username'], unique=True) + op.create_table('buildtriggerservice', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('buildtriggerservice_name', 'buildtriggerservice', ['name'], unique=False) + + op.bulk_insert(schema.tables['buildtriggerservice'], + [ + {'id':1, 'name':'github'}, + ]) + + op.create_table('federatedlogin', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('service_id', sa.Integer(), nullable=False), + sa.Column('service_ident', sa.String(length=255, collation='utf8_general_ci'), nullable=False), + sa.ForeignKeyConstraint(['service_id'], ['loginservice.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('federatedlogin_service_id', 'federatedlogin', ['service_id'], unique=False) + op.create_index('federatedlogin_service_id_service_ident', 'federatedlogin', ['service_id', 'service_ident'], unique=True) + op.create_index('federatedlogin_service_id_user_id', 'federatedlogin', ['service_id', 'user_id'], unique=True) + op.create_index('federatedlogin_user_id', 'federatedlogin', ['user_id'], unique=False) + op.create_table('oauthapplication', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('client_id', sa.String(length=255), nullable=False), + sa.Column('client_secret', sa.String(length=255), nullable=False), + sa.Column('redirect_uri', sa.String(length=255), nullable=False), + sa.Column('application_uri', sa.String(length=255), nullable=False), + sa.Column('organization_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('gravatar_email', sa.String(length=255), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('oauthapplication_client_id', 'oauthapplication', ['client_id'], unique=False) + op.create_index('oauthapplication_organization_id', 'oauthapplication', ['organization_id'], unique=False) + op.create_table('notification', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', sa.String(length=255), nullable=False), + sa.Column('kind_id', sa.Integer(), nullable=False), + sa.Column('target_id', sa.Integer(), nullable=False), + sa.Column('metadata_json', sa.Text(), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['kind_id'], ['notificationkind.id'], ), + sa.ForeignKeyConstraint(['target_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('notification_created', 'notification', ['created'], unique=False) + op.create_index('notification_kind_id', 'notification', ['kind_id'], unique=False) + op.create_index('notification_target_id', 'notification', ['target_id'], unique=False) + op.create_index('notification_uuid', 'notification', ['uuid'], unique=False) + op.create_table('emailconfirmation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('code', sa.String(length=255), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('pw_reset', sa.Boolean(), nullable=False), + sa.Column('new_email', sa.String(length=255), nullable=True), + sa.Column('email_confirm', sa.Boolean(), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('emailconfirmation_code', 'emailconfirmation', ['code'], unique=True) + op.create_index('emailconfirmation_user_id', 'emailconfirmation', ['user_id'], unique=False) + op.create_table('team', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('organization_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.ForeignKeyConstraint(['organization_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['teamrole.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('team_name', 'team', ['name'], unique=False) + op.create_index('team_name_organization_id', 'team', ['name', 'organization_id'], unique=True) + op.create_index('team_organization_id', 'team', ['organization_id'], unique=False) + op.create_index('team_role_id', 'team', ['role_id'], unique=False) + op.create_table('repository', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('namespace', sa.String(length=255), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('visibility_id', sa.Integer(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('badge_token', sa.String(length=255), nullable=False), + sa.ForeignKeyConstraint(['visibility_id'], ['visibility.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('repository_namespace_name', 'repository', ['namespace', 'name'], unique=True) + op.create_index('repository_visibility_id', 'repository', ['visibility_id'], unique=False) + op.create_table('accesstoken', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('friendly_name', sa.String(length=255), nullable=True), + sa.Column('code', sa.String(length=255), nullable=False), + sa.Column('repository_id', sa.Integer(), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('temporary', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('accesstoken_code', 'accesstoken', ['code'], unique=True) + op.create_index('accesstoken_repository_id', 'accesstoken', ['repository_id'], unique=False) + op.create_index('accesstoken_role_id', 'accesstoken', ['role_id'], unique=False) + op.create_table('repositorypermission', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('team_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('repository_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], ), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('repositorypermission_repository_id', 'repositorypermission', ['repository_id'], unique=False) + op.create_index('repositorypermission_role_id', 'repositorypermission', ['role_id'], unique=False) + op.create_index('repositorypermission_team_id', 'repositorypermission', ['team_id'], unique=False) + op.create_index('repositorypermission_team_id_repository_id', 'repositorypermission', ['team_id', 'repository_id'], unique=True) + op.create_index('repositorypermission_user_id', 'repositorypermission', ['user_id'], unique=False) + op.create_index('repositorypermission_user_id_repository_id', 'repositorypermission', ['user_id', 'repository_id'], unique=True) + op.create_table('oauthaccesstoken', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', sa.String(length=255), nullable=False), + sa.Column('application_id', sa.Integer(), nullable=False), + sa.Column('authorized_user_id', sa.Integer(), nullable=False), + sa.Column('scope', sa.String(length=255), nullable=False), + sa.Column('access_token', sa.String(length=255), nullable=False), + sa.Column('token_type', sa.String(length=255), nullable=False), + sa.Column('expires_at', sa.DateTime(), nullable=False), + sa.Column('refresh_token', sa.String(length=255), nullable=True), + sa.Column('data', sa.Text(), nullable=False), + sa.ForeignKeyConstraint(['application_id'], ['oauthapplication.id'], ), + sa.ForeignKeyConstraint(['authorized_user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('oauthaccesstoken_access_token', 'oauthaccesstoken', ['access_token'], unique=False) + op.create_index('oauthaccesstoken_application_id', 'oauthaccesstoken', ['application_id'], unique=False) + op.create_index('oauthaccesstoken_authorized_user_id', 'oauthaccesstoken', ['authorized_user_id'], unique=False) + op.create_index('oauthaccesstoken_refresh_token', 'oauthaccesstoken', ['refresh_token'], unique=False) + op.create_index('oauthaccesstoken_uuid', 'oauthaccesstoken', ['uuid'], unique=False) + op.create_table('teammember', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('team_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('teammember_team_id', 'teammember', ['team_id'], unique=False) + op.create_index('teammember_user_id', 'teammember', ['user_id'], unique=False) + op.create_index('teammember_user_id_team_id', 'teammember', ['user_id', 'team_id'], unique=True) + op.create_table('webhook', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('public_id', sa.String(length=255), nullable=False), + sa.Column('repository_id', sa.Integer(), nullable=False), + sa.Column('parameters', sa.Text(), nullable=False), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('webhook_public_id', 'webhook', ['public_id'], unique=True) + op.create_index('webhook_repository_id', 'webhook', ['repository_id'], unique=False) + op.create_table('oauthauthorizationcode', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('application_id', sa.Integer(), nullable=False), + sa.Column('code', sa.String(length=255), nullable=False), + sa.Column('scope', sa.String(length=255), nullable=False), + sa.Column('data', sa.Text(), nullable=False), + sa.ForeignKeyConstraint(['application_id'], ['oauthapplication.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('oauthauthorizationcode_application_id', 'oauthauthorizationcode', ['application_id'], unique=False) + op.create_index('oauthauthorizationcode_code', 'oauthauthorizationcode', ['code'], unique=False) + op.create_table('image', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('docker_image_id', sa.String(length=255), nullable=False), + sa.Column('checksum', sa.String(length=255), nullable=True), + sa.Column('created', sa.DateTime(), nullable=True), + sa.Column('comment', sa.Text(), nullable=True), + sa.Column('command', sa.Text(), nullable=True), + sa.Column('repository_id', sa.Integer(), nullable=False), + sa.Column('image_size', sa.BigInteger(), nullable=True), + sa.Column('ancestors', sa.String(length=60535, collation='latin1_swedish_ci'), nullable=True), + sa.Column('storage_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ), + sa.ForeignKeyConstraint(['storage_id'], ['imagestorage.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('image_ancestors', 'image', ['ancestors'], unique=False) + op.create_index('image_repository_id', 'image', ['repository_id'], unique=False) + op.create_index('image_repository_id_docker_image_id', 'image', ['repository_id', 'docker_image_id'], unique=False) + op.create_index('image_storage_id', 'image', ['storage_id'], unique=False) + op.create_table('permissionprototype', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('org_id', sa.Integer(), nullable=False), + sa.Column('uuid', sa.String(length=255), nullable=False), + sa.Column('activating_user_id', sa.Integer(), nullable=True), + sa.Column('delegate_user_id', sa.Integer(), nullable=True), + sa.Column('delegate_team_id', sa.Integer(), nullable=True), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['activating_user_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['delegate_team_id'], ['team.id'], ), + sa.ForeignKeyConstraint(['delegate_user_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['org_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['role.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('permissionprototype_activating_user_id', 'permissionprototype', ['activating_user_id'], unique=False) + op.create_index('permissionprototype_delegate_team_id', 'permissionprototype', ['delegate_team_id'], unique=False) + op.create_index('permissionprototype_delegate_user_id', 'permissionprototype', ['delegate_user_id'], unique=False) + op.create_index('permissionprototype_org_id', 'permissionprototype', ['org_id'], unique=False) + op.create_index('permissionprototype_org_id_activating_user_id', 'permissionprototype', ['org_id', 'activating_user_id'], unique=False) + op.create_index('permissionprototype_role_id', 'permissionprototype', ['role_id'], unique=False) + op.create_table('repositorytag', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('image_id', sa.Integer(), nullable=False), + sa.Column('repository_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['image_id'], ['image.id'], ), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('repositorytag_image_id', 'repositorytag', ['image_id'], unique=False) + op.create_index('repositorytag_repository_id', 'repositorytag', ['repository_id'], unique=False) + op.create_index('repositorytag_repository_id_name', 'repositorytag', ['repository_id', 'name'], unique=True) + op.create_table('logentry', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('kind_id', sa.Integer(), nullable=False), + sa.Column('account_id', sa.Integer(), nullable=False), + sa.Column('performer_id', sa.Integer(), nullable=True), + sa.Column('repository_id', sa.Integer(), nullable=True), + sa.Column('access_token_id', sa.Integer(), nullable=True), + sa.Column('datetime', sa.DateTime(), nullable=False), + sa.Column('ip', sa.String(length=255), nullable=True), + sa.Column('metadata_json', sa.Text(), nullable=False), + sa.ForeignKeyConstraint(['access_token_id'], ['accesstoken.id'], ), + sa.ForeignKeyConstraint(['account_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['kind_id'], ['logentrykind.id'], ), + sa.ForeignKeyConstraint(['performer_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('logentry_access_token_id', 'logentry', ['access_token_id'], unique=False) + op.create_index('logentry_account_id', 'logentry', ['account_id'], unique=False) + op.create_index('logentry_datetime', 'logentry', ['datetime'], unique=False) + op.create_index('logentry_kind_id', 'logentry', ['kind_id'], unique=False) + op.create_index('logentry_performer_id', 'logentry', ['performer_id'], unique=False) + op.create_index('logentry_repository_id', 'logentry', ['repository_id'], unique=False) + op.create_table('repositorybuildtrigger', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', sa.String(length=255), nullable=False), + sa.Column('service_id', sa.Integer(), nullable=False), + sa.Column('repository_id', sa.Integer(), nullable=False), + sa.Column('connected_user_id', sa.Integer(), nullable=False), + sa.Column('auth_token', sa.String(length=255), nullable=False), + sa.Column('config', sa.Text(), nullable=False), + sa.Column('write_token_id', sa.Integer(), nullable=True), + sa.Column('pull_robot_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['connected_user_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['pull_robot_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ), + sa.ForeignKeyConstraint(['service_id'], ['buildtriggerservice.id'], ), + sa.ForeignKeyConstraint(['write_token_id'], ['accesstoken.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('repositorybuildtrigger_connected_user_id', 'repositorybuildtrigger', ['connected_user_id'], unique=False) + op.create_index('repositorybuildtrigger_pull_robot_id', 'repositorybuildtrigger', ['pull_robot_id'], unique=False) + op.create_index('repositorybuildtrigger_repository_id', 'repositorybuildtrigger', ['repository_id'], unique=False) + op.create_index('repositorybuildtrigger_service_id', 'repositorybuildtrigger', ['service_id'], unique=False) + op.create_index('repositorybuildtrigger_write_token_id', 'repositorybuildtrigger', ['write_token_id'], unique=False) + op.create_table('repositorybuild', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', sa.String(length=255), nullable=False), + sa.Column('repository_id', sa.Integer(), nullable=False), + sa.Column('access_token_id', sa.Integer(), nullable=False), + sa.Column('resource_key', sa.String(length=255), nullable=False), + sa.Column('job_config', sa.Text(), nullable=False), + sa.Column('phase', sa.String(length=255), nullable=False), + sa.Column('started', sa.DateTime(), nullable=False), + sa.Column('display_name', sa.String(length=255), nullable=False), + sa.Column('trigger_id', sa.Integer(), nullable=True), + sa.Column('pull_robot_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['access_token_id'], ['accesstoken.id'], ), + sa.ForeignKeyConstraint(['pull_robot_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], ), + sa.ForeignKeyConstraint(['trigger_id'], ['repositorybuildtrigger.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('repositorybuild_access_token_id', 'repositorybuild', ['access_token_id'], unique=False) + op.create_index('repositorybuild_pull_robot_id', 'repositorybuild', ['pull_robot_id'], unique=False) + op.create_index('repositorybuild_repository_id', 'repositorybuild', ['repository_id'], unique=False) + op.create_index('repositorybuild_resource_key', 'repositorybuild', ['resource_key'], unique=False) + op.create_index('repositorybuild_trigger_id', 'repositorybuild', ['trigger_id'], unique=False) + op.create_index('repositorybuild_uuid', 'repositorybuild', ['uuid'], unique=False) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_index('repositorybuild_uuid', table_name='repositorybuild') + op.drop_index('repositorybuild_trigger_id', table_name='repositorybuild') + op.drop_index('repositorybuild_resource_key', table_name='repositorybuild') + op.drop_index('repositorybuild_repository_id', table_name='repositorybuild') + op.drop_index('repositorybuild_pull_robot_id', table_name='repositorybuild') + op.drop_index('repositorybuild_access_token_id', table_name='repositorybuild') + op.drop_table('repositorybuild') + op.drop_index('repositorybuildtrigger_write_token_id', table_name='repositorybuildtrigger') + op.drop_index('repositorybuildtrigger_service_id', table_name='repositorybuildtrigger') + op.drop_index('repositorybuildtrigger_repository_id', table_name='repositorybuildtrigger') + op.drop_index('repositorybuildtrigger_pull_robot_id', table_name='repositorybuildtrigger') + op.drop_index('repositorybuildtrigger_connected_user_id', table_name='repositorybuildtrigger') + op.drop_table('repositorybuildtrigger') + op.drop_index('logentry_repository_id', table_name='logentry') + op.drop_index('logentry_performer_id', table_name='logentry') + op.drop_index('logentry_kind_id', table_name='logentry') + op.drop_index('logentry_datetime', table_name='logentry') + op.drop_index('logentry_account_id', table_name='logentry') + op.drop_index('logentry_access_token_id', table_name='logentry') + op.drop_table('logentry') + op.drop_index('repositorytag_repository_id_name', table_name='repositorytag') + op.drop_index('repositorytag_repository_id', table_name='repositorytag') + op.drop_index('repositorytag_image_id', table_name='repositorytag') + op.drop_table('repositorytag') + op.drop_index('permissionprototype_role_id', table_name='permissionprototype') + op.drop_index('permissionprototype_org_id_activating_user_id', table_name='permissionprototype') + op.drop_index('permissionprototype_org_id', table_name='permissionprototype') + op.drop_index('permissionprototype_delegate_user_id', table_name='permissionprototype') + op.drop_index('permissionprototype_delegate_team_id', table_name='permissionprototype') + op.drop_index('permissionprototype_activating_user_id', table_name='permissionprototype') + op.drop_table('permissionprototype') + op.drop_index('image_storage_id', table_name='image') + op.drop_index('image_repository_id_docker_image_id', table_name='image') + op.drop_index('image_repository_id', table_name='image') + op.drop_index('image_ancestors', table_name='image') + op.drop_table('image') + op.drop_index('oauthauthorizationcode_code', table_name='oauthauthorizationcode') + op.drop_index('oauthauthorizationcode_application_id', table_name='oauthauthorizationcode') + op.drop_table('oauthauthorizationcode') + op.drop_index('webhook_repository_id', table_name='webhook') + op.drop_index('webhook_public_id', table_name='webhook') + op.drop_table('webhook') + op.drop_index('teammember_user_id_team_id', table_name='teammember') + op.drop_index('teammember_user_id', table_name='teammember') + op.drop_index('teammember_team_id', table_name='teammember') + op.drop_table('teammember') + op.drop_index('oauthaccesstoken_uuid', table_name='oauthaccesstoken') + op.drop_index('oauthaccesstoken_refresh_token', table_name='oauthaccesstoken') + op.drop_index('oauthaccesstoken_authorized_user_id', table_name='oauthaccesstoken') + op.drop_index('oauthaccesstoken_application_id', table_name='oauthaccesstoken') + op.drop_index('oauthaccesstoken_access_token', table_name='oauthaccesstoken') + op.drop_table('oauthaccesstoken') + op.drop_index('repositorypermission_user_id_repository_id', table_name='repositorypermission') + op.drop_index('repositorypermission_user_id', table_name='repositorypermission') + op.drop_index('repositorypermission_team_id_repository_id', table_name='repositorypermission') + op.drop_index('repositorypermission_team_id', table_name='repositorypermission') + op.drop_index('repositorypermission_role_id', table_name='repositorypermission') + op.drop_index('repositorypermission_repository_id', table_name='repositorypermission') + op.drop_table('repositorypermission') + op.drop_index('accesstoken_role_id', table_name='accesstoken') + op.drop_index('accesstoken_repository_id', table_name='accesstoken') + op.drop_index('accesstoken_code', table_name='accesstoken') + op.drop_table('accesstoken') + op.drop_index('repository_visibility_id', table_name='repository') + op.drop_index('repository_namespace_name', table_name='repository') + op.drop_table('repository') + op.drop_index('team_role_id', table_name='team') + op.drop_index('team_organization_id', table_name='team') + op.drop_index('team_name_organization_id', table_name='team') + op.drop_index('team_name', table_name='team') + op.drop_table('team') + op.drop_index('emailconfirmation_user_id', table_name='emailconfirmation') + op.drop_index('emailconfirmation_code', table_name='emailconfirmation') + op.drop_table('emailconfirmation') + op.drop_index('notification_uuid', table_name='notification') + op.drop_index('notification_target_id', table_name='notification') + op.drop_index('notification_kind_id', table_name='notification') + op.drop_index('notification_created', table_name='notification') + op.drop_table('notification') + op.drop_index('oauthapplication_organization_id', table_name='oauthapplication') + op.drop_index('oauthapplication_client_id', table_name='oauthapplication') + op.drop_table('oauthapplication') + op.drop_index('federatedlogin_user_id', table_name='federatedlogin') + op.drop_index('federatedlogin_service_id_user_id', table_name='federatedlogin') + op.drop_index('federatedlogin_service_id_service_ident', table_name='federatedlogin') + op.drop_index('federatedlogin_service_id', table_name='federatedlogin') + op.drop_table('federatedlogin') + op.drop_index('buildtriggerservice_name', table_name='buildtriggerservice') + op.drop_table('buildtriggerservice') + op.drop_index('user_username', table_name='user') + op.drop_index('user_stripe_id', table_name='user') + op.drop_index('user_robot', table_name='user') + op.drop_index('user_organization', table_name='user') + op.drop_index('user_email', table_name='user') + op.drop_table('user') + op.drop_index('visibility_name', table_name='visibility') + op.drop_table('visibility') + op.drop_index('teamrole_name', table_name='teamrole') + op.drop_table('teamrole') + op.drop_index('notificationkind_name', table_name='notificationkind') + op.drop_table('notificationkind') + op.drop_index('logentrykind_name', table_name='logentrykind') + op.drop_table('logentrykind') + op.drop_index('role_name', table_name='role') + op.drop_table('role') + op.drop_index('queueitem_queue_name', table_name='queueitem') + op.drop_index('queueitem_processing_expires', table_name='queueitem') + op.drop_index('queueitem_available_after', table_name='queueitem') + op.drop_index('queueitem_available', table_name='queueitem') + op.drop_table('queueitem') + op.drop_table('imagestorage') + op.drop_index('loginservice_name', table_name='loginservice') + op.drop_table('loginservice') + ### end Alembic commands ### diff --git a/data/model/legacy.py b/data/model/legacy.py index 1c207cb79..23b1bde77 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -8,11 +8,17 @@ from data.database import * from util.validation import * from util.names import format_robot_username -from app import storage as store - logger = logging.getLogger(__name__) -transaction_factory = app.config['DB_TRANSACTION_FACTORY'] + + +class Config(object): + def __init__(self): + self.app_config = None + self.store = None + +config = Config() + class DataModelException(Exception): pass @@ -58,7 +64,7 @@ class InvalidBuildTriggerException(DataModelException): pass -def create_user(username, password, email, is_organization=False): +def create_user(username, password, email, add_change_pw_notification=True): if not validate_email(email): raise InvalidEmailAddressException('Invalid email address: %s' % email) @@ -97,7 +103,7 @@ def create_user(username, password, email, is_organization=False): # If the password is None, then add a notification for the user to change # their password ASAP. - if not pw_hash and not is_organization: + if not pw_hash and add_change_pw_notification: create_notification('password_required', new_user) return new_user @@ -105,10 +111,18 @@ def create_user(username, password, email, is_organization=False): raise DataModelException(ex.message) +def is_username_unique(test_username): + try: + User.get((User.username == test_username)) + return False + except User.DoesNotExist: + return True + + def create_organization(name, email, creating_user): try: # Create the org - new_org = create_user(name, None, email, is_organization=True) + new_org = create_user(name, None, email, add_change_pw_notification=False) new_org.organization = True new_org.save() @@ -340,18 +354,16 @@ def attach_federated_login(user, service_name, service_id): def verify_federated_login(service_name, service_id): - selected = FederatedLogin.select(FederatedLogin, User) - with_service = selected.join(LoginService) - with_user = with_service.switch(FederatedLogin).join(User) - found = with_user.where(FederatedLogin.service_ident == service_id, - LoginService.name == service_name) - - found_list = list(found) - - if found_list: - return found_list[0].user - - return None + try: + found = (FederatedLogin + .select(FederatedLogin, User) + .join(LoginService) + .switch(FederatedLogin).join(User) + .where(FederatedLogin.service_ident == service_id, LoginService.name == service_name) + .get()) + return found.user + except FederatedLogin.DoesNotExist: + return None def list_federated_logins(user): @@ -935,7 +947,7 @@ def __translate_ancestry(old_ancestry, translations, repository, username): def find_create_or_link_image(docker_image_id, repository, username, translations): - with transaction_factory(db): + with config.app_config['DB_TRANSACTION_FACTORY'](db): repo_image = get_repo_image(repository.namespace, repository.name, docker_image_id) if repo_image: @@ -1018,7 +1030,7 @@ def set_image_size(docker_image_id, namespace_name, repository_name, def set_image_metadata(docker_image_id, namespace_name, repository_name, created_date_str, comment, command, parent=None): - with transaction_factory(db): + with config.app_config['DB_TRANSACTION_FACTORY'](db): query = (Image .select(Image, ImageStorage) .join(Repository) @@ -1098,10 +1110,10 @@ def garbage_collect_repository(namespace_name, repository_name): image_to_remove.storage.uuid) uuids_to_check_for_gc.add(image_to_remove.storage.uuid) else: - image_path = store.image_path(namespace_name, repository_name, - image_to_remove.docker_image_id, None) + image_path = config.store.image_path(namespace_name, repository_name, + image_to_remove.docker_image_id, None) logger.debug('Deleting image storage: %s', image_path) - store.remove(image_path) + config.store.remove(image_path) image_to_remove.delete_instance() @@ -1116,10 +1128,9 @@ def garbage_collect_repository(namespace_name, repository_name): for storage in storage_to_remove: logger.debug('Garbage collecting image storage: %s', storage.uuid) storage.delete_instance() - image_path = store.image_path(namespace_name, repository_name, - image_to_remove.docker_image_id, - storage.uuid) - store.remove(image_path) + image_path = config.store.image_path(namespace_name, repository_name, + image_to_remove.docker_image_id, storage.uuid) + config.store.remove(image_path) return len(to_remove) @@ -1489,8 +1500,8 @@ def get_pull_credentials(robotname): return { 'username': robot.username, 'password': login_info.service_ident, - 'registry': '%s://%s/v1/' % (app.config['PREFERRED_URL_SCHEME'], - app.config['SERVER_HOSTNAME']), + 'registry': '%s://%s/v1/' % (config.app_config['PREFERRED_URL_SCHEME'], + config.app_config['SERVER_HOSTNAME']), } diff --git a/data/users.py b/data/users.py index 895c6872c..e5064b6fd 100644 --- a/data/users.py +++ b/data/users.py @@ -1,17 +1,16 @@ import ldap import logging +from util.validation import generate_valid_usernames +from data import model logger = logging.getLogger(__name__) class DatabaseUsers(object): - def __init__(self, app_db): - self._app_db = app_db - def verify_user(self, username_or_email, password): """ Simply delegate to the model implementation. """ - return self._app_db.verify_user(username_or_email, password) + return model.verify_user(username_or_email, password) class LDAPConnection(object): @@ -31,10 +30,10 @@ class LDAPConnection(object): class LDAPUsers(object): - def __init__(self, app_db, ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, - email_attr, passwd_attr): - self._app_db = app_db + def __init__(self, ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr, + passwd_attr): self._ldap_conn = LDAPConnection(ldap_uri, admin_dn, admin_passwd) + self._ldap_uri = ldap_uri self._base_dn = base_dn self._user_rdn = user_rdn self._uid_attr = uid_attr @@ -45,36 +44,47 @@ class LDAPUsers(object): """ Verify the credentials with LDAP and if they are valid, create or update the user in our database. """ + # Make sure that even if the server supports anonymous binds, we don't allow it + if not password: + return None + with self._ldap_conn as conn: user_search_dn = ','.join(self._user_rdn + self._base_dn) - query = '(|({0}={2})({1}={2}))'.format(self._uid_attr, self._email_attr, - username_or_email) - user = conn.search_s(user_search_dn, ldap.SCOPE_SUBTREE, query) + query = u'(|({0}={2})({1}={2}))'.format(self._uid_attr, self._email_attr, + username_or_email) + user = conn.search_s(user_search_dn, ldap.SCOPE_SUBTREE, query.encode('utf-8')) if len(user) != 1: return None found_dn, found_response = user[0] - # First validate the password - valid_passwd = conn.compare_s(found_dn, self._passwd_attr, password) == 1 - if not valid_passwd: + # First validate the password by binding as the user + try: + with LDAPConnection(self._ldap_uri, found_dn, password.encode('utf-8')): + pass + except ldap.INVALID_CREDENTIALS: return None - logger.debug('LDAP Response: %s', found_response) - - # Now check if we have the same username in our DB - username = found_response[self._uid_attr][0] + # Now check if we have a federated login for this user + username = unicode(found_response[self._uid_attr][0].decode('utf-8')) email = found_response[self._email_attr][0] - password = found_response[self._passwd_attr][0] - db_user = self._app_db.get_user(username) - - logger.debug('Email: %s', email) + db_user = model.verify_federated_login('ldap', username) if not db_user: # We must create the user in our db - db_user = self._app_db.create_user(username, 'password_from_ldap', email) + valid_username = None + for valid_username in generate_valid_usernames(username): + if model.is_username_unique(valid_username): + break + + if not valid_username: + logger.error('Unable to pick a username for user: %s', username) + return None + + db_user = model.create_user(valid_username, None, email, add_change_pw_notification=False) db_user.verified = True + model.attach_federated_login(db_user, 'ldap', username) else: # Update the db attributes from ldap db_user.email = email @@ -85,18 +95,18 @@ class LDAPUsers(object): class UserAuthentication(object): - def __init__(self, app=None, model=None): + def __init__(self, app=None): self.app = app if app is not None: - self.state = self.init_app(app, model) + self.state = self.init_app(app) else: self.state = None - def init_app(self, app, model): + def init_app(self, app): authentication_type = app.config.get('AUTHENTICATION_TYPE', 'Database') if authentication_type == 'Database': - users = DatabaseUsers(model) + users = DatabaseUsers() elif authentication_type == 'LDAP': ldap_uri = app.config.get('LDAP_URI', 'ldap://localhost') base_dn = app.config.get('LDAP_BASE_DN') @@ -107,8 +117,8 @@ class UserAuthentication(object): email_attr = app.config.get('LDAP_EMAIL_ATTR', 'mail') passwd_attr = app.config.get('LDAP_PASSWD_ATTR', 'userPassword') - users = LDAPUsers(model, ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, - email_attr, passwd_attr) + users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr, + passwd_attr) else: raise RuntimeError('Unknown authentication type: %s' % authentication_type) diff --git a/endpoints/index.py b/endpoints/index.py index 6ebec2d6c..f0d233414 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -9,7 +9,7 @@ from collections import OrderedDict from data import model from data.model import oauth from data.queue import webhook_queue -from app import analytics, app +from app import analytics, app, authentication from auth.auth import process_auth from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token from util.names import parse_repository_name @@ -97,7 +97,7 @@ def create_user(): existing_user = model.get_user(username) if existing_user: - verified = model.verify_user(username, password) + verified = authentication.verify_user(username, password) if verified: # Mark that the user was logged in. event = app.config['USER_EVENTS'].get_event(username) diff --git a/initdb.py b/initdb.py index 2570b7ca9..065e0f6a1 100644 --- a/initdb.py +++ b/initdb.py @@ -181,6 +181,7 @@ def initialize_database(): Visibility.create(name='private') LoginService.create(name='github') LoginService.create(name='quayrobot') + LoginService.create(name='ldap') BuildTriggerService.create(name='github') diff --git a/requirements-nover.txt b/requirements-nover.txt index efda6ebef..ee1329ae2 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -33,3 +33,4 @@ reportlab==2.7 blinker raven python-ldap +unidecode diff --git a/test/data/test.db b/test/data/test.db index 3ea2e8dfa3f00e22b1b077f0e2a6d5fc9d862abe..1e322ef232db1141f4f2fe5d50cea86223c5cf76 100644 GIT binary patch delta 5420 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 CRY`FG delta 5359 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 4 and len(username) <= 30) + length_match = (len(username) >= MIN_LENGTH and len(username) <= MAX_LENGTH) if not length_match: - return (False, 'Username must be between 4 and 30 characters in length') + return (False, 'Username must be between %s and %s characters in length' % + (MIN_LENGTH, MAX_LENGTH)) return (True, '') @@ -27,3 +37,24 @@ def validate_password(password): if re.search(r'\s', password): return False return len(password) > 7 + + +def _gen_filler_chars(num_filler_chars): + if num_filler_chars == 0: + yield '' + else: + for char in VALID_CHARACTERS: + for suffix in _gen_filler_chars(num_filler_chars - 1): + yield char + suffix + + +def generate_valid_usernames(input_username): + normalized = unidecode(input_username).strip().lower() + prefix = re.sub(INVALID_USERNAME_CHARACTERS, '_', normalized)[:30] + + num_filler_chars = max(0, MIN_LENGTH - len(prefix)) + + while num_filler_chars + len(prefix) <= MAX_LENGTH: + for suffix in _gen_filler_chars(num_filler_chars): + yield prefix + suffix + num_filler_chars += 1