Merge pull request #31 from coreos-inc/nolurk

Add a feature flag for disabling unauthenticated access to the regist…
This commit is contained in:
Jake Moshenko 2015-06-02 16:03:49 -04:00
commit 7d1e5a0c6f
17 changed files with 315 additions and 38 deletions

View file

@ -136,6 +136,9 @@ class DefaultConfig(object):
# Feature Flag: Whether super users are supported. # Feature Flag: Whether super users are supported.
FEATURE_SUPER_USERS = True FEATURE_SUPER_USERS = True
# Feature Flag: Whether to allow anonymous users to browse and pull public repositories.
FEATURE_ANONYMOUS_ACCESS = True
# Feature Flag: Whether billing is required. # Feature Flag: Whether billing is required.
FEATURE_BILLING = False FEATURE_BILLING = False

View file

@ -19,6 +19,7 @@ from auth import scopes
from auth.auth_context import get_authenticated_user, get_validated_oauth_token from auth.auth_context import get_authenticated_user, get_validated_oauth_token
from auth.auth import process_oauth from auth.auth import process_oauth
from endpoints.csrf import csrf_protect from endpoints.csrf import csrf_protect
from endpoints.decorators import check_anon_protection
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -228,12 +229,14 @@ def parse_repository_name(func):
class ApiResource(Resource): class ApiResource(Resource):
method_decorators = [check_anon_protection]
def options(self): def options(self):
return None, 200 return None, 200
class RepositoryParamResource(ApiResource): class RepositoryParamResource(ApiResource):
method_decorators = [parse_repository_name] method_decorators = [check_anon_protection, parse_repository_name]
def require_repo_permission(permission_class, scope, allow_public=False): def require_repo_permission(permission_class, scope, allow_public=False):

View file

@ -15,6 +15,7 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques
RepositoryParamResource) RepositoryParamResource)
from endpoints.api.subscribe import subscribe from endpoints.api.subscribe import subscribe
from endpoints.common import common_login from endpoints.common import common_login
from endpoints.decorators import anon_allowed
from endpoints.api.team import try_accept_invite from endpoints.api.team import try_accept_invite
from data import model from data import model
@ -203,6 +204,7 @@ class User(ApiResource):
@require_scope(scopes.READ_USER) @require_scope(scopes.READ_USER)
@nickname('getLoggedInUser') @nickname('getLoggedInUser')
@define_json_response('UserView') @define_json_response('UserView')
@anon_allowed
def get(self): def get(self):
""" Get user information for the authenticated user. """ """ Get user information for the authenticated user. """
user = get_authenticated_user() user = get_authenticated_user()
@ -497,6 +499,7 @@ class Signin(ApiResource):
@nickname('signinUser') @nickname('signinUser')
@validate_json_request('SigninUser') @validate_json_request('SigninUser')
@anon_allowed
def post(self): def post(self):
""" Sign in the user with the specified credentials. """ """ Sign in the user with the specified credentials. """
signin_data = request.get_json() signin_data = request.get_json()

36
endpoints/decorators.py Normal file
View file

@ -0,0 +1,36 @@
""" Various decorators for endpoint and API handlers. """
import features
from flask import abort
from auth.auth_context import (get_validated_oauth_token, get_authenticated_user,
get_validated_token, get_grant_user_context)
from functools import wraps
def anon_allowed(func):
""" Marks a method to allow anonymous access where it would otherwise be disallowed. """
func.__anon_allowed = True
return func
def anon_protect(func):
""" Marks a method as requiring some form of valid user auth before it can be executed. """
func.__anon_protected = True
return check_anon_protection(func)
def check_anon_protection(func):
""" Validates a method as requiring some form of valid user auth before it can be executed. """
@wraps(func)
def wrapper(*args, **kwargs):
# Skip if anonymous access is allowed.
if features.ANONYMOUS_ACCESS or '__anon_allowed' in dir(func):
return func(*args, **kwargs)
# Check for validated context. If none exists, fail with a 401.
if (get_authenticated_user() or get_validated_oauth_token() or get_validated_token() or
get_grant_user_context()):
return func(*args, **kwargs)
abort(401)
return wrapper

View file

