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:
commit
302bfb27ae
123 changed files with 16314 additions and 3789 deletions
281
endpoints/api/__init__.py
Normal file
281
endpoints/api/__init__.py
Normal 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
326
endpoints/api/billing.py
Normal 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
213
endpoints/api/build.py
Normal 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
184
endpoints/api/discovery.py
Normal 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
92
endpoints/api/image.py
Normal 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
126
endpoints/api/logs.py
Normal 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()
|
520
endpoints/api/organization.py
Normal file
520
endpoints/api/organization.py
Normal 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
241
endpoints/api/permission.py
Normal 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
253
endpoints/api/prototype.py
Normal 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
291
endpoints/api/repository.py
Normal 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
134
endpoints/api/repotoken.py
Normal 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
98
endpoints/api/robot.py
Normal 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
116
endpoints/api/search.py
Normal 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())]
|
||||
}
|
98
endpoints/api/subscribe.py
Normal file
98
endpoints/api/subscribe.py
Normal 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
95
endpoints/api/tag.py
Normal 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
179
endpoints/api/team.py
Normal 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
267
endpoints/api/trigger.py
Normal 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
450
endpoints/api/user.py
Normal 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
77
endpoints/api/webhook.py
Normal 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
|
Reference in a new issue