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_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

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 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):

View file

@ -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()

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 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/<username>/', 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/<path:repository>/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)

View file

@ -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/<image_id>/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/<image_id>/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/<image_id>/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)

View file

@ -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/<path:repository>/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/<path:repository>/tags/<tag>',
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/<path:repository>/tags/<tag>',
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/<path:repository>/tags/<tag>',
methods=['DELETE'])
@process_auth
@anon_protect
@parse_repository_name
def delete_tag(namespace, repository, tag):
permission = ModifyRepositoryPermission(namespace, repository)

View file

@ -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/<server>/<namespace>/<repository>/<tag>/sig/<os>/<arch>/', methods=['GET'])
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci.asc/<os>/<arch>/', 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/<server>/<namespace>/<repository>/<tag>/aci/<os>/<arch>/', 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/<namespace>/<repository>/<tag>', methods=['GET'])
@process_auth
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.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/<path: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('/<namespace>')
@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:

View file

@ -33,6 +33,20 @@
</div>
</td>
</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>
<td class="non-input">User Creation:</td>
<td colspan="2">

View file

@ -10,7 +10,8 @@
</a>
<span class="user-tools visible-xs" style="float: right;">
<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>
</div>
@ -49,7 +50,8 @@
<li>
<span class="navbar-left user-tools">
<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>
</li>
<li>

View file

@ -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 = {};

View file

@ -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/<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')
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()

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'})
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)

View file

@ -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):

View file

@ -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)