@ -20,6 +20,7 @@ from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
from util.http import abort from util.http import abort
from endpoints.trackhelper import track_and_log from endpoints.trackhelper import track_and_log
from endpoints.notificationhelper import spawn_notification from endpoints.notificationhelper import spawn_notification
from endpoints.decorators import anon_protect, anon_allowed
import features import features
@ -42,6 +43,7 @@ def generate_headers(scope=GrantType.READ_REPOSITORY):
# Setting session namespace and repository # Setting session namespace and repository
session['namespace'] = namespace session['namespace'] = namespace
session['repository'] = repository session['repository'] = repository
# We run our index and registry on the same hosts for now # We run our index and registry on the same hosts for now
registry_server = urlparse.urlparse(request.url).netloc registry_server = urlparse.urlparse(request.url).netloc
response.headers['X-Docker-Endpoints'] = registry_server response.headers['X-Docker-Endpoints'] = registry_server
@ -49,24 +51,22 @@ def generate_headers(scope=GrantType.READ_REPOSITORY):
has_token_request = request.headers.get('X-Docker-Token', '') has_token_request = request.headers.get('X-Docker-Token', '')
if has_token_request: if has_token_request:
permission = AlwaysFailPermission()
grants = [] grants = []
if scope == GrantType.READ_REPOSITORY:
permission = ReadRepositoryPermission(namespace, repository)
grants.append(repository_read_grant(namespace, repository))
elif scope == GrantType.WRITE_REPOSITORY:
permission = ModifyRepositoryPermission(namespace, repository)
grants.append(repository_write_grant(namespace, repository))
if permission.can(): if scope == GrantType.READ_REPOSITORY:
# Generate a signed grant which expires here if ReadRepositoryPermission(namespace, repository).can():
grants.append(repository_read_grant(namespace, repository))
elif scope == GrantType.WRITE_REPOSITORY:
if ModifyRepositoryPermission(namespace, repository).can():
grants.append(repository_write_grant(namespace, repository))
# Generate a signed token for the user (if any) and the grants (if any)
if grants or get_authenticated_user():
user_context = get_authenticated_user() and get_authenticated_user().username user_context = get_authenticated_user() and get_authenticated_user().username
signature = generate_signed_token(grants, user_context) signature = generate_signed_token(grants, user_context)
response.headers['WWW-Authenticate'] = signature response.headers['WWW-Authenticate'] = signature
response.headers['X-Docker-Token'] = signature response.headers['X-Docker-Token'] = signature
else:
logger.warning('Registry request with invalid credentials on repository: %s/%s',
namespace, repository)
return response return response
return wrapper return wrapper
return decorator_method return decorator_method
@ -74,6 +74,7 @@ def generate_headers(scope=GrantType.READ_REPOSITORY):
@index.route('/users', methods=['POST']) @index.route('/users', methods=['POST'])
@index.route('/users/', methods=['POST']) @index.route('/users/', methods=['POST'])
@anon_allowed
def create_user(): def create_user():
user_data = request.get_json() user_data = request.get_json()
if not user_data or not 'username' in user_data: if not user_data or not 'username' in user_data:
@ -146,6 +147,7 @@ def create_user():
@index.route('/users', methods=['GET']) @index.route('/users', methods=['GET'])
@index.route('/users/', methods=['GET']) @index.route('/users/', methods=['GET'])
@process_auth @process_auth
@anon_allowed
def get_user(): def get_user():
if get_validated_oauth_token(): if get_validated_oauth_token():
return jsonify({ return jsonify({
@ -167,6 +169,7 @@ def get_user():
@index.route('/users/<username>/', methods=['PUT']) @index.route('/users/<username>/', methods=['PUT'])
@process_auth @process_auth
@anon_allowed
def update_user(username): def update_user(username):
permission = UserAdminPermission(username) permission = UserAdminPermission(username)
@ -194,6 +197,7 @@ def update_user(username):
@process_auth @process_auth
@parse_repository_name @parse_repository_name
@generate_headers(scope=GrantType.WRITE_REPOSITORY) @generate_headers(scope=GrantType.WRITE_REPOSITORY)
@anon_allowed
def create_repository(namespace, repository): def create_repository(namespace, repository):
logger.debug('Parsing image descriptions') logger.debug('Parsing image descriptions')
image_descriptions = json.loads(request.data.decode('utf8')) image_descriptions = json.loads(request.data.decode('utf8'))
@ -246,6 +250,7 @@ def create_repository(namespace, repository):
@process_auth @process_auth
@parse_repository_name @parse_repository_name
@generate_headers(scope=GrantType.WRITE_REPOSITORY) @generate_headers(scope=GrantType.WRITE_REPOSITORY)
@anon_allowed
def update_images(namespace, repository): def update_images(namespace, repository):
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace, repository)
@ -278,6 +283,7 @@ def update_images(namespace, repository):
@process_auth @process_auth
@parse_repository_name @parse_repository_name
@generate_headers(scope=GrantType.READ_REPOSITORY) @generate_headers(scope=GrantType.READ_REPOSITORY)
@anon_protect
def get_repository_images(namespace, repository): def get_repository_images(namespace, repository):
permission = ReadRepositoryPermission(namespace, repository) permission = ReadRepositoryPermission(namespace, repository)
@ -303,18 +309,21 @@ def get_repository_images(namespace, repository):
@process_auth @process_auth
@parse_repository_name @parse_repository_name
@generate_headers(scope=GrantType.WRITE_REPOSITORY) @generate_headers(scope=GrantType.WRITE_REPOSITORY)
@anon_allowed
def delete_repository_images(namespace, repository): def delete_repository_images(namespace, repository):
abort(501, 'Not Implemented', issue='not-implemented') abort(501, 'Not Implemented', issue='not-implemented')
@index.route('/repositories/<path:repository>/auth', methods=['PUT']) @index.route('/repositories/<path:repository>/auth', methods=['PUT'])
@parse_repository_name @parse_repository_name
@anon_allowed
def put_repository_auth(namespace, repository): def put_repository_auth(namespace, repository):
abort(501, 'Not Implemented', issue='not-implemented') abort(501, 'Not Implemented', issue='not-implemented')
@index.route('/search', methods=['GET']) @index.route('/search', methods=['GET'])
@process_auth @process_auth
@anon_protect
def get_search(): def get_search():
def result_view(repo): def result_view(repo):
return { return {
@ -351,11 +360,13 @@ def get_search():
# Note: This is *not* part of the Docker index spec. This is here for our own health check, # Note: This is *not* part of the Docker index spec. This is here for our own health check,
# since we have nginx handle the _ping below. # since we have nginx handle the _ping below.
@index.route('/_internal_ping') @index.route('/_internal_ping')
@anon_allowed
def internal_ping(): def internal_ping():
return make_response('true', 200) return make_response('true', 200)
@index.route('/_ping') @index.route('/_ping')
@index.route('/_ping') @index.route('/_ping')
@anon_allowed
def ping(): def ping():
# NOTE: any changes made here must also be reflected in the nginx config # NOTE: any changes made here must also be reflected in the nginx config
response = make_response('true', 200) response = make_response('true', 200)

View file

@ -16,6 +16,7 @@ from auth.permissions import (ReadRepositoryPermission,
ModifyRepositoryPermission) ModifyRepositoryPermission)
from data import model, database from data import model, database
from util import gzipstream from util import gzipstream
from endpoints.decorators import anon_protect
registry = Blueprint('registry', __name__) registry = Blueprint('registry', __name__)
@ -97,6 +98,7 @@ def set_cache_headers(f):
@extract_namespace_repo_from_session @extract_namespace_repo_from_session
@require_completion @require_completion
@set_cache_headers @set_cache_headers
@anon_protect
def head_image_layer(namespace, repository, image_id, headers): def head_image_layer(namespace, repository, image_id, headers):
permission = ReadRepositoryPermission(namespace, repository) permission = ReadRepositoryPermission(namespace, repository)
@ -130,6 +132,7 @@ def head_image_layer(namespace, repository, image_id, headers):
@extract_namespace_repo_from_session @extract_namespace_repo_from_session
@require_completion @require_completion
@set_cache_headers @set_cache_headers
@anon_protect
def get_image_layer(namespace, repository, image_id, headers): def get_image_layer(namespace, repository, image_id, headers):
permission = ReadRepositoryPermission(namespace, repository) permission = ReadRepositoryPermission(namespace, repository)
@ -171,6 +174,7 @@ def get_image_layer(namespace, repository, image_id, headers):
@registry.route('/images/<image_id>/layer', methods=['PUT']) @registry.route('/images/<image_id>/layer', methods=['PUT'])
@process_auth @process_auth
@extract_namespace_repo_from_session @extract_namespace_repo_from_session
@anon_protect
def put_image_layer(namespace, repository, image_id): def put_image_layer(namespace, repository, image_id):
logger.debug('Checking repo permissions') logger.debug('Checking repo permissions')
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace, repository)
@ -276,6 +280,7 @@ def put_image_layer(namespace, repository, image_id):
@registry.route('/images/<image_id>/checksum', methods=['PUT']) @registry.route('/images/<image_id>/checksum', methods=['PUT'])
@process_auth @process_auth
@extract_namespace_repo_from_session @extract_namespace_repo_from_session
@anon_protect
def put_image_checksum(namespace, repository, image_id): def put_image_checksum(namespace, repository, image_id):
logger.debug('Checking repo permissions') logger.debug('Checking repo permissions')
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace, repository)
@ -352,6 +357,7 @@ def put_image_checksum(namespace, repository, image_id):
@extract_namespace_repo_from_session @extract_namespace_repo_from_session
@require_completion @require_completion
@set_cache_headers @set_cache_headers
@anon_protect
def get_image_json(namespace, repository, image_id, headers): def get_image_json(namespace, repository, image_id, headers):
logger.debug('Checking repo permissions') logger.debug('Checking repo permissions')
permission = ReadRepositoryPermission(namespace, repository) permission = ReadRepositoryPermission(namespace, repository)
@ -383,6 +389,7 @@ def get_image_json(namespace, repository, image_id, headers):
@extract_namespace_repo_from_session @extract_namespace_repo_from_session
@require_completion @require_completion
@set_cache_headers @set_cache_headers
@anon_protect
def get_image_ancestry(namespace, repository, image_id, headers): def get_image_ancestry(namespace, repository, image_id, headers):
logger.debug('Checking repo permissions') logger.debug('Checking repo permissions')
permission = ReadRepositoryPermission(namespace, repository) permission = ReadRepositoryPermission(namespace, repository)
@ -434,6 +441,7 @@ def store_checksum(image_storage, checksum):
@registry.route('/images/<image_id>/json', methods=['PUT']) @registry.route('/images/<image_id>/json', methods=['PUT'])
@process_auth @process_auth
@extract_namespace_repo_from_session @extract_namespace_repo_from_session
@anon_protect
def put_image_json(namespace, repository, image_id): def put_image_json(namespace, repository, image_id):
logger.debug('Checking repo permissions') logger.debug('Checking repo permissions')
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace, repository)

View file

@ -10,6 +10,7 @@ from auth.auth import process_auth
from auth.permissions import (ReadRepositoryPermission, from auth.permissions import (ReadRepositoryPermission,
ModifyRepositoryPermission) ModifyRepositoryPermission)
from data import model from data import model
from endpoints.decorators import anon_protect
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -20,6 +21,7 @@ tags = Blueprint('tags', __name__)
@tags.route('/repositories/<path:repository>/tags', @tags.route('/repositories/<path:repository>/tags',
methods=['GET']) methods=['GET'])
@process_auth @process_auth
@anon_protect
@parse_repository_name @parse_repository_name
def get_tags(namespace, repository): def get_tags(namespace, repository):
permission = ReadRepositoryPermission(namespace, repository) permission = ReadRepositoryPermission(namespace, repository)
@ -35,6 +37,7 @@ def get_tags(namespace, repository):
@tags.route('/repositories/<path:repository>/tags/<tag>', @tags.route('/repositories/<path:repository>/tags/<tag>',
methods=['GET']) methods=['GET'])
@process_auth @process_auth
@anon_protect
@parse_repository_name @parse_repository_name
def get_tag(namespace, repository, tag): def get_tag(namespace, repository, tag):
permission = ReadRepositoryPermission(namespace, repository) permission = ReadRepositoryPermission(namespace, repository)
@ -51,6 +54,7 @@ def get_tag(namespace, repository, tag):
@tags.route('/repositories/<path:repository>/tags/<tag>', @tags.route('/repositories/<path:repository>/tags/<tag>',
methods=['PUT']) methods=['PUT'])
@process_auth @process_auth
@anon_protect
@parse_repository_name @parse_repository_name
def put_tag(namespace, repository, tag): def put_tag(namespace, repository, tag):
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace, repository)
@ -73,6 +77,7 @@ def put_tag(namespace, repository, tag):
@tags.route('/repositories/<path:repository>/tags/<tag>', @tags.route('/repositories/<path:repository>/tags/<tag>',
methods=['DELETE']) methods=['DELETE'])
@process_auth @process_auth
@anon_protect
@parse_repository_name @parse_repository_name
def delete_tag(namespace, repository, tag): def delete_tag(namespace, repository, tag):
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace, repository)

