diff --git a/conf/nginx-nossl.conf b/conf/nginx-nossl.conf index 73a9c7605..fbcce63c0 100644 --- a/conf/nginx-nossl.conf +++ b/conf/nginx-nossl.conf @@ -13,10 +13,5 @@ http { include server-base.conf; listen 80 default; - - location /static/ { - # checks for static file, if not found proxy to app - alias /static/; - } } } diff --git a/conf/nginx.conf b/conf/nginx.conf index 43c21b6ca..e208d30e0 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -23,10 +23,5 @@ http { ssl_protocols SSLv3 TLSv1; ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP; ssl_prefer_server_ciphers on; - - location /static/ { - # checks for static file, if not found proxy to app - alias /static/; - } } } diff --git a/conf/server-base.conf b/conf/server-base.conf index a13cf1424..4636afdde 100644 --- a/conf/server-base.conf +++ b/conf/server-base.conf @@ -24,4 +24,16 @@ location / { proxy_pass http://app_server; proxy_read_timeout 2000; proxy_temp_path /var/log/nginx/proxy_temp 1 2; +} + +location /static/ { + # checks for static file, if not found proxy to app + alias /static/; +} + +location /v1/_ping { + add_header Content-Type text/plain; + add_header X-Docker-Registry-Version 0.6.0; + add_header X-Docker-Registry-Standalone 0; + return 200 'okay'; } \ No newline at end of file diff --git a/config.py b/config.py index 4fe5b2cd5..6742d1a43 100644 --- a/config.py +++ b/config.py @@ -165,6 +165,9 @@ class DefaultConfig(object): # Feature Flag: Whether emails are enabled. FEATURE_MAILING = True + # Feature Flag: Whether users can be created (by non-super users). + FEATURE_USER_CREATION = True + DISTRIBUTED_STORAGE_CONFIG = { 'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}], 'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}], diff --git a/data/migrations/versions/2fb36d4be80d_translate_the_queue_names_to_reference_.py b/data/migrations/versions/2fb36d4be80d_translate_the_queue_names_to_reference_.py index ae4c5d274..53b43a4ec 100644 --- a/data/migrations/versions/2fb36d4be80d_translate_the_queue_names_to_reference_.py +++ b/data/migrations/versions/2fb36d4be80d_translate_the_queue_names_to_reference_.py @@ -1,14 +1,14 @@ """Translate the queue names to reference namespace by id, remove the namespace column. Revision ID: 2fb36d4be80d -Revises: 3f4fe1194671 +Revises: 9a1087b007d Create Date: 2014-09-30 17:31:33.308490 """ # revision identifiers, used by Alembic. revision = '2fb36d4be80d' -down_revision = '3f4fe1194671' +down_revision = '9a1087b007d' from alembic import op import sqlalchemy as sa diff --git a/data/migrations/versions/3f4fe1194671_backfill_the_namespace_user_fields.py b/data/migrations/versions/3f4fe1194671_backfill_the_namespace_user_fields.py index cf0b90199..6f40f4fc0 100644 --- a/data/migrations/versions/3f4fe1194671_backfill_the_namespace_user_fields.py +++ b/data/migrations/versions/3f4fe1194671_backfill_the_namespace_user_fields.py @@ -22,4 +22,5 @@ def upgrade(tables): def downgrade(tables): + op.drop_constraint('fk_repository_namespace_user_id_user', table_name='repository', type_='foreignkey') op.drop_index('repository_namespace_user_id_name', table_name='repository') diff --git a/data/migrations/versions/51d04d0e7e6f_email_invites_for_joining_a_team.py b/data/migrations/versions/51d04d0e7e6f_email_invites_for_joining_a_team.py index 2e4242d8a..c18335adb 100644 --- a/data/migrations/versions/51d04d0e7e6f_email_invites_for_joining_a_team.py +++ b/data/migrations/versions/51d04d0e7e6f_email_invites_for_joining_a_team.py @@ -74,8 +74,5 @@ def downgrade(tables): .where(tables.notificationkind.c.name == op.inline_literal('org_team_invite'))) ) - op.drop_index('teammemberinvite_user_id', table_name='teammemberinvite') - op.drop_index('teammemberinvite_team_id', table_name='teammemberinvite') - op.drop_index('teammemberinvite_inviter_id', table_name='teammemberinvite') op.drop_table('teammemberinvite') ### end Alembic commands ### diff --git a/data/migrations/versions/9a1087b007d_allow_the_namespace_column_to_be_.py b/data/migrations/versions/9a1087b007d_allow_the_namespace_column_to_be_.py new file mode 100644 index 000000000..9b63ae190 --- /dev/null +++ b/data/migrations/versions/9a1087b007d_allow_the_namespace_column_to_be_.py @@ -0,0 +1,28 @@ +"""Allow the namespace column to be nullable. + +Revision ID: 9a1087b007d +Revises: 3f4fe1194671 +Create Date: 2014-10-01 16:11:21.277226 + +""" + +# revision identifiers, used by Alembic. +revision = '9a1087b007d' +down_revision = '3f4fe1194671' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(tables): + op.drop_index('repository_namespace_name', table_name='repository') + op.alter_column('repository', 'namespace', nullable=True, existing_type=sa.String(length=255), + server_default=sa.text('NULL')) + + +def downgrade(tables): + conn = op.get_bind() + conn.execute('update repository set namespace = (select username from user where user.id = repository.namespace_user_id) where namespace is NULL') + + op.create_index('repository_namespace_name', 'repository', ['namespace', 'name'], unique=True) + op.alter_column('repository', 'namespace', nullable=False, existing_type=sa.String(length=255)) diff --git a/data/model/legacy.py b/data/model/legacy.py index effa2c8ef..ece14a4c2 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -1239,8 +1239,7 @@ def get_storage_by_uuid(storage_uuid): return found -def set_image_size(docker_image_id, namespace_name, repository_name, - image_size): +def set_image_size(docker_image_id, namespace_name, repository_name, image_size, uncompressed_size): try: image = (Image .select(Image, ImageStorage) @@ -1249,18 +1248,15 @@ def set_image_size(docker_image_id, namespace_name, repository_name, .switch(Image) .join(ImageStorage, JOIN_LEFT_OUTER) .where(Repository.name == repository_name, Namespace.username == namespace_name, - Image.docker_image_id == docker_image_id) + Image.docker_image_id == docker_image_id) .get()) except Image.DoesNotExist: raise DataModelException('No image with specified id and repository') - if image.storage and image.storage.id: - image.storage.image_size = image_size - image.storage.save() - else: - image.image_size = image_size - image.save() + image.storage.image_size = image_size + image.storage.uncompressed_size = uncompressed_size + image.storage.save() return image diff --git a/data/model/sqlalchemybridge.py b/data/model/sqlalchemybridge.py index 46809fb21..8b7d8b664 100644 --- a/data/model/sqlalchemybridge.py +++ b/data/model/sqlalchemybridge.py @@ -17,7 +17,12 @@ OPTION_TRANSLATIONS = { def gen_sqlalchemy_metadata(peewee_model_list): - metadata = MetaData() + metadata = MetaData(naming_convention={ + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + }) for model in peewee_model_list: meta = model._meta diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 2f5e2045e..1943051e0 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -27,8 +27,8 @@ api_bp = Blueprint('api', __name__) api = Api() api.init_app(api_bp) api.decorators = [csrf_protect, - process_oauth, - crossdomain(origin='*', headers=['Authorization', 'Content-Type'])] + crossdomain(origin='*', headers=['Authorization', 'Content-Type']), + process_oauth] class ApiException(Exception): @@ -90,6 +90,7 @@ def handle_api_error(error): if error.error_type is not None: response.headers['WWW-Authenticate'] = ('Bearer error="%s" error_description="%s"' % (error.error_type, error.error_description)) + return response @@ -191,6 +192,7 @@ def query_param(name, help_str, type=reqparse.text_type, default=None, 'default': default, 'choices': choices, 'required': required, + 'location': ('args') }) return func return add_param diff --git a/endpoints/api/user.py b/endpoints/api/user.py index b842bb466..cb0602010 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -120,6 +120,10 @@ class User(ApiResource): 'type': 'string', 'description': 'The user\'s email address', }, + 'invite_code': { + 'type': 'string', + 'description': 'The optional invite code' + } } }, 'UpdateUser': { @@ -207,16 +211,14 @@ class User(ApiResource): return user_view(user) + @show_if(features.USER_CREATION) @nickname('createNewUser') - @parse_args - @query_param('inviteCode', 'Invitation code given for creating the user.', type=str, - default='') @internal_only @validate_json_request('NewUser') - def post(self, args): + def post(self): """ Create a new user. """ user_data = request.get_json() - invite_code = args['inviteCode'] + invite_code = user_data.get('invite_code', '') existing_user = model.get_user(user_data['username']) if existing_user: diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py index 1cbd46192..637033ab6 100644 --- a/endpoints/callbacks.py +++ b/endpoints/callbacks.py @@ -26,7 +26,8 @@ def render_ologin_error(service_name, error_message='Could not load user data. The token may have expired.'): return render_page_template('ologinerror.html', service_name=service_name, error_message=error_message, - service_url=get_app_url()) + service_url=get_app_url(), + user_creation=features.USER_CREATION) def exchange_code_for_token(code, service_name='GITHUB', for_login=True, form_encode=False, redirect_suffix=''): @@ -85,7 +86,12 @@ def get_google_user(token): def conduct_oauth_login(service_name, user_id, username, email, metadata={}): to_login = model.verify_federated_login(service_name.lower(), user_id) if not to_login: - # try to create the user + # See if we can create a new user. + if not features.USER_CREATION: + error_message = 'User creation is disabled. Please contact your administrator' + return render_ologin_error(service_name, error_message) + + # Try to create the user try: valid = next(generate_valid_usernames(username)) to_login = model.create_federated_user(valid, email, service_name.lower(), @@ -147,7 +153,7 @@ def github_oauth_callback(): token = exchange_code_for_token(request.args.get('code'), service_name='GITHUB') user_data = get_github_user(token) - if not user_data: + if not user_data or not 'login' in user_data: return render_ologin_error('GitHub') username = user_data['login'] diff --git a/endpoints/index.py b/endpoints/index.py index 46c5b9771..eb52971cf 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -19,6 +19,7 @@ from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission, from util.http import abort from endpoints.notificationhelper import spawn_notification +import features logger = logging.getLogger(__name__) profile = logging.getLogger('application.profiler') @@ -65,6 +66,9 @@ def generate_headers(role='read'): @index.route('/users', methods=['POST']) @index.route('/users/', methods=['POST']) def create_user(): + if not features.USER_CREATION: + abort(400, 'User creation is disabled. Please speak to your administrator.') + user_data = request.get_json() if not 'username' in user_data: abort(400, 'Missing username') @@ -454,6 +458,7 @@ def get_search(): @index.route('/_ping') @index.route('/_ping') def ping(): + # NOTE: any changes made here must also be reflected in the nginx config response = make_response('true', 200) response.headers['X-Docker-Registry-Version'] = '0.6.0' response.headers['X-Docker-Registry-Standalone'] = '0' diff --git a/endpoints/registry.py b/endpoints/registry.py index 741601d0b..751c2a0ff 100644 --- a/endpoints/registry.py +++ b/endpoints/registry.py @@ -220,7 +220,7 @@ def put_image_layer(namespace, repository, image_id): image_size = tmp.tell() # Save the size of the image. - model.set_image_size(image_id, namespace, repository, image_size) + model.set_image_size(image_id, namespace, repository, image_size, uncompressed_size_info.size) tmp.seek(0) csums.append(checksums.compute_tarsum(tmp, json_data)) @@ -229,12 +229,6 @@ def put_image_layer(namespace, repository, image_id): logger.debug('put_image_layer: Error when computing tarsum ' '{0}'.format(e)) - # Write the uncompressed image size, if any. - if uncompressed_size_info['size'] > 0: - profile.debug('Storing uncompressed layer size: %s' % uncompressed_size_info['size']) - repo_image.storage.uncompressed_size = uncompressed_size_info['size'] - repo_image.storage.save() - if repo_image.storage.checksum is None: # We don't have a checksum stored yet, that's fine skipping the check. # Not removing the mark though, image is not downloadable yet. diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 4a10485ae..c7c47db79 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -377,7 +377,7 @@ class GithubBuildTrigger(BuildTrigger): gh_client = self._get_client(auth_token) source = config['build_source'] repo = gh_client.get_repo(source) - branches = [branch['name'] for branch in repo.get_branches()] + branches = [branch.name for branch in repo.get_branches()] if not repo.default_branch in branches: branches.insert(0, repo.default_branch) diff --git a/initdb.py b/initdb.py index 6be72f6ff..6fa8efe98 100644 --- a/initdb.py +++ b/initdb.py @@ -82,8 +82,9 @@ def __create_subtree(repo, structure, creator_username, parent): new_image = model.set_image_metadata(docker_image_id, repo.namespace_user.username, repo.name, str(creation_time), 'no comment', command, parent) - model.set_image_size(docker_image_id, repo.namespace_user.username, repo.name, - random.randrange(1, 1024 * 1024 * 1024)) + compressed_size = random.randrange(1, 1024 * 1024 * 1024) + model.set_image_size(docker_image_id, repo.namespace_user.username, repo.name, compressed_size, + int(compressed_size * 1.4)) # Populate the diff file diff_path = store.image_file_diffs_path(new_image.storage.uuid) diff --git a/static/directives/signup-form.html b/static/directives/signup-form.html index ba4efe287..9117e880a 100644 --- a/static/directives/signup-form.html +++ b/static/directives/signup-form.html @@ -1,4 +1,4 @@ -