diff --git a/Dockerfile.web b/Dockerfile.web index a202491b7..82b0f6b4e 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -47,6 +47,7 @@ ADD templates templates ADD util util ADD workers workers +ADD license.pyc license.pyc ADD app.py app.py ADD application.py application.py ADD config.py config.py diff --git a/app.py b/app.py index 92d3c516b..a8b701c66 100644 --- a/app.py +++ b/app.py @@ -19,9 +19,12 @@ from util.queuemetrics import QueueMetrics from data.billing import Billing from data.buildlogs import BuildLogs from data.queue import WorkQueue +from license import load_license +from datetime import datetime OVERRIDE_CONFIG_FILENAME = 'conf/stack/config.py' +LICENSE_FILENAME = 'conf/stack/license.enc' app = Flask(__name__) @@ -41,6 +44,12 @@ else: logger.debug('Applying config file: %s', OVERRIDE_CONFIG_FILENAME) app.config.from_pyfile(OVERRIDE_CONFIG_FILENAME) + logger.debug('Applying license config from: %s', LICENSE_FILENAME) + app.config.update(load_license(LICENSE_FILENAME)) + + if app.config.get('LICENSE_EXPIRATION', datetime.min) < datetime.utcnow(): + raise RuntimeError('License has expired, please contact support@quay.io') + features.import_features(app.config) Principal(app, use_sessions=False) diff --git a/data/model/legacy.py b/data/model/legacy.py index 80b5892de..76d0123be 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -64,7 +64,33 @@ class InvalidBuildTriggerException(DataModelException): pass -def create_user(username, password, email, add_change_pw_notification=True): +class TooManyUsersException(DataModelException): + pass + + +def is_create_user_allowed(): + return get_active_user_count() < config.app_config['LICENSE_USER_LIMIT'] + + +def create_user(username, password, email): + """ Creates a regular user, if allowed. """ + if not validate_password(password): + raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE) + + if not is_create_user_allowed(): + raise TooManyUsersException() + + created = _create_user(username, email) + + # Store the password hash + pw_hash = bcrypt.hashpw(password, bcrypt.gensalt()) + created.password_hash = pw_hash + + created.save() + + return created + +def _create_user(username, email): if not validate_email(email): raise InvalidEmailAddressException('Invalid email address: %s' % email) @@ -72,10 +98,6 @@ def create_user(username, password, email, add_change_pw_notification=True): if not username_valid: raise InvalidUsernameException('Invalid username %s: %s' % (username, username_issue)) - # We allow password none for the federated login case. - if password is not None and not validate_password(password): - raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE) - try: existing = User.get((User.username == username) | (User.email == email)) @@ -94,18 +116,7 @@ def create_user(username, password, email, add_change_pw_notification=True): pass try: - pw_hash = None - if password is not None: - pw_hash = bcrypt.hashpw(password, bcrypt.gensalt()) - - new_user = User.create(username=username, password_hash=pw_hash, - email=email) - - # If the password is None, then add a notification for the user to change - # their password ASAP. - if not pw_hash and add_change_pw_notification: - create_notification('password_required', new_user) - + new_user = User.create(username=username, email=email) return new_user except Exception as ex: raise DataModelException(ex.message) @@ -122,7 +133,7 @@ def is_username_unique(test_username): def create_organization(name, email, creating_user): try: # Create the org - new_org = create_user(name, None, email, add_change_pw_notification=False) + new_org = _create_user(name, email) new_org.organization = True new_org.save() @@ -335,8 +346,11 @@ def set_team_org_permission(team, team_role_name, set_by_username): return team -def create_federated_user(username, email, service_name, service_id): - new_user = create_user(username, None, email) +def create_federated_user(username, email, service_name, service_id, set_password_notification): + if not is_create_user_allowed(): + raise TooManyUsersException() + + new_user = _create_user(username, email) new_user.verified = True new_user.save() @@ -344,6 +358,9 @@ def create_federated_user(username, email, service_name, service_id): FederatedLogin.create(user=new_user, service=service, service_ident=service_id) + if set_password_notification: + create_notification('password_required', new_user) + return new_user diff --git a/data/users.py b/data/users.py index 65fbff0d1..4bde0518b 100644 --- a/data/users.py +++ b/data/users.py @@ -92,14 +92,12 @@ class LDAPUsers(object): 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) + db_user = model.create_federated_user(valid_username, email, 'ldap', username, + set_password_notification=False) else: # Update the db attributes from ldap db_user.email = email - - db_user.save() + db_user.save() return db_user diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py index 4085798b0..3292b349e 100644 --- a/endpoints/callbacks.py +++ b/endpoints/callbacks.py @@ -87,7 +87,7 @@ def github_oauth_callback(): # try to create the user try: to_login = model.create_federated_user(username, found_email, 'github', - github_id) + github_id, set_password_notification=True) # Success, tell analytics analytics.track(to_login.username, 'register', {'service': 'github'}) diff --git a/license.py b/license.py new file mode 100644 index 000000000..b45d90cf8 --- /dev/null +++ b/license.py @@ -0,0 +1,13 @@ +import pickle + +from Crypto.PublicKey import RSA + +n = 24311791124264168943780535074639421876317270880681911499019414944027362498498429776192966738844514582251884695124256895677070273097239290537016363098432785034818859765271229653729724078304186025013011992335454557504431888746007324285000011384941749613875855493086506022340155196030616409545906383713728780211095701026770053812741971198465120292345817928060114890913931047021503727972067476586739126160044293621653486418983183727572502888923949587290840425930251185737996066354726953382305020440374552871209809125535533731995494145421279907938079885061852265339259634996180877443852561265066616143910755505151318370667L +e = 65537L + +def load_license(license_path): + decryptor = RSA.construct((n, e)) + with open(license_path, 'rb') as encrypted_license: + decrypted_data = decryptor.encrypt(encrypted_license.read(), 0) + + return pickle.loads(decrypted_data[0]) diff --git a/license.pyc b/license.pyc new file mode 100644 index 000000000..df6085268 Binary files /dev/null and b/license.pyc differ diff --git a/requirements-nover.txt b/requirements-nover.txt index ee1329ae2..d79bd4394 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -34,3 +34,4 @@ blinker raven python-ldap unidecode +pycrypto diff --git a/tools/createlicense.py b/tools/createlicense.py new file mode 100644 index 000000000..53700d4f4 --- /dev/null +++ b/tools/createlicense.py @@ -0,0 +1,38 @@ +import argparse +import pickle + +from Crypto.PublicKey import RSA +from datetime import datetime, timedelta + +def encrypt(message, output_filename): + private_key_file = 'conf/stack/license_key' + with open(private_key_file, 'r') as private_key: + encryptor = RSA.importKey(private_key) + + encrypted_data = encryptor.decrypt(message) + + with open(output_filename, 'wb') as encrypted_file: + encrypted_file.write(encrypted_data) + +parser = argparse.ArgumentParser(description='Create a license file.') +parser.add_argument('--users', type=int, default=20, + help='Number of users allowed by the license') +parser.add_argument('--days', type=int, default=30, + help='Number of days for which the license is valid') +parser.add_argument('--warn', type=int, default=7, + help='Number of days prior to expiration to warn users') +parser.add_argument('--output', type=str, required=True, + help='File in which to store the license') + +if __name__ == "__main__": + args = parser.parse_args() + print ('Creating license for %s users for %s days in file: %s' % + (args.users, args.days, args.output)) + + license_data = { + 'LICENSE_EXPIRATION': datetime.utcnow() + timedelta(days=args.days), + 'LICENSE_USER_LIMIT': args.users, + 'LICENSE_EXPIRATION_WARNING': datetime.utcnow() + timedelta(days=(args.days - args.warn)), + } + + encrypt(pickle.dumps(license_data, 2), args.output)