View file

@ -10,6 +10,7 @@ from auth.permissions import ReadRepositoryPermission
from data import model from data import model
from data import database from data import database
from endpoints.trackhelper import track_and_log from endpoints.trackhelper import track_and_log
from endpoints.decorators import anon_protect
from storage import Storage from storage import Storage
from util.queuefile import QueueFile from util.queuefile import QueueFile
@ -256,6 +257,7 @@ def os_arch_checker(os, arch):
return checker return checker
@anon_protect
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/sig/<os>/<arch>/', methods=['GET']) @verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/sig/<os>/<arch>/', methods=['GET'])
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci.asc/<os>/<arch>/', methods=['GET']) @verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci.asc/<os>/<arch>/', methods=['GET'])
@process_auth @process_auth
@ -264,6 +266,7 @@ def get_aci_signature(server, namespace, repository, tag, os, arch):
os=os, arch=arch) os=os, arch=arch)
@anon_protect
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci/<os>/<arch>/', methods=['GET']) @verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci/<os>/<arch>/', methods=['GET'])
@process_auth @process_auth
def get_aci_image(server, namespace, repository, tag, os, arch): def get_aci_image(server, namespace, repository, tag, os, arch):
@ -271,6 +274,7 @@ def get_aci_image(server, namespace, repository, tag, os, arch):
sign=True, checker=os_arch_checker(os, arch), os=os, arch=arch) sign=True, checker=os_arch_checker(os, arch), os=os, arch=arch)
@anon_protect
@verbs.route('/squash/<namespace>/<repository>/<tag>', methods=['GET']) @verbs.route('/squash/<namespace>/<repository>/<tag>', methods=['GET'])
@process_auth @process_auth
def get_squashed_tag(namespace, repository, tag): def get_squashed_tag(namespace, repository, tag):

