import logging import stripe import re import requests import urlparse import json from flask import request, make_response, jsonify, abort, url_for from flask.ext.login import login_required, current_user, logout_user from flask.ext.principal import identity_changed, AnonymousIdentity from functools import wraps from collections import defaultdict import storage from data import model from data.userfiles import UserRequestFiles from data.queue import dockerfile_build_queue from data.plans import USER_PLANS, getPlan, isPlanActive from app import app from util.email import send_confirmation_email, send_recovery_email from util.names import parse_repository_name from util.gravatar import compute_hash from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission, AdministerRepositoryPermission) from endpoints import registry from endpoints.web import common_login from util.cache import cache_control store = storage.load() logger = logging.getLogger(__name__) def api_login_required(f): @wraps(f) def decorated_view(*args, **kwargs): if not current_user.is_authenticated(): abort(401) return f(*args, **kwargs) return decorated_view @app.errorhandler(model.DataModelException) def handle_dme(ex): return make_response(ex.message, 400) @app.route('/api/') def welcome(): return make_response('welcome', 200) @app.route('/api/plans/') def plans_list(): return jsonify({ 'plans': USER_PLANS }) @app.route('/api/user/', methods=['GET']) def get_logged_in_user(): def org_view(o): # TODO: return whether the user is really the admin of the organization return { 'name': o.username, 'gravatar': compute_hash(o.email), 'is_org_admin': True } if current_user.is_anonymous(): return jsonify({'anonymous': True}) user = current_user.db_user() organizations = model.get_user_organizations(user.username) return jsonify({ 'verified': user.verified, 'anonymous': False, 'username': user.username, 'email': user.email, 'gravatar': compute_hash(user.email), 'askForPassword': user.password_hash is None, 'organizations': [org_view(o) for o in organizations] }) @app.route('/api/user/', methods=['PUT']) @api_login_required def change_user_details(): user = current_user.db_user() user_data = request.get_json(); try: if user_data['password']: logger.debug('Changing password for user: %s', user.username) model.change_password(user, user_data['password']) except model.InvalidPasswordException, ex: error_resp = jsonify({ 'message': ex.message, }) error_resp.status_code = 400 return error_resp return jsonify({ 'verified': user.verified, 'anonymous': False, 'username': user.username, 'email': user.email, 'gravatar': compute_hash(user.email), 'askForPassword': user.password_hash is None, }) @app.route('/api/user/', methods=['POST']) def create_user_api(): user_data = request.get_json() existing_user = model.get_user(user_data['username']) if existing_user: error_resp = jsonify({ 'message': 'The username already exists' }) error_resp.status_code = 400 return error_resp try: new_user = model.create_user(user_data['username'], user_data['password'], user_data['email']) code = model.create_confirm_email_code(new_user) send_confirmation_email(new_user.username, new_user.email, code.code) return make_response('Created', 201) except model.DataModelException as ex: error_resp = jsonify({ 'message': ex.message, }) error_resp.status_code = 400 return error_resp @app.route('/api/signin', methods=['POST']) def signin_api(): signin_data = request.get_json() username = signin_data['username'] password = signin_data['password'] #TODO Allow email login needs_email_verification = False invalid_credentials = False verified = model.verify_user(username, password) if verified: if common_login(verified): return make_response('Success', 200) else: needs_email_verification = True else: invalid_credentials = True response = jsonify({ 'needsEmailVerification': needs_email_verification, 'invalidCredentials': invalid_credentials, }) response.status_code = 403 return response @app.route("/api/signout", methods=['POST']) @api_login_required def logout(): logout_user() identity_changed.send(app, identity=AnonymousIdentity()) return make_response('Success', 200) @app.route("/api/recovery", methods=['POST']) def send_recovery(): email = request.get_json()['email'] code = model.create_reset_password_email_code(email) send_recovery_email(email, code.code) return make_response('Created', 201) @app.route('/api/users/', methods=['GET']) @api_login_required def get_matching_users(prefix): users = model.get_matching_users(prefix) return jsonify({ 'users': [user.username for user in users] }) @app.route('/api/entities/', methods=['GET']) @api_login_required def get_matching_entities(prefix): users = model.get_matching_users(prefix) teams = [] organization_name = request.args.get('organization', None) organization = None if organization_name: try: organization = model.get_organization(organization_name) except: pass if organization: # TODO: ensure that the user has access to the organization teams = model.get_matching_teams(prefix, organization) def team_view(team): return { 'name': team.name, 'kind': 'team' } def user_view(user): # TODO: Return whether the user is outside the organization (if one is # specified) return { 'name': user.username, 'kind': 'user', 'outside_org': True } team_data = [team_view(team) for team in teams] user_data = [user_view(user) for user in users] return jsonify({ 'results': team_data + user_data }) user_files = UserRequestFiles(app.config['AWS_ACCESS_KEY'], app.config['AWS_SECRET_KEY'], app.config['REGISTRY_S3_BUCKET']) @app.route('/api/organization/', methods=['GET']) def get_organization(orgname): def team_view(t): return { 'id': t.id, 'name': t.name } def org_view(o, teams): return { 'name': o.username, 'gravatar': compute_hash(o.email), 'teams': [team_view(t) for t in teams] } if current_user.is_anonymous(): abort(404) user = current_user.db_user() org = model.get_organization(orgname) if not org: abort(404) teams = model.get_user_teams_within_org(user.username, org) return jsonify(org_view(org, teams)) @app.route('/api/organization//private', methods=['GET']) def get_organization_private_allowed(orgname): if current_user.is_anonymous(): abort(404) user = current_user.db_user() try: organization = model.get_organization(orgname, username = user.username) except: abort(404) private_repos = model.get_private_repo_count(organization.username) if organization.stripe_id: cus = stripe.Customer.retrieve(organization.stripe_id) if cus.subscription and isPlanActive(cus.subscription): repos_allowed = getPlan(cus.subscription.plan.id) return jsonify({ 'privateAllowed': (private_repos < repos_allowed) }) return jsonify({ 'privateAllowed': False }) def member_view(m): return { 'username': m.username } @app.route('/api/organization//team//members', methods=['GET']) def get_organization_team_members(orgname, teamname): if current_user.is_anonymous(): abort(404) # TODO: determine whether the user has permission to view the team members of this team # (i.e. they are a member of the team [maybe??] OR they are an admin of the org) user = current_user.db_user() team = None try: team = model.get_organization_team(orgname, teamname) except: abort(404) members = model.get_organization_team_members(team.id) return jsonify({ 'members': { m.username : member_view(m) for m in members } }) @app.route('/api/organization//team//members/', methods=['PUT', 'POST']) def update_organization_team_member(orgname, teamname, membername): if current_user.is_anonymous(): abort(404) # TODO: determine whether the user has permission to put this user as a member of the team. team = None user = None # Find the team. try: team = model.get_organization_team(orgname, teamname) except: abort(404) # Find the user. user = model.get_user(membername) if not user: abort(400) # Add the user to the team. model.add_user_to_team(user, team) return jsonify(member_view(user)) @app.route('/api/organization//team//members/', methods=['DELETE']) def delete_organization_team_member(orgname, teamname, membername): if current_user.is_anonymous(): abort(404) # TODO: determine whether the user has permission to delete this user as a member of the team. team = None user = None # Find the team. try: team = model.get_organization_team(orgname, teamname) except: abort(404) # Find the user. user = model.get_user(membername) if not user: abort(400) # Remote the user from the team. model.remove_user_from_team(user, team) return jsonify({ 'success': True }) @app.route('/api/repository', methods=['POST']) @api_login_required def create_repo_api(): owner = current_user.db_user() # TODO(jake): Verify that the user can create a repo in this namespace. json = request.get_json() namespace_name = json['namespace'] if 'namespace' in json else owner.username repository_name = json['repository'] visibility = json['visibility'] existing = model.get_repository(namespace_name, repository_name) if existing: return make_response('Repository already exists', 400) visibility = request.get_json()['visibility'] repo = model.create_repository(namespace_name, repository_name, owner, visibility) repo.description = json['description'] repo.save() return jsonify({ 'namespace': namespace_name, 'name': repository_name }) @app.route('/api/find/repository', methods=['GET']) def match_repos_api(): prefix = request.args.get('query', '') def repo_view(repo): return { 'namespace': repo.namespace, 'name': repo.name, 'description': repo.description } username = None if current_user.is_authenticated(): username = current_user.db_user().username matching = model.get_matching_repositories(prefix, username) response = { 'repositories': [repo_view(repo) for repo in matching] } return jsonify(response) @app.route('/api/repository/', methods=['GET']) def list_repos_api(): def repo_view(repo_obj): is_public = model.repository_is_public(repo_obj.namespace, repo_obj.name) return { 'namespace': repo_obj.namespace, 'name': repo_obj.name, 'description': repo_obj.description, 'is_public': is_public } limit = request.args.get('limit', None) namespace_filter = request.args.get('namespace', None) include_public = request.args.get('public', 'true') include_private = request.args.get('private', 'true') sort = request.args.get('sort', 'false') try: limit = int(limit) if limit else None except: limit = None include_public = include_public == 'true' include_private = include_private == 'true' sort = sort == 'true' username = None if current_user.is_authenticated() and include_private: username = current_user.db_user().username repo_query = model.get_visible_repositories(username, limit=limit, include_public=include_public, sort=sort, namespace=namespace_filter) repos = [repo_view(repo) for repo in repo_query] response = { 'repositories': repos } return jsonify(response) @app.route('/api/repository/', methods=['PUT']) @api_login_required @parse_repository_name def update_repo_api(namespace, repository): permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): repo = model.get_repository(namespace, repository) if repo: values = request.get_json() repo.description = values['description'] repo.save() return jsonify({ 'success': True }) abort(404) @app.route('/api/repository//changevisibility', methods=['POST']) @api_login_required @parse_repository_name def change_repo_visibility_api(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): repo = model.get_repository(namespace, repository) if repo: values = request.get_json() model.set_repository_visibility(repo, values['visibility']) return jsonify({ 'success': True }) abort(404) @app.route('/api/repository/', methods=['DELETE']) @api_login_required @parse_repository_name def delete_repository(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): model.purge_repository(namespace, repository) registry.delete_repository_storage(namespace, repository) return make_response('Deleted', 204) abort(404) def image_view(image): return { 'id': image.docker_image_id, 'created': image.created, 'comment': image.comment, 'ancestors': image.ancestors, 'dbid': image.id, } @app.route('/api/repository/', methods=['GET']) @parse_repository_name def get_repo_api(namespace, repository): logger.debug('Get repo: %s/%s' % (namespace, repository)) def tag_view(tag): image = model.get_tag_image(namespace, repository, tag.name) if not image: return {} return { 'name': tag.name, 'image': image_view(image), } organization = None try: organization = model.get_organization(namespace) except: pass permission = ReadRepositoryPermission(namespace, repository) is_public = model.repository_is_public(namespace, repository) if permission.can() or is_public: repo = model.get_repository(namespace, repository) if repo: tags = model.list_repository_tags(namespace, repository) tag_dict = {tag.name: tag_view(tag) for tag in tags} can_write = ModifyRepositoryPermission(namespace, repository).can() can_admin = AdministerRepositoryPermission(namespace, repository).can() active_builds = model.list_repository_builds(namespace, repository, include_inactive=False) return jsonify({ 'namespace': namespace, 'name': repository, 'description': repo.description, 'tags': tag_dict, 'can_write': can_write, 'can_admin': can_admin, 'is_public': is_public, 'is_building': len(active_builds) > 0, 'is_organization': bool(organization) }) abort(404) # Not fount abort(403) # Permission denied @app.route('/api/repository//build/', methods=['GET']) @api_login_required @parse_repository_name def get_repo_builds(namespace, repository): permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): def build_view(build_obj): if build_obj.status_url: # Delegate the status to the build node node_status = requests.get(build_obj.status_url).json() node_status['id'] = build_obj.id return node_status # If there was no status url, do the best we can return { 'id': build_obj.id, 'total_commands': None, 'total_images': None, 'current_command': None, 'current_image': None, 'image_completion_percent': None, 'status': build_obj.phase, 'message': None, } builds = model.list_repository_builds(namespace, repository) return jsonify({ 'builds': [build_view(build) for build in builds] }) abort(403) # Permissions denied @app.route('/api/filedrop/', methods=['POST']) def get_filedrop_url(): mime_type = request.get_json()['mimeType'] (url, file_id) = user_files.prepare_for_drop(mime_type) return jsonify({ 'url': url, 'file_id': file_id }) @app.route('/api/repository//build/', methods=['POST']) @api_login_required @parse_repository_name def request_repo_build(namespace, repository): permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): logger.debug('User requested repository initialization.') dockerfile_id = request.get_json()['file_id'] repo = model.get_repository(namespace, repository) token = model.create_access_token(repo, 'write') host = urlparse.urlparse(request.url).netloc tag = '%s/%s/%s' % (host, repo.namespace, repo.name) build_request = model.create_repository_build(repo, token, dockerfile_id, tag) dockerfile_build_queue.put(json.dumps({'build_id': build_request.id})) return jsonify({ 'started': True }) abort(403) # Permissions denied def role_view(repo_perm_obj, username=None): # TODO: Determine whether the user (if given) is outside of the organization. return { 'role': repo_perm_obj.role.name, 'outside_org': username != 'devtable' } @app.route('/api/repository//image/', methods=['GET']) @parse_repository_name def list_repository_images(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) if permission.can() or model.repository_is_public(namespace, repository): all_images = model.get_repository_images(namespace, repository) all_tags = model.list_repository_tags(namespace, repository) tags_by_image_id = defaultdict(list) for tag in all_tags: tags_by_image_id[tag.image.docker_image_id].append(tag.name) def add_tags(image_json): image_json['tags'] = tags_by_image_id[image_json['id']] return image_json return jsonify({ 'images': [add_tags(image_view(image)) for image in all_images] }) abort(403) @app.route('/api/repository//image/', methods=['GET']) @parse_repository_name def get_image(namespace, repository, image_id): permission = ReadRepositoryPermission(namespace, repository) if permission.can() or model.repository_is_public(namespace, repository): image = model.get_repo_image(namespace, repository, image_id) if not image: abort(404) return jsonify(image_view(image)) abort(403) @app.route('/api/repository//image//changes', methods=['GET']) @cache_control(max_age=60*60) # Cache for one hour @parse_repository_name def get_image_changes(namespace, repository, image_id): permission = ReadRepositoryPermission(namespace, repository) if permission.can() or model.repository_is_public(namespace, repository): diffs_path = store.image_file_diffs_path(namespace, repository, image_id) try: response_json = store.get_content(diffs_path) return make_response(response_json) except IOError: abort(404) abort(403) @app.route('/api/repository//tag//images', methods=['GET']) @parse_repository_name def list_tag_images(namespace, repository, tag): permission = ReadRepositoryPermission(namespace, repository) if permission.can() or model.repository_is_public(namespace, repository): tag_image = model.get_tag_image(namespace, repository, tag) parent_images = model.get_parent_images(tag_image) parents = list(parent_images) parents.reverse() all_images = [tag_image] + parents return jsonify({ 'images': [image_view(image) for image in all_images] }) abort(403) # Permission denied @app.route('/api/repository//permissions/team/', methods=['GET']) @api_login_required @parse_repository_name def list_repo_team_permissions(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): repo_perms = model.get_all_repo_teams(namespace, repository) return jsonify({ 'permissions': {repo_perm.team.name: role_view(repo_perm) for repo_perm in repo_perms} }) abort(403) # Permission denied @app.route('/api/repository//permissions/user/', methods=['GET']) @api_login_required @parse_repository_name def list_repo_user_permissions(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): repo_perms = model.get_all_repo_users(namespace, repository) return jsonify({ 'permissions': {repo_perm.user.username: role_view(repo_perm, username=repo_perm.user.username) for repo_perm in repo_perms} }) abort(403) # Permission denied @app.route('/api/repository//permissions/user/', methods=['GET']) @api_login_required @parse_repository_name def get_user_permissions(namespace, repository, username): logger.debug('Get repo: %s/%s permissions for user %s' % (namespace, repository, username)) permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): perm = model.get_user_reponame_permission(username, namespace, repository) return jsonify(role_view(perm, username=username)) abort(403) # Permission denied @app.route('/api/repository//permissions/team/', methods=['GET']) @api_login_required @parse_repository_name def get_team_permissions(namespace, repository, teamname): logger.debug('Get repo: %s/%s permissions for team %s' % (namespace, repository, teamname)) permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): perm = model.get_team_reponame_permission(username, namespace, repository) return jsonify(role_view(perm)) abort(403) # Permission denied @app.route('/api/repository//permissions/user/', methods=['PUT', 'POST']) @api_login_required @parse_repository_name def change_user_permissions(namespace, repository, username): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): new_permission = request.get_json() logger.debug('Setting permission to: %s for user %s' % (new_permission['role'], username)) try: perm = model.set_user_repo_permission(username, namespace, repository, new_permission['role']) except model.DataModelException: logger.warning('User tried to remove themselves as admin.') abort(409) resp = jsonify(role_view(perm, username=username)) if request.method == 'POST': resp.status_code = 201 return resp abort(403) # Permission denied @app.route('/api/repository//permissions/team/', methods=['PUT', 'POST']) @api_login_required @parse_repository_name def change_team_permissions(namespace, repository, teamname): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): new_permission = request.get_json() logger.debug('Setting permission to: %s for team %s' % (new_permission['role'], teamname)) try: perm = model.set_team_repo_permission(teamname, namespace, repository, new_permission['role']) except model.DataModelException: logger.warning('User tried to remove themselves as admin.') abort(409) resp = jsonify(role_view(perm)) if request.method == 'POST': resp.status_code = 201 return resp abort(403) # Permission denied @app.route('/api/repository//permissions/user/', methods=['DELETE']) @api_login_required @parse_repository_name def delete_user_permissions(namespace, repository, username): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): try: model.delete_user_permission(username, namespace, repository) except model.DataModelException: logger.warning('User tried to remove themselves as admin.') abort(409) return make_response('Deleted', 204) abort(403) # Permission denied @app.route('/api/repository//permissions/team/', methods=['DELETE']) @api_login_required @parse_repository_name def delete_team_permissions(namespace, repository, teamname): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): try: model.delete_team_permission(teamname, namespace, repository) except model.DataModelException: logger.warning('User tried to remove themselves as admin.') abort(409) return make_response('Deleted', 204) abort(403) # Permission denied def token_view(token_obj): return { 'friendlyName': token_obj.friendly_name, 'code': token_obj.code, 'role': token_obj.role.name, } @app.route('/api/repository//tokens/', methods=['GET']) @api_login_required @parse_repository_name def list_repo_tokens(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): tokens = model.get_repository_delegate_tokens(namespace, repository) return jsonify({ 'tokens': {token.code: token_view(token) for token in tokens} }) abort(403) # Permission denied @app.route('/api/repository//tokens/', methods=['GET']) @api_login_required @parse_repository_name def get_tokens(namespace, repository, code): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): perm = model.get_repo_delegate_token(namespace, repository, code) return jsonify(token_view(perm)) abort(403) # Permission denied @app.route('/api/repository//tokens/', methods=['POST']) @api_login_required @parse_repository_name def create_token(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): token_params = request.get_json() token = model.create_delegate_token(namespace, repository, token_params['friendlyName']) resp = jsonify(token_view(token)) resp.status_code = 201 return resp abort(403) # Permission denied @app.route('/api/repository//tokens/', methods=['PUT']) @api_login_required @parse_repository_name def change_token(namespace, repository, code): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): new_permission = request.get_json() logger.debug('Setting permission to: %s for code %s' % (new_permission['role'], code)) token = model.set_repo_delegate_token_role(namespace, repository, code, new_permission['role']) resp = jsonify(token_view(token)) return resp abort(403) # Permission denied @app.route('/api/repository//tokens/', methods=['DELETE']) @api_login_required @parse_repository_name def delete_token(namespace, repository, code): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): model.delete_delegate_token(namespace, repository, code) return make_response('Deleted', 204) abort(403) # Permission denied def subscription_view(stripe_subscription, used_repos): return { 'currentPeriodStart': stripe_subscription.current_period_start, 'currentPeriodEnd': stripe_subscription.current_period_end, 'plan': stripe_subscription.plan.id, 'usedPrivateRepos': used_repos, } @app.route('/api/user/plan', methods=['PUT']) @api_login_required def subscribe(): # Amount in cents amount = 500 request_data = request.get_json() plan = request_data['plan'] user = current_user.db_user() private_repos = model.get_private_repo_count(user.username) if not user.stripe_id: # Create the customer and plan simultaneously card = request_data['token'] cus = stripe.Customer.create(email=user.email, plan=plan, card=card) user.stripe_id = cus.id user.save() resp = jsonify(subscription_view(cus.subscription, private_repos)) resp.status_code = 201 return resp else: # Change the plan cus = stripe.Customer.retrieve(user.stripe_id) if plan == 'free': cus.cancel_subscription() cus.save() response_json = { 'plan': 'free', 'usedPrivateRepos': private_repos, } else: cus.plan = plan # User may have been a previous customer who is resubscribing if 'token' in request_data: cus.card = request_data['token'] cus.save() response_json = subscription_view(cus.subscription, private_repos) return jsonify(response_json) @app.route('/api/user/plan', methods=['GET']) @api_login_required def get_subscription(): user = current_user.db_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 jsonify(subscription_view(cus.subscription, private_repos)) return jsonify({ 'plan': 'free', 'usedPrivateRepos': private_repos, })