Merge remote-tracking branch 'origin/master' into tagyourit

Conflicts:
	endpoints/api.py
	static/js/app.js
	static/partials/view-repo.html
	test/data/test.db
	test/specs.py
	test/test_api_usage.py
This commit is contained in:
jakedt 2014-03-26 19:42:29 -04:00
commit 302bfb27ae
123 changed files with 16314 additions and 3789 deletions

281
endpoints/api/__init__.py Normal file
View file

@ -0,0 +1,281 @@
import logging
import json
from flask import Blueprint, request, make_response, jsonify
from flask.ext.restful import Resource, abort, Api, reqparse
from flask.ext.restful.utils.cors import crossdomain
from werkzeug.exceptions import HTTPException
from calendar import timegm
from email.utils import formatdate
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, UserReadPermission,
UserAdminPermission)
from auth import scopes
from auth.auth_context import get_authenticated_user, get_validated_oauth_token
from auth.auth import process_oauth
from endpoints.csrf import csrf_protect
logger = logging.getLogger(__name__)
api_bp = Blueprint('api', __name__)
api = Api()
api.init_app(api_bp)
api.decorators = [csrf_protect,
process_oauth,
crossdomain(origin='*', headers=['Authorization', 'Content-Type'])]
class ApiException(Exception):
def __init__(self, error_type, status_code, error_description, payload=None):
Exception.__init__(self)
self.error_description = error_description
self.status_code = status_code
self.payload = payload
self.error_type = error_type
def to_dict(self):
rv = dict(self.payload or ())
if self.error_description is not None:
rv['error_description'] = self.error_description
rv['error_type'] = self.error_type
return rv
class InvalidRequest(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'invalid_request', 400, error_description, payload)
class InvalidToken(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'invalid_token', 401, error_description, payload)
class Unauthorized(ApiException):
def __init__(self, payload=None):
user = get_authenticated_user()
if user is None or user.organization:
ApiException.__init__(self, 'invalid_token', 401, "Requires authentication", payload)
else:
ApiException.__init__(self, 'insufficient_scope', 403, 'Unauthorized', payload)
class NotFound(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, None, 404, 'Not Found', payload)
@api_bp.app_errorhandler(ApiException)
@crossdomain(origin='*', headers=['Authorization', 'Content-Type'])
def handle_api_error(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code
if error.error_type is not None:
response.headers['WWW-Authenticate'] = ('Bearer error="%s" error_description="%s"' %
(error.error_type, error.error_description))
return response
def resource(*urls, **kwargs):
def wrapper(api_resource):
api.add_resource(api_resource, *urls, **kwargs)
return api_resource
return wrapper
def truthy_bool(param):
return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'}
def format_date(date):
""" Output an RFC822 date format. """
if date is None:
return None
return formatdate(timegm(date.utctimetuple()))
def add_method_metadata(name, value):
def modifier(func):
if '__api_metadata' not in dir(func):
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.__api_metadata.get(name, None)
return None
nickname = partial(add_method_metadata, 'nickname')
related_user_resource = partial(add_method_metadata, 'related_user_resource')
internal_only = add_method_metadata('internal', True)
def query_param(name, help_str, type=reqparse.text_type, default=None,
choices=(), required=False):
def add_param(func):
if '__api_query_params' not in dir(func):
func.__api_query_params = []
func.__api_query_params.append({
'name': name,
'type': type,
'help': help_str,
'default': default,
'choices': choices,
'required': required,
})
return func
return add_param
def parse_args(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if '__api_query_params' not in dir(func):
abort(500)
parser = reqparse.RequestParser()
for arg_spec in func.__api_query_params:
parser.add_argument(**arg_spec)
parsed_args = parser.parse_args()
return func(self, parsed_args, *args, **kwargs)
return wrapper
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 ApiResource(Resource):
def options(self):
return None, 200
class RepositoryParamResource(ApiResource):
method_decorators = [parse_repository_name]
def require_repo_permission(permission_class, scope, allow_public=False):
def wrapper(func):
@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
model.repository_is_public(namespace, repository))):
return func(self, namespace, repository, *args, **kwargs)
raise Unauthorized()
return wrapped
return wrapper
require_repo_read = require_repo_permission(ReadRepositoryPermission, scopes.READ_REPO, True)
require_repo_write = require_repo_permission(ModifyRepositoryPermission, scopes.WRITE_REPO)
require_repo_admin = require_repo_permission(AdministerRepositoryPermission, scopes.ADMIN_REPO)
def require_user_permission(permission_class, scope=None):
def wrapper(func):
@add_method_metadata('oauth2_scope', scope)
@wraps(func)
def wrapped(self, *args, **kwargs):
user = get_authenticated_user()
if not user:
raise Unauthorized()
logger.debug('Checking permission %s for user %s', permission_class, user.username)
permission = permission_class(user.username)
if permission.can():
return func(self, *args, **kwargs)
raise Unauthorized()
return wrapped
return wrapper
require_user_read = require_user_permission(UserReadPermission, scopes.READ_USER)
require_user_admin = require_user_permission(UserAdminPermission, None)
def require_scope(scope_object):
def wrapper(func):
@add_method_metadata('oauth2_scope', scope_object)
@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, *args, **kwargs):
schema = self.schemas[schema_name]
try:
validate(request.get_json(), schema)
return func(self, *args, **kwargs)
except ValidationError as ex:
raise InvalidRequest(ex.message)
return wrapped
return wrapper
def request_error(exception=None, **kwargs):
data = kwargs.copy()
message = 'Request error.'
if exception:
message = exception.message
raise InvalidRequest(message, data)
def log_action(kind, user_or_orgname, metadata=None, repo=None):
if not metadata:
metadata = {}
oauth_token = get_validated_oauth_token()
if oauth_token:
metadata['oauth_token_id'] = oauth_token.id
metadata['oauth_token_application_id'] = oauth_token.application.client_id
metadata['oauth_token_application'] = oauth_token.application.name
performer = get_authenticated_user()
model.log_action(kind, user_or_orgname, performer=performer, ip=request.remote_addr,
metadata=metadata, repository=repo)
import endpoints.api.billing
import endpoints.api.build
import endpoints.api.discovery
import endpoints.api.image
import endpoints.api.logs
import endpoints.api.organization
import endpoints.api.permission
import endpoints.api.prototype
import endpoints.api.repository
import endpoints.api.repotoken
import endpoints.api.robot
import endpoints.api.search
import endpoints.api.tag
import endpoints.api.team
import endpoints.api.trigger
import endpoints.api.user
import endpoints.api.webhook

326
endpoints/api/billing.py Normal file
View file

