Merge pull request #31 from coreos-inc/nolurk
Add a feature flag for disabling unauthenticated access to the regist…
This commit is contained in:
commit
7d1e5a0c6f
17 changed files with 315 additions and 38 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
36
endpoints/decorators.py
Normal 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
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = {};
|
||||
|
||||
|
|
|
@ -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
33
test/test_anon_checked.py
Normal 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()
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
Reference in a new issue