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
73
auth/auth.py
73
auth/auth.py
|
@ -108,42 +108,50 @@ def process_token(auth):
|
|||
identity_changed.send(app, identity=Identity(token_data.code, 'token'))
|
||||
|
||||
|
||||
def process_oauth(auth):
|
||||
normalized = [part.strip() for part in auth.split(' ') if part]
|
||||
if normalized[0].lower() != 'bearer' or len(normalized) != 2:
|
||||
logger.debug('Invalid oauth bearer token format.')
|
||||
return
|
||||
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]
|
||||
if normalized[0].lower() != 'bearer' or len(normalized) != 2:
|
||||
logger.debug('Invalid oauth bearer token format.')
|
||||
return
|
||||
|
||||
token = normalized[1]
|
||||
validated = oauth.validate_access_token(token)
|
||||
if not validated:
|
||||
logger.warning('OAuth access token could not be validated: %s', token)
|
||||
authenticate_header = {
|
||||
'WWW-Authenticate': ('Bearer error="invalid_token", '
|
||||
'error_description="The access token is invalid"'),
|
||||
}
|
||||
abort(401, message="OAuth access token could not be validated: %(token)",
|
||||
issue='invalid-oauth-token', token=token, header=authenticate_header)
|
||||
elif validated.expires_at <= datetime.now():
|
||||
logger.info('OAuth access with an expired token: %s', token)
|
||||
authenticate_header = {
|
||||
'WWW-Authenticate': ('Bearer error="invalid_token", '
|
||||
'error_description="The access token expired"'),
|
||||
}
|
||||
abort(401, message="OAuth access token has expired: %(token)", issue='invalid-oauth-token',
|
||||
token=token, headers=authenticate_header)
|
||||
token = normalized[1]
|
||||
validated = oauth.validate_access_token(token)
|
||||
if not validated:
|
||||
logger.warning('OAuth access token could not be validated: %s', token)
|
||||
authenticate_header = {
|
||||
'WWW-Authenticate': ('Bearer error="invalid_token", '
|
||||
'error_description="The access token is invalid"'),
|
||||
}
|
||||
abort(401, message="OAuth access token could not be validated: %(token)",
|
||||
issue='invalid-oauth-token', token=token, header=authenticate_header)
|
||||
elif validated.expires_at <= datetime.now():
|
||||
logger.info('OAuth access with an expired token: %s', token)
|
||||
authenticate_header = {
|
||||
'WWW-Authenticate': ('Bearer error="invalid_token", '
|
||||
'error_description="The access token expired"'),
|
||||
}
|
||||
abort(401, message="OAuth access token has expired: %(token)", issue='invalid-oauth-token',
|
||||
token=token, headers=authenticate_header)
|
||||
|
||||
# We have a valid token
|
||||
scope_set = scopes.scopes_from_scope_string(validated.scope)
|
||||
logger.debug('Successfully validated oauth access token: %s with scope: %s', token,
|
||||
scope_set)
|
||||
# We have a valid token
|
||||
scope_set = scopes.scopes_from_scope_string(validated.scope)
|
||||
logger.debug('Successfully validated oauth access token: %s with scope: %s', token,
|
||||
scope_set)
|
||||
|
||||
ctx = _request_ctx_stack.top
|
||||
ctx.authenticated_user = validated.authorized_user
|
||||
ctx = _request_ctx_stack.top
|
||||
ctx.authenticated_user = validated.authorized_user
|
||||
|
||||
new_identity = QuayDeferredPermissionUser(validated.authorized_user.username, 'username',
|
||||
scope_set)
|
||||
identity_changed.send(app, identity=new_identity)
|
||||
new_identity = QuayDeferredPermissionUser(validated.authorized_user.username, 'username',
|
||||
scope_set)
|
||||
identity_changed.send(app, identity=new_identity)
|
||||
else:
|
||||
logger.debug('No auth header.')
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def process_auth(f):
|
||||
|
@ -155,7 +163,6 @@ def process_auth(f):
|
|||
logger.debug('Validating auth header: %s' % auth)
|
||||
process_token(auth)
|
||||
process_basic_auth(auth)
|
||||
process_oauth(auth)
|
||||
else:
|
||||
logger.debug('No auth header.')
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ class QuayDeferredPermissionUser(Identity):
|
|||
if self._scope_set is None:
|
||||
user_grant = UserNeed(user_object.username)
|
||||
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'
|
||||
user_namespace = _OrganizationNeed(user_object.username, self._team_role_for_scopes('admin'))
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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.utils.cors import crossdomain
|
||||
from flask.ext.login import current_user
|
||||
from calendar import timegm
|
||||
from email.utils import formatdate
|
||||
from functools import partial, wraps
|
||||
|
@ -15,12 +15,15 @@ from auth.permissions import (ReadRepositoryPermission,
|
|||
ModifyRepositoryPermission,
|
||||
AdministerRepositoryPermission)
|
||||
from auth import scopes
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth.auth import process_oauth
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
api_bp = Blueprint('api', __name__)
|
||||
api = Api(api_bp)
|
||||
api.decorators = [crossdomain(origin='*', headers=['Authorization'])]
|
||||
api.decorators = [process_oauth,
|
||||
crossdomain(origin='*', headers=['Authorization', 'Content-Type'])]
|
||||
|
||||
|
||||
def resource(*urls, **kwargs):
|
||||
|
@ -111,6 +114,8 @@ def require_repo_permission(permission_class, scope, allow_public=False):
|
|||
@add_method_metadata('oauth2_scope', scope)
|
||||
@wraps(func)
|
||||
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)
|
||||
if (permission.can() or
|
||||
(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)
|
||||
|
||||
|
||||
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 wrapper(func):
|
||||
@add_method_metadata('request_schema', schema_name)
|
||||
@wraps(func)
|
||||
def wrapped(self, namespace, repository, *args, **kwargs):
|
||||
def wrapped(self, *args, **kwargs):
|
||||
schema = self.schemas[schema_name]
|
||||
try:
|
||||
validate(request.get_json(), schema)
|
||||
return func(self, namespace, repository, *args, **kwargs)
|
||||
return func(self, *args, **kwargs)
|
||||
except ValidationError as ex:
|
||||
abort(400, message=ex.message)
|
||||
return wrapped
|
||||
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):
|
||||
performer = current_user.db_user()
|
||||
performer = get_authenticated_user()
|
||||
model.log_action(kind, user_or_orgname, performer=performer, ip=request.remote_addr,
|
||||
metadata=metadata, repository=repo)
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from functools import wraps
|
|||
from collections import defaultdict
|
||||
from urllib import quote
|
||||
|
||||
from endpoints.api import api_bp
|
||||
from endpoints.api import api_bp, log_action
|
||||
from data import model
|
||||
from data.plans import PLANS, get_plan
|
||||
from app import app
|
||||
|
@ -66,12 +66,6 @@ def request_error(exception=None, **kwargs):
|
|||
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):
|
||||
@wraps(f)
|
||||
def decorated_view(*args, **kwargs):
|
||||
|
@ -111,6 +105,7 @@ def org_api_call(user_call_name):
|
|||
return internal_decorator
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/discovery')
|
||||
def discovery():
|
||||
return jsonify(get_route_data())
|
||||
|
@ -891,6 +886,7 @@ def delete_organization_team_member(orgname, teamname, membername):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/repository', methods=['POST'])
|
||||
@api_login_required
|
||||
def create_repo():
|
||||
|
@ -948,6 +944,7 @@ def find_repos():
|
|||
return jsonify(response)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/repository/', methods=['GET'])
|
||||
def list_repos():
|
||||
def repo_view(repo_obj):
|
||||
|
@ -1003,6 +1000,7 @@ def list_repos():
|
|||
return jsonify(response)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/repository/<path:repository>', methods=['PUT'])
|
||||
@api_login_required
|
||||
@parse_repository_name
|
||||
|
@ -1025,6 +1023,7 @@ def update_repo(namespace, repository):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/repository/<path:repository>/changevisibility',
|
||||
methods=['POST'])
|
||||
@api_login_required
|
||||
|
@ -1046,6 +1045,7 @@ def change_repo_visibility(namespace, repository):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/repository/<path:repository>', methods=['DELETE'])
|
||||
@api_login_required
|
||||
@parse_repository_name
|
||||
|
@ -1077,6 +1077,7 @@ def image_view(image):
|
|||
}
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/repository/<path:repository>', methods=['GET'])
|
||||
@parse_repository_name
|
||||
def get_repo(namespace, repository):
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
import logging
|
||||
import json
|
||||
|
||||
from flask import current_app
|
||||
from flask import current_app, request
|
||||
from flask.ext.restful import reqparse, abort
|
||||
from flask.ext.login import current_user
|
||||
|
||||
from data import model
|
||||
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
|
||||
require_repo_read, RepositoryParamResource, resource, query_param,
|
||||
parse_args, ApiResource)
|
||||
require_repo_read, require_repo_write, require_repo_admin,
|
||||
RepositoryParamResource, resource, query_param, parse_args, ApiResource,
|
||||
request_error, require_scope)
|
||||
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
||||
AdministerRepositoryPermission)
|
||||
AdministerRepositoryPermission, CreateRepositoryPermission)
|
||||
from auth.auth import process_auth
|
||||
from auth import scopes
|
||||
from auth.auth_context import get_authenticated_user
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -25,14 +28,12 @@ class RepositoryList(ApiResource):
|
|||
'id': 'NewRepo',
|
||||
'type': 'object',
|
||||
'description': 'Description of a new repository',
|
||||
'required': [
|
||||
'repository',
|
||||
'visibility',
|
||||
],
|
||||
'required': True,
|
||||
'properties': {
|
||||
'repository': {
|
||||
'type': 'string',
|
||||
'description': 'Repository name',
|
||||
'required': True,
|
||||
},
|
||||
'visibility': {
|
||||
'type': 'string',
|
||||
|
@ -40,7 +41,8 @@ class RepositoryList(ApiResource):
|
|||
'enum': [
|
||||
'public',
|
||||
'private',
|
||||
]
|
||||
],
|
||||
'required': True,
|
||||
},
|
||||
'namespace': {
|
||||
'type': 'string',
|
||||
|
@ -55,11 +57,12 @@ class RepositoryList(ApiResource):
|
|||
}
|
||||
}
|
||||
|
||||
@require_scope(scopes.CREATE_REPO)
|
||||
@nickname('createRepo')
|
||||
@validate_json_request('NewRepo')
|
||||
def post(self):
|
||||
"""Create a new repository."""
|
||||
owner = current_user.db_user()
|
||||
owner = get_authenticated_user()
|
||||
req = request.get_json()
|
||||
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,
|
||||
'namespace': namespace_name}, repo=repo)
|
||||
return jsonify({
|
||||
return {
|
||||
'namespace': namespace_name,
|
||||
'name': repository_name
|
||||
})
|
||||
}, 201
|
||||
|
||||
abort(403)
|
||||
|
||||
|
@ -148,7 +151,22 @@ def image_view(image):
|
|||
@resource('/v1/repository/<path:repository>')
|
||||
class Repository(RepositoryParamResource):
|
||||
"""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
|
||||
@nickname('getRepo')
|
||||
def get(self, namespace, repository):
|
||||
|
@ -195,3 +213,70 @@ class Repository(RepositoryParamResource):
|
|||
}
|
||||
|
||||
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