@ -0,0 +1,326 @@
import stripe
from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin)
from endpoints.api.subscribe import subscribe, subscription_view
from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from data import model
from data.plans import PLANS
def carderror_response(e):
return {'carderror': e.message}, 402
def get_card(user):
card_info = {
'is_valid': False
}
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
if cus and cus.default_card:
# Find the default card.
default_card = None
for card in cus.cards.data:
if card.id == cus.default_card:
default_card = card
break
if default_card:
card_info = {
'owner': default_card.name,
'type': default_card.type,
'last4': default_card.last4
}
return {'card': card_info}
def set_card(user, token):
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
if cus:
try:
cus.card = token
cus.save()
except stripe.CardError as exc:
return carderror_response(exc)
except stripe.InvalidRequestError as exc:
return carderror_response(exc)
return get_card(user)
def get_invoices(customer_id):
def invoice_view(i):
return {
'id': i.id,
'date': i.date,
'period_start': i.period_start,
'period_end': i.period_end,
'paid': i.paid,
'amount_due': i.amount_due,
'next_payment_attempt': i.next_payment_attempt,
'attempted': i.attempted,
'closed': i.closed,
'total': i.total,
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
}
invoices = stripe.Invoice.all(customer=customer_id, count=12)
return {
'invoices': [invoice_view(i) for i in invoices.data]
}
@resource('/v1/plans/')
class ListPlans(ApiResource):
""" Resource for listing the available plans. """
@nickname('listPlans')
def get(self):
""" List the avaialble plans. """
return {
'plans': PLANS,
}
@resource('/v1/user/card')
@internal_only
class UserCard(ApiResource):
""" Resource for managing a user's credit card. """
schemas = {
'UserCard': {
'id': 'UserCard',
'type': 'object',
'description': 'Description of a user card',
'required': [
'token',
],
'properties': {
'token': {
'type': 'string',
'description': 'Stripe token that is generated by stripe checkout.js',
},
},
},
}
@require_user_admin
@nickname('getUserCard')
def get(self):
""" Get the user's credit card. """
user = get_authenticated_user()
return get_card(user)
@require_user_admin
@nickname('setUserCard')
@validate_json_request('UserCard')
def post(self):
""" Update the user's credit card. """
user = get_authenticated_user()
token = request.get_json()['token']
response = set_card(user, token)
log_action('account_change_cc', user.username)
return response
@resource('/v1/organization/<orgname>/card')
@internal_only
@related_user_resource(UserCard)
class OrganizationCard(ApiResource):
""" Resource for managing an organization's credit card. """
schemas = {
'OrgCard': {
'id': 'OrgCard',
'type': 'object',
'description': 'Description of a user card',
'required': [
'token',
],
'properties': {
'token': {
'type': 'string',
'description': 'Stripe token that is generated by stripe checkout.js',
},
},
},
}
@nickname('getOrgCard')
def get(self, orgname):
""" Get the organization's credit card. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.get_organization(orgname)
return get_card(organization)
raise Unauthorized()
@nickname('setOrgCard')
@validate_json_request('OrgCard')
def post(self, orgname):
""" Update the orgnaization's credit card. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.get_organization(orgname)
token = request.get_json()['token']
response = set_card(organization, token)
log_action('account_change_cc', orgname)
return response
raise Unauthorized()
@resource('/v1/user/plan')
@internal_only
class UserPlan(ApiResource):
""" Resource for managing a user's subscription. """
schemas = {
'UserSubscription': {
'id': 'UserSubscription',
'type': 'object',
'description': 'Description of a user card',
'required': [
'plan',
],
'properties': {
'token': {
'type': 'string',
'description': 'Stripe token that is generated by stripe checkout.js',
},
'plan': {
'type': 'string',
'description': 'Plan name to which the user wants to subscribe',
},
},
},
}
@require_user_admin
@nickname('updateUserSubscription')
@validate_json_request('UserSubscription')
def put(self):
""" Create or update the user's subscription. """
request_data = request.get_json()
plan = request_data['plan']
token = request_data['token'] if 'token' in request_data else None
user = get_authenticated_user()
return subscribe(user, plan, token, False) # Business features not required
@require_user_admin
@nickname('getUserSubscription')
def get(self):
""" Fetch any existing subscription for the user. """
user = get_authenticated_user()
private_repos = model.get_private_repo_count(user.username)
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
if cus.subscription:
return subscription_view(cus.subscription, private_repos)
return {
'plan': 'free',
'usedPrivateRepos': private_repos,
}
@resource('/v1/organization/<orgname>/plan')
@internal_only
@related_user_resource(UserPlan)
class OrganizationPlan(ApiResource):
""" Resource for managing a org's subscription. """
schemas = {
'OrgSubscription': {
'id': 'OrgSubscription',
'type': 'object',
'description': 'Description of a user card',
'required': [
'plan',
],
'properties': {
'token': {
'type': 'string',
'description': 'Stripe token that is generated by stripe checkout.js',
},
'plan': {
'type': 'string',
'description': 'Plan name to which the user wants to subscribe',
},
},
},
}
@nickname('updateOrgSubscription')
@validate_json_request('OrgSubscription')
def put(self, orgname):
""" Create or update the org's subscription. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
request_data = request.get_json()
plan = request_data['plan']
token = request_data['token'] if 'token' in request_data else None
organization = model.get_organization(orgname)
return subscribe(organization, plan, token, True) # Business plan required
raise Unauthorized()
@nickname('getOrgSubscription')
def get(self, orgname):
""" Fetch any existing subscription for the org. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
private_repos = model.get_private_repo_count(orgname)
organization = model.get_organization(orgname)
if organization.stripe_id:
cus = stripe.Customer.retrieve(organization.stripe_id)
if cus.subscription:
return subscription_view(cus.subscription, private_repos)
return {
'plan': 'free',
'usedPrivateRepos': private_repos,
}
raise Unauthorized()
@resource('/v1/user/invoices')
@internal_only
class UserInvoiceList(ApiResource):
""" Resource for listing a user's invoices. """
@require_user_admin
@nickname('listUserInvoices')
def get(self):
""" List the invoices for the current user. """
user = get_authenticated_user()
if not user.stripe_id:
raise NotFound()
return get_invoices(user.stripe_id)
@resource('/v1/organization/<orgname>/invoices')
@internal_only
@related_user_resource(UserInvoiceList)
class OrgnaizationInvoiceList(ApiResource):
""" Resource for listing an orgnaization's invoices. """
@nickname('listOrgInvoices')
def get(self, orgname):
""" List the invoices for the specified orgnaization. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.get_organization(orgname)
if not organization.stripe_id:
raise NotFound()
return get_invoices(organization.stripe_id)
raise Unauthorized()

213
endpoints/api/build.py Normal file
View file

@ -0,0 +1,213 @@
import logging
import json
from flask import request
from app import app
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
require_repo_read, require_repo_write, validate_json_request,
ApiResource, internal_only, format_date, api, Unauthorized, NotFound)
from endpoints.common import start_build
from endpoints.trigger import BuildTrigger
from data import model
from auth.permissions import ModifyRepositoryPermission
logger = logging.getLogger(__name__)
user_files = app.config['USERFILES']
build_logs = app.config['BUILDLOGS']
def get_trigger_config(trigger):
try:
return json.loads(trigger.config)
except:
return {}
def get_job_config(build_obj):
try:
return json.loads(build_obj.job_config)
except:
return None
def trigger_view(trigger):
if trigger and trigger.uuid:
config_dict = get_trigger_config(trigger)
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
return {
'service': trigger.service.name,
'config': config_dict,
'id': trigger.uuid,
'connected_user': trigger.connected_user.username,
'is_active': build_trigger.is_active(config_dict)
}
return None
def build_status_view(build_obj, can_write=False):
status = build_logs.get_status(build_obj.uuid)
logger.debug('Can write: %s job_config: %s', can_write, build_obj.job_config)
resp = {
'id': build_obj.uuid,
'phase': build_obj.phase if status else 'cannot_load',
'started': format_date(build_obj.started),
'display_name': build_obj.display_name,
'status': status or {},
'job_config': get_job_config(build_obj) if can_write else None,
'is_writer': can_write,
'trigger': trigger_view(build_obj.trigger),
'resource_key': build_obj.resource_key,
}
if can_write:
resp['archive_url'] = user_files.get_file_url(build_obj.resource_key)
return resp
@resource('/v1/repository/<repopath:repository>/build/')
class RepositoryBuildList(RepositoryParamResource):
""" Resource related to creating and listing repository builds. """
schemas = {
'RepositoryBuildRequest': {
'id': 'RepositoryBuildRequest',
'type': 'object',
'description': 'Description of a new repository build.',
'required': [
'file_id',
],
'properties': {
'file_id': {
'type': 'string',
'description': 'The file id that was generated when the build spec was uploaded',
},
'subdirectory': {
'type': 'string',
'description': 'Subdirectory in which the Dockerfile can be found',
},
},
},
}
@require_repo_read
@parse_args
@query_param('limit', 'The maximum number of builds to return', type=int, default=5)
@nickname('getRepoBuilds')
def get(self, args, namespace, repository):
""" Get the list of repository builds. """
limit = args['limit']
builds = list(model.list_repository_builds(namespace, repository, limit))
can_write = ModifyRepositoryPermission(namespace, repository).can()
return {
'builds': [build_status_view(build, can_write) for build in builds]
}
@require_repo_write
@nickname('requestRepoBuild')
@validate_json_request('RepositoryBuildRequest')
def post(self, namespace, repository):
""" Request that a repository be built and pushed from the specified input. """
logger.debug('User requested repository initialization.')
request_json = request.get_json()
dockerfile_id = request_json['file_id']
subdir = request_json['subdirectory'] if 'subdirectory' in request_json else ''
# Check if the dockerfile resource has already been used. If so, then it
# can only be reused if the user has access to the repository for which it
# was used.
associated_repository = model.get_repository_for_resource(dockerfile_id)
if associated_repository:
if not ModifyRepositoryPermission(associated_repository.namespace,
associated_repository.name):
raise Unauthorized()
# Start the build.
repo = model.get_repository(namespace, repository)
display_name = user_files.get_file_checksum(dockerfile_id)
build_request = start_build(repo, dockerfile_id, ['latest'], display_name, subdir, True)
resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository)
headers = {
'Location': api.url_for(RepositoryBuildStatus, repository=repo_string,
build_uuid=build_request.uuid),
}
return resp, 201, headers
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/status')
class RepositoryBuildStatus(RepositoryParamResource):
""" Resource for dealing with repository build status. """
@require_repo_read
@nickname('getRepoBuildStatus')
def get(self, namespace, repository, build_uuid):
""" Return the status for the builds specified by the build uuids. """
build = model.get_repository_build(namespace, repository, build_uuid)
if not build:
raise NotFound()
can_write = ModifyRepositoryPermission(namespace, repository).can()
return build_status_view(build, can_write)
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/logs')
class RepositoryBuildLogs(RepositoryParamResource):
""" Resource for loading repository build logs. """
@require_repo_write
@nickname('getRepoBuildLogs')
def get(self, namespace, repository, build_uuid):
""" Return the build logs for the build specified by the build uuid. """
response_obj = {}
build = model.get_repository_build(namespace, repository, build_uuid)
start = int(request.args.get('start', 0))
count, logs = build_logs.get_log_entries(build.uuid, start)
response_obj.update({
'start': start,
'total': count,
'logs': [log for log in logs],
})
return response_obj
@resource('/v1/filedrop/')
@internal_only
class FileDropResource(ApiResource):
""" Custom verb for setting up a client side file transfer. """
schemas = {
'FileDropRequest': {
'id': 'FileDropRequest',
'type': 'object',
'description': 'Description of the file that the user wishes to upload.',
'required': [
'mimeType',
],
'properties': {
'mimeType': {
'type': 'string',
'description': 'Type of the file which is about to be uploaded',
},
},
},
}
@nickname('getFiledropUrl')
@validate_json_request('FileDropRequest')
def post(self):
""" Request a URL to which a file may be uploaded. """
mime_type = request.get_json()['mimeType']
(url, file_id) = user_files.prepare_for_drop(mime_type)
return {
'url': url,
'file_id': str(file_id),
}

184
endpoints/api/discovery.py Normal file
View file

@ -0,0 +1,184 @@
import re
import logging
from flask.ext.restful import reqparse
from endpoints.api import (ApiResource, resource, method_metadata, nickname, truthy_bool,
parse_args, query_param)
from app import app
from auth import scopes
logger = logging.getLogger(__name__)
PARAM_REGEX = re.compile(r'<([\w]+:)?([\w]+)>')
TYPE_CONVERTER = {
truthy_bool: 'boolean',
str: 'string',
basestring: 'string',
reqparse.text_type: 'string',
int: 'integer',
}
URL_SCHEME = app.config['URL_SCHEME']
URL_HOST = app.config['URL_HOST']
def fully_qualified_name(method_view_class):
inst = method_view_class()
return '%s.%s' % (inst.__module__, inst.__class__.__name__)
def swagger_route_data(include_internal=False, compact=False):
apis = []
models = {}
for rule in app.url_map.iter_rules():
endpoint_method = app.view_functions[rule.endpoint]
if 'view_class' in dir(endpoint_method):
view_class = endpoint_method.view_class
operations = []
method_names = list(rule.methods.difference(['HEAD', 'OPTIONS']))
for method_name in method_names:
method = getattr(view_class, method_name.lower(), None)
parameters = []
for param in rule.arguments:
parameters.append({
'paramType': 'path',
'name': param,
'dataType': 'string',
'description': 'Param description.',
'required': True,
})
if method is None:
logger.debug('Unable to find method for %s in class %s', method_name, view_class)
else:
req_schema_name = method_metadata(method, 'request_schema')
if req_schema_name:
parameters.append({
'paramType': 'body',
'name': 'body',
'description': 'Request body contents.',
'dataType': req_schema_name,
'required': True,
})
schema = view_class.schemas[req_schema_name]
models[req_schema_name] = schema
if '__api_query_params' in dir(method):
for param_spec in method.__api_query_params:
new_param = {
'paramType': 'query',
'name': param_spec['name'],
'description': param_spec['help'],
'dataType': TYPE_CONVERTER[param_spec['type']],
'required': param_spec['required'],
}
if len(param_spec['choices']) > 0:
new_param['enum'] = list(param_spec['choices'])
parameters.append(new_param)
new_operation = {
'method': method_name,
'nickname': method_metadata(method, 'nickname')
}
if not compact:
new_operation.update({
'type': 'void',
'summary': method.__doc__ if method.__doc__ else '',
'parameters': parameters,
})
scope = method_metadata(method, 'oauth2_scope')
if scope and not compact:
new_operation['authorizations'] = {
'oauth2': [
{
'scope': scope.scope
}
],
}
internal = method_metadata(method, 'internal')
if internal is not None:
new_operation['internal'] = True
if not internal or (internal and include_internal):
operations.append(new_operation)
swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule)
new_resource = {
'path': swagger_path,
'description': view_class.__doc__ if view_class.__doc__ else "",
'operations': operations,
'name': fully_qualified_name(view_class),
}
related_user_res = method_metadata(view_class, 'related_user_resource')
if related_user_res is not None:
new_resource['quayUserRelated'] = fully_qualified_name(related_user_res)
internal = method_metadata(view_class, 'internal')
if internal is not None:
new_resource['internal'] = True
if not internal or (internal and include_internal):
apis.append(new_resource)
# If compact form was requested, simply return the APIs.
if compact:
return {'apis': apis}
swagger_data = {
'apiVersion': 'v1',
'swaggerVersion': '1.2',
'basePath': '%s://%s' % (URL_SCHEME, URL_HOST),
'resourcePath': '/',
'info': {
'title': 'Quay.io API',
'description': ('This API allows you to perform many of the operations required to work '
'with Quay.io repositories, users, and organizations. You can find out more '
'at <a href="https://quay.io">Quay.io</a>.'),
'termsOfServiceUrl': 'https://quay.io/tos',
'contact': 'support@quay.io',
},
'authorizations': {
'oauth2': {
'scopes': [scope._asdict() for scope in scopes.ALL_SCOPES.values()],
'grantTypes': {
"implicit": {
"tokenName": "access_token",
"loginEndpoint": {
"url": "%s://%s/oauth/authorize" % (URL_SCHEME, URL_HOST),
},
},
},
},
},
'apis': apis,
'models': models,
}
return swagger_data
@resource('/v1/discovery')
class DiscoveryResource(ApiResource):
"""Ability to inspect the API for usage information and documentation."""
@parse_args
@query_param('internal', 'Whether to include internal APIs.', type=truthy_bool, default=False)
@nickname('discovery')
def get(self, args):
""" List all of the API endpoints available in the swagger API format."""
return swagger_route_data(args['internal'])

92
endpoints/api/image.py Normal file
View file

@ -0,0 +1,92 @@
import json
from collections import defaultdict
from app import app
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
format_date, NotFound)
from data import model
from util.cache import cache_control_flask_restful
store = app.config['STORAGE']
def image_view(image):
extended_props = image
if image.storage and image.storage.id:
extended_props = image.storage
command = extended_props.command
return {
'id': image.docker_image_id,
'created': format_date(extended_props.created),
'comment': extended_props.comment,
'command': json.loads(command) if command else None,
'ancestors': image.ancestors,
'dbid': image.id,
'size': extended_props.image_size,
}
@resource('/v1/repository/<repopath:repository>/image/')
class RepositoryImageList(RepositoryParamResource):
""" Resource for listing repository images. """
@require_repo_read
@nickname('listRepositoryImages')
def get(self, namespace, repository):
""" List the images for the specified repository. """
all_images = model.get_repository_images(namespace, repository)
all_tags = model.list_repository_tags(namespace, repository)
tags_by_image_id = defaultdict(list)
for tag in all_tags:
tags_by_image_id[tag.image.docker_image_id].append(tag.name)
def add_tags(image_json):
image_json['tags'] = tags_by_image_id[image_json['id']]
return image_json
return {
'images': [add_tags(image_view(image)) for image in all_images]
}
@resource('/v1/repository/<repopath:repository>/image/<image_id>')
class RepositoryImage(RepositoryParamResource):
""" Resource for handling repository images. """
@require_repo_read
@nickname('getImage')
def get(self, namespace, repository, image_id):
""" Get the information available for the specified image. """
image = model.get_repo_image(namespace, repository, image_id)
if not image:
raise NotFound()
return image_view(image)
@resource('/v1/repository/<repopath:repository>/image/<image_id>/changes')
class RepositoryImageChanges(RepositoryParamResource):
""" Resource for handling repository image change lists. """
@cache_control_flask_restful(max_age=60*60) # Cache for one hour
@require_repo_read
@nickname('getImageChanges')
def get(self, namespace, repository, image_id):
""" Get the list of changes for the specified image. """
image = model.get_repo_image(namespace, repository, image_id)
if not image:
raise NotFound()
uuid = image.storage and image.storage.uuid
diffs_path = store.image_file_diffs_path(namespace, repository, image_id, uuid)
try:
response_json = json.loads(store.get_content(diffs_path))
return response_json
except IOError:
raise NotFound()

126
endpoints/api/logs.py Normal file
View file

@ -0,0 +1,126 @@
import json
from datetime import datetime, timedelta
from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args,
RepositoryParamResource, require_repo_admin, related_user_resource,
format_date, Unauthorized, NotFound, require_user_admin,
internal_only)
from auth.permissions import AdministerOrganizationPermission, AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from data import model
def log_view(log):
view = {
'kind': log.kind.name,
'metadata': json.loads(log.metadata_json),
'ip': log.ip,
'datetime': format_date(log.datetime),
}
if log.performer:
view['performer'] = {
'kind': 'user',
'name': log.performer.username,
'is_robot': log.performer.robot,
}
return view
def get_logs(namespace, start_time, end_time, performer_name=None,
repository=None):
performer = None
if performer_name:
performer = model.get_user(performer_name)
if start_time:
try:
start_time = datetime.strptime(start_time + ' UTC', '%m/%d/%Y %Z')
except ValueError:
start_time = None
if not start_time:
start_time = datetime.today() - timedelta(7) # One week
if end_time:
try:
end_time = datetime.strptime(end_time + ' UTC', '%m/%d/%Y %Z')
end_time = end_time + timedelta(days=1)
except ValueError:
end_time = None
if not end_time:
end_time = datetime.today()
logs = model.list_logs(namespace, start_time, end_time, performer=performer,
repository=repository)
return {
'start_time': format_date(start_time),
'end_time': format_date(end_time),
'logs': [log_view(log) for log in logs]
}
@resource('/v1/repository/<repopath:repository>/logs')
@internal_only
class RepositoryLogs(RepositoryParamResource):
""" Resource for fetching logs for the specific repository. """
@require_repo_admin
@nickname('listRepoLogs')
@parse_args
@query_param('starttime', 'Earliest time from which to get logs (%m/%d/%Y %Z)', type=str)
@query_param('endtime', 'Latest time to which to get logs (%m/%d/%Y %Z)', type=str)
def get(self, args, namespace, repository):
""" List the logs for the specified repository. """
repo = model.get_repository(namespace, repository)
if not repo:
raise NotFound()
start_time = args['starttime']
end_time = args['endtime']
return get_logs(namespace, start_time, end_time, repository=repo)
@resource('/v1/user/logs')
@internal_only
class UserLogs(ApiResource):
""" Resource for fetching logs for the current user. """
@require_user_admin
@nickname('listUserLogs')
@parse_args
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
@query_param('performer', 'Username for which to filter logs.', type=str)
def get(self, args):
""" List the logs for the current user. """
performer_name = args['performer']
start_time = args['starttime']
end_time = args['endtime']
user = get_authenticated_user()
return get_logs(user.username, start_time, end_time, performer_name=performer_name)
@resource('/v1/organization/<orgname>/logs')
@internal_only
@related_user_resource(UserLogs)
class OrgLogs(ApiResource):
""" Resource for fetching logs for the entire organization. """
@nickname('listOrgLogs')
@parse_args
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
@query_param('performer', 'Username for which to filter logs.', type=str)
def get(self, args, orgname):
""" List the logs for the specified organization. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
performer_name = args['performer']
start_time = args['starttime']
end_time = args['endtime']
return get_logs(orgname, start_time, end_time, performer_name=performer_name)
raise Unauthorized()

