Port a few more repository methods to the new API interface.

This commit is contained in:
jakedt 2014-03-12 20:33:57 -04:00
parent e74eb3ee87
commit 0e3fe8f3b1
5 changed files with 176 additions and 59 deletions

View file

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

View file

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

View file

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

View file

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

View file

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