Add a feature flag for disabling unauthenticated access to the registry in its entirety.

This commit is contained in:
Joseph Schorr 2015-05-19 17:52:44 -04:00
parent 598fc6ec46
commit 54992c23b7
15 changed files with 147 additions and 25 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 anon_protect
logger = logging.getLogger(__name__)
@ -228,12 +229,14 @@ def parse_repository_name(func):
class ApiResource(Resource):
method_decorators = [anon_protect]
def options(self):
return None, 200
class RepositoryParamResource(ApiResource):
method_decorators = [parse_repository_name]
method_decorators = [anon_protect, 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()
@ -498,6 +500,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()

30
endpoints/decorators.py Normal file
View file

@ -0,0 +1,30 @@
""" 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. """
@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
import features
@ -278,6 +279,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)
@ -315,6 +317,7 @@ def put_repository_auth(namespace, repository):
@index.route('/search', methods=['GET'])
@process_auth
@anon_protect
def get_search():
def result_view(repo):
return {

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)
@ -352,6 +355,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 +387,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)

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)

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

@ -18,6 +18,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,
@ -79,6 +80,7 @@ def snapshot(path = ''):
@web.route('/aci-signing-key')
@no_cache
@anon_protect
def aci_signing_key():
if not signer.name:
abort(404)
@ -337,6 +339,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)
@ -565,6 +568,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)
@ -582,6 +586,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

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