View file

@ -0,0 +1,520 @@
import logging
import stripe
from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin, log_action)
from endpoints.api.team import team_view
from endpoints.api.user import User, PrivateRepositories
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
CreateRepositoryPermission)
from auth.auth_context import get_authenticated_user
from data import model
from data.plans import get_plan
from util.gravatar import compute_hash
logger = logging.getLogger(__name__)
def org_view(o, teams):
admin_org = AdministerOrganizationPermission(o.username)
is_admin = admin_org.can()
view = {
'name': o.username,
'email': o.email if is_admin else '',
'gravatar': compute_hash(o.email),
'teams': {t.name : team_view(o.username, t) for t in teams},
'is_admin': is_admin
}
if is_admin:
view['invoice_email'] = o.invoice_email
return view
@resource('/v1/organization/')
@internal_only
class OrganizationList(ApiResource):
""" Resource for creating organizations. """
schemas = {
'NewOrg': {
'id': 'NewOrg',
'type': 'object',
'description': 'Description of a new organization.',
'required': [
'name',
'email',
],
'properties': {
'name': {
'type': 'string',
'description': 'Organization username',
},
'email': {
'type': 'string',
'description': 'Organization contact email',
},
},
},
}
@require_user_admin
@nickname('createOrganization')
@validate_json_request('NewOrg')
def post(self):
""" Create a new organization. """
user = get_authenticated_user()
org_data = request.get_json()
existing = None
try:
existing = model.get_organization(org_data['name'])
except model.InvalidOrganizationException:
pass
if not existing:
try:
existing = model.get_user(org_data['name'])
except model.InvalidUserException:
pass
if existing:
msg = 'A user or organization with this name already exists'
raise request_error(message=msg)
try:
model.create_organization(org_data['name'], org_data['email'], user)
return 'Created', 201
except model.DataModelException as ex:
raise request_error(exception=ex)
@resource('/v1/organization/<orgname>')
@internal_only
@related_user_resource(User)
class Organization(ApiResource):
""" Resource for managing organizations. """
schemas = {
'UpdateOrg': {
'id': 'UpdateOrg',
'type': 'object',
'description': 'Description of updates for an existing organization',
'properties': {
'email': {
'type': 'string',
'description': 'Organization contact email',
},
'invoice_email': {
'type': 'boolean',
'description': 'Whether the organization desires to receive emails for invoices',
},
},
},
}
@nickname('getOrganization')
def get(self, orgname):
""" Get the details for the specified organization """
permission = OrganizationMemberPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
teams = model.get_teams_within_org(org)
return org_view(org, teams)
raise Unauthorized()
@nickname('changeOrganizationDetails')
@validate_json_request('UpdateOrg')
def put(self, orgname):
""" Change the details for the specified organization. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
org_data = request.get_json()
if 'invoice_email' in org_data:
logger.debug('Changing invoice_email for organization: %s', org.username)
model.change_invoice_email(org, org_data['invoice_email'])
if 'email' in org_data and org_data['email'] != org.email:
new_email = org_data['email']
if model.find_user_by_email(new_email):
raise request_error(message='E-mail address already used')
logger.debug('Changing email address for organization: %s', org.username)
model.update_email(org, new_email)
teams = model.get_teams_within_org(org)
return org_view(org, teams)
raise Unauthorized()
@resource('/v1/organization/<orgname>/private')
@internal_only
@related_user_resource(PrivateRepositories)
class OrgPrivateRepositories(ApiResource):
""" Custom verb to compute whether additional private repositories are available. """
@nickname('getOrganizationPrivateAllowed')
def get(self, orgname):
""" Return whether or not this org is allowed to create new private repositories. """
permission = CreateRepositoryPermission(orgname)
if permission.can():
organization = model.get_organization(orgname)
private_repos = model.get_private_repo_count(organization.username)
data = {
'privateAllowed': False
}
if organization.stripe_id:
cus = stripe.Customer.retrieve(organization.stripe_id)
if cus.subscription:
repos_allowed = 0
plan = get_plan(cus.subscription.plan.id)
if plan:
repos_allowed = plan['privateRepos']
data['privateAllowed'] = (private_repos < repos_allowed)
if AdministerOrganizationPermission(orgname).can():
data['privateCount'] = private_repos
return data
raise Unauthorized()
@resource('/v1/organization/<orgname>/members')
@internal_only
class OrgnaizationMemberList(ApiResource):
""" Resource for listing the members of an organization. """
@nickname('getOrganizationMembers')
def get(self, orgname):
""" List the members of the specified organization. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
# Loop to create the members dictionary. Note that the members collection
# will return an entry for *every team* a member is on, so we will have
# duplicate keys (which is why we pre-build the dictionary).
members_dict = {}
members = model.get_organization_members_with_teams(org)
for member in members:
if not member.user.username in members_dict:
members_dict[member.user.username] = {'name': member.user.username,
'kind': 'user',
'is_robot': member.user.robot,
'teams': []}
members_dict[member.user.username]['teams'].append(member.team.name)
return {'members': members_dict}
raise Unauthorized()
@resource('/v1/organization/<orgname>/members/<membername>')
@internal_only
class OrganizationMember(ApiResource):
""" Resource for managing individual organization members. """
@nickname('getOrganizationMember')
def get(self, orgname, membername):
""" Get information on the specific orgnaization member. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
member_dict = None
member_teams = model.get_organization_members_with_teams(org, membername=membername)
for member in member_teams:
if not member_dict:
member_dict = {'name': member.user.username,
'kind': 'user',
'is_robot': member.user.robot,
'teams': []}
member_dict['teams'].append(member.team.name)
if not member_dict:
raise NotFound()
return {'member': member_dict}
raise Unauthorized()
@resource('/v1/app/<client_id>')
class ApplicationInformation(ApiResource):
""" Resource that returns public information about a registered application. """
@nickname('getApplicationInformation')
def get(self, client_id):
""" Get information on the specified application. """
application = model.oauth.get_application_for_client_id(client_id)
if not application:
raise NotFound()
org_hash = compute_hash(application.organization.email)
gravatar = compute_hash(application.gravatar_email) if application.gravatar_email else org_hash
return {
'name': application.name,
'description': application.description,
'uri': application.application_uri,
'gravatar': gravatar,
'organization': org_view(application.organization, [])
}
def app_view(application):
is_admin = AdministerOrganizationPermission(application.organization.username).can()
return {
'name': application.name,
'description': application.description,
'application_uri': application.application_uri,
'client_id': application.client_id,
'client_secret': application.client_secret if is_admin else None,
'redirect_uri': application.redirect_uri if is_admin else None,
'gravatar_email': application.gravatar_email if is_admin else None,
}
@resource('/v1/organization/<orgname>/applications')
@internal_only
class OrganizationApplications(ApiResource):
""" Resource for managing applications defined by an organizations. """
schemas = {
'NewApp': {
'id': 'NewApp',
'type': 'object',
'description': 'Description of a new organization application.',
'required': [
'name',
],
'properties': {
'name': {
'type': 'string',
'description': 'The name of the application',
},
'redirect_uri': {
'type': 'string',
'description': 'The URI for the application\'s OAuth redirect',
},
'application_uri': {
'type': 'string',
'description': 'The URI for the application\'s homepage',
},
'description': {
'type': 'string',
'description': 'The human-readable description for the application',
},
'gravatar_email': {
'type': 'string',
'description': 'The e-mail address of the gravatar to use for the application',
}
},
},
}
@nickname('getOrganizationApplications')
def get(self, orgname):
""" List the applications for the specified organization """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
applications = model.oauth.list_applications_for_org(org)
return {'applications': [app_view(application) for application in applications]}
raise Unauthorized()
@nickname('createOrganizationApplication')
@validate_json_request('NewApp')
def post(self, orgname):
""" Creates a new application under this organization. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
app_data = request.get_json()
application = model.oauth.create_application(
org, app_data['name'],
app_data.get('application_uri', ''),
app_data.get('redirect_uri', ''),
description = app_data.get('description', ''),
gravatar_email = app_data.get('gravatar_email', None),)
app_data.update({
'application_name': application.name,
'client_id': application.client_id
})
log_action('create_application', orgname, app_data)
return app_view(application)
raise Unauthorized()
@resource('/v1/organization/<orgname>/applications/<client_id>')
@internal_only
class OrganizationApplicationResource(ApiResource):
""" Resource for managing an application defined by an organizations. """
schemas = {
'UpdateApp': {
'id': 'UpdateApp',
'type': 'object',
'description': 'Description of an updated application.',
'required': [
'name',
'redirect_uri',
'application_uri'
],
'properties': {
'name': {
'type': 'string',
'description': 'The name of the application',
},
'redirect_uri': {
'type': 'string',
'description': 'The URI for the application\'s OAuth redirect',
},
'application_uri': {
'type': 'string',
'description': 'The URI for the application\'s homepage',
},
'description': {
'type': 'string',
'description': 'The human-readable description for the application',
},
'gravatar_email': {
'type': 'string',
'description': 'The e-mail address of the gravatar to use for the application',
}
},
},
}
@nickname('getOrganizationApplication')
def get(self, orgname, client_id):
""" Retrieves the application with the specified client_id under the specified organization """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
application = model.oauth.lookup_application(org, client_id)
if not application:
raise NotFound()
return app_view(application)
raise Unauthorized()
@nickname('updateOrganizationApplication')
@validate_json_request('UpdateApp')
def put(self, orgname, client_id):
""" Updates an application under this organization. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
application = model.oauth.lookup_application(org, client_id)
if not application:
raise NotFound()
app_data = request.get_json()
application.name = app_data['name']
application.application_uri = app_data['application_uri']
application.redirect_uri = app_data['redirect_uri']
application.description = app_data.get('description', '')
application.gravatar_email = app_data.get('gravatar_email', None)
application.save()
app_data.update({
'application_name': application.name,
'client_id': application.client_id
})
log_action('update_application', orgname, app_data)
return app_view(application)
raise Unauthorized()
@nickname('deleteOrganizationApplication')
def delete(self, orgname, client_id):
""" Deletes the application under this organization. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
application = model.oauth.delete_application(org, client_id)
if not application:
raise NotFound()
log_action('delete_application', orgname,
{'application_name': application.name, 'client_id': client_id})
return 'Deleted', 204
raise Unauthorized()
@resource('/v1/organization/<orgname>/applications/<client_id>/resetclientsecret')
@internal_only
class OrganizationApplicationResetClientSecret(ApiResource):
""" Custom verb for resetting the client secret of an application. """
@nickname('resetOrganizationApplicationClientSecret')
def post(self, orgname, client_id):
""" Resets the client secret of the application. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
application = model.oauth.lookup_application(org, client_id)
if not application:
raise NotFound()
application = model.oauth.reset_client_secret(application)
log_action('reset_application_client_secret', orgname,
{'application_name': application.name, 'client_id': client_id})
return app_view(application)
raise Unauthorized()

241
endpoints/api/permission.py Normal file
View file

@ -0,0 +1,241 @@
import logging
from flask import request
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
log_action, request_error, validate_json_request)
from data import model
logger = logging.getLogger(__name__)
def role_view(repo_perm_obj):
return {
'role': repo_perm_obj.role.name,
}
def wrap_role_view_user(role_json, user):
role_json['is_robot'] = user.robot
return role_json
def wrap_role_view_org(role_json, user, org_members):
role_json['is_org_member'] = user.robot or user.username in org_members
return role_json
@resource('/v1/repository/<repopath:repository>/permissions/team/')
class RepositoryTeamPermissionList(RepositoryParamResource):
""" Resource for repository team permissions. """
@require_repo_admin
@nickname('listRepoTeamPermissions')
def get(self, namespace, repository):
""" List all team permission. """
repo_perms = model.get_all_repo_teams(namespace, repository)
return {
'permissions': {repo_perm.team.name: role_view(repo_perm)
for repo_perm in repo_perms}
}
@resource('/v1/repository/<repopath:repository>/permissions/user/')
class RepositoryUserPermissionList(RepositoryParamResource):
""" Resource for repository user permissions. """
@require_repo_admin
@nickname('listRepoUserPermissions')
def get(self, namespace, repository):
""" List all user permissions. """
# Lookup the organization (if any).
org = None
try:
org = model.get_organization(namespace) # Will raise an error if not org
except model.InvalidOrganizationException:
# This repository isn't under an org
pass
# Determine how to wrap the role(s).
def wrapped_role_view(repo_perm):
return wrap_role_view_user(role_view(repo_perm), repo_perm.user)
role_view_func = wrapped_role_view
if org:
org_members = model.get_organization_member_set(namespace)
current_func = role_view_func
def wrapped_role_org_view(repo_perm):
return wrap_role_view_org(current_func(repo_perm), repo_perm.user,
org_members)
role_view_func = wrapped_role_org_view
# Load and return the permissions.
repo_perms = model.get_all_repo_users(namespace, repository)
return {
'permissions': {perm.user.username: role_view_func(perm)
for perm in repo_perms}
}
@resource('/v1/repository/<repopath:repository>/permissions/user/<username>')
class RepositoryUserPermission(RepositoryParamResource):
""" Resource for managing individual user permissions. """
schemas = {
'UserPermission': {
'id': 'UserPermission',
'type': 'object',
'description': 'Description of a user permission.',
'required': [
'role',
],
'properties': {
'role': {
'type': 'string',
'description': 'Role to use for the user',
'enum': [
'read',
'write',
'admin',
],
},
},
},
}
@require_repo_admin
@nickname('getUserPermissions')
def get(self, namespace, repository, username):
""" Get the Fetch the permission for the specified user. """
logger.debug('Get repo: %s/%s permissions for user %s' %
(namespace, repository, username))
perm = model.get_user_reponame_permission(username, namespace, repository)
perm_view = wrap_role_view_user(role_view(perm), perm.user)
try:
model.get_organization(namespace)
org_members = model.get_organization_member_set(namespace)
perm_view = wrap_role_view_org(perm_view, perm.user, org_members)
except model.InvalidOrganizationException:
# This repository is not part of an organization
pass
return perm_view
@require_repo_admin
@nickname('changeUserPermissions')
@validate_json_request('UserPermission')
def put(self, namespace, repository, username): # Also needs to respond to post
""" Update the perimssions for an existing repository. """
new_permission = request.get_json()
logger.debug('Setting permission to: %s for user %s' %
(new_permission['role'], username))
try:
perm = model.set_user_repo_permission(username, namespace, repository,
new_permission['role'])
except model.InvalidUsernameException as ex:
raise request_error(exception=ex)
perm_view = wrap_role_view_user(role_view(perm), perm.user)
try:
model.get_organization(namespace)
org_members = model.get_organization_member_set(namespace)
perm_view = wrap_role_view_org(perm_view, perm.user, org_members)
except model.InvalidOrganizationException:
# This repository is not part of an organization
pass
except model.DataModelException as ex:
raise request_error(exception=ex)
log_action('change_repo_permission', namespace,
{'username': username, 'repo': repository,
'role': new_permission['role']},
repo=model.get_repository(namespace, repository))
return perm_view, 200
@require_repo_admin
@nickname('deleteUserPermissions')
def delete(self, namespace, repository, username):
""" Delete the permission for the user. """
try:
model.delete_user_permission(username, namespace, repository)
except model.DataModelException as ex:
raise request_error(exception=ex)
log_action('delete_repo_permission', namespace,
{'username': username, 'repo': repository},
repo=model.get_repository(namespace, repository))
return 'Deleted', 204
@resource('/v1/repository/<repopath:repository>/permissions/team/<teamname>')
class RepositoryTeamPermission(RepositoryParamResource):
""" Resource for managing individual team permissions. """
schemas = {
'TeamPermission': {
'id': 'TeamPermission',
'type': 'object',
'description': 'Description of a team permission.',
'required': [
'role',
],
'properties': {
'role': {
'type': 'string',
'description': 'Role to use for the team',
'enum': [
'read',
'write',
'admin',
],
},
},
},
}
@require_repo_admin
@nickname('getTeamPermissions')
def get(self, namespace, repository, teamname):
""" Fetch the permission for the specified team. """
logger.debug('Get repo: %s/%s permissions for team %s' %
(namespace, repository, teamname))
perm = model.get_team_reponame_permission(teamname, namespace, repository)
return role_view(perm)
@require_repo_admin
@nickname('changeTeamPermissions')
@validate_json_request('TeamPermission')
def put(self, namespace, repository, teamname):
""" Update the existing team permission. """
new_permission = request.get_json()
logger.debug('Setting permission to: %s for team %s' %
(new_permission['role'], teamname))
perm = model.set_team_repo_permission(teamname, namespace, repository,
new_permission['role'])
log_action('change_repo_permission', namespace,
{'team': teamname, 'repo': repository,
'role': new_permission['role']},
repo=model.get_repository(namespace, repository))
return role_view(perm), 200
@require_repo_admin
@nickname('deleteTeamPermissions')
def delete(self, namespace, repository, teamname):
""" Delete the permission for the specified team. """
model.delete_team_permission(teamname, namespace, repository)
log_action('delete_repo_permission', namespace,
{'team': teamname, 'repo': repository},
repo=model.get_repository(namespace, repository))
return 'Deleted', 204

253
endpoints/api/prototype.py Normal file
View file

@ -0,0 +1,253 @@
from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
log_action, Unauthorized, NotFound, internal_only)
from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from data import model
def prototype_view(proto, org_members):
def prototype_user_view(user):
return {
'name': user.username,
'is_robot': user.robot,
'kind': 'user',
'is_org_member': user.robot or user.username in org_members,
}
if proto.delegate_user:
delegate_view = prototype_user_view(proto.delegate_user)
else:
delegate_view = {
'name': proto.delegate_team.name,
'kind': 'team',
}
return {
'activating_user': (prototype_user_view(proto.activating_user)
if proto.activating_user else None),
'delegate': delegate_view,
'role': proto.role.name,
'id': proto.uuid,
}
def log_prototype_action(action_kind, orgname, prototype, **kwargs):
username = get_authenticated_user().username
log_params = {
'prototypeid': prototype.uuid,
'username': username,
'activating_username': (prototype.activating_user.username
if prototype.activating_user else None),
'role': prototype.role.name
}
for key, value in kwargs.items():
log_params[key] = value
if prototype.delegate_user:
log_params['delegate_user'] = prototype.delegate_user.username
elif prototype.delegate_team:
log_params['delegate_team'] = prototype.delegate_team.name
log_action(action_kind, orgname, log_params)
@resource('/v1/organization/<orgname>/prototypes')
@internal_only
class PermissionPrototypeList(ApiResource):
""" Resource for listing and creating permission prototypes. """
schemas = {
'NewPrototype': {
'id': 'NewPrototype',
'type': 'object',
'description': 'Description of a new prototype',
'required': [
'role',
'delegate',
],
'properties': {
'role': {
'type': 'string',
'description': 'Role that should be applied to the delegate',
'enum': [
'read',
'write',
'admin',
],
},
'activating_user': {
'type': 'object',
'description': 'Repository creating user to whom the rule should apply',
'required': [
'name',
],
'properties': {
'name': {
'type': 'string',
'description': 'The username for the activating_user',
},
},
},
'delegate': {
'type': 'object',
'description': 'Information about the user or team to which the rule grants access',
'required': [
'name',
'kind',
],
'properties': {
'name': {
'type': 'string',
'description': 'The name for the delegate team or user',
},
'kind': {
'type': 'string',
'description': 'Whether the delegate is a user or a team',
'enum': [
'user',
'team',
],
},
},
},
},
},
}
@nickname('getOrganizationPrototypePermissions')
def get(self, orgname):
""" List the existing prototypes for this organization. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
permissions = model.get_prototype_permissions(org)
org_members = model.get_organization_member_set(orgname)
return {'prototypes': [prototype_view(p, org_members) for p in permissions]}
raise Unauthorized()
@nickname('createOrganizationPrototypePermission')
@validate_json_request('NewPrototype')
def post(self, orgname):
""" Create a new permission prototype. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
details = request.get_json()
activating_username = None
if ('activating_user' in details and details['activating_user'] and
'name' in details['activating_user']):
activating_username = details['activating_user']['name']
delegate = details['delegate'] if 'delegate' in details else {}
delegate_kind = delegate.get('kind', None)
delegate_name = delegate.get('name', None)
delegate_username = delegate_name if delegate_kind == 'user' else None
delegate_teamname = delegate_name if delegate_kind == 'team' else None
activating_user = (model.get_user(activating_username) if activating_username else None)
delegate_user = (model.get_user(delegate_username) if delegate_username else None)
delegate_team = (model.get_organization_team(orgname, delegate_teamname)
if delegate_teamname else None)
if activating_username and not activating_user:
raise request_error(message='Unknown activating user')
if not delegate_user and not delegate_team:
raise request_error(message='Missing delegate user or team')
role_name = details['role']
prototype = model.add_prototype_permission(org, role_name, activating_user,
delegate_user, delegate_team)
log_prototype_action('create_prototype_permission', orgname, prototype)
org_members = model.get_organization_member_set(orgname)
return prototype_view(prototype, org_members)
raise Unauthorized()
@resource('/v1/organization/<orgname>/prototypes/<prototypeid>')
@internal_only
class PermissionPrototype(ApiResource):
""" Resource for managingin individual permission prototypes. """
schemas = {
'PrototypeUpdate': {
'id': 'PrototypeUpdate',
'type': 'object',
'description': 'Description of a the new prototype role',
'required': [
'role',
],
'properties': {
'role': {
'type': 'string',
'description': 'Role that should be applied to the permission',
'enum': [
'read',
'write',
'admin',
],
},
},
},
}
@nickname('deleteOrganizationPrototypePermission')
def delete(self, orgname, prototypeid):
""" Delete an existing permission prototype. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
prototype = model.delete_prototype_permission(org, prototypeid)
if not prototype:
raise NotFound()
log_prototype_action('delete_prototype_permission', orgname, prototype)
return 'Deleted', 204
raise Unauthorized()
@nickname('updateOrganizationPrototypePermission')
@validate_json_request('PrototypeUpdate')
def put(self, orgname, prototypeid):
""" Update the role of an existing permission prototype. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
org = model.get_organization(orgname)
except model.InvalidOrganizationException:
raise NotFound()
existing = model.get_prototype_permission(org, prototypeid)
if not existing:
raise NotFound()
details = request.get_json()
role_name = details['role']
prototype = model.update_prototype_permission(org, prototypeid, role_name)
if not prototype:
raise NotFound()
log_prototype_action('modify_prototype_permission', orgname, prototype,
original_role=existing.role.name)
org_members = model.get_organization_member_set(orgname)
return prototype_view(prototype, org_members)
raise Unauthorized()

291
endpoints/api/repository.py Normal file
View file

@ -0,0 +1,291 @@
import logging
import json
from flask import request
from data import model
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
require_repo_read, require_repo_write, require_repo_admin,
RepositoryParamResource, resource, query_param, parse_args, ApiResource,
request_error, require_scope, Unauthorized, NotFound, InvalidRequest)
from auth.permissions import (ModifyRepositoryPermission, AdministerRepositoryPermission,
CreateRepositoryPermission, ReadRepositoryPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes
logger = logging.getLogger(__name__)
@resource('/v1/repository')
class RepositoryList(ApiResource):
"""Operations for creating and listing repositories."""
schemas = {
'NewRepo': {
'id': 'NewRepo',
'type': 'object',
'description': 'Description of a new repository',
'required': [
'repository',
'visibility',
'description',
],
'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',
},
},
},
}
@require_scope(scopes.CREATE_REPO)
@nickname('createRepo')
@validate_json_request('NewRepo')
def post(self):
"""Create a new repository."""
owner = get_authenticated_user()
req = request.get_json()
if owner is None and 'namespace' not in 'req':
raise InvalidRequest('Must provide a namespace or must be logged in.')
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:
raise 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 {
'namespace': namespace_name,
'name': repository_name
}, 201
raise Unauthorized()
@require_scope(scopes.READ_REPO)
@nickname('listRepos')
@parse_args
@query_param('page', 'Offset page number. (int)', type=int)
@query_param('limit', 'Limit on the number of results (int)', type=int)
@query_param('namespace', 'Namespace to use when querying for org repositories.', type=str)
@query_param('public', 'Whether to include public repositories.', type=truthy_bool, default=True)
@query_param('private', 'Whether to inlcude private repositories.', type=truthy_bool,
default=True)
@query_param('sort', 'Whether to sort the results.', type=truthy_bool, default=False)
@query_param('count', 'Whether to include a count of the total number of results available.',
type=truthy_bool, default=False)
def get(self, args):
"""Fetch the list of repositories under a variety of situations."""
def repo_view(repo_obj):
return {
'namespace': repo_obj.namespace,
'name': repo_obj.name,
'description': repo_obj.description,
'is_public': repo_obj.visibility.name == 'public',
}
username = None
if get_authenticated_user() and args['private']:
username = get_authenticated_user().username
response = {}
repo_count = None
if args['count']:
repo_count = model.get_visible_repository_count(username, include_public=args['public'],
namespace=args['namespace'])
response['count'] = repo_count
repo_query = model.get_visible_repositories(username, limit=args['limit'], page=args['page'],
include_public=args['public'], sort=args['sort'],
namespace=args['namespace'])
response['repositories'] = [repo_view(repo) for repo in repo_query
if (repo.visibility.name == 'public' or
ReadRepositoryPermission(repo.namespace, repo.name).can())]
return response
def image_view(image):
extended_props = image
if image.storage and image.storage.id:
extended_props = image.storage
command = extended_props.command
return {
'id': image.docker_image_id,
'created': format_date(extended_props.created),
'comment': extended_props.comment,
'command': json.loads(command) if command else None,
'ancestors': image.ancestors,
'dbid': image.id,
'size': extended_props.image_size,
}
@resource('/v1/repository/<repopath:repository>')
class Repository(RepositoryParamResource):
"""Operations for managing a specific repository."""
schemas = {
'RepoUpdate': {
'id': 'RepoUpdate',
'type': 'object',
'description': 'Fields which can be updated in a repository.',
'required': [
'description',
],
'properties': {
'description': {
'type': 'string',
'description': 'Markdown encoded description for the repository',
},
}
}
}
@require_repo_read
@nickname('getRepo')
def get(self, namespace, repository):
"""Fetch the specified repository."""
logger.debug('Get repo: %s/%s' % (namespace, repository))
def tag_view(tag):
image = model.get_tag_image(namespace, repository, tag.name)
if not image:
return {}
return {
'name': tag.name,
'image': image_view(image),
}
organization = None
try:
organization = model.get_organization(namespace)
except model.InvalidOrganizationException:
pass
is_public = model.repository_is_public(namespace, repository)
repo = model.get_repository(namespace, repository)
if repo:
tags = model.list_repository_tags(namespace, repository)
tag_dict = {tag.name: tag_view(tag) for tag in tags}
can_write = ModifyRepositoryPermission(namespace, repository).can()
can_admin = AdministerRepositoryPermission(namespace, repository).can()
active_builds = model.list_repository_builds(namespace, repository, 1,
include_inactive=False)
return {
'namespace': namespace,
'name': repository,
'description': repo.description,
'tags': tag_dict,
'can_write': can_write,
'can_admin': can_admin,
'is_public': is_public,
'is_building': len(list(active_builds)) > 0,
'is_organization': bool(organization),
'status_token': repo.badge_token if not is_public else ''
}
raise NotFound()
@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
}
raise NotFound()
@require_repo_admin
@nickname('deleteRepository')
def delete(self, namespace, repository):
""" Delete a repository. """
model.purge_repository(namespace, repository)
log_action('delete_repo', namespace,
{'repo': repository, 'namespace': namespace})
return 'Deleted', 204
@resource('/v1/repository/<repopath: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': [
'visibility',
],
'properties': {
'visibility': {
'type': 'string',
'description': 'Visibility which the repository will start with',
'enum': [
'public',
'private',
],
},
}
}
}
@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
}

134
endpoints/api/repotoken.py Normal file
View file

@ -0,0 +1,134 @@
import logging
from flask import request
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
log_action, validate_json_request, NotFound)
from data import model
logger = logging.getLogger(__name__)
def token_view(token_obj):
return {
'friendlyName': token_obj.friendly_name,
'code': token_obj.code,
'role': token_obj.role.name,
}
@resource('/v1/repository/<repopath:repository>/tokens/')
class RepositoryTokenList(RepositoryParamResource):
""" Resource for creating and listing repository tokens. """
schemas = {
'NewToken': {
'id': 'NewToken',
'type': 'object',
'description': 'Description of a new token.',
'required':[
'friendlyName',
],
'properties': {
'friendlyName': {
'type': 'string',
'description': 'Friendly name to help identify the token',
},
},
},
}
@require_repo_admin
@nickname('listRepoTokens')
def get(self, namespace, repository):
""" List the tokens for the specified repository. """
tokens = model.get_repository_delegate_tokens(namespace, repository)
return {
'tokens': {token.code: token_view(token) for token in tokens}
}
@require_repo_admin
@nickname('createToken')
@validate_json_request('NewToken')
def post(self, namespace, repository):
""" Create a new repository token. """
token_params = request.get_json()
token = model.create_delegate_token(namespace, repository,
token_params['friendlyName'])
log_action('add_repo_accesstoken', namespace,
{'repo': repository, 'token': token_params['friendlyName']},
repo = model.get_repository(namespace, repository))
return token_view(token), 201
@resource('/v1/repository/<repopath:repository>/tokens/<code>')
class RepositoryToken(RepositoryParamResource):
""" Resource for managing individual tokens. """
schemas = {
'TokenPermission': {
'id': 'TokenPermission',
'type': 'object',
'description': 'Description of a token permission',
'required': [
'role',
],
'properties': {
'role': {
'type': 'string',
'description': 'Role to use for the token',
'enum': [
'read',
'write',
'admin',
],
},
},
},
}
@require_repo_admin
@nickname('getTokens')
def get(self, namespace, repository, code):
""" Fetch the specified repository token information. """
try:
perm = model.get_repo_delegate_token(namespace, repository, code)
except model.InvalidTokenException:
raise NotFound()
return token_view(perm)
@require_repo_admin
@nickname('changeToken')
@validate_json_request('TokenPermission')
def put(self, namespace, repository, code):
""" Update the permissions for the specified repository token. """
new_permission = request.get_json()
logger.debug('Setting permission to: %s for code %s' %
(new_permission['role'], code))
token = model.set_repo_delegate_token_role(namespace, repository, code,
new_permission['role'])
log_action('change_repo_permission', namespace,
{'repo': repository, 'token': token.friendly_name, 'code': code,
'role': new_permission['role']},
repo = model.get_repository(namespace, repository))
return token_view(token)
@require_repo_admin
@nickname('deleteToken')
def delete(self, namespace, repository, code):
""" Delete the repository token. """
token = model.delete_delegate_token(namespace, repository, code)
log_action('delete_repo_accesstoken', namespace,
{'repo': repository, 'token': token.friendly_name,
'code': code},
repo = model.get_repository(namespace, repository))
return 'Deleted', 204

98
endpoints/api/robot.py Normal file
View file

@ -0,0 +1,98 @@
from endpoints.api import (resource, nickname, ApiResource, log_action, related_user_resource,
Unauthorized, require_user_admin, internal_only)
from auth.permissions import AdministerOrganizationPermission, OrganizationMemberPermission
from auth.auth_context import get_authenticated_user
from data import model
from util.names import format_robot_username
def robot_view(name, token):
return {
'name': name,
'token': token,
}
@resource('/v1/user/robots')
@internal_only
class UserRobotList(ApiResource):
""" Resource for listing user robots. """
@require_user_admin
@nickname('getUserRobots')
def get(self):
""" List the available robots for the user. """
user = get_authenticated_user()
robots = model.list_entity_robots(user.username)
return {
'robots': [robot_view(name, password) for name, password in robots]
}
@resource('/v1/user/robots/<robot_shortname>')
@internal_only
class UserRobot(ApiResource):
""" Resource for managing a user's robots. """
@require_user_admin
@nickname('createUserRobot')
def put(self, robot_shortname):
""" Create a new user robot with the specified name. """
parent = get_authenticated_user()
robot, password = model.create_robot(robot_shortname, parent)
log_action('create_robot', parent.username, {'robot': robot_shortname})
return robot_view(robot.username, password), 201
@require_user_admin
@nickname('deleteUserRobot')
def delete(self, robot_shortname):
""" Delete an existing robot. """
parent = get_authenticated_user()
model.delete_robot(format_robot_username(parent.username, robot_shortname))
log_action('delete_robot', parent.username, {'robot': robot_shortname})
return 'Deleted', 204
@resource('/v1/organization/<orgname>/robots')
@internal_only
@related_user_resource(UserRobotList)
class OrgRobotList(ApiResource):
""" Resource for listing an organization's robots. """
@nickname('getOrgRobots')
def get(self, orgname):
""" List the organization's robots. """
permission = OrganizationMemberPermission(orgname)
if permission.can():
robots = model.list_entity_robots(orgname)
return {
'robots': [robot_view(name, password) for name, password in robots]
}
raise Unauthorized()
@resource('/v1/organization/<orgname>/robots/<robot_shortname>')
@internal_only
@related_user_resource(UserRobot)
class OrgRobot(ApiResource):
""" Resource for managing an organization's robots. """
@nickname('createOrgRobot')
def put(self, orgname, robot_shortname):
""" Create a new robot in the organization. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
parent = model.get_organization(orgname)
robot, password = model.create_robot(robot_shortname, parent)
log_action('create_robot', orgname, {'robot': robot_shortname})
return robot_view(robot.username, password), 201
raise Unauthorized()
@nickname('deleteOrgRobot')
def delete(self, orgname, robot_shortname):
""" Delete an existing organization robot. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
model.delete_robot(format_robot_username(orgname, robot_shortname))
log_action('delete_robot', orgname, {'robot': robot_shortname})
return 'Deleted', 204
raise Unauthorized()

