From 7d163833bd374b2ad156abc9e0cf49a0888e061e Mon Sep 17 00:00:00 2001 From: jakedt Date: Tue, 18 Mar 2014 14:22:14 -0400 Subject: [PATCH] Some small fixes in the API. --- endpoints/api/__init__.py | 7 +- endpoints/api/billing.py | 6 + endpoints/api/legacy.py | 2577 ----------------------------------- endpoints/api/permission.py | 2 +- 4 files changed, 8 insertions(+), 2584 deletions(-) delete mode 100644 endpoints/api/legacy.py diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 35607b6af..7ba2500a1 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -45,9 +45,6 @@ class ApiException(Exception): return rv -invalid_request = partial(ApiException, 'invalid_request', 400) - - class InvalidRequest(ApiException): def __init__(self, error_description, payload=None): ApiException.__init__(self, 'invalid_request', 400, error_description, payload) @@ -206,7 +203,7 @@ def validate_json_request(schema_name): validate(request.get_json(), schema) return func(self, *args, **kwargs) except ValidationError as ex: - InvalidRequest(ex.message) + raise InvalidRequest(ex.message) return wrapped return wrapper @@ -222,8 +219,6 @@ def log_action(kind, user_or_orgname, metadata={}, repo=None): metadata=metadata, repository=repo) -import endpoints.api.legacy - import endpoints.api.billing import endpoints.api.build import endpoints.api.discovery diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index ec244e7fa..2bab19e46 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -206,12 +206,18 @@ class UserPlan(ApiResource): plan = request_data['plan'] token = request_data['token'] if 'token' in request_data else None user = get_authenticated_user() + if not user: + raise Unauthorized() + return subscribe(user, plan, token, False) # Business features not required @nickname('getUserSubscription') def get(self): """ Fetch any existing subscription for the user. """ user = get_authenticated_user() + if not user: + raise Unauthorized() + private_repos = model.get_private_repo_count(user.username) if user.stripe_id: diff --git a/endpoints/api/legacy.py b/endpoints/api/legacy.py deleted file mode 100644 index 58be6bf0f..000000000 --- a/endpoints/api/legacy.py +++ /dev/null @@ -1,2577 +0,0 @@ -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 endpoints.api import api_bp, log_action -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_bp.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 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 - - -# Ported -@api_bp.route('/discovery') -def discovery(): - return jsonify(get_route_data()) - - -@api_bp.route('/') -@internal_api_call -def welcome(): - return jsonify({'version': '0.5'}) - - -# Ported -@api_bp.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) - } - - -# Ported -@api_bp.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)) - - -# Ported -@api_bp.route('/user/private', methods=['GET']) -@api_login_required -@internal_api_call -def get_user_private_allowed(): - 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, - 'privateAllowed': (private_repos < repos_allowed) - }) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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)) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.route("/signout", methods=['POST']) -@api_login_required -@internal_api_call -def logout(): - logout_user() - identity_changed.send(app, identity=AnonymousIdentity()) - return jsonify({'success': True}) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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 - } - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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, - } - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.route('/organization//private', methods=['GET']) -@api_login_required -@internal_api_call -@org_api_call('get_user_private_allowed') -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) - data = { - 'privateAllowed': False - } - - if organization.stripe_id: - cus = stripe.Customer.retrieve(organization.stripe_id) - if cus.subscription: - repos_allowed = 0 - plan = get_plan(cus.subscription.plan.id) - if plan: - repos_allowed = plan['privateRepos'] - - data['privateAllowed'] = (private_repos < repos_allowed) - - - if AdministerOrganizationPermission(orgname).can(): - data['privateCount'] = private_repos - - return jsonify(data) - - abort(403) - - -def member_view(member): - return { - 'name': member.username, - 'kind': 'user', - 'is_robot': member.robot, - } - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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, - } - - -# Ported -@api_bp.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, 1, - 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) - logger.debug('Can write: %s job_config: %s', can_write, build_obj.job_config) - build_obj.job_config = None - 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), - 'resource_key': build_obj.resource_key, - } - - -# Ported -@api_bp.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: - limit = request.args.get('limit', 5) - builds = list(model.list_repository_builds(namespace, repository, limit)) - - can_write = ModifyRepositoryPermission(namespace, repository).can() - return jsonify({ - 'builds': [build_status_view(build, can_write) for build in builds] - }) - - abort(403) # Permission denied - - -# Ported -@api_bp.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 - - -# Ported and merged with status -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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.') - request_json = request.get_json() - - dockerfile_id = request_json['file_id'] - subdir = request_json['subdirectory'] if 'subdirectory' in request_json else '' - - # 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, - subdir, True) - - resp = jsonify(build_status_view(build_request, True)) - repo_string = '%s/%s' % (namespace, repository) - resp.headers['Location'] = url_for('api_bp.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), - } - - -# Ported -@api_bp.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_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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, '', '', '')) - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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_bp.get_repo_build_status', - repository=repo_string, - build_uuid=build_request.uuid) - resp.status_code = 201 - return resp - - abort(403) # Permission denied - - -# Ported -@api_bp.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 = list(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 - - -# Ported -@api_bp.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 - - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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: %s', 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(recursive=True) - return make_response('No Content', 204) - - abort(403) # Permission denied - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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, - } - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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, - } - - -# Ported -@api_bp.route('/user/card', methods=['GET']) -@api_login_required -@internal_api_call -def get_user_card(): - user = current_user.db_user() - return get_card(user) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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}) - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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] - }) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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, - }) - - -# Ported -@api_bp.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, - } - - -# Ported -@api_bp.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] - }) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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 - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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) - - -# Ported -@api_bp.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] - }) - diff --git a/endpoints/api/permission.py b/endpoints/api/permission.py index ba9462fb5..d225d1f5c 100644 --- a/endpoints/api/permission.py +++ b/endpoints/api/permission.py @@ -105,7 +105,7 @@ class RepositoryUserPermission(RepositoryParamResource): } @require_repo_admin - @nickname('getUserPermission') + @nickname('getUserPermissions') def get(self, namespace, repository, username): """ Get the Fetch the permission for the specified user. """ logger.debug('Get repo: %s/%s permissions for user %s' %