diff --git a/config.py b/config.py index 6bb7d0126..ff713b290 100644 --- a/config.py +++ b/config.py @@ -136,6 +136,9 @@ class DefaultConfig(object): # Feature Flag: Whether super users are supported. 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_BILLING = False diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 0252851a1..9a235d6a0 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -19,6 +19,7 @@ from auth import scopes from auth.auth_context import get_authenticated_user, get_validated_oauth_token from auth.auth import process_oauth from endpoints.csrf import csrf_protect +from endpoints.decorators import check_anon_protection logger = logging.getLogger(__name__) @@ -228,12 +229,14 @@ def parse_repository_name(func): class ApiResource(Resource): + method_decorators = [check_anon_protection] + def options(self): return None, 200 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): diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 3e093888e..6a0567963 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -15,6 +15,7 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques RepositoryParamResource) from endpoints.api.subscribe import subscribe from endpoints.common import common_login +from endpoints.decorators import anon_allowed from endpoints.api.team import try_accept_invite from data import model @@ -203,6 +204,7 @@ class User(ApiResource): @require_scope(scopes.READ_USER) @nickname('getLoggedInUser') @define_json_response('UserView') + @anon_allowed def get(self): """ Get user information for the authenticated user. """ user = get_authenticated_user() @@ -497,6 +499,7 @@ class Signin(ApiResource): @nickname('signinUser') @validate_json_request('SigninUser') + @anon_allowed def post(self): """ Sign in the user with the specified credentials. """ signin_data = request.get_json() diff --git a/endpoints/decorators.py b/endpoints/decorators.py new file mode 100644 index 000000000..421edd041 --- /dev/null +++ b/endpoints/decorators.py @@ -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 diff --git a/endpoints/index.py b/endpoints/index.py index 27427f571..c89414724 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -20,6 +20,7 @@ from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission, from util.http import abort from endpoints.trackhelper import track_and_log from endpoints.notificationhelper import spawn_notification +from endpoints.decorators import anon_protect, anon_allowed import features @@ -42,6 +43,7 @@ def generate_headers(scope=GrantType.READ_REPOSITORY): # Setting session namespace and repository session['namespace'] = namespace session['repository'] = repository + # We run our index and registry on the same hosts for now registry_server = urlparse.urlparse(request.url).netloc 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', '') if has_token_request: - permission = AlwaysFailPermission() 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(): - # Generate a signed grant which expires here + if scope == GrantType.READ_REPOSITORY: + 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 signature = generate_signed_token(grants, user_context) response.headers['WWW-Authenticate'] = signature response.headers['X-Docker-Token'] = signature - else: - logger.warning('Registry request with invalid credentials on repository: %s/%s', - namespace, repository) + return response return wrapper return decorator_method @@ -74,6 +74,7 @@ def generate_headers(scope=GrantType.READ_REPOSITORY): @index.route('/users', methods=['POST']) @index.route('/users/', methods=['POST']) +@anon_allowed def create_user(): user_data = request.get_json() 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']) @process_auth +@anon_allowed def get_user(): if get_validated_oauth_token(): return jsonify({ @@ -167,6 +169,7 @@ def get_user(): @index.route('/users//', methods=['PUT']) @process_auth +@anon_allowed def update_user(username): permission = UserAdminPermission(username) @@ -194,6 +197,7 @@ def update_user(username): @process_auth @parse_repository_name @generate_headers(scope=GrantType.WRITE_REPOSITORY) +@anon_allowed def create_repository(namespace, repository): logger.debug('Parsing image descriptions') image_descriptions = json.loads(request.data.decode('utf8')) @@ -246,6 +250,7 @@ def create_repository(namespace, repository): @process_auth @parse_repository_name @generate_headers(scope=GrantType.WRITE_REPOSITORY) +@anon_allowed def update_images(namespace, repository): permission = ModifyRepositoryPermission(namespace, repository) @@ -278,6 +283,7 @@ def update_images(namespace, repository): @process_auth @parse_repository_name @generate_headers(scope=GrantType.READ_REPOSITORY) +@anon_protect def get_repository_images(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) @@ -303,18 +309,21 @@ def get_repository_images(namespace, repository): @process_auth @parse_repository_name @generate_headers(scope=GrantType.WRITE_REPOSITORY) +@anon_allowed def delete_repository_images(namespace, repository): abort(501, 'Not Implemented', issue='not-implemented') @index.route('/repositories//auth', methods=['PUT']) @parse_repository_name +@anon_allowed def put_repository_auth(namespace, repository): abort(501, 'Not Implemented', issue='not-implemented') @index.route('/search', methods=['GET']) @process_auth +@anon_protect def get_search(): def result_view(repo): 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, # since we have nginx handle the _ping below. @index.route('/_internal_ping') +@anon_allowed def internal_ping(): return make_response('true', 200) @index.route('/_ping') @index.route('/_ping') +@anon_allowed def ping(): # NOTE: any changes made here must also be reflected in the nginx config response = make_response('true', 200) diff --git a/endpoints/registry.py b/endpoints/registry.py index 73610910e..2cb6174e4 100644 --- a/endpoints/registry.py +++ b/endpoints/registry.py @@ -16,6 +16,7 @@ from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission) from data import model, database from util import gzipstream +from endpoints.decorators import anon_protect registry = Blueprint('registry', __name__) @@ -97,6 +98,7 @@ def set_cache_headers(f): @extract_namespace_repo_from_session @require_completion @set_cache_headers +@anon_protect def head_image_layer(namespace, repository, image_id, headers): permission = ReadRepositoryPermission(namespace, repository) @@ -130,6 +132,7 @@ def head_image_layer(namespace, repository, image_id, headers): @extract_namespace_repo_from_session @require_completion @set_cache_headers +@anon_protect def get_image_layer(namespace, repository, image_id, headers): permission = ReadRepositoryPermission(namespace, repository) @@ -171,6 +174,7 @@ def get_image_layer(namespace, repository, image_id, headers): @registry.route('/images//layer', methods=['PUT']) @process_auth @extract_namespace_repo_from_session +@anon_protect def put_image_layer(namespace, repository, image_id): logger.debug('Checking repo permissions') permission = ModifyRepositoryPermission(namespace, repository) @@ -276,6 +280,7 @@ def put_image_layer(namespace, repository, image_id): @registry.route('/images//checksum', methods=['PUT']) @process_auth @extract_namespace_repo_from_session +@anon_protect def put_image_checksum(namespace, repository, image_id): logger.debug('Checking repo permissions') permission = ModifyRepositoryPermission(namespace, repository) @@ -352,6 +357,7 @@ def put_image_checksum(namespace, repository, image_id): @extract_namespace_repo_from_session @require_completion @set_cache_headers +@anon_protect def get_image_json(namespace, repository, image_id, headers): logger.debug('Checking repo permissions') permission = ReadRepositoryPermission(namespace, repository) @@ -383,6 +389,7 @@ def get_image_json(namespace, repository, image_id, headers): @extract_namespace_repo_from_session @require_completion @set_cache_headers +@anon_protect def get_image_ancestry(namespace, repository, image_id, headers): logger.debug('Checking repo permissions') permission = ReadRepositoryPermission(namespace, repository) @@ -434,6 +441,7 @@ def store_checksum(image_storage, checksum): @registry.route('/images//json', methods=['PUT']) @process_auth @extract_namespace_repo_from_session +@anon_protect def put_image_json(namespace, repository, image_id): logger.debug('Checking repo permissions') permission = ModifyRepositoryPermission(namespace, repository) diff --git a/endpoints/tags.py b/endpoints/tags.py index 9e61bf030..4b2ce90b6 100644 --- a/endpoints/tags.py +++ b/endpoints/tags.py @@ -10,6 +10,7 @@ from auth.auth import process_auth from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission) from data import model +from endpoints.decorators import anon_protect logger = logging.getLogger(__name__) @@ -20,6 +21,7 @@ tags = Blueprint('tags', __name__) @tags.route('/repositories//tags', methods=['GET']) @process_auth +@anon_protect @parse_repository_name def get_tags(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) @@ -35,6 +37,7 @@ def get_tags(namespace, repository): @tags.route('/repositories//tags/', methods=['GET']) @process_auth +@anon_protect @parse_repository_name def get_tag(namespace, repository, tag): permission = ReadRepositoryPermission(namespace, repository) @@ -51,6 +54,7 @@ def get_tag(namespace, repository, tag): @tags.route('/repositories//tags/', methods=['PUT']) @process_auth +@anon_protect @parse_repository_name def put_tag(namespace, repository, tag): permission = ModifyRepositoryPermission(namespace, repository) @@ -73,6 +77,7 @@ def put_tag(namespace, repository, tag): @tags.route('/repositories//tags/', methods=['DELETE']) @process_auth +@anon_protect @parse_repository_name def delete_tag(namespace, repository, tag): permission = ModifyRepositoryPermission(namespace, repository) diff --git a/endpoints/verbs.py b/endpoints/verbs.py index 88d9dc7b1..cd86b8baa 100644 --- a/endpoints/verbs.py +++ b/endpoints/verbs.py @@ -10,6 +10,7 @@ from auth.permissions import ReadRepositoryPermission from data import model from data import database from endpoints.trackhelper import track_and_log +from endpoints.decorators import anon_protect from storage import Storage from util.queuefile import QueueFile @@ -256,6 +257,7 @@ def os_arch_checker(os, arch): return checker +@anon_protect @verbs.route('/aci/////sig///', methods=['GET']) @verbs.route('/aci/////aci.asc///', methods=['GET']) @process_auth @@ -264,6 +266,7 @@ def get_aci_signature(server, namespace, repository, tag, os, arch): os=os, arch=arch) +@anon_protect @verbs.route('/aci/////aci///', methods=['GET']) @process_auth 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) +@anon_protect @verbs.route('/squash///', methods=['GET']) @process_auth def get_squashed_tag(namespace, repository, tag): diff --git a/endpoints/web.py b/endpoints/web.py index 7e61589af..b526790dc 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -19,6 +19,7 @@ from util.invoice import renderInvoiceToPdf from util.seo import render_snapshot from util.cache import no_cache 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.registry import set_cache_headers from endpoints.trigger import (CustomBuildTrigger, BitbucketBuildTrigger, TriggerProviderException, @@ -80,6 +81,7 @@ def snapshot(path = ''): @web.route('/aci-signing-key') @no_cache +@anon_protect def aci_signing_key(): if not signer.name: abort(404) @@ -363,6 +365,7 @@ def confirm_recovery(): @web.route('/repository//status', methods=['GET']) @parse_repository_name @no_cache +@anon_protect def build_status_badge(namespace, repository): token = request.args.get('token', None) is_public = model.repository_is_public(namespace, repository) @@ -591,6 +594,7 @@ def attach_custom_build_trigger(namespace, repository_name): @no_cache @process_oauth @parse_repository_name_and_tag +@anon_protect def redirect_to_repository(namespace, reponame, tag): permission = ReadRepositoryPermission(namespace, reponame) is_public = model.repository_is_public(namespace, reponame) @@ -608,6 +612,7 @@ def redirect_to_repository(namespace, reponame, tag): @web.route('/') @no_cache @process_oauth +@anon_protect def redirect_to_namespace(namespace): user_or_org = model.get_user_or_org(namespace) if not user_or_org: diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 065d55134..3e935e756 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -33,6 +33,20 @@ + + Anonymous Access: + +
+ + +
+
+ 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. +
+ + User Creation: diff --git a/static/directives/new-header-bar.html b/static/directives/new-header-bar.html index 805371233..e61be6fc4 100644 --- a/static/directives/new-header-bar.html +++ b/static/directives/new-header-bar.html @@ -10,7 +10,8 @@ + data-placement="bottom" data-title="Search" bs-tooltip + ng-if="searchingAllowed"> @@ -49,7 +50,8 @@
  • + data-placement="bottom" data-title="Search - Keyboard Shortcut: /" bs-tooltip + ng-if="searchingAllowed">
  • diff --git a/static/js/directives/ui/header-bar.js b/static/js/directives/ui/header-bar.js index 956cae711..e5da1073b 100644 --- a/static/js/directives/ui/header-bar.js +++ b/static/js/directives/ui/header-bar.js @@ -12,40 +12,55 @@ angular.module('quay').directive('headerBar', function () { restrict: 'C', 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(); - if ($scope.isNewLayout) { - // Register hotkeys: - hotkeys.add({ - combo: '/', - description: 'Show search', - callback: function(e) { - e.preventDefault(); - e.stopPropagation(); - $scope.toggleSearch(); - } - }); + var hotkeysAdded = false; + var userUpdated = function(cUser) { + $scope.searchingAllowed = Features.ANONYMOUS_ACCESS || !cUser.anonymous; - hotkeys.add({ - combo: 'alt+c', - description: 'Create new repository', - callback: function(e) { - e.preventDefault(); - e.stopPropagation(); - $location.url('/new'); + if (hotkeysAdded) { return; } + + if ($scope.isNewLayout) { + hotkeysAdded = true; + + // Register hotkeys. + 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.searchingAllowed = false; $scope.searchVisible = false; $scope.currentSearchQuery = null; $scope.searchResultState = null; $scope.showBuildDialogCounter = 0; // Monitor any user changes and place the current user into the scope. - UserService.updateUserIn($scope); + UserService.updateUserIn($scope, userUpdated); $scope.currentPageContext = {}; diff --git a/test/registry_tests.py b/test/registry_tests.py index bacc9ee8a..a2f98c067 100644 --- a/test/registry_tests.py +++ b/test/registry_tests.py @@ -1,6 +1,7 @@ import unittest import requests +from flask import request, jsonify from flask.blueprints import Blueprint from flask.ext.testing import LiveServerTestCase @@ -14,6 +15,7 @@ from endpoints.csrf import generate_csrf_token import endpoints.decorated import json +import features import tarfile @@ -30,15 +32,46 @@ except ValueError: 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.route('/csrf', methods=['GET']) def generate_csrf(): return generate_csrf_token() +@testbp.route('/feature/', 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') +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): maxDiff = None @@ -243,5 +276,75 @@ class RegistryTests(RegistryTestCase): 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__': unittest.main() diff --git a/test/test_anon_checked.py b/test/test_anon_checked.py new file mode 100644 index 000000000..f9eed9ee8 --- /dev/null +++ b/test/test_anon_checked.py @@ -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() + diff --git a/test/test_api_security.py b/test/test_api_security.py index e7b0f40e2..0b9b0f09d 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -371,6 +371,24 @@ class TestOrganizationList(ApiTestCase): 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): def setUp(self): ApiTestCase.setUp(self) diff --git a/test/test_api_usage.py b/test/test_api_usage.py index c4f3b1349..4683fe05b 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -1246,6 +1246,19 @@ class TestListRepos(ApiTestCase): 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): SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple' def test_updatedescription(self): diff --git a/util/config/configutil.py b/util/config/configutil.py index 7a31390c9..b17958f03 100644 --- a/util/config/configutil.py +++ b/util/config/configutil.py @@ -16,6 +16,7 @@ def add_enterprise_config_defaults(config_obj, current_secret_key, hostname): # Default features that are on. 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_ANONYMOUS_ACCESS'] = config_obj.get('FEATURE_ANONYMOUS_ACCESS', True) # Default features that are off. config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False)