116
endpoints/api/search.py Normal file
View file

@ -0,0 +1,116 @@
from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource,
require_scope)
from data import model
from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission,
ReadRepositoryPermission, UserAdminPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes
@resource('/v1/entities/<prefix>')
class EntitySearch(ApiResource):
""" Resource for searching entities. """
@parse_args
@query_param('namespace', 'Namespace to use when querying for org entities.', type=str,
default='')
@query_param('includeTeams', 'Whether to include team names.', type=truthy_bool, default=False)
@nickname('getMatchingEntities')
def get(self, args, prefix):
""" Get a list of entities that match the specified prefix. """
teams = []
namespace_name = args['namespace']
robot_namespace = None
organization = None
try:
organization = model.get_organization(namespace_name)
# namespace name was an org
permission = OrganizationMemberPermission(namespace_name)
if permission.can():
robot_namespace = namespace_name
if args['includeTeams']:
teams = model.get_matching_teams(prefix, organization)
except model.InvalidOrganizationException:
# namespace name was a user
user = get_authenticated_user()
if user and user.username == namespace_name:
# Check if there is admin user permissions (login only)
admin_permission = UserAdminPermission(user.username)
if admin_permission.can():
robot_namespace = namespace_name
users = model.get_matching_users(prefix, robot_namespace, organization)
def entity_team_view(team):
result = {
'name': team.name,
'kind': 'team',
'is_org_member': True
}
return result
def user_view(user):
user_json = {
'name': user.username,
'kind': 'user',
'is_robot': user.is_robot,
}
if organization is not None:
user_json['is_org_member'] = user.is_robot or user.is_org_member
return user_json
team_data = [entity_team_view(team) for team in teams]
user_data = [user_view(user) for user in users]
return {
'results': team_data + user_data
}
def team_view(orgname, team):
view_permission = ViewTeamPermission(orgname, team.name)
role = model.get_team_org_role(team).name
return {
'id': team.id,
'name': team.name,
'description': team.description,
'can_view': view_permission.can(),
'role': role
}
@resource('/v1/find/repository')
class FindRepositories(ApiResource):
""" Resource for finding repositories. """
@parse_args
@query_param('query', 'The prefix to use when querying for repositories.', type=str, default='')
@require_scope(scopes.READ_REPO)
@nickname('findRepos')
def get(self, args):
""" Get a list of repositories that match the specified prefix query. """
prefix = args['query']
def repo_view(repo):
return {
'namespace': repo.namespace,
'name': repo.name,
'description': repo.description
}
username = None
user = get_authenticated_user()
if user is not None:
username = user.username
matching = model.get_matching_repositories(prefix, username)
return {
'repositories': [repo_view(repo) for repo in matching
if (repo.visibility.name == 'public' or
ReadRepositoryPermission(repo.namespace, repo.name).can())]
}

