diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index b125fee94..2116018a1 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -1,7 +1,17 @@ -from flask import Blueprint +from flask import Blueprint, request +from flask.ext.restful import Resource, abort +from flask.ext.login import current_user from calendar import timegm from email.utils import formatdate -from functools import partial +from functools import partial, wraps +from jsonschema import validate, ValidationError + +from data import model +from util.names import parse_namespace_repository +from auth.permissions import (ReadRepositoryPermission, + ModifyRepositoryPermission, + AdministerRepositoryPermission) + api = Blueprint('api', __name__) @@ -19,21 +29,74 @@ def format_date(date): def add_method_metadata(name, value): def modifier(func): if '__api_metadata' not in dir(func): - func.__metadata = {} - func.__metadata[name] = value + func.__api_metadata = {} + func.__api_metadata[name] = value return func return modifier def method_metadata(func, name): if '__api_metadata' in dir(func): - return func.__metadata.get(name, None) + return func.__api_metadata.get(name, None) return None nickname = partial(add_method_metadata, 'nickname') +def parse_repository_name(func): + @wraps(func) + def wrapper(repository, *args, **kwargs): + (namespace, repository) = parse_namespace_repository(repository) + return func(namespace, repository, *args, **kwargs) + return wrapper + + +class RepositoryParamResource(Resource): + method_decorators = [parse_repository_name] + + +def require_repo_permission(permission_class, allow_public=False): + def wrapper(func): + @wraps(func) + def wrapped(self, namespace, repository, *args, **kwargs): + permission = permission_class(namespace, repository) + if (permission.can() or + (allow_public and + model.repository_is_public(namespace, repository))): + return func(self, namespace, repository, *args, **kwargs) + abort(403) + return wrapped + return wrapper + + +require_repo_read = require_repo_permission(ReadRepositoryPermission, True) +require_repo_write = require_repo_permission(ModifyRepositoryPermission) +require_repo_admin = require_repo_permission(AdministerRepositoryPermission) + + +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): + schema = self.schemas[schema_name] + try: + validate(request.get_json(), schema) + return func(self, namespace, repository, *args, **kwargs) + except ValidationError as ex: + abort(400, message=ex.message) + return wrapped + return wrapper + + +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) + + + import endpoints.api.legacy import endpoints.api.repository diff --git a/endpoints/api/discovery.py b/endpoints/api/discovery.py index 831a03835..a1bf03980 100644 --- a/endpoints/api/discovery.py +++ b/endpoints/api/discovery.py @@ -1,6 +1,7 @@ import re from flask.ext.restful import Api, Resource +from flask.ext.restful.utils.cors import crossdomain from endpoints.api import api, method_metadata, nickname from endpoints.common import get_route_data @@ -8,11 +9,13 @@ from app import app discovery_api = Api(api) +discovery_api.decorators = [crossdomain(origin='*')] param_regex = re.compile(r'<([\w]+:)?([\w]+)>') def swagger_route_data(): apis = [] + models = {} for rule in app.url_map.iter_rules(): endpoint_method = app.view_functions[rule.endpoint] @@ -30,9 +33,22 @@ def swagger_route_data(): 'name': param, 'dataType': 'string', 'description': 'Param description.', - 'required': True + 'required': True, }) + req_schema_name = method_metadata(method, 'request_schema') + if req_schema_name: + parameters.append({ + 'paramType': 'body', + 'name': 'request_body', + 'description': 'Request body contents.', + 'dataType': req_schema_name, + 'required': True, + }) + + schema = endpoint_method.view_class.schemas[req_schema_name] + models[req_schema_name] = schema + if method is not None: operations.append({ 'method': method_name, @@ -52,7 +68,9 @@ def swagger_route_data(): 'apiVersion': 'v1', 'swaggerVersion': '1.2', 'basePath': 'https://quay.io/', + 'resourcePath': '/', 'apis': apis, + 'models': models, } return swagger_data diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index abd470259..1f9dae710 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -1,34 +1,23 @@ import logging import json -from functools import wraps -from flask.ext.restful import Resource, Api, reqparse, abort, fields +from flask.ext.restful import Resource, Api, reqparse, abort from flask.ext.login import current_user from data import model -from endpoints.api import api, truthy_bool, format_date, nickname -from util.names import parse_namespace_repository +from endpoints.api import (api, truthy_bool, format_date, nickname, log_action, + validate_json_request, require_repo_read, + RepositoryParamResource) from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission, AdministerRepositoryPermission) + logger = logging.getLogger(__name__) repo_api = Api(api) -def parse_repository_name(f): - @wraps(f) - def wrapper(repository, *args, **kwargs): - (namespace, repository) = parse_namespace_repository(repository) - return f(namespace, repository, *args, **kwargs) - return wrapper - - -class RepositoryParamResource(Resource): - method_decorators = [parse_repository_name] - - def resource(*urls, **kwargs): def wrapper(api_resource): repo_api.add_resource(api_resource, *urls, **kwargs) @@ -36,32 +25,76 @@ def resource(*urls, **kwargs): return wrapper -def require_repo_permission(permission_class, allow_public=False): - def wrapper(func): - @wraps(func) - def wrapped(self, namespace, repository, *args, **kwargs): - permission = permission_class(namespace, repository) - if (permission.can() or - (allow_public and - model.repository_is_public(namespace, repository))): - return func(self, namespace, repository, *args, **kwargs) - abort(403) - func.__required_permission = 'read' - return wrapped - return wrapper - - -require_repo_read = require_repo_permission(ReadRepositoryPermission, True) -require_repo_write = require_repo_permission(ModifyRepositoryPermission) -require_repo_admin = require_repo_permission(AdministerRepositoryPermission) - - @resource('/v1/repository') class RepositoryList(Resource): + schemas = { + 'NewRepo': { + 'id': 'NewRepo', + 'type': 'object', + 'description': 'Description of a new repository.', + 'required': [ + 'repository', + 'visibility', + ], + 'properties': { + 'repository': { + 'type': 'string', + 'description': 'Repository name.', + }, + 'visibility': { + 'type': 'string', + 'description': 'Visibility which the repository will start with.', + 'enum': [ + 'public', + 'private', + ] + }, + 'namespace': { + 'type': 'string', + 'description': ('Namespace in which the repository should be ' + 'created. If omitted, the username of the caller is' + 'used.'), + }, + 'description': { + 'type': 'string', + 'description': 'Markdown encoded description for the repository.', + }, + } + } + } @nickname('createRepo') + @validate_json_request('NewRepo') def post(self): - pass + owner = current_user.db_user() + req = request.get_json() + namespace_name = req['namespace'] if 'namespace' in req else owner.username + + permission = CreateRepositoryPermission(namespace_name) + if permission.can(): + repository_name = req['repository'] + visibility = req['visibility'] + + existing = model.get_repository(namespace_name, repository_name) + if existing: + return request_error(message='Repository already exists') + + visibility = req['visibility'] + + repo = model.create_repository(namespace_name, repository_name, owner, + visibility) + repo.description = req['description'] + repo.save() + + log_action('create_repo', namespace_name, + {'repo': repository_name, 'namespace': namespace_name}, + repo=repo) + return jsonify({ + 'namespace': namespace_name, + 'name': repository_name + }) + + abort(403) @nickname('listRepos') def get(self):