diff --git a/application.py b/application.py index 706c64a8c..26101f50b 100644 --- a/application.py +++ b/application.py @@ -6,12 +6,12 @@ from app import app as application logging.basicConfig(**application.config['LOGGING_CONFIG']) -import endpoints.index -import endpoints.api -import endpoints.web -import endpoints.tags -import endpoints.registry -import endpoints.webhooks +from endpoints.api import api +from endpoints.index import index +from endpoints.web import web +from endpoints.tags import tags +from endpoints.registry import registry +from endpoints.webhooks import webhooks logger = logging.getLogger(__name__) @@ -20,6 +20,13 @@ if application.config.get('INCLUDE_TEST_ENDPOINTS', False): logger.debug('Loading test endpoints.') import endpoints.test +application.register_blueprint(web) +application.register_blueprint(index, url_prefix='/v1') +application.register_blueprint(tags, url_prefix='/v1') +application.register_blueprint(registry, url_prefix='/v1') +application.register_blueprint(api, url_prefix='/api') +application.register_blueprint(webhooks, url_prefix='/webhooks') + # Remove this for prod config application.debug = True diff --git a/endpoints/api.py b/endpoints/api.py index 0cad539b6..050ca26f7 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -4,7 +4,7 @@ import requests import urlparse import json -from flask import request, make_response, jsonify, abort, url_for +from flask import request, make_response, jsonify, abort, url_for, Blueprint, session from flask.ext.login import current_user, logout_user from flask.ext.principal import identity_changed, AnonymousIdentity from functools import wraps @@ -29,13 +29,25 @@ from endpoints.common import common_login from util.cache import cache_control from datetime import datetime, timedelta - store = app.config['STORAGE'] user_files = app.config['USERFILES'] logger = logging.getLogger(__name__) route_data = None +api = Blueprint('api', __name__) + +@api.before_request +def csrf_protect(): + if request.method != "GET" and request.method != "HEAD": + token = session.get('_csrf_token', None) + found_token = request.values.get('_csrf_token', None) + + # TODO: add if not token here, once we are sure all sessions have a token. + if token != found_token: + abort(403) + + def get_route_data(): global route_data if route_data: @@ -43,14 +55,14 @@ def get_route_data(): routes = [] for rule in app.url_map.iter_rules(): - if rule.rule.startswith('/api/'): - endpoint_method = globals()[rule.endpoint] + if rule.endpoint.startswith('api.'): + endpoint_method = globals()[rule.endpoint[4:]] # Remove api. is_internal = '__internal_call' in dir(endpoint_method) is_org_api = '__user_call' in dir(endpoint_method) methods = list(rule.methods.difference(['HEAD', 'OPTIONS'])) route = { - 'name': rule.endpoint, + 'name': rule.endpoint[4:], 'methods': methods, 'path': rule.rule, 'parameters': list(rule.arguments) @@ -111,28 +123,19 @@ def org_api_call(user_call_name): return internal_decorator -@app.errorhandler(model.DataModelException) -def handle_dme(ex): - return make_response(ex.message, 400) - -@app.errorhandler(KeyError) -def handle_dme_key_error(ex): - return make_response(ex.message, 400) - - -@app.route('/api/discovery') +@api.route('/discovery') def discovery(): return jsonify(get_route_data()) -@app.route('/api/') +@api.route('/') @internal_api_call def welcome(): return make_response('welcome', 200) -@app.route('/api/plans/') +@api.route('/plans/') def list_plans(): return jsonify({ 'plans': PLANS, @@ -175,7 +178,7 @@ def user_view(user): } -@app.route('/api/user/', methods=['GET']) +@api.route('/user/', methods=['GET']) @internal_api_call def get_logged_in_user(): if current_user.is_anonymous(): @@ -188,7 +191,7 @@ def get_logged_in_user(): return jsonify(user_view(user)) -@app.route('/api/user/private', methods=['GET']) +@api.route('/user/private', methods=['GET']) @api_login_required @internal_api_call def get_user_private_count(): @@ -209,7 +212,7 @@ def get_user_private_count(): }) -@app.route('/api/user/convert', methods=['POST']) +@api.route('/user/convert', methods=['POST']) @api_login_required @internal_api_call def convert_user_to_organization(): @@ -246,7 +249,7 @@ def convert_user_to_organization(): return conduct_signin(admin_username, admin_password) -@app.route('/api/user/', methods=['PUT']) +@api.route('/user/', methods=['PUT']) @api_login_required @internal_api_call def change_user_details(): @@ -288,7 +291,7 @@ def change_user_details(): return jsonify(user_view(user)) -@app.route('/api/user/', methods=['POST']) +@api.route('/user/', methods=['POST']) @internal_api_call def create_new_user(): user_data = request.get_json() @@ -315,7 +318,7 @@ def create_new_user(): return error_resp -@app.route('/api/signin', methods=['POST']) +@api.route('/signin', methods=['POST']) @internal_api_call def signin_user(): signin_data = request.get_json() @@ -348,7 +351,7 @@ def conduct_signin(username_or_email, password): return response -@app.route("/api/signout", methods=['POST']) +@api.route("/signout", methods=['POST']) @api_login_required @internal_api_call def logout(): @@ -357,7 +360,7 @@ def logout(): return make_response('Success', 200) -@app.route("/api/recovery", methods=['POST']) +@api.route("/recovery", methods=['POST']) @internal_api_call def request_recovery_email(): email = request.get_json()['email'] @@ -366,7 +369,7 @@ def request_recovery_email(): return make_response('Created', 201) -@app.route('/api/users/<prefix>', methods=['GET']) +@api.route('/users/<prefix>', methods=['GET']) @api_login_required def get_matching_users(prefix): users = model.get_matching_users(prefix) @@ -376,7 +379,7 @@ def get_matching_users(prefix): }) -@app.route('/api/entities/<prefix>', methods=['GET']) +@api.route('/entities/<prefix>', methods=['GET']) @api_login_required def get_matching_entities(prefix): teams = [] @@ -442,7 +445,7 @@ def team_view(orgname, team): } -@app.route('/api/organization/', methods=['POST']) +@api.route('/organization/', methods=['POST']) @api_login_required @internal_api_call def create_organization(): @@ -491,7 +494,7 @@ def org_view(o, teams): return view -@app.route('/api/organization/<orgname>', methods=['GET']) +@api.route('/organization/<orgname>', methods=['GET']) @api_login_required def get_organization(orgname): permission = OrganizationMemberPermission(orgname) @@ -507,7 +510,7 @@ def get_organization(orgname): abort(403) -@app.route('/api/organization/<orgname>', methods=['PUT']) +@api.route('/organization/<orgname>', methods=['PUT']) @api_login_required @org_api_call('change_user_details') def change_organization_details(orgname): @@ -701,7 +704,7 @@ def update_organization_prototype_permission(orgname, prototypeid): abort(403) -@app.route('/api/organization/<orgname>/members', methods=['GET']) +@api.route('/organization/<orgname>/members', methods=['GET']) @api_login_required def get_organization_members(orgname): permission = AdministerOrganizationPermission(orgname) @@ -730,7 +733,7 @@ def get_organization_members(orgname): abort(403) -@app.route('/api/organization/<orgname>/members/<membername>', methods=['GET']) +@api.route('/organization/<orgname>/members/<membername>', methods=['GET']) @api_login_required def get_organization_member(orgname, membername): permission = AdministerOrganizationPermission(orgname) @@ -759,7 +762,7 @@ def get_organization_member(orgname, membername): abort(403) -@app.route('/api/organization/<orgname>/private', methods=['GET']) +@api.route('/organization/<orgname>/private', methods=['GET']) @api_login_required @internal_api_call def get_organization_private_allowed(orgname): @@ -795,7 +798,7 @@ def member_view(member): } -@app.route('/api/organization/<orgname>/team/<teamname>', +@api.route('/organization/<orgname>/team/<teamname>', methods=['PUT', 'POST']) @api_login_required def update_organization_team(orgname, teamname): @@ -841,7 +844,7 @@ def update_organization_team(orgname, teamname): abort(403) -@app.route('/api/organization/<orgname>/team/<teamname>', +@api.route('/organization/<orgname>/team/<teamname>', methods=['DELETE']) @api_login_required def delete_organization_team(orgname, teamname): @@ -854,7 +857,7 @@ def delete_organization_team(orgname, teamname): abort(403) -@app.route('/api/organization/<orgname>/team/<teamname>/members', +@api.route('/organization/<orgname>/team/<teamname>/members', methods=['GET']) @api_login_required def get_organization_team_members(orgname, teamname): @@ -877,7 +880,7 @@ def get_organization_team_members(orgname, teamname): abort(403) -@app.route('/api/organization/<orgname>/team/<teamname>/members/<membername>', +@api.route('/organization/<orgname>/team/<teamname>/members/<membername>', methods=['PUT', 'POST']) @api_login_required def update_organization_team_member(orgname, teamname, membername): @@ -906,7 +909,7 @@ def update_organization_team_member(orgname, teamname, membername): abort(403) -@app.route('/api/organization/<orgname>/team/<teamname>/members/<membername>', +@api.route('/organization/<orgname>/team/<teamname>/members/<membername>', methods=['DELETE']) @api_login_required def delete_organization_team_member(orgname, teamname, membername): @@ -922,7 +925,7 @@ def delete_organization_team_member(orgname, teamname, membername): abort(403) -@app.route('/api/repository', methods=['POST']) +@api.route('/repository', methods=['POST']) @api_login_required def create_repo(): owner = current_user.db_user() @@ -956,7 +959,7 @@ def create_repo(): abort(403) -@app.route('/api/find/repository', methods=['GET']) +@api.route('/find/repository', methods=['GET']) def find_repos(): prefix = request.args.get('query', '') @@ -979,7 +982,7 @@ def find_repos(): return jsonify(response) -@app.route('/api/repository/', methods=['GET']) +@api.route('/repository/', methods=['GET']) def list_repos(): def repo_view(repo_obj): return { @@ -1039,7 +1042,7 @@ def list_repos(): return jsonify(response) -@app.route('/api/repository/<path:repository>', methods=['PUT']) +@api.route('/repository/<path:repository>', methods=['PUT']) @api_login_required @parse_repository_name def update_repo(namespace, repository): @@ -1061,7 +1064,7 @@ def update_repo(namespace, repository): abort(403) -@app.route('/api/repository/<path:repository>/changevisibility', +@api.route('/repository/<path:repository>/changevisibility', methods=['POST']) @api_login_required @parse_repository_name @@ -1082,7 +1085,7 @@ def change_repo_visibility(namespace, repository): abort(403) -@app.route('/api/repository/<path:repository>', methods=['DELETE']) +@api.route('/repository/<path:repository>', methods=['DELETE']) @api_login_required @parse_repository_name def delete_repository(namespace, repository): @@ -1108,7 +1111,7 @@ def image_view(image): } -@app.route('/api/repository/<path:repository>', methods=['GET']) +@api.route('/repository/<path:repository>', methods=['GET']) @parse_repository_name def get_repo(namespace, repository): logger.debug('Get repo: %s/%s' % (namespace, repository)) @@ -1157,7 +1160,7 @@ def get_repo(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/build/', methods=['GET']) +@api.route('/repository/<path:repository>/build/', methods=['GET']) @api_login_required @parse_repository_name def get_repo_builds(namespace, repository): @@ -1190,7 +1193,7 @@ def get_repo_builds(namespace, repository): abort(403) # Permissions denied -@app.route('/api/repository/<path:repository>/build/', methods=['POST']) +@api.route('/repository/<path:repository>/build/', methods=['POST']) @api_login_required @parse_repository_name def request_repo_build(namespace, repository): @@ -1228,7 +1231,7 @@ def webhook_view(webhook): } -@app.route('/api/repository/<path:repository>/webhook/', methods=['POST']) +@api.route('/repository/<path:repository>/webhook/', methods=['POST']) @api_login_required @parse_repository_name def create_webhook(namespace, repository): @@ -1248,7 +1251,7 @@ def create_webhook(namespace, repository): abort(403) # Permissions denied -@app.route('/api/repository/<path:repository>/webhook/<public_id>', +@api.route('/repository/<path:repository>/webhook/<public_id>', methods=['GET']) @api_login_required @parse_repository_name @@ -1261,7 +1264,7 @@ def get_webhook(namespace, repository, public_id): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/webhook/', methods=['GET']) +@api.route('/repository/<path:repository>/webhook/', methods=['GET']) @api_login_required @parse_repository_name def list_webhooks(namespace, repository): @@ -1275,7 +1278,7 @@ def list_webhooks(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/webhook/<public_id>', +@api.route('/repository/<path:repository>/webhook/<public_id>', methods=['DELETE']) @api_login_required @parse_repository_name @@ -1291,7 +1294,7 @@ def delete_webhook(namespace, repository, public_id): abort(403) # Permission denied -@app.route('/api/filedrop/', methods=['POST']) +@api.route('/filedrop/', methods=['POST']) @api_login_required @internal_api_call def get_filedrop_url(): @@ -1318,7 +1321,7 @@ def wrap_role_view_org(role_json, user, org_members): return role_json -@app.route('/api/repository/<path:repository>/image/', methods=['GET']) +@api.route('/repository/<path:repository>/image/', methods=['GET']) @parse_repository_name def list_repository_images(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) @@ -1343,7 +1346,7 @@ def list_repository_images(namespace, repository): abort(403) -@app.route('/api/repository/<path:repository>/image/<image_id>', +@api.route('/repository/<path:repository>/image/<image_id>', methods=['GET']) @parse_repository_name def get_image(namespace, repository, image_id): @@ -1357,7 +1360,7 @@ def get_image(namespace, repository, image_id): abort(403) -@app.route('/api/repository/<path:repository>/image/<image_id>/changes', +@api.route('/repository/<path:repository>/image/<image_id>/changes', methods=['GET']) @cache_control(max_age=60*60) # Cache for one hour @parse_repository_name @@ -1375,7 +1378,7 @@ def get_image_changes(namespace, repository, image_id): abort(403) -@app.route('/api/repository/<path:repository>/tag/<tag>', +@api.route('/api/repository/<path:repository>/tag/<tag>', methods=['DELETE']) @parse_repository_name def delete_full_tag(namespace, repository, tag): @@ -1393,7 +1396,7 @@ def delete_full_tag(namespace, repository, tag): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/tag/<tag>/images', +@api.route('/repository/<path:repository>/tag/<tag>/images', methods=['GET']) @parse_repository_name def list_tag_images(namespace, repository, tag): @@ -1417,7 +1420,7 @@ def list_tag_images(namespace, repository, tag): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/permissions/team/', +@api.route('/repository/<path:repository>/permissions/team/', methods=['GET']) @api_login_required @parse_repository_name @@ -1434,7 +1437,7 @@ def list_repo_team_permissions(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/permissions/user/', +@api.route('/repository/<path:repository>/permissions/user/', methods=['GET']) @api_login_required @parse_repository_name @@ -1475,7 +1478,7 @@ def list_repo_user_permissions(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/permissions/user/<username>', +@api.route('/repository/<path:repository>/permissions/user/<username>', methods=['GET']) @api_login_required @parse_repository_name @@ -1500,7 +1503,7 @@ def get_user_permissions(namespace, repository, username): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/permissions/team/<teamname>', +@api.route('/repository/<path:repository>/permissions/team/<teamname>', methods=['GET']) @api_login_required @parse_repository_name @@ -1515,7 +1518,7 @@ def get_team_permissions(namespace, repository, teamname): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/permissions/user/<username>', +@api.route('/repository/<path:repository>/permissions/user/<username>', methods=['PUT', 'POST']) @api_login_required @parse_repository_name @@ -1558,7 +1561,7 @@ def change_user_permissions(namespace, repository, username): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/permissions/team/<teamname>', +@api.route('/repository/<path:repository>/permissions/team/<teamname>', methods=['PUT', 'POST']) @api_login_required @parse_repository_name @@ -1586,7 +1589,7 @@ def change_team_permissions(namespace, repository, teamname): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/permissions/user/<username>', +@api.route('/repository/<path:repository>/permissions/user/<username>', methods=['DELETE']) @api_login_required @parse_repository_name @@ -1611,7 +1614,7 @@ def delete_user_permissions(namespace, repository, username): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/permissions/team/<teamname>', +@api.route('/repository/<path:repository>/permissions/team/<teamname>', methods=['DELETE']) @api_login_required @parse_repository_name @@ -1637,7 +1640,7 @@ def token_view(token_obj): } -@app.route('/api/repository/<path:repository>/tokens/', methods=['GET']) +@api.route('/repository/<path:repository>/tokens/', methods=['GET']) @api_login_required @parse_repository_name def list_repo_tokens(namespace, repository): @@ -1652,7 +1655,7 @@ def list_repo_tokens(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/tokens/<code>', methods=['GET']) +@api.route('/repository/<path:repository>/tokens/<code>', methods=['GET']) @api_login_required @parse_repository_name def get_tokens(namespace, repository, code): @@ -1664,7 +1667,7 @@ def get_tokens(namespace, repository, code): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/tokens/', methods=['POST']) +@api.route('/repository/<path:repository>/tokens/', methods=['POST']) @api_login_required @parse_repository_name def create_token(namespace, repository): @@ -1686,7 +1689,7 @@ def create_token(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/tokens/<code>', methods=['PUT']) +@api.route('/repository/<path:repository>/tokens/<code>', methods=['PUT']) @api_login_required @parse_repository_name def change_token(namespace, repository, code): @@ -1711,7 +1714,7 @@ def change_token(namespace, repository, code): abort(403) # Permission denied -@app.route('/api/repository/<path:repository>/tokens/<code>', +@api.route('/repository/<path:repository>/tokens/<code>', methods=['DELETE']) @api_login_required @parse_repository_name @@ -1739,7 +1742,7 @@ def subscription_view(stripe_subscription, used_repos): } -@app.route('/api/user/card', methods=['GET']) +@api.route('/user/card', methods=['GET']) @api_login_required @internal_api_call def get_user_card(): @@ -1747,7 +1750,7 @@ def get_user_card(): return get_card(user) -@app.route('/api/organization/<orgname>/card', methods=['GET']) +@api.route('/organization/<orgname>/card', methods=['GET']) @api_login_required @internal_api_call @org_api_call('get_user_card') @@ -1760,7 +1763,7 @@ def get_org_card(orgname): abort(403) -@app.route('/api/user/card', methods=['POST']) +@api.route('/user/card', methods=['POST']) @api_login_required @internal_api_call def set_user_card(): @@ -1771,7 +1774,7 @@ def set_user_card(): return response -@app.route('/api/organization/<orgname>/card', methods=['POST']) +@api.route('/organization/<orgname>/card', methods=['POST']) @api_login_required @org_api_call('set_user_card') def set_org_card(orgname): @@ -1823,7 +1826,7 @@ def get_card(user): return jsonify({'card': card_info}) -@app.route('/api/user/plan', methods=['PUT']) +@api.route('/user/plan', methods=['PUT']) @api_login_required @internal_api_call def update_user_subscription(): @@ -1916,7 +1919,7 @@ def subscribe(user, plan, token, require_business_plan): return resp -@app.route('/api/user/invoices', methods=['GET']) +@api.route('/user/invoices', methods=['GET']) @api_login_required def list_user_invoices(): user = current_user.db_user() @@ -1926,7 +1929,7 @@ def list_user_invoices(): return get_invoices(user.stripe_id) -@app.route('/api/organization/<orgname>/invoices', methods=['GET']) +@api.route('/organization/<orgname>/invoices', methods=['GET']) @api_login_required @org_api_call('list_user_invoices') def list_org_invoices(orgname): @@ -1963,7 +1966,7 @@ def get_invoices(customer_id): }) -@app.route('/api/organization/<orgname>/plan', methods=['PUT']) +@api.route('/organization/<orgname>/plan', methods=['PUT']) @api_login_required @internal_api_call @org_api_call('update_user_subscription') @@ -1979,7 +1982,7 @@ def update_org_subscription(orgname): abort(403) -@app.route('/api/user/plan', methods=['GET']) +@api.route('/user/plan', methods=['GET']) @api_login_required @internal_api_call def get_user_subscription(): @@ -1998,7 +2001,7 @@ def get_user_subscription(): }) -@app.route('/api/organization/<orgname>/plan', methods=['GET']) +@api.route('/organization/<orgname>/plan', methods=['GET']) @api_login_required @internal_api_call @org_api_call('get_user_subscription') @@ -2028,7 +2031,7 @@ def robot_view(name, token): } -@app.route('/api/user/robots', methods=['GET']) +@api.route('/user/robots', methods=['GET']) @api_login_required def get_user_robots(): user = current_user.db_user() @@ -2038,7 +2041,7 @@ def get_user_robots(): }) -@app.route('/api/organization/<orgname>/robots', methods=['GET']) +@api.route('/organization/<orgname>/robots', methods=['GET']) @api_login_required @org_api_call('get_user_robots') def get_org_robots(orgname): @@ -2052,7 +2055,7 @@ def get_org_robots(orgname): abort(403) -@app.route('/api/user/robots/<robot_shortname>', methods=['PUT']) +@api.route('/user/robots/<robot_shortname>', methods=['PUT']) @api_login_required def create_user_robot(robot_shortname): parent = current_user.db_user() @@ -2063,7 +2066,7 @@ def create_user_robot(robot_shortname): return resp -@app.route('/api/organization/<orgname>/robots/<robot_shortname>', +@api.route('/organization/<orgname>/robots/<robot_shortname>', methods=['PUT']) @api_login_required @org_api_call('create_user_robot') @@ -2080,7 +2083,7 @@ def create_org_robot(orgname, robot_shortname): abort(403) -@app.route('/api/user/robots/<robot_shortname>', methods=['DELETE']) +@api.route('/user/robots/<robot_shortname>', methods=['DELETE']) @api_login_required def delete_user_robot(robot_shortname): parent = current_user.db_user() @@ -2089,7 +2092,7 @@ def delete_user_robot(robot_shortname): return make_response('No Content', 204) -@app.route('/api/organization/<orgname>/robots/<robot_shortname>', +@api.route('/organization/<orgname>/robots/<robot_shortname>', methods=['DELETE']) @api_login_required @org_api_call('delete_user_robot') @@ -2122,7 +2125,7 @@ def log_view(log): -@app.route('/api/repository/<path:repository>/logs', methods=['GET']) +@api.route('/repository/<path:repository>/logs', methods=['GET']) @api_login_required @parse_repository_name def list_repo_logs(namespace, repository): @@ -2139,7 +2142,7 @@ def list_repo_logs(namespace, repository): abort(403) -@app.route('/api/organization/<orgname>/logs', methods=['GET']) +@api.route('/organization/<orgname>/logs', methods=['GET']) @api_login_required @org_api_call('list_user_logs') def list_org_logs(orgname): @@ -2155,7 +2158,7 @@ def list_org_logs(orgname): abort(403) -@app.route('/api/user/logs', methods=['GET']) +@api.route('/user/logs', methods=['GET']) @api_login_required def list_user_logs(): performer_name = request.args.get('performer', None) diff --git a/endpoints/common.py b/endpoints/common.py index d6bd0125a..2648f5e9c 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -1,5 +1,8 @@ import logging +import os +import base64 +from flask import request, abort, session from flask.ext.login import login_user, UserMixin from flask.ext.principal import identity_changed @@ -46,3 +49,22 @@ def common_login(db_user): else: logger.debug('User could not be logged in, inactive?.') return False + + +@app.errorhandler(model.DataModelException) +def handle_dme(ex): + return make_response(ex.message, 400) + + +@app.errorhandler(KeyError) +def handle_dme_key_error(ex): + return make_response(ex.message, 400) + + +def generate_csrf_token(): + if '_csrf_token' not in session: + session['_csrf_token'] = base64.b64encode(os.urandom(48)) + + return session['_csrf_token'] + +app.jinja_env.globals['csrf_token'] = generate_csrf_token diff --git a/endpoints/index.py b/endpoints/index.py index f07405092..b3896bf91 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -2,7 +2,7 @@ import json import logging import urlparse -from flask import request, make_response, jsonify, abort, session +from flask import request, make_response, jsonify, abort, session, Blueprint from functools import wraps from data import model @@ -18,6 +18,7 @@ from auth.permissions import (ModifyRepositoryPermission, UserPermission, logger = logging.getLogger(__name__) +index = Blueprint('index', __name__) def generate_headers(role='read'): def decorator_method(f): @@ -51,8 +52,8 @@ def generate_headers(role='read'): return decorator_method -@app.route('/v1/users', methods=['POST']) -@app.route('/v1/users/', methods=['POST']) +@index.route('/users', methods=['POST']) +@index.route('/users/', methods=['POST']) def create_user(): user_data = request.get_json() username = user_data['username'] @@ -87,8 +88,8 @@ def create_user(): return make_response('Created', 201) -@app.route('/v1/users', methods=['GET']) -@app.route('/v1/users/', methods=['GET']) +@index.route('/users', methods=['GET']) +@index.route('/users/', methods=['GET']) @process_auth def get_user(): if get_authenticated_user(): @@ -99,7 +100,7 @@ def get_user(): abort(404) -@app.route('/v1/users/<username>/', methods=['PUT']) +@index.route('/users/<username>/', methods=['PUT']) @process_auth def update_user(username): permission = UserPermission(username) @@ -124,7 +125,7 @@ def update_user(username): abort(403) -@app.route('/v1/repositories/<path:repository>', methods=['PUT']) +@index.route('/repositories/<path:repository>', methods=['PUT']) @process_auth @parse_repository_name @generate_headers(role='write') @@ -188,7 +189,7 @@ def create_repository(namespace, repository): return response -@app.route('/v1/repositories/<path:repository>/images', methods=['PUT']) +@index.route('/repositories/<path:repository>/images', methods=['PUT']) @process_auth @parse_repository_name @generate_headers(role='write') @@ -234,7 +235,7 @@ def update_images(namespace, repository): abort(403) -@app.route('/v1/repositories/<path:repository>/images', methods=['GET']) +@index.route('/repositories/<path:repository>/images', methods=['GET']) @process_auth @parse_repository_name @generate_headers(role='read') @@ -290,7 +291,7 @@ def get_repository_images(namespace, repository): abort(403) -@app.route('/v1/repositories/<path:repository>/images', methods=['DELETE']) +@index.route('/repositories/<path:repository>/images', methods=['DELETE']) @process_auth @parse_repository_name @generate_headers(role='write') @@ -298,19 +299,19 @@ def delete_repository_images(namespace, repository): return make_response('Not Implemented', 501) -@app.route('/v1/repositories/<path:repository>/auth', methods=['PUT']) +@index.route('/repositories/<path:repository>/auth', methods=['PUT']) @parse_repository_name def put_repository_auth(namespace, repository): return make_response('Not Implemented', 501) -@app.route('/v1/search', methods=['GET']) +@index.route('/search', methods=['GET']) def get_search(): return make_response('Not Implemented', 501) -@app.route('/_ping') -@app.route('/v1/_ping') +@index.route('/_ping') +@index.route('/_ping') def ping(): response = make_response('true', 200) response.headers['X-Docker-Registry-Version'] = '0.6.0' diff --git a/endpoints/registry.py b/endpoints/registry.py index f1b6ae223..57895aebe 100644 --- a/endpoints/registry.py +++ b/endpoints/registry.py @@ -1,7 +1,8 @@ import logging import json -from flask import make_response, request, session, Response, abort, redirect +from flask import (make_response, request, session, Response, abort, + redirect, Blueprint) from functools import wraps from datetime import datetime from time import time @@ -15,6 +16,7 @@ from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission) from data import model +registry = Blueprint('registry', __name__) store = app.config['STORAGE'] logger = logging.getLogger(__name__) @@ -73,7 +75,7 @@ def set_cache_headers(f): return wrapper -@app.route('/v1/images/<image_id>/layer', methods=['GET']) +@registry.route('/images/<image_id>/layer', methods=['GET']) @process_auth @extract_namespace_repo_from_session @require_completion @@ -94,7 +96,7 @@ def get_image_layer(namespace, repository, image_id, headers): abort(403) -@app.route('/v1/images/<image_id>/layer', methods=['PUT']) +@registry.route('/images/<image_id>/layer', methods=['PUT']) @process_auth @extract_namespace_repo_from_session def put_image_layer(namespace, repository, image_id): @@ -165,7 +167,7 @@ def put_image_layer(namespace, repository, image_id): return make_response('true', 200) -@app.route('/v1/images/<image_id>/checksum', methods=['PUT']) +@registry.route('/images/<image_id>/checksum', methods=['PUT']) @process_auth @extract_namespace_repo_from_session def put_image_checksum(namespace, repository, image_id): @@ -208,7 +210,7 @@ def put_image_checksum(namespace, repository, image_id): return make_response('true', 200) -@app.route('/v1/images/<image_id>/json', methods=['GET']) +@registry.route('/images/<image_id>/json', methods=['GET']) @process_auth @extract_namespace_repo_from_session @require_completion @@ -238,7 +240,7 @@ def get_image_json(namespace, repository, image_id, headers): return response -@app.route('/v1/images/<image_id>/ancestry', methods=['GET']) +@registry.route('/images/<image_id>/ancestry', methods=['GET']) @process_auth @extract_namespace_repo_from_session @require_completion @@ -283,7 +285,7 @@ def store_checksum(namespace, repository, image_id, checksum): store.put_content(checksum_path, checksum) -@app.route('/v1/images/<image_id>/json', methods=['PUT']) +@registry.route('/images/<image_id>/json', methods=['PUT']) @process_auth @extract_namespace_repo_from_session def put_image_json(namespace, repository, image_id): diff --git a/endpoints/tags.py b/endpoints/tags.py index 7267d7b7e..f6b0e1163 100644 --- a/endpoints/tags.py +++ b/endpoints/tags.py @@ -2,7 +2,7 @@ import logging import json -from flask import abort, request, jsonify, make_response +from flask import abort, request, jsonify, make_response, Blueprint from app import app from util.names import parse_repository_name @@ -14,8 +14,10 @@ from data import model logger = logging.getLogger(__name__) +tags = Blueprint('tags', __name__) -@app.route('/v1/repositories/<path:repository>/tags', + +@tags.route('/repositories/<path:repository>/tags', methods=['GET']) @process_auth @parse_repository_name @@ -30,7 +32,7 @@ def get_tags(namespace, repository): abort(403) -@app.route('/v1/repositories/<path:repository>/tags/<tag>', +@tags.route('/repositories/<path:repository>/tags/<tag>', methods=['GET']) @process_auth @parse_repository_name @@ -46,7 +48,7 @@ def get_tag(namespace, repository, tag): abort(403) -@app.route('/v1/repositories/<path:repository>/tags/<tag>', +@tags.route('/repositories/<path:repository>/tags/<tag>', methods=['PUT']) @process_auth @parse_repository_name @@ -62,7 +64,7 @@ def put_tag(namespace, repository, tag): abort(403) -@app.route('/v1/repositories/<path:repository>/tags/<tag>', +@tags.route('/repositories/<path:repository>/tags/<tag>', methods=['DELETE']) @process_auth @parse_repository_name @@ -77,7 +79,7 @@ def delete_tag(namespace, repository, tag): abort(403) -@app.route('/v1/repositories/<path:repository>/tags', +@tags.route('/repositories/<path:repository>/tags', methods=['DELETE']) @process_auth @parse_repository_name diff --git a/endpoints/web.py b/endpoints/web.py index 4d4a7503d..57d15164a 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -3,7 +3,7 @@ import requests import stripe from flask import (abort, redirect, request, url_for, render_template, - make_response, Response) + make_response, Response, Blueprint) from flask.ext.login import login_required, current_user from urlparse import urlparse @@ -19,23 +19,28 @@ from endpoints.common import common_login logger = logging.getLogger(__name__) +web = Blueprint('web', __name__) + def render_page_template(name, **kwargs): - return make_response(render_template(name, route_data=get_route_data(), + + resp = make_response(render_template(name, route_data=get_route_data(), **kwargs)) + resp.headers['X-FRAME-OPTIONS'] = 'DENY' + return resp -@app.route('/', methods=['GET'], defaults={'path': ''}) -@app.route('/repository/<path:path>', methods=['GET']) -@app.route('/organization/<path:path>', methods=['GET']) +@web.route('/', methods=['GET'], defaults={'path': ''}) +@web.route('/repository/<path:path>', methods=['GET']) +@web.route('/organization/<path:path>', methods=['GET']) @no_cache def index(path): return render_page_template('index.html') -@app.route('/snapshot', methods=['GET']) -@app.route('/snapshot/', methods=['GET']) -@app.route('/snapshot/<path:path>', methods=['GET']) +@web.route('/snapshot', methods=['GET']) +@web.route('/snapshot/', methods=['GET']) +@web.route('/snapshot/<path:path>', methods=['GET']) def snapshot(path = ''): parsed = urlparse(request.url) final_url = '%s://%s/%s' % (parsed.scheme, 'localhost', path) @@ -46,92 +51,93 @@ def snapshot(path = ''): abort(404) -@app.route('/plans/') +@web.route('/plans/') @no_cache def plans(): return index('') -@app.route('/guide/') +@web.route('/guide/') @no_cache def guide(): return index('') -@app.route('/organizations/') -@app.route('/organizations/new/') +@web.route('/organizations/') +@web.route('/organizations/new/') @no_cache def organizations(): return index('') -@app.route('/user/') +@web.route('/user/') @no_cache def user(): return index('') -@app.route('/signin/') +@web.route('/signin/') @no_cache def signin(): return index('') -@app.route('/contact/') +@web.route('/contact/') +@no_cache def contact(): return index('') -@app.route('/new/') +@web.route('/new/') @no_cache def new(): return index('') -@app.route('/repository/') +@web.route('/repository/') @no_cache def repository(): return index('') -@app.route('/security/') +@web.route('/security/') @no_cache def security(): return index('') -@app.route('/v1') -@app.route('/v1/') +@web.route('/v1') +@web.route('/v1/') @no_cache def v1(): return index('') -@app.route('/status', methods=['GET']) +@web.route('/status', methods=['GET']) @no_cache def status(): return make_response('Healthy') -@app.route('/tos', methods=['GET']) +@web.route('/tos', methods=['GET']) @no_cache def tos(): return render_page_template('tos.html') -@app.route('/disclaimer', methods=['GET']) +@web.route('/disclaimer', methods=['GET']) @no_cache def disclaimer(): return render_page_template('disclaimer.html') -@app.route('/privacy', methods=['GET']) +@web.route('/privacy', methods=['GET']) @no_cache def privacy(): return render_page_template('privacy.html') -@app.route('/receipt', methods=['GET']) +@web.route('/receipt', methods=['GET']) def receipt(): if not current_user.is_authenticated(): abort(401) @@ -188,7 +194,7 @@ def get_github_user(token): return get_user.json() -@app.route('/oauth2/github/callback', methods=['GET']) +@web.route('/oauth2/github/callback', methods=['GET']) def github_oauth_callback(): error = request.args.get('error', None) if error: @@ -241,7 +247,7 @@ def github_oauth_callback(): return render_page_template('githuberror.html') -@app.route('/oauth2/github/callback/attach', methods=['GET']) +@web.route('/oauth2/github/callback/attach', methods=['GET']) @login_required def github_oauth_attach(): token = exchange_github_code_for_token(request.args.get('code')) @@ -252,7 +258,7 @@ def github_oauth_attach(): return redirect(url_for('user')) -@app.route('/confirm', methods=['GET']) +@web.route('/confirm', methods=['GET']) def confirm_email(): code = request.values['code'] user = None @@ -268,7 +274,7 @@ def confirm_email(): return redirect(url_for('user', tab='email') if new_email else url_for('index')) -@app.route('/recovery', methods=['GET']) +@web.route('/recovery', methods=['GET']) def confirm_recovery(): code = request.values['code'] user = model.validate_reset_code(code) diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py index f93ef7a70..5a6c0ad3d 100644 --- a/endpoints/webhooks.py +++ b/endpoints/webhooks.py @@ -1,7 +1,7 @@ import logging import stripe -from flask import request, make_response +from flask import request, make_response, Blueprint from data import model from app import app @@ -11,8 +11,9 @@ from util.email import send_invoice_email logger = logging.getLogger(__name__) +webhooks = Blueprint('webhooks', __name__) -@app.route('/webhooks/stripe', methods=['POST']) +@webhooks.route('/stripe', methods=['POST']) def stripe_webhook(): request_data = request.get_json() logger.debug('Stripe webhook call: %s' % request_data) diff --git a/static/js/app.js b/static/js/app.js index 022b9f852..f89281beb 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2512,6 +2512,10 @@ quayApp.directive('ngBlur', function() { quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout) { + + // Handle session security. + Restangular.setDefaultRequestParams({'_csrf_token': window.__token || ''}); + // Handle session expiration. Restangular.setErrorInterceptor(function(response) { if (response.status == 401) { diff --git a/templates/base.html b/templates/base.html index 6f40a4784..731e19d18 100644 --- a/templates/base.html +++ b/templates/base.html @@ -69,6 +69,7 @@ <script type="text/javascript"> window.__endpoints = {{ route_data|safe }}.endpoints; + window.__token = '{{ csrf_token() }}'; </script> <script src="static/js/app.js"></script>