View file

@ -0,0 +1,98 @@
import logging
import stripe
from endpoints.api import request_error, log_action, NotFound
from endpoints.common import check_repository_usage
from data import model
from data.plans import PLANS
logger = logging.getLogger(__name__)
def carderror_response(exc):
return {'carderror': exc.message}, 402
def subscription_view(stripe_subscription, used_repos):
return {
'currentPeriodStart': stripe_subscription.current_period_start,
'currentPeriodEnd': stripe_subscription.current_period_end,
'plan': stripe_subscription.plan.id,
'usedPrivateRepos': used_repos,
}
def subscribe(user, plan, token, require_business_plan):
plan_found = None
for plan_obj in PLANS:
if plan_obj['stripeId'] == plan:
plan_found = plan_obj
if not plan_found or plan_found['deprecated']:
logger.warning('Plan not found or deprecated: %s', plan)
raise NotFound()
if (require_business_plan and not plan_found['bus_features'] and not
plan_found['price'] == 0):
logger.warning('Business attempting to subscribe to personal plan: %s',
user.username)
raise request_error(message='No matching plan found')
private_repos = model.get_private_repo_count(user.username)
# This is the default response
response_json = {
'plan': plan,
'usedPrivateRepos': private_repos,
}
status_code = 200
if not user.stripe_id:
# Check if a non-paying user is trying to subscribe to a free plan
if not plan_found['price'] == 0:
# They want a real paying plan, create the customer and plan
# simultaneously
card = token
try:
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
user.stripe_id = cus.id
user.save()
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan})
except stripe.CardError as e:
return carderror_response(e)
response_json = subscription_view(cus.subscription, private_repos)
status_code = 201
else:
# Change the plan
cus = stripe.Customer.retrieve(user.stripe_id)
if plan_found['price'] == 0:
if cus.subscription is not None:
# We only have to cancel the subscription if they actually have one
cus.cancel_subscription()
cus.save()
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan})
else:
# User may have been a previous customer who is resubscribing
if token:
cus.card = token
cus.plan = plan
try:
cus.save()
except stripe.CardError as e:
return carderror_response(e)
response_json = subscription_view(cus.subscription, private_repos)
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan})
return response_json, status_code

