import logging import stripe import urlparse import json 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 from collections import defaultdict from urllib import quote from data import model from data.plans import PLANS, get_plan from app import app from util.email import (send_confirmation_email, send_recovery_email, send_change_email) from util.names import parse_repository_name, format_robot_username from util.gravatar import compute_hash from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission, AdministerRepositoryPermission, CreateRepositoryPermission, AdministerOrganizationPermission, OrganizationMemberPermission, ViewTeamPermission, UserPermission) from endpoints.common import (common_login, get_route_data, truthy_param, start_build) from endpoints.trigger import (BuildTrigger, TriggerActivationException, TriggerDeactivationException, EmptyRepositoryException) from util.cache import cache_control from datetime import datetime, timedelta store = app.config['STORAGE'] user_files = app.config['USERFILES'] build_logs = app.config['BUILDLOGS'] logger = logging.getLogger(__name__) 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: msg = 'CSRF Failure. Session token was %s and request token was %s' logger.error(msg, token, found_token) if not token: logger.warning('No CSRF token in session.') def request_error(exception=None, **kwargs): data = kwargs.copy() if exception: data['message'] = exception.message return make_response(jsonify(data), 400) def log_action(kind, user_or_orgname, metadata={}, repo=None): performer = current_user.db_user() model.log_action(kind, user_or_orgname, performer=performer, ip=request.remote_addr, metadata=metadata, repository=repo) def api_login_required(f): @wraps(f) def decorated_view(*args, **kwargs): if not current_user.is_authenticated(): abort(401) if (current_user and current_user.db_user() and current_user.db_user().organization): abort(401) if (current_user and current_user.db_user() and current_user.db_user().robot): abort(401) return f(*args, **kwargs) return decorated_view def internal_api_call(f): @wraps(f) def decorated_view(*args, **kwargs): return f(*args, **kwargs) decorated_view.__internal_call = True return decorated_view def org_api_call(user_call_name): def internal_decorator(f): @wraps(f) def decorated_view(*args, **kwargs): return f(*args, **kwargs) decorated_view.__user_call = user_call_name return decorated_view return internal_decorator @api.route('/discovery') def discovery(): return jsonify(get_route_data()) @api.route('/') @internal_api_call def welcome(): return jsonify({'version': '0.5'}) @api.route('/plans/') def list_plans(): return jsonify({ 'plans': PLANS, }) def user_view(user): def org_view(o): admin_org = AdministerOrganizationPermission(o.username) return { 'name': o.username, 'gravatar': compute_hash(o.email), 'is_org_admin': admin_org.can(), 'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(), 'preferred_namespace': not (o.stripe_id is None) } organizations = model.get_user_organizations(user.username) def login_view(login): return { 'service': login.service.name, 'service_identifier': login.service_ident, } logins = model.list_federated_logins(user) return { '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], 'logins': [login_view(login) for login in logins], 'can_create_repo': True, 'invoice_email': user.invoice_email, 'preferred_namespace': not (user.stripe_id is None) } @api.route('/user/', methods=['GET']) @internal_api_call def get_logged_in_user(): if current_user.is_anonymous(): return jsonify({'anonymous': True}) user = current_user.db_user() if not user or user.organization: return jsonify({'anonymous': True}) return jsonify(user_view(user)) @api.route('/user/private', methods=['GET']) @api_login_required @internal_api_call def get_user_private_count(): user = current_user.db_user() private_repos = model.get_private_repo_count(user.username) repos_allowed = 0 if user.stripe_id: cus = stripe.Customer.retrieve(user.stripe_id) if cus.subscription: plan = get_plan(cus.subscription.plan.id) if plan: repos_allowed = plan['privateRepos'] return jsonify({ 'privateCount': private_repos, 'reposAllowed': repos_allowed }) @api.route('/user/convert', methods=['POST']) @api_login_required @internal_api_call def convert_user_to_organization(): user = current_user.db_user() convert_data = request.get_json() # Ensure that the new admin user is the not user being converted. admin_username = convert_data['adminUser'] if admin_username == user.username: return request_error(reason='invaliduser', message='The admin user is not valid') # Ensure that the sign in credentials work. admin_password = convert_data['adminPassword'] if not model.verify_user(admin_username, admin_password): return request_error(reason='invaliduser', message='The admin user credentials are not valid') # Subscribe the organization to the new plan. plan = convert_data['plan'] subscribe(user, plan, None, True) # Require business plans # Convert the user to an organization. model.convert_user_to_organization(user, model.get_user(admin_username)) log_action('account_convert', user.username) # And finally login with the admin credentials. return conduct_signin(admin_username, admin_password) @api.route('/user/', methods=['PUT']) @api_login_required @internal_api_call def change_user_details(): user = current_user.db_user() user_data = request.get_json() try: if 'password' in user_data: logger.debug('Changing password for user: %s', user.username) log_action('account_change_password', user.username) model.change_password(user, user_data['password']) if 'invoice_email' in user_data: logger.debug('Changing invoice_email for user: %s', user.username) model.change_invoice_email(user, user_data['invoice_email']) if 'email' in user_data and user_data['email'] != user.email: new_email = user_data['email'] if model.find_user_by_email(new_email): # Email already used. return request_error(message='E-mail address already used') logger.debug('Sending email to change email address for user: %s', user.username) code = model.create_confirm_email_code(user, new_email=new_email) send_change_email(user.username, user_data['email'], code.code) except model.InvalidPasswordException, ex: return request_error(exception=ex) return jsonify(user_view(user)) @api.route('/user/', methods=['POST']) @internal_api_call def create_new_user(): user_data = request.get_json() existing_user = model.get_user(user_data['username']) if existing_user: return request_error(message='The username already exists') 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: return request_error(exception=ex) @api.route('/signin', methods=['POST']) @internal_api_call def signin_user(): signin_data = request.get_json() if not signin_data: abort(404) username = signin_data['username'] password = signin_data['password'] return conduct_signin(username, password) def conduct_signin(username_or_email, password): needs_email_verification = False invalid_credentials = False verified = model.verify_user(username_or_email, password) if verified: if common_login(verified): return jsonify({'success': True}) else: needs_email_verification = True else: invalid_credentials = True response = jsonify({ 'needsEmailVerification': needs_email_verification, 'invalidCredentials': invalid_credentials, }) response.status_code = 403 return response @api.route("/signout", methods=['POST']) @api_login_required @internal_api_call def logout(): logout_user() identity_changed.send(app, identity=AnonymousIdentity()) return jsonify({'success': True}) @api.route("/recovery", methods=['POST']) @internal_api_call def request_recovery_email(): email = request.get_json()['email'] code = model.create_reset_password_email_code(email) send_recovery_email(email, code.code) return make_response('Created', 201) @api.route('/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] }) @api.route('/entities/', methods=['GET']) @api_login_required def get_matching_entities(prefix): teams = [] namespace_name = request.args.get('namespace', '') robot_namespace = None organization = None try: organization = model.get_organization(namespace_name) # namespace name was an org permission = OrganizationMemberPermission(namespace_name) if permission.can(): robot_namespace = namespace_name if truthy_param(request.args.get('includeTeams', False)): teams = model.get_matching_teams(prefix, organization) except model.InvalidOrganizationException: # namespace name was a user if current_user.db_user().username == namespace_name: robot_namespace = namespace_name users = model.get_matching_users(prefix, robot_namespace, organization) def entity_team_view(team): result = { 'name': team.name, 'kind': 'team', 'is_org_member': True } return result def user_view(user): user_json = { 'name': user.username, 'kind': 'user', 'is_robot': user.is_robot, } if organization is not None: user_json['is_org_member'] = user.is_robot or user.is_org_member return user_json team_data = [entity_team_view(team) for team in teams] user_data = [user_view(user) for user in users] return jsonify({ 'results': team_data + user_data }) def team_view(orgname, team): view_permission = ViewTeamPermission(orgname, team.name) role = model.get_team_org_role(team).name return { 'id': team.id, 'name': team.name, 'description': team.description, 'can_view': view_permission.can(), 'role': role } @api.route('/organization/', methods=['POST']) @api_login_required @internal_api_call def create_organization(): org_data = request.get_json() existing = None try: existing = model.get_organization(org_data['name']) except model.InvalidOrganizationException: pass if not existing: try: existing = model.get_user(org_data['name']) except model.InvalidUserException: pass if existing: msg = 'A user or organization with this name already exists' return request_error(message=msg) try: model.create_organization(org_data['name'], org_data['email'], current_user.db_user()) return make_response('Created', 201) except model.DataModelException as ex: return request_error(exception=ex) def org_view(o, teams): admin_org = AdministerOrganizationPermission(o.username) is_admin = admin_org.can() view = { 'name': o.username, 'email': o.email if is_admin else '', 'gravatar': compute_hash(o.email), 'teams': {t.name : team_view(o.username, t) for t in teams}, 'is_admin': is_admin } if is_admin: view['invoice_email'] = o.invoice_email return view @api.route('/organization/', methods=['GET']) @api_login_required def get_organization(orgname): permission = OrganizationMemberPermission(orgname) if permission.can(): try: org = model.get_organization(orgname) except model.InvalidOrganizationException: abort(404) teams = model.get_teams_within_org(org) return jsonify(org_view(org, teams)) abort(403) @api.route('/organization/', methods=['PUT']) @api_login_required @org_api_call('change_user_details') def change_organization_details(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): try: org = model.get_organization(orgname) except model.InvalidOrganizationException: abort(404) org_data = request.get_json() if 'invoice_email' in org_data: logger.debug('Changing invoice_email for organization: %s', org.username) model.change_invoice_email(org, org_data['invoice_email']) if 'email' in org_data and org_data['email'] != org.email: new_email = org_data['email'] if model.find_user_by_email(new_email): return request_error(message='E-mail address already used') logger.debug('Changing email address for organization: %s', org.username) model.update_email(org, new_email) teams = model.get_teams_within_org(org) return jsonify(org_view(org, teams)) abort(403) def prototype_view(proto, org_members): def prototype_user_view(user): return { 'name': user.username, 'is_robot': user.robot, 'kind': 'user', 'is_org_member': user.robot or user.username in org_members, } if proto.delegate_user: delegate_view = prototype_user_view(proto.delegate_user) else: delegate_view = { 'name': proto.delegate_team.name, 'kind': 'team', } return { 'activating_user': prototype_user_view(proto.activating_user) if proto.activating_user else None, 'delegate': delegate_view, 'role': proto.role.name, 'id': proto.uuid, } @api.route('/organization//prototypes', methods=['GET']) @api_login_required def get_organization_prototype_permissions(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): try: org = model.get_organization(orgname) except model.InvalidOrganizationException: abort(404) permissions = model.get_prototype_permissions(org) org_members = model.get_organization_member_set(orgname) return jsonify({'prototypes': [prototype_view(p, org_members) for p in permissions]}) abort(403) def log_prototype_action(action_kind, orgname, prototype, **kwargs): username = current_user.db_user().username log_params = { 'prototypeid': prototype.uuid, 'username': username, 'activating_username': prototype.activating_user.username if prototype.activating_user else None, 'role': prototype.role.name } for key, value in kwargs.items(): log_params[key] = value if prototype.delegate_user: log_params['delegate_user'] = prototype.delegate_user.username elif prototype.delegate_team: log_params['delegate_team'] = prototype.delegate_team.name log_action(action_kind, orgname, log_params) @api.route('/organization//prototypes', methods=['POST']) @api_login_required def create_organization_prototype_permission(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): try: org = model.get_organization(orgname) except model.InvalidOrganizationException: abort(404) details = request.get_json() activating_username = None if ('activating_user' in details and details['activating_user'] and 'name' in details['activating_user']): activating_username = details['activating_user']['name'] delegate = details['delegate'] if 'delegate' in details else {} delegate_kind = delegate.get('kind', None) delegate_name = delegate.get('name', None) delegate_username = delegate_name if delegate_kind == 'user' else None delegate_teamname = delegate_name if delegate_kind == 'team' else None activating_user = (model.get_user(activating_username) if activating_username else None) delegate_user = (model.get_user(delegate_username) if delegate_username else None) delegate_team = (model.get_organization_team(orgname, delegate_teamname) if delegate_teamname else None) if activating_username and not activating_user: return request_error(message='Unknown activating user') if not delegate_user and not delegate_team: return request_error(message='Missing delegate user or team') role_name = details['role'] prototype = model.add_prototype_permission(org, role_name, activating_user, delegate_user, delegate_team) log_prototype_action('create_prototype_permission', orgname, prototype) org_members = model.get_organization_member_set(orgname) return jsonify(prototype_view(prototype, org_members)) abort(403) @api.route('/organization//prototypes/', methods=['DELETE']) @api_login_required def delete_organization_prototype_permission(orgname, prototypeid): permission = AdministerOrganizationPermission(orgname) if permission.can(): try: org = model.get_organization(orgname) except model.InvalidOrganizationException: abort(404) prototype = model.delete_prototype_permission(org, prototypeid) if not prototype: abort(404) log_prototype_action('delete_prototype_permission', orgname, prototype) return make_response('Deleted', 204) abort(403) @api.route('/organization//prototypes/', methods=['PUT']) @api_login_required def update_organization_prototype_permission(orgname, prototypeid): permission = AdministerOrganizationPermission(orgname) if permission.can(): try: org = model.get_organization(orgname) except model.InvalidOrganizationException: abort(404) existing = model.get_prototype_permission(org, prototypeid) if not existing: abort(404) details = request.get_json() role_name = details['role'] prototype = model.update_prototype_permission(org, prototypeid, role_name) if not prototype: abort(404) log_prototype_action('modify_prototype_permission', orgname, prototype, original_role=existing.role.name) org_members = model.get_organization_member_set(orgname) return jsonify(prototype_view(prototype, org_members)) abort(403) @api.route('/organization//members', methods=['GET']) @api_login_required def get_organization_members(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): try: org = model.get_organization(orgname) except model.InvalidOrganizationException: abort(404) # Loop to create the members dictionary. Note that the members collection # will return an entry for *every team* a member is on, so we will have # duplicate keys (which is why we pre-build the dictionary). members_dict = {} members = model.get_organization_members_with_teams(org) for member in members: if not member.user.username in members_dict: members_dict[member.user.username] = {'name': member.user.username, 'kind': 'user', 'is_robot': member.user.robot, 'teams': []} members_dict[member.user.username]['teams'].append(member.team.name) return jsonify({'members': members_dict}) abort(403) @api.route('/organization//members/', methods=['GET']) @api_login_required def get_organization_member(orgname, membername): permission = AdministerOrganizationPermission(orgname) if permission.can(): try: org = model.get_organization(orgname) except model.InvalidOrganizationException: abort(404) member_dict = None member_teams = model.get_organization_members_with_teams(org, membername=membername) for member in member_teams: if not member_dict: member_dict = {'name': member.user.username, 'kind': 'user', 'is_robot': member.user.robot, 'teams': []} member_dict['teams'].append(member.team.name) if not member_dict: abort(404) return jsonify({'member': member_dict}) abort(403) @api.route('/organization//private', methods=['GET']) @api_login_required @internal_api_call def get_organization_private_allowed(orgname): permission = CreateRepositoryPermission(orgname) if permission.can(): organization = model.get_organization(orgname) private_repos = model.get_private_repo_count(organization.username) if organization.stripe_id: cus = stripe.Customer.retrieve(organization.stripe_id) if cus.subscription: repos_allowed = 0 plan = get_plan(cus.subscription.plan.id) if plan: repos_allowed = plan['privateRepos'] return jsonify({ 'privateAllowed': (private_repos < repos_allowed) }) return jsonify({ 'privateAllowed': False }) abort(403) def member_view(member): return { 'name': member.username, 'kind': 'user', 'is_robot': member.robot, } @api.route('/organization//team/', methods=['PUT', 'POST']) @api_login_required def update_organization_team(orgname, teamname): edit_permission = AdministerOrganizationPermission(orgname) if edit_permission.can(): team = None details = request.get_json() is_existing = False try: team = model.get_organization_team(orgname, teamname) is_existing = True except model.InvalidTeamException: # Create the new team. description = details['description'] if 'description' in details else '' role = details['role'] if 'role' in details else 'member' org = model.get_organization(orgname) team = model.create_team(teamname, org, role, description) log_action('org_create_team', orgname, {'team': teamname}) if is_existing: if ('description' in details and team.description != details['description']): team.description = details['description'] team.save() log_action('org_set_team_description', orgname, {'team': teamname, 'description': team.description}) if 'role' in details: role = model.get_team_org_role(team).name if role != details['role']: team = model.set_team_org_permission(team, details['role'], current_user.db_user().username) log_action('org_set_team_role', orgname, {'team': teamname, 'role': details['role']}) resp = jsonify(team_view(orgname, team)) if not is_existing: resp.status_code = 201 return resp abort(403) @api.route('/organization//team/', methods=['DELETE']) @api_login_required def delete_organization_team(orgname, teamname): permission = AdministerOrganizationPermission(orgname) if permission.can(): model.remove_team(orgname, teamname, current_user.db_user().username) log_action('org_delete_team', orgname, {'team': teamname}) return make_response('Deleted', 204) abort(403) @api.route('/organization//team//members', methods=['GET']) @api_login_required def get_organization_team_members(orgname, teamname): view_permission = ViewTeamPermission(orgname, teamname) edit_permission = AdministerOrganizationPermission(orgname) if view_permission.can(): team = None try: team = model.get_organization_team(orgname, teamname) except model.InvalidTeamException: abort(404) members = model.get_organization_team_members(team.id) return jsonify({ 'members': { m.username : member_view(m) for m in members }, 'can_edit': edit_permission.can() }) abort(403) @api.route('/organization//team//members/', methods=['PUT', 'POST']) @api_login_required def update_organization_team_member(orgname, teamname, membername): permission = AdministerOrganizationPermission(orgname) if permission.can(): team = None user = None # Find the team. try: team = model.get_organization_team(orgname, teamname) except model.InvalidTeamException: abort(404) # Find the user. user = model.get_user(membername) if not user: return request_error(message='Unknown user') # Add the user to the team. model.add_user_to_team(user, team) log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname}) return jsonify(member_view(user)) abort(403) @api.route('/organization//team//members/', methods=['DELETE']) @api_login_required def delete_organization_team_member(orgname, teamname, membername): permission = AdministerOrganizationPermission(orgname) if permission.can(): # Remote the user from the team. invoking_user = current_user.db_user().username model.remove_user_from_team(orgname, teamname, membername, invoking_user) log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname}) return make_response('Deleted', 204) abort(403) @api.route('/repository', methods=['POST']) @api_login_required def create_repo(): owner = current_user.db_user() req = request.get_json() namespace_name = req['namespace'] if 'namespace' in req else owner.username permission = CreateRepositoryPermission(namespace_name) if permission.can(): repository_name = req['repository'] visibility = req['visibility'] existing = model.get_repository(namespace_name, repository_name) if existing: return request_error(message='Repository already exists') visibility = req['visibility'] repo = model.create_repository(namespace_name, repository_name, owner, visibility) repo.description = req['description'] repo.save() log_action('create_repo', namespace_name, {'repo': repository_name, 'namespace': namespace_name}, repo=repo) return jsonify({ 'namespace': namespace_name, 'name': repository_name }) abort(403) @api.route('/find/repository', methods=['GET']) def find_repos(): 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) @api.route('/repository/', methods=['GET']) def list_repos(): def repo_view(repo_obj): return { 'namespace': repo_obj.namespace, 'name': repo_obj.name, 'description': repo_obj.description, 'is_public': repo_obj.visibility.name == 'public', } page = request.args.get('page', None) limit = request.args.get('limit', None) namespace_filter = request.args.get('namespace', None) include_public = truthy_param(request.args.get('public', True)) include_private = truthy_param(request.args.get('private', True)) sort = truthy_param(request.args.get('sort', False)) include_count = truthy_param(request.args.get('count', False)) try: limit = int(limit) if limit else None except TypeError: limit = None if page: try: page = int(page) except Exception: page = None username = None if current_user.is_authenticated() and include_private: username = current_user.db_user().username repo_count = None if include_count: repo_count = model.get_visible_repository_count(username, include_public=include_public, namespace=namespace_filter) repo_query = model.get_visible_repositories(username, limit=limit, page=page, include_public=include_public, sort=sort, namespace=namespace_filter) repos = [repo_view(repo) for repo in repo_query] response = { 'repositories': repos } if include_count: response['count'] = repo_count return jsonify(response) @api.route('/repository/', methods=['PUT']) @api_login_required @parse_repository_name def update_repo(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() log_action('set_repo_description', namespace, {'repo': repository, 'description': values['description']}, repo=repo) return jsonify({ 'success': True }) abort(403) @api.route('/repository//changevisibility', methods=['POST']) @api_login_required @parse_repository_name def change_repo_visibility(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']) log_action('change_repo_visibility', namespace, {'repo': repository, 'visibility': values['visibility']}, repo=repo) return jsonify({ 'success': True }) abort(403) @api.route('/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) log_action('delete_repo', namespace, {'repo': repository, 'namespace': namespace}) return make_response('Deleted', 204) abort(403) def image_view(image): extended_props = image if image.storage and image.storage.id: extended_props = image.storage command = extended_props.command return { 'id': image.docker_image_id, 'created': extended_props.created, 'comment': extended_props.comment, 'command': json.loads(command) if command else None, 'ancestors': image.ancestors, 'dbid': image.id, 'size': extended_props.image_size, } @api.route('/repository/', methods=['GET']) @parse_repository_name def get_repo(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 model.InvalidOrganizationException: 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(list(active_builds)) > 0, 'is_organization': bool(organization), 'status_token': repo.badge_token if not is_public else '' }) abort(404) # Not found abort(403) # Permission denied def trigger_view(trigger): if trigger and trigger.uuid: config_dict = json.loads(trigger.config) build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name) return { 'service': trigger.service.name, 'config': config_dict, 'id': trigger.uuid, 'connected_user': trigger.connected_user.username, 'is_active': build_trigger.is_active(config_dict) } return None def build_status_view(build_obj, can_write=False): status = build_logs.get_status(build_obj.uuid) return { 'id': build_obj.uuid, 'phase': build_obj.phase, 'started': build_obj.started, 'display_name': build_obj.display_name, 'status': status, 'job_config': json.loads(build_obj.job_config) if can_write else None, 'is_writer': can_write, 'trigger': trigger_view(build_obj.trigger), } @api.route('/repository//build/', methods=['GET']) @parse_repository_name def get_repo_builds(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) is_public = model.repository_is_public(namespace, repository) if permission.can() or is_public: can_write = ModifyRepositoryPermission(namespace, repository).can() builds = model.list_repository_builds(namespace, repository) return jsonify({ 'builds': [build_status_view(build, can_write) for build in builds] }) abort(403) # Permission denied @api.route('/repository//build//status', methods=['GET']) @parse_repository_name def get_repo_build_status(namespace, repository, build_uuid): permission = ReadRepositoryPermission(namespace, repository) is_public = model.repository_is_public(namespace, repository) if permission.can() or is_public: build = model.get_repository_build(namespace, repository, build_uuid) if not build: abort(404) can_write = ModifyRepositoryPermission(namespace, repository).can() return jsonify(build_status_view(build, can_write)) abort(403) # Permission denied @api.route('/repository//build//archiveurl', methods=['GET']) @parse_repository_name def get_repo_build_archive_url(namespace, repository, build_uuid): permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): build = model.get_repository_build(namespace, repository, build_uuid) if not build: abort(404) url = user_files.get_file_url(build.resource_key) return jsonify({ 'url': url }) abort(403) # Permission denied @api.route('/repository//build//logs', methods=['GET']) @parse_repository_name def get_repo_build_logs(namespace, repository, build_uuid): permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): response_obj = {} build = model.get_repository_build(namespace, repository, build_uuid) start = int(request.args.get('start', 0)) count, logs = build_logs.get_log_entries(build.uuid, start) response_obj.update({ 'start': start, 'total': count, 'logs': [log for log in logs], }) return jsonify(response_obj) abort(403) # Permission denied @api.route('/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'] # Check if the dockerfile resource has already been used. If so, then it # can only be reused if the user has access to the repository for which it # was used. associated_repository = model.get_repository_for_resource(dockerfile_id) if associated_repository: if not ModifyRepositoryPermission(associated_repository.namespace, associated_repository.name): abort(403) # Start the build. repo = model.get_repository(namespace, repository) display_name = user_files.get_file_checksum(dockerfile_id) build_request = start_build(repo, dockerfile_id, ['latest'], display_name, '', True) resp = jsonify(build_status_view(build_request, True)) repo_string = '%s/%s' % (namespace, repository) resp.headers['Location'] = url_for('api.get_repo_build_status', repository=repo_string, build_uuid=build_request.uuid) resp.status_code = 201 return resp abort(403) # Permissions denied def webhook_view(webhook): return { 'public_id': webhook.public_id, 'parameters': json.loads(webhook.parameters), } @api.route('/repository//webhook/', methods=['POST']) @api_login_required @parse_repository_name def create_webhook(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): repo = model.get_repository(namespace, repository) webhook = model.create_webhook(repo, request.get_json()) resp = jsonify(webhook_view(webhook)) repo_string = '%s/%s' % (namespace, repository) resp.headers['Location'] = url_for('api.get_webhook', repository=repo_string, public_id=webhook.public_id) log_action('add_repo_webhook', namespace, {'repo': repository, 'webhook_id': webhook.public_id}, repo=repo) return resp abort(403) # Permissions denied @api.route('/repository//webhook/', methods=['GET']) @api_login_required @parse_repository_name def get_webhook(namespace, repository, public_id): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): try: webhook = model.get_webhook(namespace, repository, public_id) except model.InvalidWebhookException: abort(404) return jsonify(webhook_view(webhook)) abort(403) # Permission denied @api.route('/repository//webhook/', methods=['GET']) @api_login_required @parse_repository_name def list_webhooks(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): webhooks = model.list_webhooks(namespace, repository) return jsonify({ 'webhooks': [webhook_view(webhook) for webhook in webhooks] }) abort(403) # Permission denied @api.route('/repository//webhook/', methods=['DELETE']) @api_login_required @parse_repository_name def delete_webhook(namespace, repository, public_id): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): model.delete_webhook(namespace, repository, public_id) log_action('delete_repo_webhook', namespace, {'repo': repository, 'webhook_id': public_id}, repo=model.get_repository(namespace, repository)) return make_response('No Content', 204) abort(403) # Permission denied @api.route('/repository//trigger/', methods=['GET']) @api_login_required @parse_repository_name def get_build_trigger(namespace, repository, trigger_uuid): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) return jsonify(trigger_view(trigger)) abort(403) # Permission denied def _prepare_webhook_url(scheme, username, password, hostname, path): auth_hostname = '%s:%s@%s' % (quote(username), quote(password), hostname) return urlparse.urlunparse((scheme, auth_hostname, path, '', '', '')) @api.route('/repository//trigger//subdir', methods=['POST']) @api_login_required @parse_repository_name def list_build_trigger_subdirs(namespace, repository, trigger_uuid): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) return handler = BuildTrigger.get_trigger_for_service(trigger.service.name) user_permission = UserPermission(trigger.connected_user.username) if user_permission.can(): new_config_dict = request.get_json() try: subdirs = handler.list_build_subdirs(trigger.auth_token, new_config_dict) return jsonify({ 'subdir': subdirs, 'status': 'success' }) except EmptyRepositoryException as e: return jsonify({ 'status': 'error', 'message': e.msg }) abort(403) # Permission denied @api.route('/repository//trigger//activate', methods=['POST']) @api_login_required @parse_repository_name def activate_build_trigger(namespace, repository, trigger_uuid): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) return handler = BuildTrigger.get_trigger_for_service(trigger.service.name) existing_config_dict = json.loads(trigger.config) if handler.is_active(existing_config_dict): abort(400) return user_permission = UserPermission(trigger.connected_user.username) if user_permission.can(): new_config_dict = request.get_json() token_name = 'Build Trigger: %s' % trigger.service.name token = model.create_delegate_token(namespace, repository, token_name, 'write') try: repository_path = '%s/%s' % (trigger.repository.namespace, trigger.repository.name) path = url_for('webhooks.build_trigger_webhook', repository=repository_path, trigger_uuid=trigger.uuid) authed_url = _prepare_webhook_url(app.config['URL_SCHEME'], '$token', token.code, app.config['URL_HOST'], path) final_config = handler.activate(trigger.uuid, authed_url, trigger.auth_token, new_config_dict) except TriggerActivationException as e: token.delete_instance() return request_error(message=e.message) # Save the updated config. trigger.config = json.dumps(final_config) trigger.write_token = token trigger.save() # Log the trigger setup. repo = model.get_repository(namespace, repository) log_action('setup_repo_trigger', namespace, {'repo': repository, 'namespace': namespace, 'trigger_id': trigger.uuid, 'service': trigger.service.name, 'config': final_config}, repo=repo) return jsonify(trigger_view(trigger)) abort(403) # Permission denied @api.route('/repository//trigger//start', methods=['POST']) @api_login_required @parse_repository_name def manually_start_build_trigger(namespace, repository, trigger_uuid): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) return handler = BuildTrigger.get_trigger_for_service(trigger.service.name) existing_config_dict = json.loads(trigger.config) if not handler.is_active(existing_config_dict): abort(400) return specs = handler.manual_start(trigger.auth_token, json.loads(trigger.config)) dockerfile_id, tags, name, subdir = specs repo = model.get_repository(namespace, repository) build_request = start_build(repo, dockerfile_id, tags, name, subdir, True) resp = jsonify(build_status_view(build_request, True)) repo_string = '%s/%s' % (namespace, repository) resp.headers['Location'] = url_for('api.get_repo_build_status', repository=repo_string, build_uuid=build_request.uuid) resp.status_code = 201 return resp abort(403) # Permission denied @api.route('/repository//trigger//builds', methods=['GET']) @api_login_required @parse_repository_name def list_trigger_recent_builds(namespace, repository, trigger_uuid): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): limit = request.args.get('limit', 5) builds = model.list_trigger_builds(namespace, repository, trigger_uuid, limit) return jsonify({ 'builds': [build_status_view(build, True) for build in builds] }) abort(403) # Permission denied @api.route('/repository//trigger//sources', methods=['GET']) @api_login_required @parse_repository_name def list_trigger_build_sources(namespace, repository, trigger_uuid): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) user_permission = UserPermission(trigger.connected_user.username) if user_permission.can(): trigger_handler = BuildTrigger.get_trigger_for_service(trigger.service.name) return jsonify({ 'sources': trigger_handler.list_build_sources(trigger.auth_token) }) abort(403) # Permission denied @api.route('/repository//trigger/', methods=['GET']) @api_login_required @parse_repository_name def list_build_triggers(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): triggers = model.list_build_triggers(namespace, repository) return jsonify({ 'triggers': [trigger_view(trigger) for trigger in triggers] }) abort(403) # Permission denied @api.route('/repository//trigger/', methods=['DELETE']) @api_login_required @parse_repository_name def delete_build_trigger(namespace, repository, trigger_uuid): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) return handler = BuildTrigger.get_trigger_for_service(trigger.service.name) config_dict = json.loads(trigger.config) if handler.is_active(config_dict): try: handler.deactivate(trigger.auth_token, config_dict) except TriggerDeactivationException as ex: # We are just going to eat this error logger.warning('Trigger deactivation problem.', ex) log_action('delete_repo_trigger', namespace, {'repo': repository, 'trigger_id': trigger_uuid, 'service': trigger.service.name, 'config': config_dict}, repo=model.get_repository(namespace, repository)) trigger.delete_instance() return make_response('No Content', 204) abort(403) # Permission denied @api.route('/filedrop/', methods=['POST']) @api_login_required @internal_api_call 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 }) def role_view(repo_perm_obj): return { 'role': repo_perm_obj.role.name, } def wrap_role_view_user(role_json, user): role_json['is_robot'] = user.robot return role_json def wrap_role_view_org(role_json, user, org_members): role_json['is_org_member'] = user.robot or user.username in org_members return role_json @api.route('/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) @api.route('/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) @api.route('/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): image = model.get_repo_image(namespace, repository, image_id) if not image: abort(404) uuid = image.storage and image.storage.uuid diffs_path = store.image_file_diffs_path(namespace, repository, image_id, uuid) try: response_json = store.get_content(diffs_path) return make_response(response_json) except IOError: abort(404) abort(403) @api.route('/repository//tag/', methods=['DELETE']) @parse_repository_name def delete_full_tag(namespace, repository, tag): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): model.delete_tag(namespace, repository, tag) model.garbage_collect_repository(namespace, repository) username = current_user.db_user().username log_action('delete_tag', namespace, {'username': username, 'repo': repository, 'tag': tag}, repo=model.get_repository(namespace, repository)) return make_response('Deleted', 204) abort(403) # Permission denied @api.route('/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): try: tag_image = model.get_tag_image(namespace, repository, tag) except model.DataModelException: abort(404) 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 @api.route('/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 @api.route('/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(): # Lookup the organization (if any). org = None try: org = model.get_organization(namespace) # Will raise an error if not org except model.InvalidOrganizationException: # This repository isn't under an org pass # Determine how to wrap the role(s). def wrapped_role_view(repo_perm): return wrap_role_view_user(role_view(repo_perm), repo_perm.user) role_view_func = wrapped_role_view if org: org_members = model.get_organization_member_set(namespace) current_func = role_view_func def wrapped_role_org_view(repo_perm): return wrap_role_view_org(current_func(repo_perm), repo_perm.user, org_members) role_view_func = wrapped_role_org_view # Load and return the permissions. repo_perms = model.get_all_repo_users(namespace, repository) return jsonify({ 'permissions': {perm.user.username: role_view_func(perm) for perm in repo_perms} }) abort(403) # Permission denied @api.route('/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) perm_view = wrap_role_view_user(role_view(perm), perm.user) try: model.get_organization(namespace) org_members = model.get_organization_member_set(namespace) perm_view = wrap_role_view_org(perm_view, perm.user, org_members) except model.InvalidOrganizationException: # This repository is not part of an organization pass return jsonify(perm_view) abort(403) # Permission denied @api.route('/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(teamname, namespace, repository) return jsonify(role_view(perm)) abort(403) # Permission denied @api.route('/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)) perm = model.set_user_repo_permission(username, namespace, repository, new_permission['role']) perm_view = wrap_role_view_user(role_view(perm), perm.user) try: model.get_organization(namespace) org_members = model.get_organization_member_set(namespace) perm_view = wrap_role_view_org(perm_view, perm.user, org_members) except model.InvalidOrganizationException: # This repository is not part of an organization pass except model.DataModelException as ex: return request_error(exception=ex) log_action('change_repo_permission', namespace, {'username': username, 'repo': repository, 'role': new_permission['role']}, repo=model.get_repository(namespace, repository)) resp = jsonify(perm_view) if request.method == 'POST': resp.status_code = 201 return resp abort(403) # Permission denied @api.route('/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)) perm = model.set_team_repo_permission(teamname, namespace, repository, new_permission['role']) log_action('change_repo_permission', namespace, {'team': teamname, 'repo': repository, 'role': new_permission['role']}, repo=model.get_repository(namespace, repository)) resp = jsonify(role_view(perm)) if request.method == 'POST': resp.status_code = 201 return resp abort(403) # Permission denied @api.route('/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 as ex: return request_error(exception=ex) log_action('delete_repo_permission', namespace, {'username': username, 'repo': repository}, repo=model.get_repository(namespace, repository)) return make_response('Deleted', 204) abort(403) # Permission denied @api.route('/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(): model.delete_team_permission(teamname, namespace, repository) log_action('delete_repo_permission', namespace, {'team': teamname, 'repo': repository}, repo=model.get_repository(namespace, repository)) 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, } @api.route('/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 @api.route('/repository//tokens/', methods=['GET']) @api_login_required @parse_repository_name def get_tokens(namespace, repository, code): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): try: perm = model.get_repo_delegate_token(namespace, repository, code) except model.InvalidTokenException: abort(404) return jsonify(token_view(perm)) abort(403) # Permission denied @api.route('/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']) log_action('add_repo_accesstoken', namespace, {'repo': repository, 'token': token_params['friendlyName']}, repo = model.get_repository(namespace, repository)) resp = jsonify(token_view(token)) resp.status_code = 201 return resp abort(403) # Permission denied @api.route('/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']) log_action('change_repo_permission', namespace, {'repo': repository, 'token': token.friendly_name, 'code': code, 'role': new_permission['role']}, repo = model.get_repository(namespace, repository)) resp = jsonify(token_view(token)) return resp abort(403) # Permission denied @api.route('/repository//tokens/', methods=['DELETE']) @api_login_required @parse_repository_name def delete_token(namespace, repository, code): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): token = model.delete_delegate_token(namespace, repository, code) log_action('delete_repo_accesstoken', namespace, {'repo': repository, 'token': token.friendly_name, 'code': code}, repo = model.get_repository(namespace, repository)) 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, } @api.route('/user/card', methods=['GET']) @api_login_required @internal_api_call def get_user_card(): user = current_user.db_user() return get_card(user) @api.route('/organization//card', methods=['GET']) @api_login_required @internal_api_call @org_api_call('get_user_card') def get_org_card(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): organization = model.get_organization(orgname) return get_card(organization) abort(403) @api.route('/user/card', methods=['POST']) @api_login_required @internal_api_call def set_user_card(): user = current_user.db_user() token = request.get_json()['token'] response = set_card(user, token) log_action('account_change_cc', user.username) return response @api.route('/organization//card', methods=['POST']) @api_login_required @org_api_call('set_user_card') def set_org_card(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): organization = model.get_organization(orgname) token = request.get_json()['token'] response = set_card(organization, token) log_action('account_change_cc', orgname) return response abort(403) def set_card(user, token): if user.stripe_id: cus = stripe.Customer.retrieve(user.stripe_id) if cus: try: cus.card = token cus.save() except stripe.CardError as e: return carderror_response(e) except stripe.InvalidRequestError as e: return carderror_response(e) return get_card(user) def get_card(user): card_info = { 'is_valid': False } if user.stripe_id: cus = stripe.Customer.retrieve(user.stripe_id) if cus and cus.default_card: # Find the default card. default_card = None for card in cus.cards.data: if card.id == cus.default_card: default_card = card break if default_card: card_info = { 'owner': default_card.name, 'type': default_card.type, 'last4': default_card.last4 } return jsonify({'card': card_info}) @api.route('/user/plan', methods=['PUT']) @api_login_required @internal_api_call def update_user_subscription(): request_data = request.get_json() plan = request_data['plan'] token = request_data['token'] if 'token' in request_data else None user = current_user.db_user() return subscribe(user, plan, token, False) # Business features not required def carderror_response(e): resp = jsonify({ 'carderror': e.message, }) resp.status_code = 402 return resp def subscribe(user, plan, token, require_business_plan): plan_found = None for plan_obj in PLANS: if plan_obj['stripeId'] == plan: plan_found = plan_obj if not plan_found or plan_found['deprecated']: logger.warning('Plan not found or deprecated: %s', plan) abort(404) if (require_business_plan and not plan_found['bus_features'] and not plan_found['price'] == 0): logger.warning('Business attempting to subscribe to personal plan: %s', user.username) return request_error(message='No matching plan found') private_repos = model.get_private_repo_count(user.username) # This is the default response response_json = { 'plan': plan, 'usedPrivateRepos': private_repos, } status_code = 200 if not user.stripe_id: # Check if a non-paying user is trying to subscribe to a free plan if not plan_found['price'] == 0: # They want a real paying plan, create the customer and plan # simultaneously card = token try: cus = stripe.Customer.create(email=user.email, plan=plan, card=card) user.stripe_id = cus.id user.save() log_action('account_change_plan', user.username, {'plan': plan}) except stripe.CardError as e: return carderror_response(e) response_json = subscription_view(cus.subscription, private_repos) status_code = 201 else: # Change the plan cus = stripe.Customer.retrieve(user.stripe_id) if plan_found['price'] == 0: if cus.subscription is not None: # We only have to cancel the subscription if they actually have one cus.cancel_subscription() cus.save() log_action('account_change_plan', user.username, {'plan': plan}) else: # User may have been a previous customer who is resubscribing if token: cus.card = token cus.plan = plan try: cus.save() except stripe.CardError as e: return carderror_response(e) response_json = subscription_view(cus.subscription, private_repos) log_action('account_change_plan', user.username, {'plan': plan}) resp = jsonify(response_json) resp.status_code = status_code return resp @api.route('/user/invoices', methods=['GET']) @api_login_required def list_user_invoices(): user = current_user.db_user() if not user.stripe_id: abort(404) return get_invoices(user.stripe_id) @api.route('/organization//invoices', methods=['GET']) @api_login_required @org_api_call('list_user_invoices') def list_org_invoices(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): organization = model.get_organization(orgname) if not organization.stripe_id: abort(404) return get_invoices(organization.stripe_id) abort(403) def get_invoices(customer_id): def invoice_view(i): return { 'id': i.id, 'date': i.date, 'period_start': i.period_start, 'period_end': i.period_end, 'paid': i.paid, 'amount_due': i.amount_due, 'next_payment_attempt': i.next_payment_attempt, 'attempted': i.attempted, 'closed': i.closed, 'total': i.total, 'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None } invoices = stripe.Invoice.all(customer=customer_id, count=12) return jsonify({ 'invoices': [invoice_view(i) for i in invoices.data] }) @api.route('/organization//plan', methods=['PUT']) @api_login_required @internal_api_call @org_api_call('update_user_subscription') def update_org_subscription(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): request_data = request.get_json() plan = request_data['plan'] token = request_data['token'] if 'token' in request_data else None organization = model.get_organization(orgname) return subscribe(organization, plan, token, True) # Business plan required abort(403) @api.route('/user/plan', methods=['GET']) @api_login_required @internal_api_call def get_user_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, }) @api.route('/organization//plan', methods=['GET']) @api_login_required @internal_api_call @org_api_call('get_user_subscription') def get_org_subscription(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): private_repos = model.get_private_repo_count(orgname) organization = model.get_organization(orgname) if organization.stripe_id: cus = stripe.Customer.retrieve(organization.stripe_id) if cus.subscription: return jsonify(subscription_view(cus.subscription, private_repos)) return jsonify({ 'plan': 'free', 'usedPrivateRepos': private_repos, }) abort(403) def robot_view(name, token): return { 'name': name, 'token': token, } @api.route('/user/robots', methods=['GET']) @api_login_required def get_user_robots(): user = current_user.db_user() robots = model.list_entity_robots(user.username) return jsonify({ 'robots': [robot_view(name, password) for name, password in robots] }) @api.route('/organization//robots', methods=['GET']) @api_login_required @org_api_call('get_user_robots') def get_org_robots(orgname): permission = OrganizationMemberPermission(orgname) if permission.can(): robots = model.list_entity_robots(orgname) return jsonify({ 'robots': [robot_view(name, password) for name, password in robots] }) abort(403) @api.route('/user/robots/', methods=['PUT']) @api_login_required def create_user_robot(robot_shortname): parent = current_user.db_user() robot, password = model.create_robot(robot_shortname, parent) resp = jsonify(robot_view(robot.username, password)) log_action('create_robot', parent.username, {'robot': robot_shortname}) resp.status_code = 201 return resp @api.route('/organization//robots/', methods=['PUT']) @api_login_required @org_api_call('create_user_robot') def create_org_robot(orgname, robot_shortname): permission = AdministerOrganizationPermission(orgname) if permission.can(): parent = model.get_organization(orgname) robot, password = model.create_robot(robot_shortname, parent) resp = jsonify(robot_view(robot.username, password)) log_action('create_robot', orgname, {'robot': robot_shortname}) resp.status_code = 201 return resp abort(403) @api.route('/user/robots/', methods=['DELETE']) @api_login_required def delete_user_robot(robot_shortname): parent = current_user.db_user() model.delete_robot(format_robot_username(parent.username, robot_shortname)) log_action('delete_robot', parent.username, {'robot': robot_shortname}) return make_response('Deleted', 204) @api.route('/organization//robots/', methods=['DELETE']) @api_login_required @org_api_call('delete_user_robot') def delete_org_robot(orgname, robot_shortname): permission = AdministerOrganizationPermission(orgname) if permission.can(): model.delete_robot(format_robot_username(orgname, robot_shortname)) log_action('delete_robot', orgname, {'robot': robot_shortname}) return make_response('Deleted', 204) abort(403) def log_view(log): view = { 'kind': log.kind.name, 'metadata': json.loads(log.metadata_json), 'ip': log.ip, 'datetime': log.datetime, } if log.performer: view['performer'] = { 'kind': 'user', 'name': log.performer.username, 'is_robot': log.performer.robot, } return view @api.route('/repository//logs', methods=['GET']) @api_login_required @parse_repository_name def list_repo_logs(namespace, repository): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): repo = model.get_repository(namespace, repository) if not repo: abort(404) start_time = request.args.get('starttime', None) end_time = request.args.get('endtime', None) return get_logs(namespace, start_time, end_time, repository=repo) abort(403) @api.route('/organization//logs', methods=['GET']) @api_login_required @org_api_call('list_user_logs') def list_org_logs(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): performer_name = request.args.get('performer', None) start_time = request.args.get('starttime', None) end_time = request.args.get('endtime', None) return get_logs(orgname, start_time, end_time, performer_name=performer_name) abort(403) @api.route('/user/logs', methods=['GET']) @api_login_required def list_user_logs(): performer_name = request.args.get('performer', None) start_time = request.args.get('starttime', None) end_time = request.args.get('endtime', None) return get_logs(current_user.db_user().username, start_time, end_time, performer_name=performer_name) def get_logs(namespace, start_time, end_time, performer_name=None, repository=None): performer = None if performer_name: performer = model.get_user(performer_name) if start_time: try: start_time = datetime.strptime(start_time + ' UTC', '%m/%d/%Y %Z') except ValueError: start_time = None if not start_time: start_time = datetime.today() - timedelta(7) # One week if end_time: try: end_time = datetime.strptime(end_time + ' UTC', '%m/%d/%Y %Z') end_time = end_time + timedelta(days=1) except ValueError: end_time = None if not end_time: end_time = datetime.today() logs = model.list_logs(namespace, start_time, end_time, performer=performer, repository=repository) return jsonify({ 'start_time': start_time, 'end_time': end_time, 'logs': [log_view(log) for log in logs] })