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 Flag: Whether super users are supported.
|
||||||
FEATURE_SUPER_USERS = True
|
FEATURE_SUPER_USERS = True
|
||||||
|
|
||||||
|
# Feature Flag: Whether to allow anonymous users to browse and pull public repositories.
|
||||||
|
FEATURE_ANONYMOUS_ACCESS = True
|
||||||
|
|
||||||
# Feature Flag: Whether billing is required.
|
# Feature Flag: Whether billing is required.
|
||||||
FEATURE_BILLING = False
|
FEATURE_BILLING = False
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ from auth import scopes
|
||||||
from auth.auth_context import get_authenticated_user, get_validated_oauth_token
|
from auth.auth_context import get_authenticated_user, get_validated_oauth_token
|
||||||
from auth.auth import process_oauth
|
from auth.auth import process_oauth
|
||||||
from endpoints.csrf import csrf_protect
|
from endpoints.csrf import csrf_protect
|
||||||
|
from endpoints.decorators import check_anon_protection
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -228,12 +229,14 @@ def parse_repository_name(func):
|
||||||
|
|
||||||
|
|
||||||
class ApiResource(Resource):
|
class ApiResource(Resource):
|
||||||
|
method_decorators = [check_anon_protection]
|
||||||
|
|
||||||
def options(self):
|
def options(self):
|
||||||
return None, 200
|
return None, 200
|
||||||
|
|
||||||
|
|
||||||
class RepositoryParamResource(ApiResource):
|
class RepositoryParamResource(ApiResource):
|
||||||
method_decorators = [parse_repository_name]
|
method_decorators = [check_anon_protection, parse_repository_name]
|
||||||
|
|
||||||
|
|
||||||
def require_repo_permission(permission_class, scope, allow_public=False):
|
def require_repo_permission(permission_class, scope, allow_public=False):
|
||||||
|
|
|
@ -15,6 +15,7 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques
|
||||||
RepositoryParamResource)
|
RepositoryParamResource)
|
||||||
from endpoints.api.subscribe import subscribe
|
from endpoints.api.subscribe import subscribe
|
||||||
from endpoints.common import common_login
|
from endpoints.common import common_login
|
||||||
|
from endpoints.decorators import anon_allowed
|
||||||
from endpoints.api.team import try_accept_invite
|
from endpoints.api.team import try_accept_invite
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
|
@ -203,6 +204,7 @@ class User(ApiResource):
|
||||||
@require_scope(scopes.READ_USER)
|
@require_scope(scopes.READ_USER)
|
||||||
@nickname('getLoggedInUser')
|
@nickname('getLoggedInUser')
|
||||||
@define_json_response('UserView')
|
@define_json_response('UserView')
|
||||||
|
@anon_allowed
|
||||||
def get(self):
|
def get(self):
|
||||||
""" Get user information for the authenticated user. """
|
""" Get user information for the authenticated user. """
|
||||||
user = get_authenticated_user()
|
user = get_authenticated_user()
|
||||||
|
@ -497,6 +499,7 @@ class Signin(ApiResource):
|
||||||
|
|
||||||
@nickname('signinUser')
|
@nickname('signinUser')
|
||||||
@validate_json_request('SigninUser')
|
@validate_json_request('SigninUser')
|
||||||
|
@anon_allowed
|
||||||
def post(self):
|
def post(self):
|
||||||
""" Sign in the user with the specified credentials. """
|
""" Sign in the user with the specified credentials. """
|
||||||
signin_data = request.get_json()
|
signin_data = request.get_json()
|
||||||
|
|
36
endpoints/decorators.py
Normal file
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 util.http import abort
|
||||||
from endpoints.trackhelper import track_and_log
|
from endpoints.trackhelper import track_and_log
|
||||||
from endpoints.notificationhelper import spawn_notification
|
from endpoints.notificationhelper import spawn_notification
|
||||||
|
from endpoints.decorators import anon_protect, anon_allowed
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ def generate_headers(scope=GrantType.READ_REPOSITORY):
|
||||||
# Setting session namespace and repository
|
# Setting session namespace and repository
|
||||||
session['namespace'] = namespace
|
session['namespace'] = namespace
|
||||||
session['repository'] = repository
|
session['repository'] = repository
|
||||||
|
|
||||||
# We run our index and registry on the same hosts for now
|
# We run our index and registry on the same hosts for now
|
||||||
registry_server = urlparse.urlparse(request.url).netloc
|
registry_server = urlparse.urlparse(request.url).netloc
|
||||||
response.headers['X-Docker-Endpoints'] = registry_server
|
response.headers['X-Docker-Endpoints'] = registry_server
|
||||||
|
@ -49,24 +51,22 @@ def generate_headers(scope=GrantType.READ_REPOSITORY):
|
||||||
has_token_request = request.headers.get('X-Docker-Token', '')
|
has_token_request = request.headers.get('X-Docker-Token', '')
|
||||||
|
|
||||||
if has_token_request:
|
if has_token_request:
|
||||||
permission = AlwaysFailPermission()
|
|
||||||
grants = []
|
grants = []
|
||||||
if scope == GrantType.READ_REPOSITORY:
|
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
|
||||||
grants.append(repository_read_grant(namespace, repository))
|
|
||||||
elif scope == GrantType.WRITE_REPOSITORY:
|
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
|
||||||
grants.append(repository_write_grant(namespace, repository))
|
|
||||||
|
|
||||||
if permission.can():
|
if scope == GrantType.READ_REPOSITORY:
|
||||||
# Generate a signed grant which expires here
|
if ReadRepositoryPermission(namespace, repository).can():
|
||||||
|
grants.append(repository_read_grant(namespace, repository))
|
||||||
|
elif scope == GrantType.WRITE_REPOSITORY:
|
||||||
|
if ModifyRepositoryPermission(namespace, repository).can():
|
||||||
|
grants.append(repository_write_grant(namespace, repository))
|
||||||
|
|
||||||
|
# Generate a signed token for the user (if any) and the grants (if any)
|
||||||
|
if grants or get_authenticated_user():
|
||||||
user_context = get_authenticated_user() and get_authenticated_user().username
|
user_context = get_authenticated_user() and get_authenticated_user().username
|
||||||
signature = generate_signed_token(grants, user_context)
|
signature = generate_signed_token(grants, user_context)
|
||||||
response.headers['WWW-Authenticate'] = signature
|
response.headers['WWW-Authenticate'] = signature
|
||||||
response.headers['X-Docker-Token'] = signature
|
response.headers['X-Docker-Token'] = signature
|
||||||
else:
|
|
||||||
logger.warning('Registry request with invalid credentials on repository: %s/%s',
|
|
||||||
namespace, repository)
|
|
||||||
return response
|
return response
|
||||||
return wrapper
|
return wrapper
|
||||||
return decorator_method
|
return decorator_method
|
||||||
|
@ -74,6 +74,7 @@ def generate_headers(scope=GrantType.READ_REPOSITORY):
|
||||||
|
|
||||||
@index.route('/users', methods=['POST'])
|
@index.route('/users', methods=['POST'])
|
||||||
@index.route('/users/', methods=['POST'])
|
@index.route('/users/', methods=['POST'])
|
||||||
|
@anon_allowed
|
||||||
def create_user():
|
def create_user():
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
if not user_data or not 'username' in user_data:
|
if not user_data or not 'username' in user_data:
|
||||||
|
@ -146,6 +147,7 @@ def create_user():
|
||||||
@index.route('/users', methods=['GET'])
|
@index.route('/users', methods=['GET'])
|
||||||
@index.route('/users/', methods=['GET'])
|
@index.route('/users/', methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
|
@anon_allowed
|
||||||
def get_user():
|
def get_user():
|
||||||
if get_validated_oauth_token():
|
if get_validated_oauth_token():
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
@ -167,6 +169,7 @@ def get_user():
|
||||||
|
|
||||||
@index.route('/users/<username>/', methods=['PUT'])
|
@index.route('/users/<username>/', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
|
@anon_allowed
|
||||||
def update_user(username):
|
def update_user(username):
|
||||||
permission = UserAdminPermission(username)
|
permission = UserAdminPermission(username)
|
||||||
|
|
||||||
|
@ -194,6 +197,7 @@ def update_user(username):
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||||
|
@anon_allowed
|
||||||
def create_repository(namespace, repository):
|
def create_repository(namespace, repository):
|
||||||
logger.debug('Parsing image descriptions')
|
logger.debug('Parsing image descriptions')
|
||||||
image_descriptions = json.loads(request.data.decode('utf8'))
|
image_descriptions = json.loads(request.data.decode('utf8'))
|
||||||
|
@ -246,6 +250,7 @@ def create_repository(namespace, repository):
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||||
|
@anon_allowed
|
||||||
def update_images(namespace, repository):
|
def update_images(namespace, repository):
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
|
||||||
|
@ -278,6 +283,7 @@ def update_images(namespace, repository):
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
@generate_headers(scope=GrantType.READ_REPOSITORY)
|
@generate_headers(scope=GrantType.READ_REPOSITORY)
|
||||||
|
@anon_protect
|
||||||
def get_repository_images(namespace, repository):
|
def get_repository_images(namespace, repository):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
|
||||||
|
@ -303,18 +309,21 @@ def get_repository_images(namespace, repository):
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||||
|
@anon_allowed
|
||||||
def delete_repository_images(namespace, repository):
|
def delete_repository_images(namespace, repository):
|
||||||
abort(501, 'Not Implemented', issue='not-implemented')
|
abort(501, 'Not Implemented', issue='not-implemented')
|
||||||
|
|
||||||
|
|
||||||
@index.route('/repositories/<path:repository>/auth', methods=['PUT'])
|
@index.route('/repositories/<path:repository>/auth', methods=['PUT'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
@anon_allowed
|
||||||
def put_repository_auth(namespace, repository):
|
def put_repository_auth(namespace, repository):
|
||||||
abort(501, 'Not Implemented', issue='not-implemented')
|
abort(501, 'Not Implemented', issue='not-implemented')
|
||||||
|
|
||||||
|
|
||||||
@index.route('/search', methods=['GET'])
|
@index.route('/search', methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
|
@anon_protect
|
||||||
def get_search():
|
def get_search():
|
||||||
def result_view(repo):
|
def result_view(repo):
|
||||||
return {
|
return {
|
||||||
|
@ -351,11 +360,13 @@ def get_search():
|
||||||
# Note: This is *not* part of the Docker index spec. This is here for our own health check,
|
# Note: This is *not* part of the Docker index spec. This is here for our own health check,
|
||||||
# since we have nginx handle the _ping below.
|
# since we have nginx handle the _ping below.
|
||||||
@index.route('/_internal_ping')
|
@index.route('/_internal_ping')
|
||||||
|
@anon_allowed
|
||||||
def internal_ping():
|
def internal_ping():
|
||||||
return make_response('true', 200)
|
return make_response('true', 200)
|
||||||
|
|
||||||
@index.route('/_ping')
|
@index.route('/_ping')
|
||||||
@index.route('/_ping')
|
@index.route('/_ping')
|
||||||
|
@anon_allowed
|
||||||
def ping():
|
def ping():
|
||||||
# NOTE: any changes made here must also be reflected in the nginx config
|
# NOTE: any changes made here must also be reflected in the nginx config
|
||||||
response = make_response('true', 200)
|
response = make_response('true', 200)
|
||||||
|
|
|
@ -16,6 +16,7 @@ from auth.permissions import (ReadRepositoryPermission,
|
||||||
ModifyRepositoryPermission)
|
ModifyRepositoryPermission)
|
||||||
from data import model, database
|
from data import model, database
|
||||||
from util import gzipstream
|
from util import gzipstream
|
||||||
|
from endpoints.decorators import anon_protect
|
||||||
|
|
||||||
|
|
||||||
registry = Blueprint('registry', __name__)
|
registry = Blueprint('registry', __name__)
|
||||||
|
@ -97,6 +98,7 @@ def set_cache_headers(f):
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
@require_completion
|
@require_completion
|
||||||
@set_cache_headers
|
@set_cache_headers
|
||||||
|
@anon_protect
|
||||||
def head_image_layer(namespace, repository, image_id, headers):
|
def head_image_layer(namespace, repository, image_id, headers):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
|
||||||
|
@ -130,6 +132,7 @@ def head_image_layer(namespace, repository, image_id, headers):
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
@require_completion
|
@require_completion
|
||||||
@set_cache_headers
|
@set_cache_headers
|
||||||
|
@anon_protect
|
||||||
def get_image_layer(namespace, repository, image_id, headers):
|
def get_image_layer(namespace, repository, image_id, headers):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
|
||||||
|
@ -171,6 +174,7 @@ def get_image_layer(namespace, repository, image_id, headers):
|
||||||
@registry.route('/images/<image_id>/layer', methods=['PUT'])
|
@registry.route('/images/<image_id>/layer', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
|
@anon_protect
|
||||||
def put_image_layer(namespace, repository, image_id):
|
def put_image_layer(namespace, repository, image_id):
|
||||||
logger.debug('Checking repo permissions')
|
logger.debug('Checking repo permissions')
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
@ -276,6 +280,7 @@ def put_image_layer(namespace, repository, image_id):
|
||||||
@registry.route('/images/<image_id>/checksum', methods=['PUT'])
|
@registry.route('/images/<image_id>/checksum', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
|
@anon_protect
|
||||||
def put_image_checksum(namespace, repository, image_id):
|
def put_image_checksum(namespace, repository, image_id):
|
||||||
logger.debug('Checking repo permissions')
|
logger.debug('Checking repo permissions')
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
@ -352,6 +357,7 @@ def put_image_checksum(namespace, repository, image_id):
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
@require_completion
|
@require_completion
|
||||||
@set_cache_headers
|
@set_cache_headers
|
||||||
|
@anon_protect
|
||||||
def get_image_json(namespace, repository, image_id, headers):
|
def get_image_json(namespace, repository, image_id, headers):
|
||||||
logger.debug('Checking repo permissions')
|
logger.debug('Checking repo permissions')
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
@ -383,6 +389,7 @@ def get_image_json(namespace, repository, image_id, headers):
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
@require_completion
|
@require_completion
|
||||||
@set_cache_headers
|
@set_cache_headers
|
||||||
|
@anon_protect
|
||||||
def get_image_ancestry(namespace, repository, image_id, headers):
|
def get_image_ancestry(namespace, repository, image_id, headers):
|
||||||
logger.debug('Checking repo permissions')
|
logger.debug('Checking repo permissions')
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
@ -434,6 +441,7 @@ def store_checksum(image_storage, checksum):
|
||||||
@registry.route('/images/<image_id>/json', methods=['PUT'])
|
@registry.route('/images/<image_id>/json', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@extract_namespace_repo_from_session
|
@extract_namespace_repo_from_session
|
||||||
|
@anon_protect
|
||||||
def put_image_json(namespace, repository, image_id):
|
def put_image_json(namespace, repository, image_id):
|
||||||
logger.debug('Checking repo permissions')
|
logger.debug('Checking repo permissions')
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from auth.auth import process_auth
|
||||||
from auth.permissions import (ReadRepositoryPermission,
|
from auth.permissions import (ReadRepositoryPermission,
|
||||||
ModifyRepositoryPermission)
|
ModifyRepositoryPermission)
|
||||||
from data import model
|
from data import model
|
||||||
|
from endpoints.decorators import anon_protect
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -20,6 +21,7 @@ tags = Blueprint('tags', __name__)
|
||||||
@tags.route('/repositories/<path:repository>/tags',
|
@tags.route('/repositories/<path:repository>/tags',
|
||||||
methods=['GET'])
|
methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
|
@anon_protect
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def get_tags(namespace, repository):
|
def get_tags(namespace, repository):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
@ -35,6 +37,7 @@ def get_tags(namespace, repository):
|
||||||
@tags.route('/repositories/<path:repository>/tags/<tag>',
|
@tags.route('/repositories/<path:repository>/tags/<tag>',
|
||||||
methods=['GET'])
|
methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
|
@anon_protect
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def get_tag(namespace, repository, tag):
|
def get_tag(namespace, repository, tag):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
@ -51,6 +54,7 @@ def get_tag(namespace, repository, tag):
|
||||||
@tags.route('/repositories/<path:repository>/tags/<tag>',
|
@tags.route('/repositories/<path:repository>/tags/<tag>',
|
||||||
methods=['PUT'])
|
methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
|
@anon_protect
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def put_tag(namespace, repository, tag):
|
def put_tag(namespace, repository, tag):
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
@ -73,6 +77,7 @@ def put_tag(namespace, repository, tag):
|
||||||
@tags.route('/repositories/<path:repository>/tags/<tag>',
|
@tags.route('/repositories/<path:repository>/tags/<tag>',
|
||||||
methods=['DELETE'])
|
methods=['DELETE'])
|
||||||
@process_auth
|
@process_auth
|
||||||
|
@anon_protect
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def delete_tag(namespace, repository, tag):
|
def delete_tag(namespace, repository, tag):
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from auth.permissions import ReadRepositoryPermission
|
||||||
from data import model
|
from data import model
|
||||||
from data import database
|
from data import database
|
||||||
from endpoints.trackhelper import track_and_log
|
from endpoints.trackhelper import track_and_log
|
||||||
|
from endpoints.decorators import anon_protect
|
||||||
from storage import Storage
|
from storage import Storage
|
||||||
|
|
||||||
from util.queuefile import QueueFile
|
from util.queuefile import QueueFile
|
||||||
|
@ -256,6 +257,7 @@ def os_arch_checker(os, arch):
|
||||||
return checker
|
return checker
|
||||||
|
|
||||||
|
|
||||||
|
@anon_protect
|
||||||
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/sig/<os>/<arch>/', methods=['GET'])
|
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/sig/<os>/<arch>/', methods=['GET'])
|
||||||
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci.asc/<os>/<arch>/', methods=['GET'])
|
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci.asc/<os>/<arch>/', methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
|
@ -264,6 +266,7 @@ def get_aci_signature(server, namespace, repository, tag, os, arch):
|
||||||
os=os, arch=arch)
|
os=os, arch=arch)
|
||||||
|
|
||||||
|
|
||||||
|
@anon_protect
|
||||||
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci/<os>/<arch>/', methods=['GET'])
|
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci/<os>/<arch>/', methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
def get_aci_image(server, namespace, repository, tag, os, arch):
|
def get_aci_image(server, namespace, repository, tag, os, arch):
|
||||||
|
@ -271,6 +274,7 @@ def get_aci_image(server, namespace, repository, tag, os, arch):
|
||||||
sign=True, checker=os_arch_checker(os, arch), os=os, arch=arch)
|
sign=True, checker=os_arch_checker(os, arch), os=os, arch=arch)
|
||||||
|
|
||||||
|
|
||||||
|
@anon_protect
|
||||||
@verbs.route('/squash/<namespace>/<repository>/<tag>', methods=['GET'])
|
@verbs.route('/squash/<namespace>/<repository>/<tag>', methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
def get_squashed_tag(namespace, repository, tag):
|
def get_squashed_tag(namespace, repository, tag):
|
||||||
|
|
|
@ -19,6 +19,7 @@ from util.invoice import renderInvoiceToPdf
|
||||||
from util.seo import render_snapshot
|
from util.seo import render_snapshot
|
||||||
from util.cache import no_cache
|
from util.cache import no_cache
|
||||||
from endpoints.common import common_login, render_page_template, route_show_if, param_required
|
from endpoints.common import common_login, render_page_template, route_show_if, param_required
|
||||||
|
from endpoints.decorators import anon_protect
|
||||||
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
|
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
|
||||||
from endpoints.registry import set_cache_headers
|
from endpoints.registry import set_cache_headers
|
||||||
from endpoints.trigger import (CustomBuildTrigger, BitbucketBuildTrigger, TriggerProviderException,
|
from endpoints.trigger import (CustomBuildTrigger, BitbucketBuildTrigger, TriggerProviderException,
|
||||||
|
@ -80,6 +81,7 @@ def snapshot(path = ''):
|
||||||
|
|
||||||
@web.route('/aci-signing-key')
|
@web.route('/aci-signing-key')
|
||||||
@no_cache
|
@no_cache
|
||||||
|
@anon_protect
|
||||||
def aci_signing_key():
|
def aci_signing_key():
|
||||||
if not signer.name:
|
if not signer.name:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
@ -363,6 +365,7 @@ def confirm_recovery():
|
||||||
@web.route('/repository/<path:repository>/status', methods=['GET'])
|
@web.route('/repository/<path:repository>/status', methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
@no_cache
|
@no_cache
|
||||||
|
@anon_protect
|
||||||
def build_status_badge(namespace, repository):
|
def build_status_badge(namespace, repository):
|
||||||
token = request.args.get('token', None)
|
token = request.args.get('token', None)
|
||||||
is_public = model.repository_is_public(namespace, repository)
|
is_public = model.repository_is_public(namespace, repository)
|
||||||
|
@ -591,6 +594,7 @@ def attach_custom_build_trigger(namespace, repository_name):
|
||||||
@no_cache
|
@no_cache
|
||||||
@process_oauth
|
@process_oauth
|
||||||
@parse_repository_name_and_tag
|
@parse_repository_name_and_tag
|
||||||
|
@anon_protect
|
||||||
def redirect_to_repository(namespace, reponame, tag):
|
def redirect_to_repository(namespace, reponame, tag):
|
||||||
permission = ReadRepositoryPermission(namespace, reponame)
|
permission = ReadRepositoryPermission(namespace, reponame)
|
||||||
is_public = model.repository_is_public(namespace, reponame)
|
is_public = model.repository_is_public(namespace, reponame)
|
||||||
|
@ -608,6 +612,7 @@ def redirect_to_repository(namespace, reponame, tag):
|
||||||
@web.route('/<namespace>')
|
@web.route('/<namespace>')
|
||||||
@no_cache
|
@no_cache
|
||||||
@process_oauth
|
@process_oauth
|
||||||
|
@anon_protect
|
||||||
def redirect_to_namespace(namespace):
|
def redirect_to_namespace(namespace):
|
||||||
user_or_org = model.get_user_or_org(namespace)
|
user_or_org = model.get_user_or_org(namespace)
|
||||||
if not user_or_org:
|
if not user_or_org:
|
||||||
|
|
|
@ -33,6 +33,20 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="non-input">Anonymous Access:</td>
|
||||||
|
<td colspan="2">
|
||||||
|
<div class="co-checkbox">
|
||||||
|
<input id="ftaa" type="checkbox" ng-model="config.FEATURE_ANONYMOUS_ACCESS">
|
||||||
|
<label for="ftaa">Enable Anonymous Access</label>
|
||||||
|
</div>
|
||||||
|
<div class="help-text">
|
||||||
|
If enabled, public repositories and search can be accessed by anyone that can
|
||||||
|
reach the registry, even if they are not authenticated. Disable to only allow
|
||||||
|
authenticated users to view and pull "public" resources.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="non-input">User Creation:</td>
|
<td class="non-input">User Creation:</td>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
</a>
|
</a>
|
||||||
<span class="user-tools visible-xs" style="float: right;">
|
<span class="user-tools visible-xs" style="float: right;">
|
||||||
<i class="fa fa-search fa-lg user-tool" ng-click="toggleSearch()"
|
<i class="fa fa-search fa-lg user-tool" ng-click="toggleSearch()"
|
||||||
data-placement="bottom" data-title="Search" bs-tooltip></i>
|
data-placement="bottom" data-title="Search" bs-tooltip
|
||||||
|
ng-if="searchingAllowed"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -49,7 +50,8 @@
|
||||||
<li>
|
<li>
|
||||||
<span class="navbar-left user-tools">
|
<span class="navbar-left user-tools">
|
||||||
<i class="fa fa-search fa-lg user-tool" ng-click="toggleSearch()"
|
<i class="fa fa-search fa-lg user-tool" ng-click="toggleSearch()"
|
||||||
data-placement="bottom" data-title="Search - Keyboard Shortcut: /" bs-tooltip></i>
|
data-placement="bottom" data-title="Search - Keyboard Shortcut: /" bs-tooltip
|
||||||
|
ng-if="searchingAllowed"></i>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -12,40 +12,55 @@ angular.module('quay').directive('headerBar', function () {
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
},
|
},
|
||||||
controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService, PlanService, ApiService, NotificationService, Config, CreateService) {
|
controller: function($rootScope, $scope, $element, $location, $timeout, hotkeys, UserService,
|
||||||
|
PlanService, ApiService, NotificationService, Config, CreateService, Features) {
|
||||||
$scope.isNewLayout = Config.isNewLayout();
|
$scope.isNewLayout = Config.isNewLayout();
|
||||||
|
|
||||||
if ($scope.isNewLayout) {
|
var hotkeysAdded = false;
|
||||||
// Register hotkeys:
|
var userUpdated = function(cUser) {
|
||||||
hotkeys.add({
|
$scope.searchingAllowed = Features.ANONYMOUS_ACCESS || !cUser.anonymous;
|
||||||
combo: '/',
|
|
||||||
description: 'Show search',
|
|
||||||
callback: function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
$scope.toggleSearch();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hotkeys.add({
|
if (hotkeysAdded) { return; }
|
||||||
combo: 'alt+c',
|
|
||||||
description: 'Create new repository',
|
if ($scope.isNewLayout) {
|
||||||
callback: function(e) {
|
hotkeysAdded = true;
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
// Register hotkeys.
|
||||||
$location.url('/new');
|
if ($scope.searchingAllowed) {
|
||||||
|
hotkeys.add({
|
||||||
|
combo: '/',
|
||||||
|
description: 'Show search',
|
||||||
|
callback: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
$scope.toggleSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
if (!cUser.anonymous) {
|
||||||
|
hotkeys.add({
|
||||||
|
combo: 'alt+c',
|
||||||
|
description: 'Create new repository',
|
||||||
|
callback: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
$location.url('/new');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$scope.notificationService = NotificationService;
|
$scope.notificationService = NotificationService;
|
||||||
|
$scope.searchingAllowed = false;
|
||||||
$scope.searchVisible = false;
|
$scope.searchVisible = false;
|
||||||
$scope.currentSearchQuery = null;
|
$scope.currentSearchQuery = null;
|
||||||
$scope.searchResultState = null;
|
$scope.searchResultState = null;
|
||||||
$scope.showBuildDialogCounter = 0;
|
$scope.showBuildDialogCounter = 0;
|
||||||
|
|
||||||
// Monitor any user changes and place the current user into the scope.
|
// Monitor any user changes and place the current user into the scope.
|
||||||
UserService.updateUserIn($scope);
|
UserService.updateUserIn($scope, userUpdated);
|
||||||
|
|
||||||
$scope.currentPageContext = {};
|
$scope.currentPageContext = {};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import unittest
|
import unittest
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from flask import request, jsonify
|
||||||
from flask.blueprints import Blueprint
|
from flask.blueprints import Blueprint
|
||||||
from flask.ext.testing import LiveServerTestCase
|
from flask.ext.testing import LiveServerTestCase
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@ from endpoints.csrf import generate_csrf_token
|
||||||
|
|
||||||
import endpoints.decorated
|
import endpoints.decorated
|
||||||
import json
|
import json
|
||||||
|
import features
|
||||||
|
|
||||||
import tarfile
|
import tarfile
|
||||||
|
|
||||||
|
@ -30,15 +32,46 @@ except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Add a test blueprint for generating CSRF tokens.
|
# Add a test blueprint for generating CSRF tokens and setting feature flags.
|
||||||
testbp = Blueprint('testbp', __name__)
|
testbp = Blueprint('testbp', __name__)
|
||||||
|
|
||||||
@testbp.route('/csrf', methods=['GET'])
|
@testbp.route('/csrf', methods=['GET'])
|
||||||
def generate_csrf():
|
def generate_csrf():
|
||||||
return generate_csrf_token()
|
return generate_csrf_token()
|
||||||
|
|
||||||
|
@testbp.route('/feature/<feature_name>', methods=['POST'])
|
||||||
|
def set_feature(feature_name):
|
||||||
|
import features
|
||||||
|
old_value = features._FEATURES[feature_name].value
|
||||||
|
features._FEATURES[feature_name].value = request.get_json()['value']
|
||||||
|
return jsonify({'old_value': old_value})
|
||||||
|
|
||||||
app.register_blueprint(testbp, url_prefix='/__test')
|
app.register_blueprint(testbp, url_prefix='/__test')
|
||||||
|
|
||||||
|
|
||||||
|
class TestFeature(object):
|
||||||
|
""" Helper object which temporarily sets the value of a feature flag.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, test_case, feature_flag, test_value):
|
||||||
|
self.test_case = test_case
|
||||||
|
self.feature_flag = feature_flag
|
||||||
|
self.test_value = test_value
|
||||||
|
self.old_value = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
result = self.test_case.conduct('POST', '/__test/feature/' + self.feature_flag,
|
||||||
|
data=json.dumps(dict(value=self.test_value)),
|
||||||
|
headers={'Content-Type': 'application/json'})
|
||||||
|
|
||||||
|
result_data = json.loads(result.text)
|
||||||
|
self.old_value = result_data['old_value']
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
self.test_case.conduct('POST', '/__test/feature/' + self.feature_flag,
|
||||||
|
data=json.dumps(dict(value=self.old_value)),
|
||||||
|
headers={'Content-Type': 'application/json'})
|
||||||
|
|
||||||
class RegistryTestCase(LiveServerTestCase):
|
class RegistryTestCase(LiveServerTestCase):
|
||||||
maxDiff = None
|
maxDiff = None
|
||||||
|
|
||||||
|
@ -243,5 +276,75 @@ class RegistryTests(RegistryTestCase):
|
||||||
self.do_pull('devtable', 'newrepo', 'devtable', 'password')
|
self.do_pull('devtable', 'newrepo', 'devtable', 'password')
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_no_anonymous_access_with_auth(self):
|
||||||
|
# Turn off anonymous access.
|
||||||
|
with TestFeature(self, 'ANONYMOUS_ACCESS', False):
|
||||||
|
# Add a new repository under the public user, so we have a real repository to pull.
|
||||||
|
images = [{
|
||||||
|
'id': 'onlyimagehere'
|
||||||
|
}]
|
||||||
|
self.do_push('public', 'newrepo', 'public', 'password', images)
|
||||||
|
self.clearSession()
|
||||||
|
|
||||||
|
# First try to pull the (currently private) repo as devtable, which should fail as it belongs
|
||||||
|
# to public.
|
||||||
|
self.do_pull('public', 'newrepo', 'devtable', 'password', expected_code=403)
|
||||||
|
|
||||||
|
# Make the repository public.
|
||||||
|
self.conduct_api_login('public', 'password')
|
||||||
|
self.change_repo_visibility('public', 'newrepo', 'public')
|
||||||
|
self.clearSession()
|
||||||
|
|
||||||
|
# Pull the repository as devtable, which should succeed because the repository is public.
|
||||||
|
self.do_pull('public', 'newrepo', 'devtable', 'password')
|
||||||
|
|
||||||
|
|
||||||
|
def test_private_no_anonymous_access(self):
|
||||||
|
# Turn off anonymous access.
|
||||||
|
with TestFeature(self, 'ANONYMOUS_ACCESS', False):
|
||||||
|
# Add a new repository under the public user, so we have a real repository to pull.
|
||||||
|
images = [{
|
||||||
|
'id': 'onlyimagehere'
|
||||||
|
}]
|
||||||
|
self.do_push('public', 'newrepo', 'public', 'password', images)
|
||||||
|
self.clearSession()
|
||||||
|
|
||||||
|
# First try to pull the (currently private) repo as devtable, which should fail as it belongs
|
||||||
|
# to public.
|
||||||
|
self.do_pull('public', 'newrepo', 'devtable', 'password', expected_code=403)
|
||||||
|
|
||||||
|
# Pull the repository as public, which should succeed because the repository is owned by public.
|
||||||
|
self.do_pull('public', 'newrepo', 'public', 'password')
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_no_anonymous_access_no_auth(self):
|
||||||
|
# Turn off anonymous access.
|
||||||
|
with TestFeature(self, 'ANONYMOUS_ACCESS', False):
|
||||||
|
# Add a new repository under the public user, so we have a real repository to pull.
|
||||||
|
images = [{
|
||||||
|
'id': 'onlyimagehere'
|
||||||
|
}]
|
||||||
|
self.do_push('public', 'newrepo', 'public', 'password', images)
|
||||||
|
self.clearSession()
|
||||||
|
|
||||||
|
# First try to pull the (currently private) repo as anonymous, which should fail as it
|
||||||
|
# is private.
|
||||||
|
self.do_pull('public', 'newrepo', expected_code=401)
|
||||||
|
|
||||||
|
# Make the repository public.
|
||||||
|
self.conduct_api_login('public', 'password')
|
||||||
|
self.change_repo_visibility('public', 'newrepo', 'public')
|
||||||
|
self.clearSession()
|
||||||
|
|
||||||
|
# Try again to pull the (currently public) repo as anonymous, which should fail as
|
||||||
|
# anonymous access is disabled.
|
||||||
|
self.do_pull('public', 'newrepo', expected_code=401)
|
||||||
|
|
||||||
|
# Pull the repository as public, which should succeed because the repository is owned by public.
|
||||||
|
self.do_pull('public', 'newrepo', 'public', 'password')
|
||||||
|
|
||||||
|
# Pull the repository as devtable, which should succeed because the repository is public.
|
||||||
|
self.do_pull('public', 'newrepo', 'devtable', 'password')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
33
test/test_anon_checked.py
Normal file
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'})
|
self._run_test('POST', 400, 'devtable', {u'name': 'KSIS', u'email': 'DHVZ'})
|
||||||
|
|
||||||
|
|
||||||
|
class TestPublicRepository(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(Repository, repository="public/publicrepo")
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 200, None, None)
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 200, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 200, 'reader', None)
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 200, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
class TestRepositoryList(ApiTestCase):
|
class TestRepositoryList(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
|
|
|
@ -1246,6 +1246,19 @@ class TestListRepos(ApiTestCase):
|
||||||
self.assertEquals(len(json['repositories']), 2)
|
self.assertEquals(len(json['repositories']), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestViewPublicRepository(ApiTestCase):
|
||||||
|
def test_normalview(self):
|
||||||
|
self.getJsonResponse(Repository, params=dict(repository='public/publicrepo'))
|
||||||
|
|
||||||
|
def test_anon_access_disabled(self):
|
||||||
|
import features
|
||||||
|
features.ANONYMOUS_ACCESS = False
|
||||||
|
try:
|
||||||
|
self.getResponse(Repository, params=dict(repository='public/publicrepo'), expected_code=401)
|
||||||
|
finally:
|
||||||
|
features.ANONYMOUS_ACCESS = True
|
||||||
|
|
||||||
|
|
||||||
class TestUpdateRepo(ApiTestCase):
|
class TestUpdateRepo(ApiTestCase):
|
||||||
SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple'
|
SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple'
|
||||||
def test_updatedescription(self):
|
def test_updatedescription(self):
|
||||||
|
|
|
@ -16,6 +16,7 @@ def add_enterprise_config_defaults(config_obj, current_secret_key, hostname):
|
||||||
# Default features that are on.
|
# Default features that are on.
|
||||||
config_obj['FEATURE_USER_LOG_ACCESS'] = config_obj.get('FEATURE_USER_LOG_ACCESS', True)
|
config_obj['FEATURE_USER_LOG_ACCESS'] = config_obj.get('FEATURE_USER_LOG_ACCESS', True)
|
||||||
config_obj['FEATURE_USER_CREATION'] = config_obj.get('FEATURE_USER_CREATION', True)
|
config_obj['FEATURE_USER_CREATION'] = config_obj.get('FEATURE_USER_CREATION', True)
|
||||||
|
config_obj['FEATURE_ANONYMOUS_ACCESS'] = config_obj.get('FEATURE_ANONYMOUS_ACCESS', True)
|
||||||
|
|
||||||
# Default features that are off.
|
# Default features that are off.
|
||||||
config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False)
|
config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False)
|
||||||
|
|
Reference in a new issue