95
endpoints/api/tag.py Normal file
View file

@ -0,0 +1,95 @@
from flask import request
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
RepositoryParamResource, log_action, NotFound, validate_json_request)
from endpoints.api.image import image_view
from data import model
from auth.auth_context import get_authenticated_user
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
class RepositoryTag(RepositoryParamResource):
""" Resource for managing repository tags. """
schemas = {
'MoveTag': {
'id': 'MoveTag',
'type': 'object',
'description': 'Description of to which image a new or existing tag should point',
'required': [
'image',
],
'properties': {
'image': {
'type': 'string',
'description': 'Image identifier to which the tag should point',
},
},
},
}
@require_repo_write
@nickname('changeTagImage')
@validate_json_request('MoveTag')
def put(self, namespace, repository, tag):
""" Change which image a tag points to or create a new tag."""
image_id = request.get_json()['image']
image = model.get_repo_image(namespace, repository, image_id)
if not image:
raise NotFound()
original_image_id = None
try:
original_tag_image = model.get_tag_image(namespace, repository, tag)
if original_tag_image:
original_image_id = original_tag_image.docker_image_id
except model.DataModelException:
# This is a new tag.
pass
model.create_or_update_tag(namespace, repository, tag, image_id)
model.garbage_collect_repository(namespace, repository)
username = get_authenticated_user().username
log_action('move_tag' if original_image_id else 'create_tag', namespace,
{ 'username': username, 'repo': repository, 'tag': tag,
'image': image_id, 'original_image': original_image_id },
repo=model.get_repository(namespace, repository))
return 'Updated', 201
@require_repo_write
@nickname('deleteFullTag')
def delete(self, namespace, repository, tag):
""" Delete the specified repository tag. """
model.delete_tag(namespace, repository, tag)
model.garbage_collect_repository(namespace, repository)
username = get_authenticated_user().username
log_action('delete_tag', namespace,
{'username': username, 'repo': repository, 'tag': tag},
repo=model.get_repository(namespace, repository))
return 'Deleted', 204
@resource('/v1/repository/<repopath:repository>/tag/<tag>/images')
class RepositoryTagImages(RepositoryParamResource):
""" Resource for listing the images in a specific repository tag. """
@require_repo_read
@nickname('listTagImages')
def get(self, namespace, repository, tag):
""" List the images for the specified repository tag. """
try:
tag_image = model.get_tag_image(namespace, repository, tag)
except model.DataModelException:
raise NotFound()
parent_images = model.get_parent_images(tag_image)
parents = list(parent_images)
parents.reverse()
all_images = [tag_image] + parents
return {
'images': [image_view(image) for image in all_images]
}

179
endpoints/api/team.py Normal file
View file

