Port a few more repository methods to the new API interface.
This commit is contained in:
parent
e74eb3ee87
commit
0e3fe8f3b1
5 changed files with 176 additions and 59 deletions
11
auth/auth.py
11
auth/auth.py
|
@ -108,7 +108,11 @@ def process_token(auth):
|
||||||
identity_changed.send(app, identity=Identity(token_data.code, 'token'))
|
identity_changed.send(app, identity=Identity(token_data.code, 'token'))
|
||||||
|
|
||||||
|
|
||||||
def process_oauth(auth):
|
def process_oauth(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
auth = request.headers.get('authorization', '')
|
||||||
|
if auth:
|
||||||
normalized = [part.strip() for part in auth.split(' ') if part]
|
normalized = [part.strip() for part in auth.split(' ') if part]
|
||||||
if normalized[0].lower() != 'bearer' or len(normalized) != 2:
|
if normalized[0].lower() != 'bearer' or len(normalized) != 2:
|
||||||
logger.debug('Invalid oauth bearer token format.')
|
logger.debug('Invalid oauth bearer token format.')
|
||||||
|
@ -144,6 +148,10 @@ def process_oauth(auth):
|
||||||
new_identity = QuayDeferredPermissionUser(validated.authorized_user.username, 'username',
|
new_identity = QuayDeferredPermissionUser(validated.authorized_user.username, 'username',
|
||||||
scope_set)
|
scope_set)
|
||||||
identity_changed.send(app, identity=new_identity)
|
identity_changed.send(app, identity=new_identity)
|
||||||
|
else:
|
||||||
|
logger.debug('No auth header.')
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def process_auth(f):
|
def process_auth(f):
|
||||||
|
@ -155,7 +163,6 @@ def process_auth(f):
|
||||||
logger.debug('Validating auth header: %s' % auth)
|
logger.debug('Validating auth header: %s' % auth)
|
||||||
process_token(auth)
|
process_token(auth)
|
||||||
process_basic_auth(auth)
|
process_basic_auth(auth)
|
||||||
process_oauth(auth)
|
|
||||||
else:
|
else:
|
||||||
logger.debug('No auth header.')
|
logger.debug('No auth header.')
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,7 @@ class QuayDeferredPermissionUser(Identity):
|
||||||
if self._scope_set is None:
|
if self._scope_set is None:
|
||||||
user_grant = UserNeed(user_object.username)
|
user_grant = UserNeed(user_object.username)
|
||||||
self.provides.add(user_grant)
|
self.provides.add(user_grant)
|
||||||
|
logger.debug('Add admin to user namespace: %s', user_object.username)
|
||||||
|
|
||||||
# Every user is the admin of their own 'org'
|
# Every user is the admin of their own 'org'
|
||||||
user_namespace = _OrganizationNeed(user_object.username, self._team_role_for_scopes('admin'))
|
user_namespace = _OrganizationNeed(user_object.username, self._team_role_for_scopes('admin'))
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
from flask import Blueprint, request
|
from flask import Blueprint, request, make_response
|
||||||
from flask.ext.restful import Resource, abort, Api, reqparse
|
from flask.ext.restful import Resource, abort, Api, reqparse
|
||||||
from flask.ext.restful.utils.cors import crossdomain
|
from flask.ext.restful.utils.cors import crossdomain
|
||||||
from flask.ext.login import current_user
|
|
||||||
from calendar import timegm
|
from calendar import timegm
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
|
@ -15,12 +15,15 @@ from auth.permissions import (ReadRepositoryPermission,
|
||||||
ModifyRepositoryPermission,
|
ModifyRepositoryPermission,
|
||||||
AdministerRepositoryPermission)
|
AdministerRepositoryPermission)
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
|
from auth.auth_context import get_authenticated_user
|
||||||
|
from auth.auth import process_oauth
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
api_bp = Blueprint('api', __name__)
|
api_bp = Blueprint('api', __name__)
|
||||||
api = Api(api_bp)
|
api = Api(api_bp)
|
||||||
api.decorators = [crossdomain(origin='*', headers=['Authorization'])]
|
api.decorators = [process_oauth,
|
||||||
|
crossdomain(origin='*', headers=['Authorization', 'Content-Type'])]
|
||||||
|
|
||||||
|
|
||||||
def resource(*urls, **kwargs):
|
def resource(*urls, **kwargs):
|
||||||
|
@ -111,6 +114,8 @@ def require_repo_permission(permission_class, scope, allow_public=False):
|
||||||
@add_method_metadata('oauth2_scope', scope)
|
@add_method_metadata('oauth2_scope', scope)
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapped(self, namespace, repository, *args, **kwargs):
|
def wrapped(self, namespace, repository, *args, **kwargs):
|
||||||
|
logger.debug('Checking permission %s for repo: %s/%s', permission_class, namespace,
|
||||||
|
repository)
|
||||||
permission = permission_class(namespace, repository)
|
permission = permission_class(namespace, repository)
|
||||||
if (permission.can() or
|
if (permission.can() or
|
||||||
(allow_public and
|
(allow_public and
|
||||||
|
@ -126,23 +131,41 @@ require_repo_write = require_repo_permission(ModifyRepositoryPermission, scopes.
|
||||||
require_repo_admin = require_repo_permission(AdministerRepositoryPermission, scopes.ADMIN_REPO)
|
require_repo_admin = require_repo_permission(AdministerRepositoryPermission, scopes.ADMIN_REPO)
|
||||||
|
|
||||||
|
|
||||||
|
def require_scope(scope_object):
|
||||||
|
def wrapper(func):
|
||||||
|
@add_method_metadata('oauth2_scope', scope_object['scope'])
|
||||||
|
@wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapped
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def validate_json_request(schema_name):
|
def validate_json_request(schema_name):
|
||||||
def wrapper(func):
|
def wrapper(func):
|
||||||
@add_method_metadata('request_schema', schema_name)
|
@add_method_metadata('request_schema', schema_name)
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapped(self, namespace, repository, *args, **kwargs):
|
def wrapped(self, *args, **kwargs):
|
||||||
schema = self.schemas[schema_name]
|
schema = self.schemas[schema_name]
|
||||||
try:
|
try:
|
||||||
validate(request.get_json(), schema)
|
validate(request.get_json(), schema)
|
||||||
return func(self, namespace, repository, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
except ValidationError as ex:
|
except ValidationError as ex:
|
||||||
abort(400, message=ex.message)
|
abort(400, message=ex.message)
|
||||||
return wrapped
|
return wrapped
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def request_error(exception=None, **kwargs):
|
||||||
|
data = kwargs.copy()
|
||||||
|
if exception:
|
||||||
|
data['message'] = exception.message
|
||||||
|
|
||||||
|
return json.dumps(data), 400
|
||||||
|
|
||||||
|
|
||||||
def log_action(kind, user_or_orgname, metadata={}, repo=None):
|
def log_action(kind, user_or_orgname, metadata={}, repo=None):
|
||||||
performer = current_user.db_user()
|
performer = get_authenticated_user()
|
||||||
model.log_action(kind, user_or_orgname, performer=performer, ip=request.remote_addr,
|
model.log_action(kind, user_or_orgname, performer=performer, ip=request.remote_addr,
|
||||||
metadata=metadata, repository=repo)
|
metadata=metadata, repository=repo)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from functools import wraps
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from urllib import quote
|
from urllib import quote
|
||||||
|
|
||||||
from endpoints.api import api_bp
|
from endpoints.api import api_bp, log_action
|
||||||
from data import model
|
from data import model
|
||||||
from data.plans import PLANS, get_plan
|
from data.plans import PLANS, get_plan
|
||||||
from app import app
|
from app import app
|
||||||
|
@ -66,12 +66,6 @@ def request_error(exception=None, **kwargs):
|
||||||
return make_response(jsonify(data), 400)
|
return make_response(jsonify(data), 400)
|
||||||
|
|
||||||
|
|
||||||
def log_action(kind, user_or_orgname, metadata={}, repo=None):
|
|
||||||
performer = current_user.db_user()
|
|
||||||
model.log_action(kind, user_or_orgname, performer=performer,
|
|
||||||
ip=request.remote_addr, metadata=metadata, repository=repo)
|
|
||||||
|
|
||||||
|
|
||||||
def api_login_required(f):
|
def api_login_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args, **kwargs):
|
||||||
|
@ -111,6 +105,7 @@ def org_api_call(user_call_name):
|
||||||
return internal_decorator
|
return internal_decorator
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/discovery')
|
@api_bp.route('/discovery')
|
||||||
def discovery():
|
def discovery():
|
||||||
return jsonify(get_route_data())
|
return jsonify(get_route_data())
|
||||||
|
@ -891,6 +886,7 @@ def delete_organization_team_member(orgname, teamname, membername):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository', methods=['POST'])
|
@api_bp.route('/repository', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def create_repo():
|
def create_repo():
|
||||||
|
@ -948,6 +944,7 @@ def find_repos():
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/', methods=['GET'])
|
@api_bp.route('/repository/', methods=['GET'])
|
||||||
def list_repos():
|
def list_repos():
|
||||||
def repo_view(repo_obj):
|
def repo_view(repo_obj):
|
||||||
|
@ -1003,6 +1000,7 @@ def list_repos():
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>', methods=['PUT'])
|
@api_bp.route('/repository/<path:repository>', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
@ -1025,6 +1023,7 @@ def update_repo(namespace, repository):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>/changevisibility',
|
@api_bp.route('/repository/<path:repository>/changevisibility',
|
||||||
methods=['POST'])
|
methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@ -1046,6 +1045,7 @@ def change_repo_visibility(namespace, repository):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>', methods=['DELETE'])
|
@api_bp.route('/repository/<path:repository>', methods=['DELETE'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
@ -1077,6 +1077,7 @@ def image_view(image):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>', methods=['GET'])
|
@api_bp.route('/repository/<path:repository>', methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def get_repo(namespace, repository):
|
def get_repo(namespace, repository):
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app, request
|
||||||
from flask.ext.restful import reqparse, abort
|
from flask.ext.restful import reqparse, abort
|
||||||
from flask.ext.login import current_user
|
from flask.ext.login import current_user
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
|
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
|
||||||
require_repo_read, RepositoryParamResource, resource, query_param,
|
require_repo_read, require_repo_write, require_repo_admin,
|
||||||
parse_args, ApiResource)
|
RepositoryParamResource, resource, query_param, parse_args, ApiResource,
|
||||||
|
request_error, require_scope)
|
||||||
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
||||||
AdministerRepositoryPermission)
|
AdministerRepositoryPermission, CreateRepositoryPermission)
|
||||||
from auth.auth import process_auth
|
from auth.auth import process_auth
|
||||||
|
from auth import scopes
|
||||||
|
from auth.auth_context import get_authenticated_user
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -25,14 +28,12 @@ class RepositoryList(ApiResource):
|
||||||
'id': 'NewRepo',
|
'id': 'NewRepo',
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'description': 'Description of a new repository',
|
'description': 'Description of a new repository',
|
||||||
'required': [
|
'required': True,
|
||||||
'repository',
|
|
||||||
'visibility',
|
|
||||||
],
|
|
||||||
'properties': {
|
'properties': {
|
||||||
'repository': {
|
'repository': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'Repository name',
|
'description': 'Repository name',
|
||||||
|
'required': True,
|
||||||
},
|
},
|
||||||
'visibility': {
|
'visibility': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
|
@ -40,7 +41,8 @@ class RepositoryList(ApiResource):
|
||||||
'enum': [
|
'enum': [
|
||||||
'public',
|
'public',
|
||||||
'private',
|
'private',
|
||||||
]
|
],
|
||||||
|
'required': True,
|
||||||
},
|
},
|
||||||
'namespace': {
|
'namespace': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
|
@ -55,11 +57,12 @@ class RepositoryList(ApiResource):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_scope(scopes.CREATE_REPO)
|
||||||
@nickname('createRepo')
|
@nickname('createRepo')
|
||||||
@validate_json_request('NewRepo')
|
@validate_json_request('NewRepo')
|
||||||
def post(self):
|
def post(self):
|
||||||
"""Create a new repository."""
|
"""Create a new repository."""
|
||||||
owner = current_user.db_user()
|
owner = get_authenticated_user()
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
namespace_name = req['namespace'] if 'namespace' in req else owner.username
|
namespace_name = req['namespace'] if 'namespace' in req else owner.username
|
||||||
|
|
||||||
|
@ -81,10 +84,10 @@ class RepositoryList(ApiResource):
|
||||||
|
|
||||||
log_action('create_repo', namespace_name, {'repo': repository_name,
|
log_action('create_repo', namespace_name, {'repo': repository_name,
|
||||||
'namespace': namespace_name}, repo=repo)
|
'namespace': namespace_name}, repo=repo)
|
||||||
return jsonify({
|
return {
|
||||||
'namespace': namespace_name,
|
'namespace': namespace_name,
|
||||||
'name': repository_name
|
'name': repository_name
|
||||||
})
|
}, 201
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
@ -148,7 +151,22 @@ def image_view(image):
|
||||||
@resource('/v1/repository/<path:repository>')
|
@resource('/v1/repository/<path:repository>')
|
||||||
class Repository(RepositoryParamResource):
|
class Repository(RepositoryParamResource):
|
||||||
"""Operations for managing a specific repository."""
|
"""Operations for managing a specific repository."""
|
||||||
@process_auth
|
schemas = {
|
||||||
|
'RepoUpdate': {
|
||||||
|
'id': 'RepoUpdate',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Fields which can be updated in a repository.',
|
||||||
|
'required': True,
|
||||||
|
'properties': {
|
||||||
|
'description': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Markdown encoded description for the repository',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@nickname('getRepo')
|
@nickname('getRepo')
|
||||||
def get(self, namespace, repository):
|
def get(self, namespace, repository):
|
||||||
|
@ -195,3 +213,70 @@ class Repository(RepositoryParamResource):
|
||||||
}
|
}
|
||||||
|
|
||||||
abort(404) # Not found
|
abort(404) # Not found
|
||||||
|
|
||||||
|
@require_repo_write
|
||||||
|
@nickname('updateRepo')
|
||||||
|
@validate_json_request('RepoUpdate')
|
||||||
|
def put(self, namespace, repository):
|
||||||
|
""" Update the description in the specified repository. """
|
||||||
|
repo = model.get_repository(namespace, repository)
|
||||||
|
if repo:
|
||||||
|
values = request.get_json()
|
||||||
|
repo.description = values['description']
|
||||||
|
repo.save()
|
||||||
|
|
||||||
|
log_action('set_repo_description', namespace,
|
||||||
|
{'repo': repository, 'description': values['description']},
|
||||||
|
repo=repo)
|
||||||
|
return {
|
||||||
|
'success': True
|
||||||
|
}
|
||||||
|
abort(404) # Not found
|
||||||
|
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('deleteRepository')
|
||||||
|
def delete(self, namespace, repository):
|
||||||
|
model.purge_repository(namespace, repository)
|
||||||
|
log_action('delete_repo', namespace,
|
||||||
|
{'repo': repository, 'namespace': namespace})
|
||||||
|
return 'Deleted', 204
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<path:repository>/changevisibility')
|
||||||
|
class RepositoryVisibility(RepositoryParamResource):
|
||||||
|
""" Custom verb for changing the visibility of the repository. """
|
||||||
|
schemas = {
|
||||||
|
'ChangeVisibility': {
|
||||||
|
'id': 'ChangeVisibility',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Change the visibility for the repository.',
|
||||||
|
'required': True,
|
||||||
|
'properties': {
|
||||||
|
'visibility': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Visibility which the repository will start with',
|
||||||
|
'enum': [
|
||||||
|
'public',
|
||||||
|
'private',
|
||||||
|
],
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('changeRepoVisibility')
|
||||||
|
@validate_json_request('ChangeVisibility')
|
||||||
|
def post(self, namespace, repository):
|
||||||
|
""" Change the visibility of a repository. """
|
||||||
|
repo = model.get_repository(namespace, repository)
|
||||||
|
if repo:
|
||||||
|
values = request.get_json()
|
||||||
|
model.set_repository_visibility(repo, values['visibility'])
|
||||||
|
log_action('change_repo_visibility', namespace,
|
||||||
|
{'repo': repository, 'visibility': values['visibility']},
|
||||||
|
repo=repo)
|
||||||
|
return {
|
||||||
|
'success': True
|
||||||
|
}
|
||||||
|
|
Reference in a new issue