diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index a281b075a..1a70c566d 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -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 diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py new file mode 100644 index 000000000..a9be91200 --- /dev/null +++ b/endpoints/api/billing.py @@ -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//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//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//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) \ No newline at end of file diff --git a/endpoints/api/build.py b/endpoints/api/build.py index ace627433..8cf0af485 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -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//build//logs') +@resource('/v1/repository//build//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 + } diff --git a/endpoints/api/discovery.py b/endpoints/api/discovery.py index b2b7f3ba5..8e24b0c80 100644 --- a/endpoints/api/discovery.py +++ b/endpoints/api/discovery.py @@ -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']) diff --git a/endpoints/api/image.py b/endpoints/api/image.py index 4aff05e93..f52e4544f 100644 --- a/endpoints/api/image.py +++ b/endpoints/api/image.py @@ -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): diff --git a/endpoints/api/legacy.py b/endpoints/api/legacy.py index 3f811f958..58be6bf0f 100644 --- a/endpoints/api/legacy.py +++ b/endpoints/api/legacy.py @@ -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/', methods=['GET']) @api_login_required def get_organization(orgname): @@ -470,6 +473,7 @@ def get_organization(orgname): abort(403) +# Ported @api_bp.route('/organization/', 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//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//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//prototypes/', methods=['DELETE']) @api_login_required @@ -631,6 +638,7 @@ def delete_organization_prototype_permission(orgname, prototypeid): abort(403) +# Ported @api_bp.route('/organization//prototypes/', methods=['PUT']) @api_login_required @@ -660,6 +668,7 @@ def update_organization_prototype_permission(orgname, prototypeid): abort(403) +# Ported @api_bp.route('/organization//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//members/', 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//private', methods=['GET']) @api_login_required @internal_api_call @@ -758,6 +769,7 @@ def member_view(member): } +# Ported @api_bp.route('/organization//team/', methods=['PUT', 'POST']) @api_login_required @@ -804,6 +816,7 @@ def update_organization_team(orgname, teamname): abort(403) +# Ported @api_bp.route('/organization//team/', methods=['DELETE']) @api_login_required @@ -817,6 +830,7 @@ def delete_organization_team(orgname, teamname): abort(403) +# Ported @api_bp.route('/organization//team//members', methods=['GET']) @api_login_required @@ -840,6 +854,7 @@ def get_organization_team_members(orgname, teamname): abort(403) +# Ported @api_bp.route('/organization//team//members/', 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//team//members/', 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//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//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//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//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//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//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/', 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//robots/', methods=['PUT']) @api_login_required @@ -2421,6 +2452,7 @@ def create_org_robot(orgname, robot_shortname): abort(403) +# Ported @api_bp.route('/user/robots/', 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//robots/', methods=['DELETE']) @api_login_required @@ -2462,7 +2495,7 @@ def log_view(log): return view - +# Ported @api_bp.route('/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//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(): diff --git a/endpoints/api/logs.py b/endpoints/api/logs.py new file mode 100644 index 000000000..eaee69aae --- /dev/null +++ b/endpoints/api/logs.py @@ -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//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//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) \ No newline at end of file diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py new file mode 100644 index 000000000..d271309e3 --- /dev/null +++ b/endpoints/api/organization.py @@ -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/') +@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//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//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//members/') +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) \ No newline at end of file diff --git a/endpoints/api/permission.py b/endpoints/api/permission.py index df2633a52..417e7eed8 100644 --- a/endpoints/api/permission.py +++ b/endpoints/api/permission.py @@ -79,8 +79,7 @@ class RepositoryUserPermissionList(RepositoryParamResource): } -@resource('/v1/repository//permissions/user/', - methods=['GET']) +@resource('/v1/repository//permissions/user/') 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') diff --git a/endpoints/api/prototype.py b/endpoints/api/prototype.py new file mode 100644 index 000000000..3987941b4 --- /dev/null +++ b/endpoints/api/prototype.py @@ -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//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//prototypes/') +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) diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index cf49ff71e..59a30c224 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -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 = {} diff --git a/endpoints/api/repotoken.py b/endpoints/api/repotoken.py index c674f463f..ca806ad3e 100644 --- a/endpoints/api/repotoken.py +++ b/endpoints/api/repotoken.py @@ -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': { diff --git a/endpoints/api/robot.py b/endpoints/api/robot.py new file mode 100644 index 000000000..6bcc34027 --- /dev/null +++ b/endpoints/api/robot.py @@ -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/') +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//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//robots/') +@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) diff --git a/endpoints/api/team.py b/endpoints/api/team.py new file mode 100644 index 000000000..6b342e596 --- /dev/null +++ b/endpoints/api/team.py @@ -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//team/') +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//team//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//team//members/') +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) diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index 640f27e9f..9c5ccc698 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -209,7 +209,7 @@ class BuildTriggerActivate(RepositoryParamResource): abort(403) -@resource('/repository//trigger//start') +@resource('/v1/repository//trigger//start') class ActivateBuildTrigger(RepositoryParamResource): """ Custom verb to manually activate a build trigger. """ @@ -260,7 +260,7 @@ class TriggerBuildList(RepositoryParamResource): } -@resource('/repository//trigger//sources') +@resource('/v1/repository//trigger//sources') class BuildTriggerSources(RepositoryParamResource): """ Custom verb to fetch the list of build sources for the trigger config. """ @require_repo_admin diff --git a/endpoints/api/user.py b/endpoints/api/user.py index abbf62dca..e7f4e58f1 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -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 = { diff --git a/util/cache.py b/util/cache.py index 5ba0a7a34..ade76781f 100644 --- a/util/cache.py +++ b/util/cache.py @@ -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):