- Change the subscribe method to allow for subscribing to the free plan, even when an org - Change the frontend to no longer have different plan groups - Change the frontend to display the proper plans (i.e. hide the deprecated plans unless it is the current plan, etc)
1850 lines
54 KiB
Python
1850 lines
54 KiB
Python
import logging
|
|
import stripe
|
|
import requests
|
|
import urlparse
|
|
import json
|
|
|
|
from flask import request, make_response, jsonify, abort, url_for
|
|
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 data import model
|
|
from data.queue import dockerfile_build_queue
|
|
from data.plans import PLANS, get_plan
|
|
from app import app
|
|
from util.email import send_confirmation_email, send_recovery_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)
|
|
from endpoints import registry
|
|
from endpoints.web import common_login
|
|
from util.cache import cache_control
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
store = app.config['STORAGE']
|
|
user_files = app.config['USERFILES']
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
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
|
|
|
|
|
|
@app.errorhandler(model.DataModelException)
|
|
def handle_dme(ex):
|
|
return make_response(ex.message, 400)
|
|
|
|
|
|
@app.errorhandler(KeyError)
|
|
def handle_dme_key_error(ex):
|
|
return make_response(ex.message, 400)
|
|
|
|
|
|
@app.route('/api/')
|
|
def welcome():
|
|
return make_response('welcome', 200)
|
|
|
|
|
|
@app.route('/api/plans/')
|
|
def plans_list():
|
|
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()
|
|
}
|
|
|
|
organizations = model.get_user_organizations(user.username)
|
|
|
|
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],
|
|
'can_create_repo': True,
|
|
'invoice_email': user.invoice_email
|
|
}
|
|
|
|
|
|
@app.route('/api/user/', methods=['GET'])
|
|
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))
|
|
|
|
|
|
@app.route('/api/user/private', methods=['GET'])
|
|
@api_login_required
|
|
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
|
|
})
|
|
|
|
|
|
@app.route('/api/user/convert', methods=['POST'])
|
|
@api_login_required
|
|
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:
|
|
error_resp = jsonify({
|
|
'reason': 'invaliduser'
|
|
})
|
|
error_resp.status_code = 400
|
|
return error_resp
|
|
|
|
# Ensure that the sign in credentials work.
|
|
admin_password = convert_data['adminPassword']
|
|
if not model.verify_user(admin_username, admin_password):
|
|
error_resp = jsonify({
|
|
'reason': 'invaliduser'
|
|
})
|
|
error_resp.status_code = 400
|
|
return error_resp
|
|
|
|
# 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)
|
|
|
|
|
|
@app.route('/api/user/', methods=['PUT'])
|
|
@api_login_required
|
|
def change_user_details():
|
|
user = current_user.db_user()
|
|
|
|
user_data = request.get_json()
|
|
|
|
try:
|
|
if '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'])
|
|
|
|
except model.InvalidPasswordException, ex:
|
|
error_resp = jsonify({
|
|
'message': ex.message,
|
|
})
|
|
error_resp.status_code = 400
|
|
return error_resp
|
|
|
|
return jsonify(user_view(user))
|
|
|
|
|
|
@app.route('/api/user/', methods=['POST'])
|
|
def create_user_api():
|
|
user_data = request.get_json()
|
|
|
|
existing_user = model.get_user(user_data['username'])
|
|
if existing_user:
|
|
error_resp = jsonify({
|
|
'message': 'The username already exists'
|
|
})
|
|
error_resp.status_code = 400
|
|
return error_resp
|
|
|
|
try:
|
|
new_user = model.create_user(user_data['username'], user_data['password'],
|
|
user_data['email'])
|
|
code = model.create_confirm_email_code(new_user)
|
|
send_confirmation_email(new_user.username, new_user.email, code.code)
|
|
return make_response('Created', 201)
|
|
except model.DataModelException as ex:
|
|
error_resp = jsonify({
|
|
'message': ex.message,
|
|
})
|
|
error_resp.status_code = 400
|
|
return error_resp
|
|
|
|
|
|
@app.route('/api/signin', methods=['POST'])
|
|
def signin_api():
|
|
signin_data = request.get_json()
|
|
|
|
username = signin_data['username']
|
|
password = signin_data['password']
|
|
|
|
return conduct_signin(username, password)
|
|
|
|
|
|
def conduct_signin(username, password):
|
|
#TODO Allow email login
|
|
needs_email_verification = False
|
|
invalid_credentials = False
|
|
|
|
verified = model.verify_user(username, password)
|
|
if verified:
|
|
if common_login(verified):
|
|
return make_response('Success', 200)
|
|
else:
|
|
needs_email_verification = True
|
|
|
|
else:
|
|
invalid_credentials = True
|
|
|
|
response = jsonify({
|
|
'needsEmailVerification': needs_email_verification,
|
|
'invalidCredentials': invalid_credentials,
|
|
})
|
|
response.status_code = 403
|
|
return response
|
|
|
|
|
|
@app.route("/api/signout", methods=['POST'])
|
|
@api_login_required
|
|
def logout():
|
|
logout_user()
|
|
identity_changed.send(app, identity=AnonymousIdentity())
|
|
return make_response('Success', 200)
|
|
|
|
|
|
@app.route("/api/recovery", methods=['POST'])
|
|
def send_recovery():
|
|
email = request.get_json()['email']
|
|
code = model.create_reset_password_email_code(email)
|
|
send_recovery_email(email, code.code)
|
|
return make_response('Created', 201)
|
|
|
|
|
|
@app.route('/api/users/<prefix>', methods=['GET'])
|
|
@api_login_required
|
|
def get_matching_users(prefix):
|
|
users = model.get_matching_users(prefix)
|
|
|
|
return jsonify({
|
|
'users': [user.username for user in users]
|
|
})
|
|
|
|
|
|
@app.route('/api/entities/<prefix>', 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 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
|
|
}
|
|
|
|
|
|
@app.route('/api/organization/', methods=['POST'])
|
|
@api_login_required
|
|
def create_organization_api():
|
|
org_data = request.get_json()
|
|
existing = None
|
|
|
|
try:
|
|
existing = (model.get_organization(org_data['name']) or
|
|
model.get_user(org_data['name']))
|
|
except model.InvalidOrganizationException:
|
|
pass
|
|
|
|
if existing:
|
|
error_resp = jsonify({
|
|
'message': 'A user or organization with this name already exists'
|
|
})
|
|
error_resp.status_code = 400
|
|
return error_resp
|
|
|
|
try:
|
|
model.create_organization(org_data['name'], org_data['email'],
|
|
current_user.db_user())
|
|
return make_response('Created', 201)
|
|
except model.DataModelException as ex:
|
|
error_resp = jsonify({
|
|
'message': ex.message,
|
|
})
|
|
error_resp.status_code = 400
|
|
return error_resp
|
|
|
|
|
|
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
|
|
|
|
|
|
@app.route('/api/organization/<orgname>', 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)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>', methods=['PUT'])
|
|
@api_login_required
|
|
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'])
|
|
|
|
teams = model.get_teams_within_org(org)
|
|
return jsonify(org_view(org, teams))
|
|
|
|
abort(403)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/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] = {'username': member.user.username,
|
|
'is_robot': member.user.robot,
|
|
'teams': []}
|
|
|
|
members_dict[member.user.username]['teams'].append(member.team.name)
|
|
|
|
return jsonify({'members': members_dict})
|
|
|
|
abort(403)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/members/<membername>', 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 = {'username': member.user.username,
|
|
'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)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/private', methods=['GET'])
|
|
@api_login_required
|
|
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 {
|
|
'username': member.username,
|
|
'is_robot': member.robot,
|
|
}
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/team/<teamname>',
|
|
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)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/team/<teamname>',
|
|
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)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/team/<teamname>/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)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/team/<teamname>/members/<membername>',
|
|
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:
|
|
abort(400)
|
|
|
|
# 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)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/team/<teamname>/members/<membername>',
|
|
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)
|
|
|
|
|
|
@app.route('/api/repository', methods=['POST'])
|
|
@api_login_required
|
|
def create_repo_api():
|
|
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 make_response('Repository already exists', 400)
|
|
|
|
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)
|
|
|
|
|
|
@app.route('/api/find/repository', methods=['GET'])
|
|
def match_repos_api():
|
|
prefix = request.args.get('query', '')
|
|
|
|
def repo_view(repo):
|
|
return {
|
|
'namespace': repo.namespace,
|
|
'name': repo.name,
|
|
'description': repo.description
|
|
}
|
|
|
|
username = None
|
|
if current_user.is_authenticated():
|
|
username = current_user.db_user().username
|
|
|
|
matching = model.get_matching_repositories(prefix, username)
|
|
response = {
|
|
'repositories': [repo_view(repo) for repo in matching]
|
|
}
|
|
|
|
return jsonify(response)
|
|
|
|
|
|
@app.route('/api/repository/', methods=['GET'])
|
|
def list_repos_api():
|
|
def repo_view(repo_obj):
|
|
return {
|
|
'namespace': repo_obj.namespace,
|
|
'name': repo_obj.name,
|
|
'description': repo_obj.description,
|
|
'is_public': repo_obj.visibility.name == 'public',
|
|
}
|
|
|
|
limit = request.args.get('limit', None)
|
|
namespace_filter = request.args.get('namespace', None)
|
|
include_public = request.args.get('public', 'true')
|
|
include_private = request.args.get('private', 'true')
|
|
sort = request.args.get('sort', 'false')
|
|
|
|
try:
|
|
limit = int(limit) if limit else None
|
|
except TypeError:
|
|
limit = None
|
|
|
|
include_public = include_public == 'true'
|
|
include_private = include_private == 'true'
|
|
sort = sort == 'true'
|
|
|
|
username = None
|
|
if current_user.is_authenticated() and include_private:
|
|
username = current_user.db_user().username
|
|
|
|
repo_query = model.get_visible_repositories(username, limit=limit,
|
|
include_public=include_public,
|
|
sort=sort,
|
|
namespace=namespace_filter)
|
|
repos = [repo_view(repo) for repo in repo_query]
|
|
response = {
|
|
'repositories': repos
|
|
}
|
|
|
|
return jsonify(response)
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>', methods=['PUT'])
|
|
@api_login_required
|
|
@parse_repository_name
|
|
def update_repo_api(namespace, repository):
|
|
permission = ModifyRepositoryPermission(namespace, repository)
|
|
if permission.can():
|
|
repo = model.get_repository(namespace, repository)
|
|
if repo:
|
|
values = request.get_json()
|
|
repo.description = values['description']
|
|
repo.save()
|
|
|
|
log_action('set_repo_description', namespace,
|
|
{'repo': repository, 'description': values['description']},
|
|
repo=repo)
|
|
return jsonify({
|
|
'success': True
|
|
})
|
|
|
|
abort(403)
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/changevisibility',
|
|
methods=['POST'])
|
|
@api_login_required
|
|
@parse_repository_name
|
|
def change_repo_visibility_api(namespace, repository):
|
|
permission = AdministerRepositoryPermission(namespace, repository)
|
|
if permission.can():
|
|
repo = model.get_repository(namespace, repository)
|
|
if repo:
|
|
values = request.get_json()
|
|
model.set_repository_visibility(repo, values['visibility'])
|
|
log_action('change_repo_visibility', namespace,
|
|
{'repo': repository, 'visibility': values['visibility']},
|
|
repo=repo)
|
|
return jsonify({
|
|
'success': True
|
|
})
|
|
|
|
abort(403)
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>', methods=['DELETE'])
|
|
@api_login_required
|
|
@parse_repository_name
|
|
def delete_repository(namespace, repository):
|
|
permission = AdministerRepositoryPermission(namespace, repository)
|
|
if permission.can():
|
|
model.purge_repository(namespace, repository)
|
|
registry.delete_repository_storage(namespace, repository)
|
|
log_action('delete_repo', namespace,
|
|
{'repo': repository, 'namespace': namespace})
|
|
return make_response('Deleted', 204)
|
|
|
|
abort(403)
|
|
|
|
|
|
def image_view(image):
|
|
return {
|
|
'id': image.docker_image_id,
|
|
'created': image.created,
|
|
'comment': image.comment,
|
|
'ancestors': image.ancestors,
|
|
'dbid': image.id,
|
|
}
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>', methods=['GET'])
|
|
@parse_repository_name
|
|
def get_repo_api(namespace, repository):
|
|
logger.debug('Get repo: %s/%s' % (namespace, repository))
|
|
|
|
def tag_view(tag):
|
|
image = model.get_tag_image(namespace, repository, tag.name)
|
|
if not image:
|
|
return {}
|
|
|
|
return {
|
|
'name': tag.name,
|
|
'image': image_view(image),
|
|
}
|
|
|
|
organization = None
|
|
try:
|
|
organization = model.get_organization(namespace)
|
|
except 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(active_builds) > 0,
|
|
'is_organization': bool(organization)
|
|
})
|
|
|
|
abort(404) # Not found
|
|
abort(403) # Permission denied
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/build/', methods=['GET'])
|
|
@api_login_required
|
|
@parse_repository_name
|
|
def get_repo_builds(namespace, repository):
|
|
permission = ModifyRepositoryPermission(namespace, repository)
|
|
if permission.can():
|
|
def build_view(build_obj):
|
|
if build_obj.status_url:
|
|
# Delegate the status to the build node
|
|
node_status = requests.get(build_obj.status_url).json()
|
|
node_status['id'] = build_obj.id
|
|
return node_status
|
|
|
|
# If there was no status url, do the best we can
|
|
return {
|
|
'id': build_obj.id,
|
|
'total_commands': None,
|
|
'total_images': None,
|
|
'current_command': None,
|
|
'current_image': None,
|
|
'image_completion_percent': None,
|
|
'status': build_obj.phase,
|
|
'message': None,
|
|
}
|
|
|
|
builds = model.list_repository_builds(namespace, repository)
|
|
return jsonify({
|
|
'builds': [build_view(build) for build in builds]
|
|
})
|
|
|
|
abort(403) # Permissions denied
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/build/', methods=['POST'])
|
|
@api_login_required
|
|
@parse_repository_name
|
|
def request_repo_build(namespace, repository):
|
|
permission = ModifyRepositoryPermission(namespace, repository)
|
|
if permission.can():
|
|
logger.debug('User requested repository initialization.')
|
|
dockerfile_id = request.get_json()['file_id']
|
|
|
|
repo = model.get_repository(namespace, repository)
|
|
token = model.create_access_token(repo, 'write')
|
|
|
|
host = urlparse.urlparse(request.url).netloc
|
|
tag = '%s/%s/%s' % (host, repo.namespace, repo.name)
|
|
build_request = model.create_repository_build(repo, token, dockerfile_id,
|
|
tag)
|
|
dockerfile_build_queue.put(json.dumps({'build_id': build_request.id}))
|
|
|
|
log_action('build_dockerfile', namespace,
|
|
{'repo': repository, 'namespace': namespace,
|
|
'fileid': dockerfile_id}, repo=repo)
|
|
|
|
resp = jsonify({
|
|
'started': True
|
|
})
|
|
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),
|
|
}
|
|
|
|
|
|
@app.route('/api/repository/<path: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('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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/webhook/<public_id>',
|
|
methods=['GET'])
|
|
@api_login_required
|
|
@parse_repository_name
|
|
def get_webhook(namespace, repository, public_id):
|
|
permission = AdministerRepositoryPermission(namespace, repository)
|
|
if permission.can():
|
|
webhook = model.get_webhook(namespace, repository, public_id)
|
|
return jsonify(webhook_view(webhook))
|
|
|
|
abort(403) # Permission denied
|
|
|
|
|
|
@app.route('/api/repository/<path: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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/webhook/<public_id>',
|
|
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
|
|
|
|
|
|
@app.route('/api/filedrop/', methods=['POST'])
|
|
@api_login_required
|
|
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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/image/', methods=['GET'])
|
|
@parse_repository_name
|
|
def list_repository_images(namespace, repository):
|
|
permission = ReadRepositoryPermission(namespace, repository)
|
|
if permission.can() or model.repository_is_public(namespace, repository):
|
|
all_images = model.get_repository_images(namespace, repository)
|
|
all_tags = model.list_repository_tags(namespace, repository)
|
|
|
|
tags_by_image_id = defaultdict(list)
|
|
for tag in all_tags:
|
|
tags_by_image_id[tag.image.docker_image_id].append(tag.name)
|
|
|
|
|
|
def add_tags(image_json):
|
|
image_json['tags'] = tags_by_image_id[image_json['id']]
|
|
return image_json
|
|
|
|
|
|
return jsonify({
|
|
'images': [add_tags(image_view(image)) for image in all_images]
|
|
})
|
|
|
|
abort(403)
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/image/<image_id>',
|
|
methods=['GET'])
|
|
@parse_repository_name
|
|
def get_image(namespace, repository, image_id):
|
|
permission = ReadRepositoryPermission(namespace, repository)
|
|
if permission.can() or model.repository_is_public(namespace, repository):
|
|
image = model.get_repo_image(namespace, repository, image_id)
|
|
if not image:
|
|
abort(404)
|
|
|
|
return jsonify(image_view(image))
|
|
abort(403)
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/image/<image_id>/changes',
|
|
methods=['GET'])
|
|
@cache_control(max_age=60*60) # Cache for one hour
|
|
@parse_repository_name
|
|
def get_image_changes(namespace, repository, image_id):
|
|
permission = ReadRepositoryPermission(namespace, repository)
|
|
if permission.can() or model.repository_is_public(namespace, repository):
|
|
diffs_path = store.image_file_diffs_path(namespace, repository, image_id)
|
|
|
|
try:
|
|
response_json = store.get_content(diffs_path)
|
|
return make_response(response_json)
|
|
except IOError:
|
|
abort(404)
|
|
|
|
abort(403)
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/tag/<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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/permissions/team/',
|
|
methods=['GET'])
|
|
@api_login_required
|
|
@parse_repository_name
|
|
def list_repo_team_permissions(namespace, repository):
|
|
permission = AdministerRepositoryPermission(namespace, repository)
|
|
if permission.can():
|
|
repo_perms = model.get_all_repo_teams(namespace, repository)
|
|
|
|
return jsonify({
|
|
'permissions': {repo_perm.team.name: role_view(repo_perm)
|
|
for repo_perm in repo_perms}
|
|
})
|
|
|
|
abort(403) # Permission denied
|
|
|
|
|
|
@app.route('/api/repository/<path: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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/permissions/user/<username>',
|
|
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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/permissions/team/<teamname>',
|
|
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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/permissions/user/<username>',
|
|
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:
|
|
error_resp = jsonify({
|
|
'message': ex.message,
|
|
})
|
|
error_resp.status_code = 400
|
|
return error_resp
|
|
|
|
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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/permissions/team/<teamname>',
|
|
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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/permissions/user/<username>',
|
|
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:
|
|
error_resp = jsonify({
|
|
'message': ex.message,
|
|
})
|
|
error_resp.status_code = 400
|
|
return error_resp
|
|
|
|
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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/permissions/team/<teamname>',
|
|
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,
|
|
}
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/tokens/', methods=['GET'])
|
|
@api_login_required
|
|
@parse_repository_name
|
|
def list_repo_tokens(namespace, repository):
|
|
permission = AdministerRepositoryPermission(namespace, repository)
|
|
if permission.can():
|
|
tokens = model.get_repository_delegate_tokens(namespace, repository)
|
|
|
|
return jsonify({
|
|
'tokens': {token.code: token_view(token) for token in tokens}
|
|
})
|
|
|
|
abort(403) # Permission denied
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/tokens/<code>', methods=['GET'])
|
|
@api_login_required
|
|
@parse_repository_name
|
|
def get_tokens(namespace, repository, code):
|
|
permission = AdministerRepositoryPermission(namespace, repository)
|
|
if permission.can():
|
|
perm = model.get_repo_delegate_token(namespace, repository, code)
|
|
return jsonify(token_view(perm))
|
|
|
|
abort(403) # Permission denied
|
|
|
|
|
|
@app.route('/api/repository/<path: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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/tokens/<code>', 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
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/tokens/<code>',
|
|
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,
|
|
}
|
|
|
|
|
|
@app.route('/api/user/card', methods=['GET'])
|
|
@api_login_required
|
|
def get_user_card_api():
|
|
user = current_user.db_user()
|
|
return get_card(user)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/card', methods=['GET'])
|
|
@api_login_required
|
|
def get_org_card_api(orgname):
|
|
permission = AdministerOrganizationPermission(orgname)
|
|
if permission.can():
|
|
organization = model.get_organization(orgname)
|
|
return get_card(organization)
|
|
|
|
abort(403)
|
|
|
|
|
|
@app.route('/api/user/card', methods=['POST'])
|
|
@api_login_required
|
|
def set_user_card_api():
|
|
user = current_user.db_user()
|
|
token = request.get_json()['token']
|
|
response = set_card(user, token)
|
|
log_action('account_change_cc', user.username)
|
|
return response
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/card', methods=['POST'])
|
|
@api_login_required
|
|
def set_org_card_api(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)
|
|
|
|
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})
|
|
|
|
@app.route('/api/user/plan', methods=['PUT'])
|
|
@api_login_required
|
|
def subscribe_api():
|
|
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:
|
|
abort(404)
|
|
|
|
if require_business_plan and not plan_found['bus_features'] and not plan_found['price'] == 0:
|
|
abort(404)
|
|
|
|
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
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/invoices', methods=['GET'])
|
|
@api_login_required
|
|
def org_invoices_api(orgname):
|
|
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
|
|
}
|
|
|
|
permission = AdministerOrganizationPermission(orgname)
|
|
if permission.can():
|
|
organization = model.get_organization(orgname)
|
|
if not organization.stripe_id:
|
|
abort(404)
|
|
|
|
invoices = stripe.Invoice.all(customer=organization.stripe_id, count=12)
|
|
return jsonify({
|
|
'invoices': [invoice_view(i) for i in invoices.data]
|
|
})
|
|
|
|
abort(403)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/plan', methods=['PUT'])
|
|
@api_login_required
|
|
def subscribe_org_api(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)
|
|
|
|
|
|
@app.route('/api/user/plan', methods=['GET'])
|
|
@api_login_required
|
|
def get_subscription():
|
|
user = current_user.db_user()
|
|
private_repos = model.get_private_repo_count(user.username)
|
|
|
|
if user.stripe_id:
|
|
cus = stripe.Customer.retrieve(user.stripe_id)
|
|
|
|
if cus.subscription:
|
|
return jsonify(subscription_view(cus.subscription, private_repos))
|
|
|
|
return jsonify({
|
|
'plan': 'free',
|
|
'usedPrivateRepos': private_repos,
|
|
})
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/plan', methods=['GET'])
|
|
@api_login_required
|
|
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,
|
|
}
|
|
|
|
|
|
@app.route('/api/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]
|
|
})
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/robots', methods=['GET'])
|
|
@api_login_required
|
|
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)
|
|
|
|
|
|
@app.route('/api/user/robots/<robot_shortname>', methods=['PUT'])
|
|
@api_login_required
|
|
def create_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
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/robots/<robot_shortname>',
|
|
methods=['PUT'])
|
|
@api_login_required
|
|
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)
|
|
|
|
|
|
@app.route('/api/user/robots/<robot_shortname>', methods=['DELETE'])
|
|
@api_login_required
|
|
def delete_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('No Content', 204)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/robots/<robot_shortname>',
|
|
methods=['DELETE'])
|
|
@api_login_required
|
|
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('No Content', 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'] = {
|
|
'username': log.performer.username,
|
|
'is_robot': log.performer.robot,
|
|
}
|
|
|
|
return view
|
|
|
|
|
|
|
|
@app.route('/api/repository/<path:repository>/logs', methods=['GET'])
|
|
@api_login_required
|
|
@parse_repository_name
|
|
def repo_logs_api(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)
|
|
|
|
|
|
@app.route('/api/organization/<orgname>/logs', methods=['GET'])
|
|
@api_login_required
|
|
def org_logs_api(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)
|
|
|
|
|
|
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]
|
|
})
|
|
|