From 2e3be90054f8f7592f25843bb01a65b434b7900c Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Sat, 28 Dec 2013 13:28:52 -0500 Subject: [PATCH 1/4] Make sure Quay cannot be shown in frames --- endpoints/web.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/endpoints/web.py b/endpoints/web.py index 82ee2bc50..63c68a8b7 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -20,7 +20,9 @@ logger = logging.getLogger(__name__) def render_page_template(name): - return render_template(name, route_data = get_route_data()) + resp = make_response(render_template(name, route_data = get_route_data())) + resp.headers['X-FRAME-OPTIONS'] = 'DENY' + return resp @app.route('/', methods=['GET'], defaults={'path': ''}) From 21ac1c9210204ff05189fbc8a00337f4108477e6 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Sat, 28 Dec 2013 14:07:44 -0500 Subject: [PATCH 2/4] Add CSRF protection to every API call --- endpoints/common.py | 22 ++++++++++++++++++++++ static/js/app.js | 6 +++++- templates/base.html | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/endpoints/common.py b/endpoints/common.py index d6bd0125a..25e25abf3 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -1,5 +1,8 @@ import logging +import os +import base64 +from flask import request, make_response, jsonify, abort, url_for, 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.before_request +def csrf_protect(): + if request.method != "GET" and request.method != "HEAD": + token = session.get('_csrf_token', None) + found_token = request.args.get('_csrf_token', request.form.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 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/static/js/app.js b/static/js/app.js index 414c05357..ee5136b99 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -724,7 +724,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest otherwise({redirectTo: '/'}); }]). config(function(RestangularProvider) { - RestangularProvider.setBaseUrl('/api/'); + RestangularProvider.setBaseUrl('/api/'); }); @@ -2204,6 +2204,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 af71c9bde..2fdbca35a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -67,6 +67,7 @@ From b598c7ec854f08d2311c5178bd1c4e7f43f86c02 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Sat, 28 Dec 2013 19:56:23 -0500 Subject: [PATCH 3/4] Style fixes --- endpoints/common.py | 11 ++++++----- endpoints/web.py | 2 +- static/js/app.js | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/endpoints/common.py b/endpoints/common.py index 25e25abf3..49793244a 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -2,7 +2,7 @@ import logging import os import base64 -from flask import request, make_response, jsonify, abort, url_for, session +from flask import request, abort, session from flask.ext.login import login_user, UserMixin from flask.ext.principal import identity_changed @@ -55,7 +55,7 @@ def common_login(db_user): def csrf_protect(): if request.method != "GET" and request.method != "HEAD": token = session.get('_csrf_token', None) - found_token = request.args.get('_csrf_token', request.form.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: @@ -63,8 +63,9 @@ def csrf_protect(): def generate_csrf_token(): - if '_csrf_token' not in session: - session['_csrf_token'] = base64.b64encode(os.urandom(48)) - return session['_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/web.py b/endpoints/web.py index 63c68a8b7..62c798cb2 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) def render_page_template(name): - resp = make_response(render_template(name, route_data = get_route_data())) + resp = make_response(render_template(name, route_data=get_route_data())) resp.headers['X-FRAME-OPTIONS'] = 'DENY' return resp diff --git a/static/js/app.js b/static/js/app.js index ee5136b99..cf9d76f6e 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -724,7 +724,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest otherwise({redirectTo: '/'}); }]). config(function(RestangularProvider) { - RestangularProvider.setBaseUrl('/api/'); + RestangularProvider.setBaseUrl('/api/'); }); From 310c98df508e32cdadfae287ec594ceaf9d73e47 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 30 Dec 2013 17:05:27 -0500 Subject: [PATCH 4/4] Move each flask module into a Blueprint and have CSRF protection only on the API blueprint --- application.py | 19 +++-- endpoints/api.py | 179 +++++++++++++++++++++--------------------- endpoints/common.py | 15 ++-- endpoints/index.py | 29 +++---- endpoints/registry.py | 16 ++-- endpoints/tags.py | 14 ++-- endpoints/web.py | 59 +++++++------- endpoints/webhooks.py | 5 +- 8 files changed, 174 insertions(+), 162 deletions(-) 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 8d484ee96..fa7dd8f5c 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 @@ -30,13 +30,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: @@ -44,14 +56,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) @@ -112,28 +124,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, @@ -165,7 +168,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(): @@ -178,7 +181,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(): @@ -199,7 +202,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(): @@ -236,7 +239,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(): @@ -264,7 +267,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() @@ -291,7 +294,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() @@ -325,7 +328,7 @@ def conduct_signin(username, password): return response -@app.route("/api/signout", methods=['POST']) +@api.route("/signout", methods=['POST']) @api_login_required @internal_api_call def logout(): @@ -334,7 +337,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'] @@ -343,7 +346,7 @@ def request_recovery_email(): return make_response('Created', 201) -@app.route('/api/users/', methods=['GET']) +@api.route('/users/', methods=['GET']) @api_login_required def get_matching_users(prefix): users = model.get_matching_users(prefix) @@ -353,7 +356,7 @@ def get_matching_users(prefix): }) -@app.route('/api/entities/', methods=['GET']) +@api.route('/entities/', methods=['GET']) @api_login_required def get_matching_entities(prefix): teams = [] @@ -419,7 +422,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(): @@ -468,7 +471,7 @@ def org_view(o, teams): return view -@app.route('/api/organization/', methods=['GET']) +@api.route('/organization/', methods=['GET']) @api_login_required def get_organization(orgname): permission = OrganizationMemberPermission(orgname) @@ -484,7 +487,7 @@ def get_organization(orgname): abort(403) -@app.route('/api/organization/', methods=['PUT']) +@api.route('/organization/', methods=['PUT']) @api_login_required @org_api_call('change_user_details') def change_organization_details(orgname): @@ -506,7 +509,7 @@ def change_organization_details(orgname): abort(403) -@app.route('/api/organization//members', methods=['GET']) +@api.route('/organization//members', methods=['GET']) @api_login_required def get_organization_members(orgname): permission = AdministerOrganizationPermission(orgname) @@ -534,7 +537,7 @@ def get_organization_members(orgname): abort(403) -@app.route('/api/organization//members/', methods=['GET']) +@api.route('/organization//members/', methods=['GET']) @api_login_required def get_organization_member(orgname, membername): permission = AdministerOrganizationPermission(orgname) @@ -562,7 +565,7 @@ def get_organization_member(orgname, membername): abort(403) -@app.route('/api/organization//private', methods=['GET']) +@api.route('/organization//private', methods=['GET']) @api_login_required @internal_api_call def get_organization_private_allowed(orgname): @@ -597,7 +600,7 @@ def member_view(member): } -@app.route('/api/organization//team/', +@api.route('/organization//team/', methods=['PUT', 'POST']) @api_login_required def update_organization_team(orgname, teamname): @@ -643,7 +646,7 @@ def update_organization_team(orgname, teamname): abort(403) -@app.route('/api/organization//team/', +@api.route('/organization//team/', methods=['DELETE']) @api_login_required def delete_organization_team(orgname, teamname): @@ -656,7 +659,7 @@ def delete_organization_team(orgname, teamname): abort(403) -@app.route('/api/organization//team//members', +@api.route('/organization//team//members', methods=['GET']) @api_login_required def get_organization_team_members(orgname, teamname): @@ -679,7 +682,7 @@ def get_organization_team_members(orgname, teamname): abort(403) -@app.route('/api/organization//team//members/', +@api.route('/organization//team//members/', methods=['PUT', 'POST']) @api_login_required def update_organization_team_member(orgname, teamname, membername): @@ -708,7 +711,7 @@ def update_organization_team_member(orgname, teamname, membername): abort(403) -@app.route('/api/organization//team//members/', +@api.route('/organization//team//members/', methods=['DELETE']) @api_login_required def delete_organization_team_member(orgname, teamname, membername): @@ -724,7 +727,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() @@ -758,7 +761,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', '') @@ -781,7 +784,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 { @@ -822,7 +825,7 @@ def list_repos(): return jsonify(response) -@app.route('/api/repository/', methods=['PUT']) +@api.route('/repository/', methods=['PUT']) @api_login_required @parse_repository_name def update_repo(namespace, repository): @@ -844,7 +847,7 @@ def update_repo(namespace, repository): abort(403) -@app.route('/api/repository//changevisibility', +@api.route('/repository//changevisibility', methods=['POST']) @api_login_required @parse_repository_name @@ -865,7 +868,7 @@ def change_repo_visibility(namespace, repository): abort(403) -@app.route('/api/repository/', methods=['DELETE']) +@api.route('/repository/', methods=['DELETE']) @api_login_required @parse_repository_name def delete_repository(namespace, repository): @@ -890,7 +893,7 @@ def image_view(image): } -@app.route('/api/repository/', methods=['GET']) +@api.route('/repository/', methods=['GET']) @parse_repository_name def get_repo(namespace, repository): logger.debug('Get repo: %s/%s' % (namespace, repository)) @@ -939,7 +942,7 @@ def get_repo(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository//build/', methods=['GET']) +@api.route('/repository//build/', methods=['GET']) @api_login_required @parse_repository_name def get_repo_builds(namespace, repository): @@ -972,7 +975,7 @@ def get_repo_builds(namespace, repository): abort(403) # Permissions denied -@app.route('/api/repository//build/', methods=['POST']) +@api.route('/repository//build/', methods=['POST']) @api_login_required @parse_repository_name def request_repo_build(namespace, repository): @@ -1010,7 +1013,7 @@ def webhook_view(webhook): } -@app.route('/api/repository//webhook/', methods=['POST']) +@api.route('/repository//webhook/', methods=['POST']) @api_login_required @parse_repository_name def create_webhook(namespace, repository): @@ -1030,7 +1033,7 @@ def create_webhook(namespace, repository): abort(403) # Permissions denied -@app.route('/api/repository//webhook/', +@api.route('/repository//webhook/', methods=['GET']) @api_login_required @parse_repository_name @@ -1043,7 +1046,7 @@ def get_webhook(namespace, repository, public_id): abort(403) # Permission denied -@app.route('/api/repository//webhook/', methods=['GET']) +@api.route('/repository//webhook/', methods=['GET']) @api_login_required @parse_repository_name def list_webhooks(namespace, repository): @@ -1057,7 +1060,7 @@ def list_webhooks(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository//webhook/', +@api.route('/repository//webhook/', methods=['DELETE']) @api_login_required @parse_repository_name @@ -1073,7 +1076,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(): @@ -1101,7 +1104,7 @@ def wrap_role_view_org(role_json, user, org_members): return role_json -@app.route('/api/repository//image/', methods=['GET']) +@api.route('/repository//image/', methods=['GET']) @parse_repository_name def list_repository_images(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) @@ -1126,7 +1129,7 @@ def list_repository_images(namespace, repository): abort(403) -@app.route('/api/repository//image/', +@api.route('/repository//image/', methods=['GET']) @parse_repository_name def get_image(namespace, repository, image_id): @@ -1140,7 +1143,7 @@ def get_image(namespace, repository, image_id): abort(403) -@app.route('/api/repository//image//changes', +@api.route('/repository//image//changes', methods=['GET']) @cache_control(max_age=60*60) # Cache for one hour @parse_repository_name @@ -1158,7 +1161,7 @@ def get_image_changes(namespace, repository, image_id): abort(403) -@app.route('/api/repository//tag//images', +@api.route('/repository//tag//images', methods=['GET']) @parse_repository_name def list_tag_images(namespace, repository, tag): @@ -1182,7 +1185,7 @@ def list_tag_images(namespace, repository, tag): abort(403) # Permission denied -@app.route('/api/repository//permissions/team/', +@api.route('/repository//permissions/team/', methods=['GET']) @api_login_required @parse_repository_name @@ -1199,7 +1202,7 @@ def list_repo_team_permissions(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository//permissions/user/', +@api.route('/repository//permissions/user/', methods=['GET']) @api_login_required @parse_repository_name @@ -1240,7 +1243,7 @@ def list_repo_user_permissions(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository//permissions/user/', +@api.route('/repository//permissions/user/', methods=['GET']) @api_login_required @parse_repository_name @@ -1265,7 +1268,7 @@ def get_user_permissions(namespace, repository, username): abort(403) # Permission denied -@app.route('/api/repository//permissions/team/', +@api.route('/repository//permissions/team/', methods=['GET']) @api_login_required @parse_repository_name @@ -1280,7 +1283,7 @@ def get_team_permissions(namespace, repository, teamname): abort(403) # Permission denied -@app.route('/api/repository//permissions/user/', +@api.route('/repository//permissions/user/', methods=['PUT', 'POST']) @api_login_required @parse_repository_name @@ -1323,7 +1326,7 @@ def change_user_permissions(namespace, repository, username): abort(403) # Permission denied -@app.route('/api/repository//permissions/team/', +@api.route('/repository//permissions/team/', methods=['PUT', 'POST']) @api_login_required @parse_repository_name @@ -1351,7 +1354,7 @@ def change_team_permissions(namespace, repository, teamname): abort(403) # Permission denied -@app.route('/api/repository//permissions/user/', +@api.route('/repository//permissions/user/', methods=['DELETE']) @api_login_required @parse_repository_name @@ -1376,7 +1379,7 @@ def delete_user_permissions(namespace, repository, username): abort(403) # Permission denied -@app.route('/api/repository//permissions/team/', +@api.route('/repository//permissions/team/', methods=['DELETE']) @api_login_required @parse_repository_name @@ -1402,7 +1405,7 @@ def token_view(token_obj): } -@app.route('/api/repository//tokens/', methods=['GET']) +@api.route('/repository//tokens/', methods=['GET']) @api_login_required @parse_repository_name def list_repo_tokens(namespace, repository): @@ -1417,7 +1420,7 @@ def list_repo_tokens(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository//tokens/', methods=['GET']) +@api.route('/repository//tokens/', methods=['GET']) @api_login_required @parse_repository_name def get_tokens(namespace, repository, code): @@ -1429,7 +1432,7 @@ def get_tokens(namespace, repository, code): abort(403) # Permission denied -@app.route('/api/repository//tokens/', methods=['POST']) +@api.route('/repository//tokens/', methods=['POST']) @api_login_required @parse_repository_name def create_token(namespace, repository): @@ -1451,7 +1454,7 @@ def create_token(namespace, repository): abort(403) # Permission denied -@app.route('/api/repository//tokens/', methods=['PUT']) +@api.route('/repository//tokens/', methods=['PUT']) @api_login_required @parse_repository_name def change_token(namespace, repository, code): @@ -1476,7 +1479,7 @@ def change_token(namespace, repository, code): abort(403) # Permission denied -@app.route('/api/repository//tokens/', +@api.route('/repository//tokens/', methods=['DELETE']) @api_login_required @parse_repository_name @@ -1504,7 +1507,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(): @@ -1512,7 +1515,7 @@ def get_user_card(): return get_card(user) -@app.route('/api/organization//card', methods=['GET']) +@api.route('/organization//card', methods=['GET']) @api_login_required @internal_api_call @org_api_call('get_user_card') @@ -1525,7 +1528,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(): @@ -1536,7 +1539,7 @@ def set_user_card(): return response -@app.route('/api/organization//card', methods=['POST']) +@api.route('/organization//card', methods=['POST']) @api_login_required @org_api_call('set_user_card') def set_org_card(orgname): @@ -1588,7 +1591,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(): @@ -1681,7 +1684,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() @@ -1691,7 +1694,7 @@ def list_user_invoices(): return get_invoices(user.stripe_id) -@app.route('/api/organization//invoices', methods=['GET']) +@api.route('/organization//invoices', methods=['GET']) @api_login_required @org_api_call('list_user_invoices') def list_org_invoices(orgname): @@ -1728,7 +1731,7 @@ def get_invoices(customer_id): }) -@app.route('/api/organization//plan', methods=['PUT']) +@api.route('/organization//plan', methods=['PUT']) @api_login_required @internal_api_call @org_api_call('update_user_subscription') @@ -1744,7 +1747,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(): @@ -1763,7 +1766,7 @@ def get_user_subscription(): }) -@app.route('/api/organization//plan', methods=['GET']) +@api.route('/organization//plan', methods=['GET']) @api_login_required @internal_api_call @org_api_call('get_user_subscription') @@ -1793,7 +1796,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() @@ -1803,7 +1806,7 @@ def get_user_robots(): }) -@app.route('/api/organization//robots', methods=['GET']) +@api.route('/organization//robots', methods=['GET']) @api_login_required @org_api_call('get_user_robots') def get_org_robots(orgname): @@ -1817,7 +1820,7 @@ def get_org_robots(orgname): abort(403) -@app.route('/api/user/robots/', methods=['PUT']) +@api.route('/user/robots/', methods=['PUT']) @api_login_required def create_user_robot(robot_shortname): parent = current_user.db_user() @@ -1828,7 +1831,7 @@ def create_user_robot(robot_shortname): return resp -@app.route('/api/organization//robots/', +@api.route('/organization//robots/', methods=['PUT']) @api_login_required @org_api_call('create_user_robot') @@ -1845,7 +1848,7 @@ def create_org_robot(orgname, robot_shortname): abort(403) -@app.route('/api/user/robots/', methods=['DELETE']) +@api.route('/user/robots/', methods=['DELETE']) @api_login_required def delete_user_robot(robot_shortname): parent = current_user.db_user() @@ -1854,7 +1857,7 @@ def delete_user_robot(robot_shortname): return make_response('No Content', 204) -@app.route('/api/organization//robots/', +@api.route('/organization//robots/', methods=['DELETE']) @api_login_required @org_api_call('delete_user_robot') @@ -1886,7 +1889,7 @@ def log_view(log): -@app.route('/api/repository//logs', methods=['GET']) +@api.route('/repository//logs', methods=['GET']) @api_login_required @parse_repository_name def list_repo_logs(namespace, repository): @@ -1903,7 +1906,7 @@ def list_repo_logs(namespace, repository): abort(403) -@app.route('/api/organization//logs', methods=['GET']) +@api.route('/organization//logs', methods=['GET']) @api_login_required @org_api_call('list_user_logs') def list_org_logs(orgname): @@ -1919,7 +1922,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 49793244a..2648f5e9c 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -51,15 +51,14 @@ def common_login(db_user): return False -@app.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) +@app.errorhandler(model.DataModelException) +def handle_dme(ex): + return make_response(ex.message, 400) - # TODO: add if not token here, once we are sure all sessions have a token. - if token != found_token: - abort(403) + +@app.errorhandler(KeyError) +def handle_dme_key_error(ex): + return make_response(ex.message, 400) def generate_csrf_token(): diff --git a/endpoints/index.py b/endpoints/index.py index c8b519c7e..29ecc1bdb 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//', methods=['PUT']) +@index.route('/users//', 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/', methods=['PUT']) +@index.route('/repositories/', methods=['PUT']) @process_auth @parse_repository_name @generate_headers(role='write') @@ -192,7 +193,7 @@ def create_repository(namespace, repository): return response -@app.route('/v1/repositories//images', methods=['PUT']) +@index.route('/repositories//images', methods=['PUT']) @process_auth @parse_repository_name @generate_headers(role='write') @@ -238,7 +239,7 @@ def update_images(namespace, repository): abort(403) -@app.route('/v1/repositories//images', methods=['GET']) +@index.route('/repositories//images', methods=['GET']) @process_auth @parse_repository_name @generate_headers(role='read') @@ -294,7 +295,7 @@ def get_repository_images(namespace, repository): abort(403) -@app.route('/v1/repositories//images', methods=['DELETE']) +@index.route('/repositories//images', methods=['DELETE']) @process_auth @parse_repository_name @generate_headers(role='write') @@ -302,19 +303,19 @@ def delete_repository_images(namespace, repository): return make_response('Not Implemented', 501) -@app.route('/v1/repositories//auth', methods=['PUT']) +@index.route('/repositories//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 e6b34493e..5d6174cd6 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__) @@ -72,7 +74,7 @@ def set_cache_headers(f): return wrapper -@app.route('/v1/images//layer', methods=['GET']) +@registry.route('/images//layer', methods=['GET']) @process_auth @extract_namespace_repo_from_session @require_completion @@ -92,7 +94,7 @@ def get_image_layer(namespace, repository, image_id, headers): abort(403) -@app.route('/v1/images//layer', methods=['PUT']) +@registry.route('/images//layer', methods=['PUT']) @process_auth @extract_namespace_repo_from_session def put_image_layer(namespace, repository, image_id): @@ -158,7 +160,7 @@ def put_image_layer(namespace, repository, image_id): return make_response('true', 200) -@app.route('/v1/images//checksum', methods=['PUT']) +@registry.route('/images//checksum', methods=['PUT']) @process_auth @extract_namespace_repo_from_session def put_image_checksum(namespace, repository, image_id): @@ -199,7 +201,7 @@ def put_image_checksum(namespace, repository, image_id): return make_response('true', 200) -@app.route('/v1/images//json', methods=['GET']) +@registry.route('/images//json', methods=['GET']) @process_auth @extract_namespace_repo_from_session @require_completion @@ -229,7 +231,7 @@ def get_image_json(namespace, repository, image_id, headers): return response -@app.route('/v1/images//ancestry', methods=['GET']) +@registry.route('/images//ancestry', methods=['GET']) @process_auth @extract_namespace_repo_from_session @require_completion @@ -274,7 +276,7 @@ def store_checksum(namespace, repository, image_id, checksum): store.put_content(checksum_path, checksum) -@app.route('/v1/images//json', methods=['PUT']) +@registry.route('/images//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//tags', + +@tags.route('/repositories//tags', methods=['GET']) @process_auth @parse_repository_name @@ -30,7 +32,7 @@ def get_tags(namespace, repository): abort(403) -@app.route('/v1/repositories//tags/', +@tags.route('/repositories//tags/', methods=['GET']) @process_auth @parse_repository_name @@ -46,7 +48,7 @@ def get_tag(namespace, repository, tag): abort(403) -@app.route('/v1/repositories//tags/', +@tags.route('/repositories//tags/', methods=['PUT']) @process_auth @parse_repository_name @@ -62,7 +64,7 @@ def put_tag(namespace, repository, tag): abort(403) -@app.route('/v1/repositories//tags/', +@tags.route('/repositories//tags/', methods=['DELETE']) @process_auth @parse_repository_name @@ -77,7 +79,7 @@ def delete_tag(namespace, repository, tag): abort(403) -@app.route('/v1/repositories//tags', +@tags.route('/repositories//tags', methods=['DELETE']) @process_auth @parse_repository_name diff --git a/endpoints/web.py b/endpoints/web.py index 62c798cb2..c4774a020 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 current_user from urlparse import urlparse @@ -18,6 +18,8 @@ from endpoints.common import common_login logger = logging.getLogger(__name__) +web = Blueprint('web', __name__) + def render_page_template(name): resp = make_response(render_template(name, route_data=get_route_data())) @@ -25,16 +27,16 @@ def render_page_template(name): return resp -@app.route('/', methods=['GET'], defaults={'path': ''}) -@app.route('/repository/', methods=['GET']) -@app.route('/organization/', methods=['GET']) +@web.route('/', methods=['GET'], defaults={'path': ''}) +@web.route('/repository/', methods=['GET']) +@web.route('/organization/', methods=['GET']) def index(path): return render_page_template('index.html') -@app.route('/snapshot', methods=['GET']) -@app.route('/snapshot/', methods=['GET']) -@app.route('/snapshot/', methods=['GET']) +@web.route('/snapshot', methods=['GET']) +@web.route('/snapshot/', methods=['GET']) +@web.route('/snapshot/', methods=['GET']) def snapshot(path = ''): parsed = urlparse(request.url) final_url = '%s://%s/%s' % (parsed.scheme, 'localhost', path) @@ -45,74 +47,74 @@ def snapshot(path = ''): abort(404) -@app.route('/plans/') +@web.route('/plans/') def plans(): return index('') -@app.route('/guide/') +@web.route('/guide/') def guide(): return index('') -@app.route('/organizations/') -@app.route('/organizations/new/') +@web.route('/organizations/') +@web.route('/organizations/new/') def organizations(): return index('') -@app.route('/user/') +@web.route('/user/') def user(): return index('') -@app.route('/signin/') +@web.route('/signin/') def signin(): return index('') -@app.route('/new/') +@web.route('/new/') def new(): return index('') -@app.route('/repository/') +@web.route('/repository/') def repository(): return index('') -@app.route('/security/') +@web.route('/security/') def security(): return index('') -@app.route('/v1') -@app.route('/v1/') +@web.route('/v1') +@web.route('/v1/') def v1(): return index('') -@app.route('/status', methods=['GET']) +@web.route('/status', methods=['GET']) def status(): return make_response('Healthy') -@app.route('/tos', methods=['GET']) +@web.route('/tos', methods=['GET']) def tos(): return render_page_template('tos.html') -@app.route('/disclaimer', methods=['GET']) +@web.route('/disclaimer', methods=['GET']) def disclaimer(): return render_page_template('disclaimer.html') -@app.route('/privacy', methods=['GET']) +@web.route('/privacy', methods=['GET']) 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) @@ -142,7 +144,7 @@ def receipt(): abort(404) -@app.route('/oauth2/github/callback', methods=['GET']) +@web.route('/oauth2/github/callback', methods=['GET']) def github_oauth_callback(): code = request.args.get('code') payload = { @@ -205,7 +207,7 @@ def github_oauth_callback(): return render_page_template('githuberror.html') -@app.route('/confirm', methods=['GET']) +@web.route('/confirm', methods=['GET']) def confirm_email(): code = request.values['code'] @@ -219,7 +221,7 @@ def confirm_email(): return redirect(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) @@ -229,8 +231,3 @@ def confirm_recovery(): return redirect(url_for('user')) else: abort(403) - - -@app.route('/reset', methods=['GET']) -def password_reset(): - pass 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)