@ -0,0 +1,179 @@
from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
log_action, Unauthorized, NotFound, internal_only)
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
from auth.auth_context import get_authenticated_user
from data import model
def team_view(orgname, team):
view_permission = ViewTeamPermission(orgname, team.name)
role = model.get_team_org_role(team).name
return {
'id': team.id,
'name': team.name,
'description': team.description,
'can_view': view_permission.can(),
'role': role
}
def member_view(member):
return {
'name': member.username,
'kind': 'user',
'is_robot': member.robot,
}
@resource('/v1/organization/<orgname>/team/<teamname>')
@internal_only
class OrganizationTeam(ApiResource):
""" Resource for manging an organization's teams. """
schemas = {
'TeamDescription': {
'id': 'TeamDescription',
'type': 'object',
'description': 'Description of a team',
'required': [
'role',
],
'properties': {
'role': {
'type': 'string',
'description': 'Org wide permissions that should apply to the team',
'enum': [
'member',
'creator',
'admin',
],
},
'description': {
'type': 'string',
'description': 'Markdown description for the team',
},
},
},
}
@nickname('updateOrganizationTeam')
@validate_json_request('TeamDescription')
def put(self, orgname, teamname):
""" Update the org-wide permission for the specified team. """
edit_permission = AdministerOrganizationPermission(orgname)
if edit_permission.can():
team = None
details = request.get_json()
is_existing = False
try:
team = model.get_organization_team(orgname, teamname)
is_existing = True
except model.InvalidTeamException:
# Create the new team.
description = details['description'] if 'description' in details else ''
role = details['role'] if 'role' in details else 'member'
org = model.get_organization(orgname)
team = model.create_team(teamname, org, role, description)
log_action('org_create_team', orgname, {'team': teamname})
if is_existing:
if ('description' in details and
team.description != details['description']):
team.description = details['description']
team.save()
log_action('org_set_team_description', orgname,
{'team': teamname, 'description': team.description})
if 'role' in details:
role = model.get_team_org_role(team).name
if role != details['role']:
team = model.set_team_org_permission(team, details['role'],
get_authenticated_user().username)
log_action('org_set_team_role', orgname, {'team': teamname, 'role': details['role']})
return team_view(orgname, team), 200
raise Unauthorized()
@nickname('deleteOrganizationTeam')
def delete(self, orgname, teamname):
""" Delete the specified team. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
model.remove_team(orgname, teamname, get_authenticated_user().username)
log_action('org_delete_team', orgname, {'team': teamname})
return 'Deleted', 204
raise Unauthorized()
@resource('/v1/organization/<orgname>/team/<teamname>/members')
@internal_only
class TeamMemberList(ApiResource):
""" Resource for managing the list of members for a team. """
@nickname('getOrganizationTeamMembers')
def get(self, orgname, teamname):
""" Retrieve the list of members for the specified team. """
view_permission = ViewTeamPermission(orgname, teamname)
edit_permission = AdministerOrganizationPermission(orgname)
if view_permission.can():
team = None
try:
team = model.get_organization_team(orgname, teamname)
except model.InvalidTeamException:
raise NotFound()
members = model.get_organization_team_members(team.id)
return {
'members': {m.username : member_view(m) for m in members},
'can_edit': edit_permission.can()
}
raise Unauthorized()
@resource('/v1/organization/<orgname>/team/<teamname>/members/<membername>')
@internal_only
class TeamMember(ApiResource):
""" Resource for managing individual members of a team. """
@nickname('updateOrganizationTeamMember')
def put(self, orgname, teamname, membername):
""" Add a member to an existing team. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
team = None
user = None
# Find the team.
try:
team = model.get_organization_team(orgname, teamname)
except model.InvalidTeamException:
raise NotFound()
# Find the user.
user = model.get_user(membername)
if not user:
raise request_error(message='Unknown user')
# Add the user to the team.
model.add_user_to_team(user, team)
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
return member_view(user)
raise Unauthorized()
@nickname('deleteOrganizationTeamMember')
def delete(self, orgname, teamname, membername):
""" Delete an existing member of a team. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
# Remote the user from the team.
invoking_user = get_authenticated_user().username
model.remove_user_from_team(orgname, teamname, membername, invoking_user)
log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname})
return 'Deleted', 204
raise Unauthorized()

267
endpoints/api/trigger.py Normal file
View file

@ -0,0 +1,267 @@
import json
import logging
from flask import request, url_for
from urllib import quote
from urlparse import urlunparse
from app import app
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, request_error, query_param, parse_args, internal_only,
validate_json_request, api, Unauthorized, NotFound, InvalidRequest)
from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuildStatus,
get_trigger_config)
from endpoints.common import start_build
from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException,
TriggerActivationException, EmptyRepositoryException)
from data import model
from auth.permissions import UserAdminPermission
logger = logging.getLogger(__name__)
def _prepare_webhook_url(scheme, username, password, hostname, path):
auth_hostname = '%s:%s@%s' % (quote(username), quote(password), hostname)
return urlunparse((scheme, auth_hostname, path, '', '', ''))
@resource('/v1/repository/<repopath:repository>/trigger/')
class BuildTriggerList(RepositoryParamResource):
""" Resource for listing repository build triggers. """
@require_repo_admin
@nickname('listBuildTriggers')
def get(self, namespace, repository):
""" List the triggers for the specified repository. """
triggers = model.list_build_triggers(namespace, repository)
return {
'triggers': [trigger_view(trigger) for trigger in triggers]
}
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>')
class BuildTrigger(RepositoryParamResource):
""" Resource for managing specific build triggers. """
@require_repo_admin
@nickname('getBuildTrigger')
def get(self, namespace, repository, trigger_uuid):
""" Get information for the specified build trigger. """
try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException:
raise NotFound()
return trigger_view(trigger)
@require_repo_admin
@nickname('deleteBuildTrigger')
def delete(self, namespace, repository, trigger_uuid):
""" Delete the specified build trigger. """
try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException:
raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
config_dict = get_trigger_config(trigger)
if handler.is_active(config_dict):
try:
handler.deactivate(trigger.auth_token, config_dict)
except TriggerDeactivationException as ex:
# We are just going to eat this error
logger.warning('Trigger deactivation problem: %s', ex)
log_action('delete_repo_trigger', namespace,
{'repo': repository, 'trigger_id': trigger_uuid,
'service': trigger.service.name, 'config': config_dict},
repo=model.get_repository(namespace, repository))
trigger.delete_instance(recursive=True)
return 'No Content', 204
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/subdir')
@internal_only
class BuildTriggerSubdirs(RepositoryParamResource):
""" Custom verb for fetching the subdirs which are buildable for a trigger. """
schemas = {
'BuildTriggerSubdirRequest': {
'id': 'BuildTriggerSubdirRequest',
'type': 'object',
'description': 'Arbitrary json.',
},
}
@require_repo_admin
@nickname('listBuildTriggerSubdirs')
@validate_json_request('BuildTriggerSubdirRequest')
def post(self, namespace, repository, trigger_uuid):
""" List the subdirectories available for the specified build trigger and source. """
try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException:
raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can():
new_config_dict = request.get_json()
try:
subdirs = handler.list_build_subdirs(trigger.auth_token, new_config_dict)
return {
'subdir': subdirs,
'status': 'success'
}
except EmptyRepositoryException as exc:
return {
'status': 'error',
'message': exc.msg
}
else:
raise Unauthorized()
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/activate')
@internal_only
class BuildTriggerActivate(RepositoryParamResource):
""" Custom verb for activating a build trigger once all required information has been collected.
"""
schemas = {
'BuildTriggerActivateRequest': {
'id': 'BuildTriggerActivateRequest',
'type': 'object',
'description': 'Arbitrary json.',
},
}
@require_repo_admin
@nickname('activateBuildTrigger')
@validate_json_request('BuildTriggerActivateRequest')
def post(self, namespace, repository, trigger_uuid):
""" Activate the specified build trigger. """
try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException:
raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
existing_config_dict = get_trigger_config(trigger)
if handler.is_active(existing_config_dict):
raise InvalidRequest('Trigger config is not sufficient for activation.')
user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can():
new_config_dict = request.get_json()
token_name = 'Build Trigger: %s' % trigger.service.name
token = model.create_delegate_token(namespace, repository, token_name,
'write')
try:
repository_path = '%s/%s' % (trigger.repository.namespace,
trigger.repository.name)
path = url_for('webhooks.build_trigger_webhook',
repository=repository_path, trigger_uuid=trigger.uuid)
authed_url = _prepare_webhook_url(app.config['URL_SCHEME'], '$token',
token.code, app.config['URL_HOST'],
path)
final_config = handler.activate(trigger.uuid, authed_url,
trigger.auth_token, new_config_dict)
except TriggerActivationException as exc:
token.delete_instance()
raise request_error(message=exc.message)
# Save the updated config.
trigger.config = json.dumps(final_config)
trigger.write_token = token
trigger.save()
# Log the trigger setup.
repo = model.get_repository(namespace, repository)
log_action('setup_repo_trigger', namespace,
{'repo': repository, 'namespace': namespace,
'trigger_id': trigger.uuid, 'service': trigger.service.name,
'config': final_config}, repo=repo)
return trigger_view(trigger)
else:
raise Unauthorized()
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
class ActivateBuildTrigger(RepositoryParamResource):
""" Custom verb to manually activate a build trigger. """
@require_repo_admin
@nickname('manuallyStartBuildTrigger')
def post(self, namespace, repository, trigger_uuid):
""" Manually start a build from the specified trigger. """
try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException:
raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
config_dict = get_trigger_config(trigger)
if not handler.is_active(config_dict):
raise InvalidRequest('Trigger is not active.')
specs = handler.manual_start(trigger.auth_token, config_dict)
dockerfile_id, tags, name, subdir = specs
repo = model.get_repository(namespace, repository)
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True)
resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository)
headers = {
'Location': api.url_for(RepositoryBuildStatus, repository=repo_string,
build_uuid=build_request.uuid),
}
return resp, 201, headers
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/builds')
class TriggerBuildList(RepositoryParamResource):
""" Resource to represent builds that were activated from the specified trigger. """
@require_repo_admin
@parse_args
@query_param('limit', 'The maximum number of builds to return', type=int, default=5)
@nickname('listTriggerRecentBuilds')
def get(self, args, namespace, repository, trigger_uuid):
""" List the builds started by the specified trigger. """
limit = args['limit']
builds = list(model.list_trigger_builds(namespace, repository,
trigger_uuid, limit))
return {
'builds': [build_status_view(build, True) for build in builds]
}
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
@internal_only
class BuildTriggerSources(RepositoryParamResource):
""" Custom verb to fetch the list of build sources for the trigger config. """
@require_repo_admin
@nickname('listTriggerBuildSources')
def get(self, namespace, repository, trigger_uuid):
""" List the build sources for the trigger configuration thus far. """
try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException:
raise NotFound()
user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can():
trigger_handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
return {
'sources': trigger_handler.list_build_sources(trigger.auth_token)
}
else:
raise Unauthorized()

450
endpoints/api/user.py Normal file
View file

@ -0,0 +1,450 @@
import logging
import stripe
import json
from flask import request
from flask.ext.login import logout_user
from flask.ext.principal import identity_changed, AnonymousIdentity
from app import app
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin,
InvalidToken, require_scope, format_date)
from endpoints.api.subscribe import subscribe
from endpoints.common import common_login
from data import model
from data.plans import get_plan
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
UserAdminPermission, UserReadPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes
from util.gravatar import compute_hash
from util.email import (send_confirmation_email, send_recovery_email,
send_change_email)
logger = logging.getLogger(__name__)
def user_view(user):
def org_view(o):
admin_org = AdministerOrganizationPermission(o.username)
return {
'name': o.username,
'gravatar': compute_hash(o.email),
'is_org_admin': admin_org.can(),
'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
'preferred_namespace': not (o.stripe_id is None)
}
organizations = model.get_user_organizations(user.username)
def login_view(login):
return {
'service': login.service.name,
'service_identifier': login.service_ident,
}
logins = model.list_federated_logins(user)
user_response = {
'verified': user.verified,
'anonymous': False,
'username': user.username,
'email': user.email,
'gravatar': compute_hash(user.email),
}
user_admin = UserAdminPermission(user.username)
if user_admin.can():
user_response.update({
'organizations': [org_view(o) for o in organizations],
'logins': [login_view(login) for login in logins],
'can_create_repo': True,
'invoice_email': user.invoice_email,
'preferred_namespace': not (user.stripe_id is None),
})
return user_response
def notification_view(notification):
return {
'organization': notification.target.username if notification.target.organization else None,
'kind': notification.kind.name,
'created': format_date(notification.created),
'metadata': json.loads(notification.metadata_json),
}
@resource('/v1/user/')
class User(ApiResource):
""" Operations related to users. """
schemas = {
'NewUser': {
'id': 'NewUser',
'type': 'object',
'description': 'Fields which must be specified for a new user.',
'required': [
'username',
'password',
'email',
],
'properties': {
'username': {
'type': 'string',
'description': 'The user\'s username',
},
'password': {
'type': 'string',
'description': 'The user\'s password',
},
'email': {
'type': 'string',
'description': 'The user\'s email address',
},
}
},
'UpdateUser': {
'id': 'UpdateUser',
'type': 'object',
'description': 'Fields which can be updated in a user.',
'properties': {
'password': {
'type': 'string',
'description': 'The user\'s password',
},
'invoice_email': {
'type': 'boolean',
'description': 'Whether the user desires to receive an invoice email.',
},
'email': {
'type': 'string',
'description': 'The user\'s email address',
},
},
},
}
@require_scope(scopes.READ_USER)
@nickname('getLoggedInUser')
def get(self):
""" Get user information for the authenticated user. """
user = get_authenticated_user()
if user is None or user.organization or not UserReadPermission(user.username).can():
raise InvalidToken("Requires authentication", payload={'session_required': False})
return user_view(user)
@require_user_admin
@nickname('changeUserDetails')
@internal_only
@validate_json_request('UpdateUser')
def put(self):
""" Update a users details such as password or email. """
user = get_authenticated_user()
user_data = request.get_json()
try:
if 'password' in user_data:
logger.debug('Changing password for user: %s', user.username)
log_action('account_change_password', user.username)
model.change_password(user, user_data['password'])
if 'invoice_email' in user_data:
logger.debug('Changing invoice_email for user: %s', user.username)
model.change_invoice_email(user, user_data['invoice_email'])
if 'email' in user_data and user_data['email'] != user.email:
new_email = user_data['email']
if model.find_user_by_email(new_email):
# Email already used.
raise request_error(message='E-mail address already used')
logger.debug('Sending email to change email address for user: %s',
user.username)
code = model.create_confirm_email_code(user, new_email=new_email)
send_change_email(user.username, user_data['email'], code.code)
except model.InvalidPasswordException, ex:
raise request_error(exception=ex)
return user_view(user)
@nickname('createNewUser')
@internal_only
@validate_json_request('NewUser')
def post(self):
""" Create a new user. """
user_data = request.get_json()
existing_user = model.get_user(user_data['username'])
if existing_user:
raise request_error(message='The username already exists')
try:
new_user = model.create_user(user_data['username'], user_data['password'],
user_data['email'])
code = model.create_confirm_email_code(new_user)
send_confirmation_email(new_user.username, new_user.email, code.code)
return 'Created', 201
except model.DataModelException as ex:
raise request_error(exception=ex)
@resource('/v1/user/private')
@internal_only
class PrivateRepositories(ApiResource):
""" Operations dealing with the available count of private repositories. """
@require_user_admin
@nickname('getUserPrivateAllowed')
def get(self):
""" Get the number of private repos this user has, and whether they are allowed to create more.
"""
user = get_authenticated_user()
private_repos = model.get_private_repo_count(user.username)
repos_allowed = 0
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
if cus.subscription:
plan = get_plan(cus.subscription.plan.id)
if plan:
repos_allowed = plan['privateRepos']
return {
'privateCount': private_repos,
'privateAllowed': (private_repos < repos_allowed)
}
def conduct_signin(username_or_email, password):
needs_email_verification = False
invalid_credentials = False
verified = model.verify_user(username_or_email, password)
if verified:
if common_login(verified):
return {'success': True}
else:
needs_email_verification = True
else:
invalid_credentials = True
return {
'needsEmailVerification': needs_email_verification,
'invalidCredentials': invalid_credentials,
}, 403
@resource('/v1/user/convert')
@internal_only
class ConvertToOrganization(ApiResource):
""" Operations for converting a user to an organization. """
schemas = {
'ConvertUser': {
'id': 'ConvertUser',
'type': 'object',
'description': 'Information required to convert a user to an organization.',
'required': [
'adminUser',
'adminPassword',
'plan',
],
'properties': {
'adminUser': {
'type': 'string',
'description': 'The user who will become an org admin\'s username',
},
'adminPassword': {
'type': 'string',
'description': 'The user who will become an org admin\'s password',
},
'plan': {
'type': 'string',
'description': 'The plan to which the organizatino should be subscribed',
},
},
},
}
@require_user_admin
@nickname('convertUserToOrganization')
@validate_json_request('ConvertUser')
def post(self):
""" Convert the user to an organization. """
user = get_authenticated_user()
convert_data = request.get_json()
# Ensure that the new admin user is the not user being converted.
admin_username = convert_data['adminUser']
if admin_username == user.username:
raise request_error(reason='invaliduser',
message='The admin user is not valid')
# Ensure that the sign in credentials work.
admin_password = convert_data['adminPassword']
if not model.verify_user(admin_username, admin_password):
raise request_error(reason='invaliduser',
message='The admin user credentials are not valid')
# Subscribe the organization to the new plan.
plan = convert_data['plan']
subscribe(user, plan, None, True) # Require business plans
# Convert the user to an organization.
model.convert_user_to_organization(user, model.get_user(admin_username))
log_action('account_convert', user.username)
# And finally login with the admin credentials.
return conduct_signin(admin_username, admin_password)
@resource('/v1/signin')
@internal_only
class Signin(ApiResource):
""" Operations for signing in the user. """
schemas = {
'SigninUser': {
'id': 'SigninUser',
'type': 'object',
'description': 'Information required to sign in a user.',
'required': [
'username',
'password',
],
'properties': {
'username': {
'type': 'string',
'description': 'The user\'s username',
},
'password': {
'type': 'string',
'description': 'The user\'s password',
},
},
},
}
@nickname('signinUser')
@validate_json_request('SigninUser')
def post(self):
""" Sign in the user with the specified credentials. """
signin_data = request.get_json()
if not signin_data:
raise NotFound()
username = signin_data['username']
password = signin_data['password']
return conduct_signin(username, password)
@resource('/v1/signout')
@internal_only
class Signout(ApiResource):
""" Resource for signing out users. """
@nickname('logout')
def post(self):
""" Request that the current user be signed out. """
logout_user()
identity_changed.send(app, identity=AnonymousIdentity())
return {'success': True}
@resource("/v1/recovery")
@internal_only
class Recovery(ApiResource):
""" Resource for requesting a password recovery email. """
schemas = {
'RequestRecovery': {
'id': 'RequestRecovery',
'type': 'object',
'description': 'Information required to sign in a user.',
'required': [
'email',
],
'properties': {
'email': {
'type': 'string',
'description': 'The user\'s email address',
},
},
},
}
@nickname('requestRecoveryEmail')
@validate_json_request('RequestRecovery')
def post(self):
""" Request a password recovery email."""
email = request.get_json()['email']
code = model.create_reset_password_email_code(email)
send_recovery_email(email, code.code)
return 'Created', 201
@resource('/v1/user/notifications')
@internal_only
class UserNotificationList(ApiResource):
@require_user_admin
@nickname('listUserNotifications')
def get(self):
notifications = model.list_notifications(get_authenticated_user())
return {
'notifications': [notification_view(notification) for notification in notifications]
}
def authorization_view(access_token):
oauth_app = access_token.application
return {
'application': {
'name': oauth_app.name,
'description': oauth_app.description,
'url': oauth_app.application_uri,
'gravatar': compute_hash(oauth_app.gravatar_email or oauth_app.organization.email),
'organization': {
'name': oauth_app.organization.username,
'gravatar': compute_hash(oauth_app.organization.email)
}
},
'scopes': scopes.get_scope_information(access_token.scope),
'uuid': access_token.uuid
}
@resource('/v1/user/authorizations')
@internal_only
class UserAuthorizationList(ApiResource):
@require_user_admin
@nickname('listUserAuthorizations')
def get(self):
access_tokens = model.oauth.list_access_tokens_for_user(get_authenticated_user())
return {
'authorizations': [authorization_view(token) for token in access_tokens]
}
@resource('/v1/user/authorizations/<access_token_uuid>')
@internal_only
class UserAuthorization(ApiResource):
@require_user_admin
@nickname('getUserAuthorization')
def get(self, access_token_uuid):
access_token = model.oauth.lookup_access_token_for_user(get_authenticated_user(),
access_token_uuid)
if not access_token:
raise NotFound()
return authorization_view(access_token)
@require_user_admin
@nickname('deleteUserAuthorization')
def delete(self, access_token_uuid):
access_token = model.oauth.lookup_access_token_for_user(get_authenticated_user(),
access_token_uuid)
if not access_token:
raise NotFound()
access_token.delete_instance(recursive=True, delete_nullable=True)
return 'Deleted', 204

77
endpoints/api/webhook.py Normal file
View file

@ -0,0 +1,77 @@
import json
from flask import request
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, validate_json_request, api, NotFound)
from data import model
def webhook_view(webhook):
return {
'public_id': webhook.public_id,
'parameters': json.loads(webhook.parameters),
}
@resource('/v1/repository/<repopath:repository>/webhook/')
class WebhookList(RepositoryParamResource):
""" Resource for dealing with listing and creating webhooks. """
schemas = {
'WebhookCreateRequest': {
'id': 'WebhookCreateRequest',
'type': 'object',
'description': 'Arbitrary json.',
},
}
@require_repo_admin
@nickname('createWebhook')
@validate_json_request('WebhookCreateRequest')
def post(self, namespace, repository):
""" Create a new webhook for the specified repository. """
repo = model.get_repository(namespace, repository)
webhook = model.create_webhook(repo, request.get_json())
resp = webhook_view(webhook)
repo_string = '%s/%s' % (namespace, repository)
headers = {
'Location': api.url_for(Webhook, repository=repo_string, public_id=webhook.public_id),
}
log_action('add_repo_webhook', namespace,
{'repo': repository, 'webhook_id': webhook.public_id},
repo=repo)
return resp, 201, headers
@require_repo_admin
@nickname('listWebhooks')
def get(self, namespace, repository):
""" List the webhooks for the specified repository. """
webhooks = model.list_webhooks(namespace, repository)
return {
'webhooks': [webhook_view(webhook) for webhook in webhooks]
}
@resource('/v1/repository/<repopath:repository>/webhook/<public_id>')
class Webhook(RepositoryParamResource):
""" Resource for dealing with specific webhooks. """
@require_repo_admin
@nickname('getWebhook')
def get(self, namespace, repository, public_id):
""" Get information for the specified webhook. """
try:
webhook = model.get_webhook(namespace, repository, public_id)
except model.InvalidWebhookException:
raise NotFound()
return webhook_view(webhook)
@require_repo_admin
@nickname('deleteWebhook')
def delete(self, namespace, repository, public_id):
""" Delete the specified webhook. """
model.delete_webhook(namespace, repository, public_id)
log_action('delete_repo_webhook', namespace,
{'repo': repository, 'webhook_id': public_id},
repo=model.get_repository(namespace, repository))
return 'No Content', 204