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