Merge branch 'swaggerlikeus' of https://bitbucket.org/yackob03/quay into swaggerlikeus
This commit is contained in:
commit
767ab1085a
17 changed files with 1357 additions and 28 deletions
|
@ -58,7 +58,8 @@ def method_metadata(func, name):
|
|||
|
||||
|
||||
nickname = partial(add_method_metadata, 'nickname')
|
||||
internal_api = add_method_metadata('internal_api', True)
|
||||
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,
|
||||
|
@ -173,14 +174,20 @@ def log_action(kind, user_or_orgname, metadata={}, repo=None):
|
|||
|
||||
import endpoints.api.legacy
|
||||
|
||||
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
|
||||
|
|
316
endpoints/api/billing.py
Normal file
316
endpoints/api/billing.py
Normal file
|
@ -0,0 +1,316 @@
|
|||
import logging
|
||||
import stripe
|
||||
|
||||
from flask import request
|
||||
from flask.ext.restful import abort
|
||||
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
|
||||
related_user_resource, internal_only)
|
||||
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': True,
|
||||
'properties': {
|
||||
'token': {
|
||||
'type': 'string',
|
||||
'description': 'Stripe token that is generated by stripe checkout.js',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@nickname('getUserCard')
|
||||
def get(self):
|
||||
""" Get the user's credit card. """
|
||||
user = get_authenticated_user()
|
||||
return get_card(user)
|
||||
|
||||
@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': True,
|
||||
'properties': {
|
||||
'token': {
|
||||
'type': 'string',
|
||||
'description': 'Stripe token that is generated by stripe checkout.js',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
abort(403)
|
||||
|
||||
@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
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@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': True,
|
||||
'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',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@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': True,
|
||||
'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',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
abort(403)
|
||||
|
||||
@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,
|
||||
}
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@resource('/v1/user/invoices')
|
||||
class UserInvoiceList(ApiResource):
|
||||
""" Resource for listing a user's invoices. """
|
||||
@nickname('listUserInvoices')
|
||||
def get(self):
|
||||
""" List the invoices for the current user. """
|
||||
user = get_authenticated_user()
|
||||
if not user.stripe_id:
|
||||
abort(404)
|
||||
|
||||
return get_invoices(user.stripe_id)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/invoices')
|
||||
@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:
|
||||
abort(404)
|
||||
|
||||
return get_invoices(organization.stripe_id)
|
||||
|
||||
abort(403)
|
|
@ -6,7 +6,8 @@ from flask.ext.restful import abort
|
|||
|
||||
from app import app
|
||||
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
|
||||
require_repo_read, require_repo_write, validate_json_request)
|
||||
require_repo_read, require_repo_write, validate_json_request,
|
||||
ApiResource, internal_only)
|
||||
from endpoints.common import start_build
|
||||
from data import model
|
||||
from auth.permissions import ModifyRepositoryPermission
|
||||
|
@ -126,7 +127,7 @@ class RepositoryBuildStatus(RepositoryParamResource):
|
|||
return build_status_view(build, can_write)
|
||||
|
||||
|
||||
@resource('/repository/<path:repository>/build/<build_uuid>/logs')
|
||||
@resource('/v1/repository/<path:repository>/build/<build_uuid>/logs')
|
||||
class RepositoryBuildLogs(RepositoryParamResource):
|
||||
""" Resource for loading repository build logs. """
|
||||
@require_repo_write
|
||||
|
@ -148,3 +149,35 @@ class RepositoryBuildLogs(RepositoryParamResource):
|
|||
})
|
||||
|
||||
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': True,
|
||||
'properties': {
|
||||
'mimeType': {
|
||||
'type': 'string',
|
||||
'description': 'Type of the file which is about to be uploaded',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@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': file_id
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@ import logging
|
|||
|
||||
from flask.ext.restful import reqparse
|
||||
|
||||
from endpoints.api import ApiResource, resource, method_metadata, nickname, truthy_bool
|
||||
from endpoints.api import (ApiResource, resource, method_metadata, nickname, truthy_bool,
|
||||
parse_args, query_param)
|
||||
from app import app
|
||||
from auth import scopes
|
||||
|
||||
|
@ -23,7 +24,12 @@ TYPE_CONVERTER = {
|
|||
}
|
||||
|
||||
|
||||
def swagger_route_data():
|
||||
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):
|
||||
apis = []
|
||||
models = {}
|
||||
for rule in app.url_map.iter_rules():
|
||||
|
@ -47,7 +53,9 @@ def swagger_route_data():
|
|||
'required': True,
|
||||
})
|
||||
|
||||
if method is not None:
|
||||
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({
|
||||
|
@ -87,17 +95,34 @@ def swagger_route_data():
|
|||
scope = method_metadata(method, 'oauth2_scope')
|
||||
if scope:
|
||||
new_operation['authorizations'] = {
|
||||
'oauth2': [scope]
|
||||
'oauth2': [scope],
|
||||
}
|
||||
|
||||
operations.append(new_operation)
|
||||
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)
|
||||
apis.append({
|
||||
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)
|
||||
|
||||
swagger_data = {
|
||||
'apiVersion': 'v1',
|
||||
|
@ -133,7 +158,9 @@ def swagger_route_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):
|
||||
def get(self, args):
|
||||
""" List all of the API endpoints available in the swagger API format."""
|
||||
return swagger_route_data()
|
||||
return swagger_route_data(args['internal'])
|
||||
|
|
|
@ -6,7 +6,7 @@ from flask.ext.restful import abort
|
|||
from app import app
|
||||
from endpoints.api import resource, nickname, require_repo_read, RepositoryParamResource
|
||||
from data import model
|
||||
from util.cache import cache_control
|
||||
from util.cache import cache_control_flask_restful
|
||||
|
||||
|
||||
store = app.config['STORAGE']
|
||||
|
@ -72,7 +72,7 @@ class RepositoryImage(RepositoryParamResource):
|
|||
class RepositoryImageChanges(RepositoryParamResource):
|
||||
""" Resource for handling repository image change lists. """
|
||||
|
||||
@cache_control(max_age=60*60) # Cache for one hour
|
||||
@cache_control_flask_restful(max_age=60*60) # Cache for one hour
|
||||
@require_repo_read
|
||||
@nickname('getImageChanges')
|
||||
def get(self, namespace, repository, image_id):
|
||||
|
|
|
@ -117,6 +117,7 @@ def welcome():
|
|||
return jsonify({'version': '0.5'})
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/plans/')
|
||||
def list_plans():
|
||||
return jsonify({
|
||||
|
@ -407,6 +408,7 @@ def team_view(orgname, team):
|
|||
}
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/', methods=['POST'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
@ -454,6 +456,7 @@ def org_view(o, teams):
|
|||
return view
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_organization(orgname):
|
||||
|
@ -470,6 +473,7 @@ def get_organization(orgname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>', methods=['PUT'])
|
||||
@api_login_required
|
||||
@org_api_call('change_user_details')
|
||||
|
@ -523,6 +527,7 @@ def prototype_view(proto, org_members):
|
|||
'id': proto.uuid,
|
||||
}
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/prototypes', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_organization_prototype_permissions(orgname):
|
||||
|
@ -561,6 +566,7 @@ def log_prototype_action(action_kind, orgname, prototype, **kwargs):
|
|||
log_action(action_kind, orgname, log_params)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/prototypes', methods=['POST'])
|
||||
@api_login_required
|
||||
def create_organization_prototype_permission(orgname):
|
||||
|
@ -609,6 +615,7 @@ def create_organization_prototype_permission(orgname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/prototypes/<prototypeid>',
|
||||
methods=['DELETE'])
|
||||
@api_login_required
|
||||
|
@ -631,6 +638,7 @@ def delete_organization_prototype_permission(orgname, prototypeid):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/prototypes/<prototypeid>',
|
||||
methods=['PUT'])
|
||||
@api_login_required
|
||||
|
@ -660,6 +668,7 @@ def update_organization_prototype_permission(orgname, prototypeid):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/members', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_organization_members(orgname):
|
||||
|
@ -689,6 +698,7 @@ def get_organization_members(orgname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/members/<membername>', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_organization_member(orgname, membername):
|
||||
|
@ -718,6 +728,7 @@ def get_organization_member(orgname, membername):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/private', methods=['GET'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
@ -758,6 +769,7 @@ def member_view(member):
|
|||
}
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/team/<teamname>',
|
||||
methods=['PUT', 'POST'])
|
||||
@api_login_required
|
||||
|
@ -804,6 +816,7 @@ def update_organization_team(orgname, teamname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/team/<teamname>',
|
||||
methods=['DELETE'])
|
||||
@api_login_required
|
||||
|
@ -817,6 +830,7 @@ def delete_organization_team(orgname, teamname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/team/<teamname>/members',
|
||||
methods=['GET'])
|
||||
@api_login_required
|
||||
|
@ -840,6 +854,7 @@ def get_organization_team_members(orgname, teamname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/team/<teamname>/members/<membername>',
|
||||
methods=['PUT', 'POST'])
|
||||
@api_login_required
|
||||
|
@ -869,6 +884,7 @@ def update_organization_team_member(orgname, teamname, membername):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/team/<teamname>/members/<membername>',
|
||||
methods=['DELETE'])
|
||||
@api_login_required
|
||||
|
@ -1608,6 +1624,7 @@ def delete_build_trigger(namespace, repository, trigger_uuid):
|
|||
abort(403) # Permission denied
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/filedrop/', methods=['POST'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
@ -2078,6 +2095,7 @@ def subscription_view(stripe_subscription, used_repos):
|
|||
}
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/user/card', methods=['GET'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
@ -2086,6 +2104,7 @@ def get_user_card():
|
|||
return get_card(user)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/card', methods=['GET'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
@ -2099,6 +2118,7 @@ def get_org_card(orgname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/user/card', methods=['POST'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
@ -2110,6 +2130,7 @@ def set_user_card():
|
|||
return response
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/card', methods=['POST'])
|
||||
@api_login_required
|
||||
@org_api_call('set_user_card')
|
||||
|
@ -2164,6 +2185,7 @@ def get_card(user):
|
|||
|
||||
return jsonify({'card': card_info})
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/user/plan', methods=['PUT'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
@ -2257,6 +2279,7 @@ def subscribe(user, plan, token, require_business_plan):
|
|||
return resp
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/user/invoices', methods=['GET'])
|
||||
@api_login_required
|
||||
def list_user_invoices():
|
||||
|
@ -2267,6 +2290,7 @@ def list_user_invoices():
|
|||
return get_invoices(user.stripe_id)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/invoices', methods=['GET'])
|
||||
@api_login_required
|
||||
@org_api_call('list_user_invoices')
|
||||
|
@ -2304,6 +2328,7 @@ def get_invoices(customer_id):
|
|||
})
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/plan', methods=['PUT'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
@ -2320,6 +2345,7 @@ def update_org_subscription(orgname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/user/plan', methods=['GET'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
@ -2339,6 +2365,7 @@ def get_user_subscription():
|
|||
})
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/plan', methods=['GET'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
@ -2369,6 +2396,7 @@ def robot_view(name, token):
|
|||
}
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/user/robots', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_user_robots():
|
||||
|
@ -2379,6 +2407,7 @@ def get_user_robots():
|
|||
})
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/robots', methods=['GET'])
|
||||
@api_login_required
|
||||
@org_api_call('get_user_robots')
|
||||
|
@ -2393,6 +2422,7 @@ def get_org_robots(orgname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/user/robots/<robot_shortname>', methods=['PUT'])
|
||||
@api_login_required
|
||||
def create_user_robot(robot_shortname):
|
||||
|
@ -2404,6 +2434,7 @@ def create_user_robot(robot_shortname):
|
|||
return resp
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/robots/<robot_shortname>',
|
||||
methods=['PUT'])
|
||||
@api_login_required
|
||||
|
@ -2421,6 +2452,7 @@ def create_org_robot(orgname, robot_shortname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/user/robots/<robot_shortname>', methods=['DELETE'])
|
||||
@api_login_required
|
||||
def delete_user_robot(robot_shortname):
|
||||
|
@ -2430,6 +2462,7 @@ def delete_user_robot(robot_shortname):
|
|||
return make_response('Deleted', 204)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/robots/<robot_shortname>',
|
||||
methods=['DELETE'])
|
||||
@api_login_required
|
||||
|
@ -2462,7 +2495,7 @@ def log_view(log):
|
|||
return view
|
||||
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/repository/<path:repository>/logs', methods=['GET'])
|
||||
@api_login_required
|
||||
@parse_repository_name
|
||||
|
@ -2480,6 +2513,7 @@ def list_repo_logs(namespace, repository):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/organization/<orgname>/logs', methods=['GET'])
|
||||
@api_login_required
|
||||
@org_api_call('list_user_logs')
|
||||
|
@ -2496,6 +2530,7 @@ def list_org_logs(orgname):
|
|||
abort(403)
|
||||
|
||||
|
||||
# Ported
|
||||
@api_bp.route('/user/logs', methods=['GET'])
|
||||
@api_login_required
|
||||
def list_user_logs():
|
||||
|
|
121
endpoints/api/logs.py
Normal file
121
endpoints/api/logs.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
import json
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from flask.ext.restful import abort
|
||||
|
||||
from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args,
|
||||
RepositoryParamResource, require_repo_admin, related_user_resource)
|
||||
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': 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': start_time,
|
||||
'end_time': end_time,
|
||||
'logs': [log_view(log) for log in logs]
|
||||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<path:repository>/logs')
|
||||
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:
|
||||
abort(404)
|
||||
|
||||
start_time = args['starttime']
|
||||
end_time = args['endtime']
|
||||
return get_logs(namespace, start_time, end_time, repository=repo)
|
||||
|
||||
|
||||
@resource('/v1/user/logs')
|
||||
class UserLogs(ApiResource):
|
||||
""" Resource for fetching logs for the current user. """
|
||||
@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']
|
||||
|
||||
return get_logs(get_authenticated_user().username, start_time, end_time,
|
||||
performer_name=performer_name)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/logs')
|
||||
@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)
|
||||
|
||||
abort(403)
|
252
endpoints/api/organization.py
Normal file
252
endpoints/api/organization.py
Normal file
|
@ -0,0 +1,252 @@
|
|||
import logging
|
||||
import stripe
|
||||
|
||||
from flask import request
|
||||
from flask.ext.restful import abort
|
||||
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||
related_user_resource, internal_only)
|
||||
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': True,
|
||||
'properties': {
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Organization username',
|
||||
'required': True,
|
||||
},
|
||||
'email': {
|
||||
'type': 'string',
|
||||
'description': 'Organization contact email',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@nickname('createOrganization')
|
||||
@validate_json_request('NewOrg')
|
||||
def post(self):
|
||||
""" Create a new organization. """
|
||||
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'
|
||||
return request_error(message=msg)
|
||||
|
||||
try:
|
||||
model.create_organization(org_data['name'], org_data['email'], get_authenticated_user())
|
||||
return 'Created', 201
|
||||
except model.DataModelException as ex:
|
||||
return request_error(exception=ex)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>')
|
||||
@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',
|
||||
'required': True,
|
||||
'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:
|
||||
abort(404)
|
||||
|
||||
teams = model.get_teams_within_org(org)
|
||||
return org_view(org, teams)
|
||||
|
||||
abort(403)
|
||||
|
||||
@nickname('changeOrganizationDetails')
|
||||
@validate_json_request('UpdateOrg')
|
||||
def put(self, orgname):
|
||||
""" Change the details for the specified organization. """
|
||||
try:
|
||||
org = model.get_organization(orgname)
|
||||
except model.InvalidOrganizationException:
|
||||
abort(404)
|
||||
|
||||
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):
|
||||
return 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)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/private')
|
||||
@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
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/members')
|
||||
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:
|
||||
abort(404)
|
||||
|
||||
# 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}
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/members/<membername>')
|
||||
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:
|
||||
abort(404)
|
||||
|
||||
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:
|
||||
abort(404)
|
||||
|
||||
return {'member': member_dict}
|
||||
|
||||
abort(403)
|
|
@ -79,8 +79,7 @@ class RepositoryUserPermissionList(RepositoryParamResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<path:repository>/permissions/user/<username>',
|
||||
methods=['GET'])
|
||||
@resource('/v1/repository/<path:repository>/permissions/user/<username>')
|
||||
class RepositoryUserPermission(RepositoryParamResource):
|
||||
""" Resource for managing individual user permissions. """
|
||||
schemas = {
|
||||
|
@ -152,7 +151,7 @@ class RepositoryUserPermission(RepositoryParamResource):
|
|||
'role': new_permission['role']},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
return perm_view, 200 # 201 for post
|
||||
return perm_view, 200
|
||||
|
||||
@require_repo_admin
|
||||
@nickname('deleteUserPermissions')
|
||||
|
@ -221,7 +220,7 @@ class RepositoryTeamPermission(RepositoryParamResource):
|
|||
'role': new_permission['role']},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
return role_view(perm), 200 # Should be 201 for post
|
||||
return role_view(perm), 200
|
||||
|
||||
@require_repo_admin
|
||||
@nickname('deleteTeamPermissions')
|
||||
|
|
248
endpoints/api/prototype.py
Normal file
248
endpoints/api/prototype.py
Normal file
|
@ -0,0 +1,248 @@
|
|||
from flask import request
|
||||
from flask.ext.restful import abort
|
||||
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||
log_action)
|
||||
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')
|
||||
class PermissionPrototypeList(ApiResource):
|
||||
""" Resource for listing and creating permission prototypes. """
|
||||
schemas = {
|
||||
'NewPrototype': {
|
||||
'id': 'NewPrototype',
|
||||
'type': 'object',
|
||||
'description': 'Description of a new prototype',
|
||||
'required': True,
|
||||
'properties': {
|
||||
'role': {
|
||||
'type': 'string',
|
||||
'description': 'Role that should be applied to the delegate',
|
||||
'required': True,
|
||||
'enum': [
|
||||
'read',
|
||||
'write',
|
||||
'admin',
|
||||
],
|
||||
},
|
||||
'activating_user': {
|
||||
'type': 'object',
|
||||
'description': 'Repository creating user to whom the rule should apply',
|
||||
'properties': {
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'The username for the activating_user',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
},
|
||||
'delegate': {
|
||||
'type': 'object',
|
||||
'description': 'Information about the user or team to which the rule grants access',
|
||||
'required': True,
|
||||
'properties': {
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'The name for the delegate team or user',
|
||||
'required': True,
|
||||
},
|
||||
'kind': {
|
||||
'type': 'string',
|
||||
'description': 'Whether the delegate is a user or a team',
|
||||
'required': True,
|
||||
'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:
|
||||
abort(404)
|
||||
|
||||
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]}
|
||||
|
||||
abort(403)
|
||||
|
||||
@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:
|
||||
abort(404)
|
||||
|
||||
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:
|
||||
return request_error(message='Unknown activating user')
|
||||
|
||||
if not delegate_user and not delegate_team:
|
||||
return 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)
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/prototypes/<prototypeid>')
|
||||
class PermissionPrototype(ApiResource):
|
||||
""" Resource for managingin individual permission prototypes. """
|
||||
schemas = {
|
||||
'PrototypeUpdate': {
|
||||
'id': 'PrototypeUpdate',
|
||||
'type': 'object',
|
||||
'description': 'Description of a the new prototype role',
|
||||
'required': True,
|
||||
'properties': {
|
||||
'role': {
|
||||
'type': 'string',
|
||||
'description': 'Role that should be applied to the permission',
|
||||
'required': True,
|
||||
'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:
|
||||
abort(404)
|
||||
|
||||
prototype = model.delete_prototype_permission(org, prototypeid)
|
||||
if not prototype:
|
||||
abort(404)
|
||||
|
||||
log_prototype_action('delete_prototype_permission', orgname, prototype)
|
||||
|
||||
return 'Deleted', 204
|
||||
|
||||
abort(403)
|
||||
|
||||
@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:
|
||||
abort(404)
|
||||
|
||||
existing = model.get_prototype_permission(org, prototypeid)
|
||||
if not existing:
|
||||
abort(404)
|
||||
|
||||
details = request.get_json()
|
||||
role_name = details['role']
|
||||
prototype = model.update_prototype_permission(org, prototypeid, role_name)
|
||||
if not prototype:
|
||||
abort(404)
|
||||
|
||||
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)
|
||||
|
||||
abort(403)
|
|
@ -3,7 +3,6 @@ import json
|
|||
|
||||
from flask import current_app, request
|
||||
from flask.ext.restful import reqparse, abort
|
||||
from flask.ext.login import current_user
|
||||
|
||||
from data import model
|
||||
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
|
||||
|
@ -13,8 +12,8 @@ from endpoints.api import (truthy_bool, format_date, nickname, log_action, valid
|
|||
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
||||
AdministerRepositoryPermission, CreateRepositoryPermission)
|
||||
from auth.auth import process_auth
|
||||
from auth import scopes
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -113,8 +112,8 @@ class RepositoryList(ApiResource):
|
|||
}
|
||||
|
||||
username = None
|
||||
if current_user.is_authenticated() and args['private']:
|
||||
username = current_user.db_user().username
|
||||
if get_authenticated_user() and args['private']:
|
||||
username = get_authenticated_user().username
|
||||
|
||||
response = {}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class RepositoryTokenList(RepositoryParamResource):
|
|||
'properties': {
|
||||
'friendlyName': {
|
||||
'type': 'string',
|
||||
'description': 'Friendly name to help identify the token.',
|
||||
'description': 'Friendly name to help identify the token',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
|
@ -72,7 +72,7 @@ class RepositoryToken(RepositoryParamResource):
|
|||
'TokenPermission': {
|
||||
'id': 'TokenPermission',
|
||||
'type': 'object',
|
||||
'description': 'Description of a token permission.',
|
||||
'description': 'Description of a token permission',
|
||||
'required': True,
|
||||
'properties': {
|
||||
'role': {
|
||||
|
|
96
endpoints/api/robot.py
Normal file
96
endpoints/api/robot.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
from flask.ext.restful import abort
|
||||
|
||||
from endpoints.api import resource, nickname, ApiResource, log_action, related_user_resource
|
||||
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')
|
||||
class UserRobotList(ApiResource):
|
||||
""" Resource for listing user robots. """
|
||||
@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>')
|
||||
class UserRobot(ApiResource):
|
||||
""" Resource for managing a user's robots. """
|
||||
@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)
|
||||
resp = robot_view(robot.username, password)
|
||||
log_action('create_robot', parent.username, {'robot': robot_shortname})
|
||||
resp.status_code = 201
|
||||
return resp
|
||||
|
||||
@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')
|
||||
@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]
|
||||
}
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/robots/<robot_shortname>')
|
||||
@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)
|
||||
resp = robot_view(robot.username, password)
|
||||
log_action('create_robot', orgname, {'robot': robot_shortname})
|
||||
resp.status_code = 201
|
||||
return resp
|
||||
|
||||
abort(403)
|
||||
|
||||
@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
|
||||
|
||||
abort(403)
|
177
endpoints/api/team.py
Normal file
177
endpoints/api/team.py
Normal file
|
@ -0,0 +1,177 @@
|
|||
from flask import request
|
||||
from flask.ext.restful import abort
|
||||
|
||||
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
|
||||
log_action)
|
||||
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>')
|
||||
class OrganizationTeam(ApiResource):
|
||||
""" Resource for manging an organization's teams. """
|
||||
schemas = {
|
||||
'TeamDescription': {
|
||||
'id': 'TeamDescription',
|
||||
'type': 'object',
|
||||
'description': 'Description of a team',
|
||||
'required': True,
|
||||
'properties': {
|
||||
'role': {
|
||||
'type': 'string',
|
||||
'description': 'Org wide permissions that should apply to the team',
|
||||
'required': True,
|
||||
'enum': [
|
||||
'member',
|
||||
'creator',
|
||||
'admin',
|
||||
],
|
||||
},
|
||||
'description': {
|
||||
'type': 'string',
|
||||
'description': 'Markdown description for the team',
|
||||
'required': True,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
abort(403)
|
||||
|
||||
@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
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/team/<teamname>/members')
|
||||
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:
|
||||
abort(404)
|
||||
|
||||
members = model.get_organization_team_members(team.id)
|
||||
return {
|
||||
'members': {m.username : member_view(m) for m in members},
|
||||
'can_edit': edit_permission.can()
|
||||
}
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/team/<teamname>/members/<membername>')
|
||||
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:
|
||||
abort(404)
|
||||
|
||||
# Find the user.
|
||||
user = model.get_user(membername)
|
||||
if not user:
|
||||
return 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)
|
||||
|
||||
abort(403)
|
||||
|
||||
@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
|
||||
|
||||
abort(403)
|
|
@ -209,7 +209,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
abort(403)
|
||||
|
||||
|
||||
@resource('/repository/<path:repository>/trigger/<trigger_uuid>/start')
|
||||
@resource('/v1/repository/<path:repository>/trigger/<trigger_uuid>/start')
|
||||
class ActivateBuildTrigger(RepositoryParamResource):
|
||||
""" Custom verb to manually activate a build trigger. """
|
||||
|
||||
|
@ -260,7 +260,7 @@ class TriggerBuildList(RepositoryParamResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/repository/<path:repository>/trigger/<trigger_uuid>/sources')
|
||||
@resource('/v1/repository/<path:repository>/trigger/<trigger_uuid>/sources')
|
||||
class BuildTriggerSources(RepositoryParamResource):
|
||||
""" Custom verb to fetch the list of build sources for the trigger config. """
|
||||
@require_repo_admin
|
||||
|
|
|
@ -8,7 +8,7 @@ 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)
|
||||
log_action, internal_only)
|
||||
from endpoints.api.subscribe import subscribe
|
||||
from endpoints.common import common_login
|
||||
from data import model
|
||||
|
@ -121,6 +121,7 @@ class User(ApiResource):
|
|||
return user_view(user)
|
||||
|
||||
@nickname('changeUserDetails')
|
||||
@internal_only
|
||||
@validate_json_request('UpdateUser')
|
||||
def put(self):
|
||||
""" Update a users details such as password or email. """
|
||||
|
@ -154,6 +155,7 @@ class User(ApiResource):
|
|||
return user_view(user)
|
||||
|
||||
@nickname('createNewUser')
|
||||
@internal_only
|
||||
@validate_json_request('NewUser')
|
||||
def post(self):
|
||||
""" Create a new user. """
|
||||
|
@ -218,6 +220,7 @@ def conduct_signin(username_or_email, password):
|
|||
|
||||
|
||||
@resource('/v1/user/convert')
|
||||
@internal_only
|
||||
class ConvertToOrganization(ApiResource):
|
||||
""" Operations for converting a user to an organization. """
|
||||
schemas = {
|
||||
|
@ -278,6 +281,7 @@ class ConvertToOrganization(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/signin')
|
||||
@internal_only
|
||||
class Signin(ApiResource):
|
||||
""" Operations for signing in the user. """
|
||||
schemas = {
|
||||
|
@ -316,6 +320,7 @@ class Signin(ApiResource):
|
|||
|
||||
|
||||
@resource('/v1/signout')
|
||||
@internal_only
|
||||
class Signout(ApiResource):
|
||||
""" Resource for signing out users. """
|
||||
@nickname('logout')
|
||||
|
@ -327,6 +332,7 @@ class Signout(ApiResource):
|
|||
|
||||
|
||||
@resource("/v1/recovery")
|
||||
@internal_only
|
||||
class Recovery(ApiResource):
|
||||
""" Resource for requesting a password recovery email. """
|
||||
schemas = {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from functools import wraps
|
||||
from flask.ext.restful.utils import unpack
|
||||
|
||||
|
||||
def cache_control(max_age=55):
|
||||
|
@ -12,6 +13,18 @@ def cache_control(max_age=55):
|
|||
return wrap
|
||||
|
||||
|
||||
def cache_control_flask_restful(max_age=55):
|
||||
def wrap(f):
|
||||
@wraps(f)
|
||||
def add_max_age(*args, **kwargs):
|
||||
response = f(*args, **kwargs)
|
||||
body, status_code, headers = unpack(response)
|
||||
headers['Cache-Control'] = 'max-age=%d' % max_age
|
||||
return body, status_code, headers
|
||||
return add_max_age
|
||||
return wrap
|
||||
|
||||
|
||||
def no_cache(f):
|
||||
@wraps(f)
|
||||
def add_no_cache(*args, **kwargs):
|
||||
|
|
Reference in a new issue