View file

@ -19,6 +19,7 @@ from util.invoice import renderInvoiceToPdf
from util.seo import render_snapshot from util.seo import render_snapshot
from util.cache import no_cache from util.cache import no_cache
from endpoints.common import common_login, render_page_template, route_show_if, param_required from endpoints.common import common_login, render_page_template, route_show_if, param_required
from endpoints.decorators import anon_protect
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
from endpoints.registry import set_cache_headers from endpoints.registry import set_cache_headers
from endpoints.trigger import (CustomBuildTrigger, BitbucketBuildTrigger, TriggerProviderException, from endpoints.trigger import (CustomBuildTrigger, BitbucketBuildTrigger, TriggerProviderException,
@ -80,6 +81,7 @@ def snapshot(path = ''):
@web.route('/aci-signing-key') @web.route('/aci-signing-key')
@no_cache @no_cache
@anon_protect
def aci_signing_key(): def aci_signing_key():
if not signer.name: if not signer.name:
abort(404) abort(404)
@ -363,6 +365,7 @@ def confirm_recovery():
@web.route('/repository/<path:repository>/status', methods=['GET']) @web.route('/repository/<path:repository>/status', methods=['GET'])
@parse_repository_name @parse_repository_name
@no_cache @no_cache
@anon_protect
def build_status_badge(namespace, repository): def build_status_badge(namespace, repository):
token = request.args.get('token', None) token = request.args.get('token', None)
is_public = model.repository_is_public(namespace, repository) is_public = model.repository_is_public(namespace, repository)
@ -591,6 +594,7 @@ def attach_custom_build_trigger(namespace, repository_name):
@no_cache @no_cache
@process_oauth @process_oauth
@parse_repository_name_and_tag @parse_repository_name_and_tag
@anon_protect
def redirect_to_repository(namespace, reponame, tag): def redirect_to_repository(namespace, reponame, tag):
permission = ReadRepositoryPermission(namespace, reponame) permission = ReadRepositoryPermission(namespace, reponame)
is_public = model.repository_is_public(namespace, reponame) is_public = model.repository_is_public(namespace, reponame)
@ -608,6 +612,7 @@ def redirect_to_repository(namespace, reponame, tag):
@web.route('/<namespace>') @web.route('/<namespace>')
@no_cache @no_cache
@process_oauth @process_oauth
@anon_protect
def redirect_to_namespace(namespace): def redirect_to_namespace(namespace):
user_or_org = model.get_user_or_org(namespace) user_or_org = model.get_user_or_org(namespace)
if not user_or_org: if not user_or_org:

View file

@ -33,6 +33,20 @@
</div> </div>
</td> </td>
</tr> </tr>
<tr>
<td class="non-input">Anonymous Access:</td>
<td colspan="2">
<div class="co-checkbox">
<input id="ftaa" type="checkbox" ng-model="config.FEATURE_ANONYMOUS_ACCESS">
<label for="ftaa">Enable Anonymous Access</label>
</div>
<div class="help-text">
If enabled, public repositories and search can be accessed by anyone that can
reach the registry, even if they are not authenticated. Disable to only allow
authenticated users to view and pull "public" resources.
</div>
</td>
</tr>
<tr> <tr>
<td class="non-input">User Creation:</td> <td class="non-input">User Creation:</td>
<td colspan="2"> <td colspan="2">

View file

@ -10,7 +10,8 @@
</a> </a>
<span class="user-tools visible-xs" style="float: right;"> <span class="user-tools visible-xs" style="float: right;">
<i class="fa fa-search fa-lg user-tool" ng-click="toggleSearch()" <i class="fa fa-search fa-lg user-tool" ng-click="toggleSearch()"
data-placement="bottom" data-title="Search" bs-tooltip></i> data-placement="bottom" data-title="Search" bs-tooltip
ng-if="searchingAllowed"></i>
</span> </span>
</div> </div>
@ -49,7 +50,8 @@
<li> <li>
<span class="navbar-left user-tools"> <span class="navbar-left user-tools">
<i class="fa fa-search fa-lg user-tool" ng-click="toggleSearch()" <i class="fa fa-search fa-lg user-tool" ng-click="toggleSearch()"
data-placement="bottom" data-title="Search - Keyboard Shortcut: /" bs-tooltip></i> data-placement="bottom" data-title="Search - Keyboard Shortcut: /" bs-tooltip
ng-if="searchingAllowed"></i>
</span> </span>
</li> </li>
<li> <li>

View file

@ -12,40 +12,55 @@ angular.module('quay').directive('headerBar', function () {
restrict: 'C', restrict: 'C',
scope: { scope: {
}, },
controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService, PlanService, ApiService, NotificationService, Config, CreateService) { controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService,
PlanService, ApiService, NotificationService, Config, CreateService, Features) {
$scope.isNewLayout = Config.isNewLayout(); $scope.isNewLayout = Config.isNewLayout();
if ($scope.isNewLayout) { var hotkeysAdded = false;
// Register hotkeys: var userUpdated = function(cUser) {
hotkeys.add({ $scope.searchingAllowed = Features.ANONYMOUS_ACCESS || !cUser.anonymous;
combo: '/',
description: 'Show search',
callback: function(e) {
e.preventDefault();
e.stopPropagation();
$scope.toggleSearch();
}
});
hotkeys.add({ if (hotkeysAdded) { return; }
combo: 'alt+c',
description: 'Create new repository', if ($scope.isNewLayout) {
callback: function(e) { hotkeysAdded = true;
e.preventDefault();
e.stopPropagation(); // Register hotkeys.
$location.url('/new'); if ($scope.searchingAllowed) {
hotkeys.add({
combo: '/',
description: 'Show search',
callback: function(e) {
e.preventDefault();
e.stopPropagation();
$scope.toggleSearch();
}
});
} }
});
} if (!cUser.anonymous) {
hotkeys.add({
combo: 'alt+c',
description: 'Create new repository',
callback: function(e) {
e.preventDefault();
e.stopPropagation();
$location.url('/new');
}
});
}
}
};
$scope.notificationService = NotificationService; $scope.notificationService = NotificationService;
$scope.searchingAllowed = false;
$scope.searchVisible = false; $scope.searchVisible = false;
$scope.currentSearchQuery = null; $scope.currentSearchQuery = null;
$scope.searchResultState = null; $scope.searchResultState = null;
$scope.showBuildDialogCounter = 0; $scope.showBuildDialogCounter = 0;
// Monitor any user changes and place the current user into the scope. // Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope); UserService.updateUserIn($scope, userUpdated);
$scope.currentPageContext = {}; $scope.currentPageContext = {};

View file

@ -1,6 +1,7 @@
import unittest import unittest
import requests import requests
from flask import request, jsonify
from flask.blueprints import Blueprint from flask.blueprints import Blueprint
from flask.ext.testing import LiveServerTestCase from flask.ext.testing import LiveServerTestCase
@ -14,6 +15,7 @@ from endpoints.csrf import generate_csrf_token
import endpoints.decorated import endpoints.decorated
import json import json
import features
import tarfile import tarfile
@ -30,15 +32,46 @@ except ValueError:
pass pass
# Add a test blueprint for generating CSRF tokens. # Add a test blueprint for generating CSRF tokens and setting feature flags.
testbp = Blueprint('testbp', __name__) testbp = Blueprint('testbp', __name__)
@testbp.route('/csrf', methods=['GET']) @testbp.route('/csrf', methods=['GET'])
def generate_csrf(): def generate_csrf():
return generate_csrf_token() return generate_csrf_token()
@testbp.route('/feature/<feature_name>', methods=['POST'])
def set_feature(feature_name):
import features
old_value = features._FEATURES[feature_name].value
features._FEATURES[feature_name].value = request.get_json()['value']
return jsonify({'old_value': old_value})
app.register_blueprint(testbp, url_prefix='/__test') app.register_blueprint(testbp, url_prefix='/__test')
class TestFeature(object):
""" Helper object which temporarily sets the value of a feature flag.
"""
def __init__(self, test_case, feature_flag, test_value):
self.test_case = test_case
self.feature_flag = feature_flag
self.test_value = test_value
self.old_value = None
def __enter__(self):
result = self.test_case.conduct('POST', '/__test/feature/' + self.feature_flag,
data=json.dumps(dict(value=self.test_value)),
headers={'Content-Type': 'application/json'})
result_data = json.loads(result.text)
self.old_value = result_data['old_value']
def __exit__(self, type, value, traceback):
self.test_case.conduct('POST', '/__test/feature/' + self.feature_flag,
data=json.dumps(dict(value=self.old_value)),
headers={'Content-Type': 'application/json'})
class RegistryTestCase(LiveServerTestCase): class RegistryTestCase(LiveServerTestCase):
maxDiff = None maxDiff = None
@ -243,5 +276,75 @@ class RegistryTests(RegistryTestCase):
self.do_pull('devtable', 'newrepo', 'devtable', 'password') self.do_pull('devtable', 'newrepo', 'devtable', 'password')
def test_public_no_anonymous_access_with_auth(self):
# Turn off anonymous access.
with TestFeature(self, 'ANONYMOUS_ACCESS', False):
# Add a new repository under the public user, so we have a real repository to pull.
images = [{
'id': 'onlyimagehere'
}]
self.do_push('public', 'newrepo', 'public', 'password', images)
self.clearSession()
# First try to pull the (currently private) repo as devtable, which should fail as it belongs
# to public.
self.do_pull('public', 'newrepo', 'devtable', 'password', expected_code=403)
# Make the repository public.
self.conduct_api_login('public', 'password')
self.change_repo_visibility('public', 'newrepo', 'public')
self.clearSession()
# Pull the repository as devtable, which should succeed because the repository is public.
self.do_pull('public', 'newrepo', 'devtable', 'password')
def test_private_no_anonymous_access(self):
# Turn off anonymous access.
with TestFeature(self, 'ANONYMOUS_ACCESS', False):
# Add a new repository under the public user, so we have a real repository to pull.
images = [{
'id': 'onlyimagehere'
}]
self.do_push('public', 'newrepo', 'public', 'password', images)
self.clearSession()
# First try to pull the (currently private) repo as devtable, which should fail as it belongs
# to public.
self.do_pull('public', 'newrepo', 'devtable', 'password', expected_code=403)
# Pull the repository as public, which should succeed because the repository is owned by public.
self.do_pull('public', 'newrepo', 'public', 'password')
def test_public_no_anonymous_access_no_auth(self):
# Turn off anonymous access.
with TestFeature(self, 'ANONYMOUS_ACCESS', False):
# Add a new repository under the public user, so we have a real repository to pull.
images = [{
'id': 'onlyimagehere'
}]
self.do_push('public', 'newrepo', 'public', 'password', images)
self.clearSession()
# First try to pull the (currently private) repo as anonymous, which should fail as it
# is private.
self.do_pull('public', 'newrepo', expected_code=401)
# Make the repository public.
self.conduct_api_login('public', 'password')
self.change_repo_visibility('public', 'newrepo', 'public')
self.clearSession()
# Try again to pull the (currently public) repo as anonymous, which should fail as
# anonymous access is disabled.
self.do_pull('public', 'newrepo', expected_code=401)
# Pull the repository as public, which should succeed because the repository is owned by public.
self.do_pull('public', 'newrepo', 'public', 'password')
# Pull the repository as devtable, which should succeed because the repository is public.
self.do_pull('public', 'newrepo', 'devtable', 'password')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

33
test/test_anon_checked.py Normal file
View file

@ -0,0 +1,33 @@
import unittest
from endpoints.tags import tags
from endpoints.registry import registry
from endpoints.index import index
from endpoints.verbs import verbs
class TestAnonymousAccessChecked(unittest.TestCase):
def verifyBlueprint(self, blueprint):
class Checker(object):
def __init__(self, test_case):
self.test_case = test_case
def add_url_rule(self, rule, endpoint, view_function, methods=None):
if (not '__anon_protected' in dir(view_function) and
not '__anon_allowed' in dir(view_function)):
error_message = ('Missing anonymous access protection decorator on function ' +
'%s under blueprint %s' % (endpoint, blueprint.name))
self.test_case.fail(error_message)
for deferred_function in blueprint.deferred_functions:
deferred_function(Checker(self))
def test_anonymous_access_checked(self):
self.verifyBlueprint(tags)
self.verifyBlueprint(registry)
self.verifyBlueprint(index)
self.verifyBlueprint(verbs)
if __name__ == '__main__':
unittest.main()

View file

@ -371,6 +371,24 @@ class TestOrganizationList(ApiTestCase):
self._run_test('POST', 400, 'devtable', {u'name': 'KSIS', u'email': 'DHVZ'}) self._run_test('POST', 400, 'devtable', {u'name': 'KSIS', u'email': 'DHVZ'})
class TestPublicRepository(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(Repository, repository="public/publicrepo")
def test_get_anonymous(self):
self._run_test('GET', 200, None, None)
def test_get_freshuser(self):
self._run_test('GET', 200, 'freshuser', None)
def test_get_reader(self):
self._run_test('GET', 200, 'reader', None)
def test_get_devtable(self):
self._run_test('GET', 200, 'devtable', None)
class TestRepositoryList(ApiTestCase): class TestRepositoryList(ApiTestCase):
def setUp(self): def setUp(self):
ApiTestCase.setUp(self) ApiTestCase.setUp(self)

View file

@ -1246,6 +1246,19 @@ class TestListRepos(ApiTestCase):
self.assertEquals(len(json['repositories']), 2) self.assertEquals(len(json['repositories']), 2)
class TestViewPublicRepository(ApiTestCase):
def test_normalview(self):
self.getJsonResponse(Repository, params=dict(repository='public/publicrepo'))
def test_anon_access_disabled(self):
import features
features.ANONYMOUS_ACCESS = False
try:
self.getResponse(Repository, params=dict(repository='public/publicrepo'), expected_code=401)
finally:
features.ANONYMOUS_ACCESS = True
class TestUpdateRepo(ApiTestCase): class TestUpdateRepo(ApiTestCase):
SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple' SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple'
def test_updatedescription(self): def test_updatedescription(self):

View file

@ -16,6 +16,7 @@ def add_enterprise_config_defaults(config_obj, current_secret_key, hostname):
# Default features that are on. # Default features that are on.
config_obj['FEATURE_USER_LOG_ACCESS'] = config_obj.get('FEATURE_USER_LOG_ACCESS', True) config_obj['FEATURE_USER_LOG_ACCESS'] = config_obj.get('FEATURE_USER_LOG_ACCESS', True)
config_obj['FEATURE_USER_CREATION'] = config_obj.get('FEATURE_USER_CREATION', True) config_obj['FEATURE_USER_CREATION'] = config_obj.get('FEATURE_USER_CREATION', True)
config_obj['FEATURE_ANONYMOUS_ACCESS'] = config_obj.get('FEATURE_ANONYMOUS_ACCESS', True)
# Default features that are off. # Default features that are off.
config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False) config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False)