0c4dec6de4
- Add a "can_create_repo" entry to the organization and have orgs grayed out in the new repo view if the user cannot create a repo - Fix the multiple-orgs bug in the model - Have the "create new repository" button disappear on landing if the org is selected and the user does not have create permissions for that org
1241 lines
36 KiB
Python
1241 lines
36 KiB
Python
import logging
|
|
import stripe
|
|
import re
|
|
import requests
|
|
import urlparse
|
|
import json
|
|
|
|
from flask import request, make_response, jsonify, abort
|
|
from flask.ext.login import login_required, current_user, logout_user
|
|
from flask.ext.principal import identity_changed, AnonymousIdentity
|
|
from functools import wraps
|
|
from collections import defaultdict
|
|
|
|
import storage
|
|
|
|
from data import model
|
|
from data.userfiles import UserRequestFiles
|
|
from data.queue import dockerfile_build_queue
|
|
from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan
|
|
from app import app
|
|
from util.email import send_confirmation_email, send_recovery_email
|
|
from util.names import parse_repository_name
|
|
from util.gravatar import compute_hash
|
|
from auth.permissions import (ReadRepositoryPermission,
|
|
ModifyRepositoryPermission,
|
|
AdministerRepositoryPermission,
|
|
CreateRepositoryPermission,
|
|
AdministerOrganizationPermission,
|
|
OrganizationMemberPermission,
|
|
ViewTeamPermission)
|
|
from endpoints import registry
|
|
from endpoints.web import common_login
|
|
from util.cache import cache_control
|
|
|
|
|
|
store = storage.load()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def api_login_required(f):
|
|
@wraps(f)
|
|
def decorated_view(*args, **kwargs):
|
|
if not current_user.is_authenticated():
|
|
abort(401)
|
|
return f(*args, **kwargs)
|
|
return decorated_view
|
|
|
|
|
|
def required_json_args(*required_args):
|
|
def wrap(f):
|
|
@wraps(f)
|
|
def wrapped(*args, **kwargs):
|
|
request_data = request.get_json()
|
|
for arg in required_args:
|
|
if arg not in request_data:
|
|
abort(400)
|
|
return f(*args, **kwargs)
|
|
return wrapped
|
|
return wrap
|
|
|
|
|
|
@app.errorhandler(model.DataModelException)
|
|
def handle_dme(ex):
|
|
return make_response(ex.message, 400)
|
|
|
|
|
|
@app.route('/api/')
|
|
def welcome():
|
|
return make_response('welcome', 200)
|
|
|
|
|
|
@app.route('/api/plans/')
|
|
def plans_list():
|
|
return jsonify({
|
|
'user': USER_PLANS,
|
|
'business': BUSINESS_PLANS,
|
|
})
|
|
|
|
|
|
@app.route('/api/user/', methods=['GET'])
|
|
def get_logged_in_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()
|
|
}
|
|
|
|
if current_user.is_anonymous():
|
|
return jsonify({'anonymous': True})
|
|
|
|
user = current_user.db_user()
|
|
organizations = model.get_user_organizations(user.username)
|
|
|
|
return jsonify({
|
|
'verified': user.verified,
|
|
'anonymous': False,
|
|
'username': user.username,
|
|
'email': user.email,
|
|
'gravatar': compute_hash(user.email),
|
|
'askForPassword': user.password_hash is None,
|
|
'organizations': [org_view(o) for o in organizations],
|
|
'can_create_repo': True
|
|
})
|
|
|
|
|
|
@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)
|
|
model.change_password(user, user_data['password'])
|
|
except model.InvalidPasswordException, ex:
|
|
error_resp = jsonify({
|
|
'message': ex.message,
|
|
})
|
|
error_resp.status_code = 400
|
|
return error_resp
|
|
|
|
return jsonify({
|
|
'verified': user.verified,
|
|
'anonymous': False,
|
|
'username': user.username,
|
|
'email': user.email,
|
|
'gravatar': compute_hash(user.email),
|
|
'askForPassword': user.password_hash is None,
|
|
})
|
|
|
|
|
|
@app.route('/api/user/', methods=['POST'])
|
|
@required_json_args('username', 'password', 'email')
|
|
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'])
|
|
@required_json_args('username', 'password')
|
|
def signin_api():
|
|
signin_data = request.get_json()
|
|
|
|
username = signin_data['username']
|
|
password = signin_data['password']
|
|
|
|
#TODO Allow email login
|
|
needs_email_verification = False
|
|
invalid_credentials = False
|
|
|
|
verified = model.verify_user(username, password)
|
|
if verified:
|
|
if common_login(verified):
|
|
return make_response('Success', 200)
|
|
else:
|
|
needs_email_verification = True
|
|
|
|
else:
|
|
invalid_credentials = True
|
|
|
|
response = jsonify({
|
|
'needsEmailVerification': needs_email_verification,
|
|
'invalidCredentials': invalid_credentials,
|
|
})
|
|
response.status_code = 403
|
|
return response
|
|
|
|
|
|
@app.route("/api/signout", methods=['POST'])
|
|
@api_login_required
|
|
def logout():
|
|
logout_user()
|
|
|
|
identity_changed.send(app, identity=AnonymousIdentity())
|
|
|
|
return make_response('Success', 200)
|
|
|
|
|
|
@app.route("/api/recovery", methods=['POST'])
|
|
@required_json_args('email')
|
|
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 = []
|
|
|
|
organization_name = request.args.get('organization', None)
|
|
organization = None
|
|
if organization_name:
|
|
permission = OrganizationMemberPermission(organization_name)
|
|
if permission.can():
|
|
try:
|
|
organization = model.get_organization(organization_name)
|
|
except:
|
|
pass
|
|
|
|
if organization:
|
|
teams = model.get_matching_teams(prefix, organization)
|
|
|
|
users = model.get_matching_users(prefix, organization)
|
|
|
|
def team_view(team):
|
|
result = {
|
|
'name': team.name,
|
|
'kind': 'team',
|
|
'is_org_member': True
|
|
}
|
|
|
|
def user_view(user):
|
|
user_json = {
|
|
'name': user.username,
|
|
'kind': 'user',
|
|
}
|
|
|
|
if user.is_org_member is not None:
|
|
user_json['is_org_member'] = user.is_org_member
|
|
|
|
return user_json
|
|
|
|
team_data = [team_view(team) for team in teams]
|
|
user_data = [user_view(user) for user in users]
|
|
return jsonify({
|
|
'results': team_data + user_data
|
|
})
|
|
|
|
|
|
user_files = UserRequestFiles(app.config['AWS_ACCESS_KEY'],
|
|
app.config['AWS_SECRET_KEY'],
|
|
app.config['REGISTRY_S3_BUCKET'])
|
|
|
|
|
|
def team_view(orgname, t):
|
|
view_permission = ViewTeamPermission(orgname, t.name)
|
|
role = model.get_team_org_role(t).name
|
|
return {
|
|
'id': t.id,
|
|
'name': t.name,
|
|
'description': t.description,
|
|
'can_view': view_permission.can(),
|
|
'role': role
|
|
}
|
|
|
|
|
|
@app.route('/api/organization/<orgname>', methods=['GET'])
|
|
@api_login_required
|
|
def get_organization(orgname):
|
|
permission = OrganizationMemberPermission(orgname)
|
|
if permission.can():
|
|
user = current_user.db_user()
|
|
|
|
def org_view(o, teams):
|
|
admin_org = AdministerOrganizationPermission(orgname)
|
|
is_admin = admin_org.can()
|
|
return {
|
|
'name': o.username,
|
|
'gravatar': compute_hash(o.email),
|
|
'teams': {t.name : team_view(orgname, t) for t in teams},
|
|
'is_admin': is_admin
|
|
}
|
|
|
|
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>/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, 'teams': []}
|
|
|
|
members_dict[member.user.username]['teams'].append(member.team.name)
|
|
|
|
return jsonify({'members': members_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 = get_plan(cus.subscription.plan.id)
|
|
return jsonify({
|
|
'privateAllowed': (private_repos < repos_allowed)
|
|
})
|
|
|
|
return jsonify({
|
|
'privateAllowed': False
|
|
})
|
|
|
|
abort(403)
|
|
|
|
|
|
def member_view(m):
|
|
return {
|
|
'username': m.username
|
|
}
|
|
|
|
|
|
@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
|
|
|
|
json = request.get_json()
|
|
is_existing = False
|
|
try:
|
|
team = model.get_organization_team(orgname, teamname)
|
|
is_existing = True
|
|
except:
|
|
# Create the new team.
|
|
description = json['description'] if 'description' in json else ''
|
|
role = json['role'] if 'role' in json else 'member'
|
|
|
|
org = model.get_organization(orgname)
|
|
team = model.create_team(teamname, org, role, description)
|
|
|
|
if is_existing:
|
|
if 'description' in json:
|
|
team.description = json['description']
|
|
team.save()
|
|
if 'role' in json:
|
|
team = model.set_team_org_permission(team, json['role'],
|
|
current_user.db_user().username)
|
|
|
|
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)
|
|
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():
|
|
user = current_user.db_user()
|
|
team = None
|
|
|
|
try:
|
|
team = model.get_organization_team(orgname, teamname)
|
|
except:
|
|
abort(404)
|
|
|
|
members = model.get_organization_team_members(team.id)
|
|
return jsonify({
|
|
'members': { m.username : member_view(m) for m in members },
|
|
'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:
|
|
abort(404)
|
|
|
|
# Find the user.
|
|
user = model.get_user(membername)
|
|
if not user:
|
|
abort(400)
|
|
|
|
# Add the user to the team.
|
|
model.add_user_to_team(user, team)
|
|
|
|
return jsonify(member_view(user))
|
|
|
|
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)
|
|
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()
|
|
json = request.get_json()
|
|
namespace_name = json['namespace'] if 'namespace' in json else owner.username
|
|
|
|
permission = CreateRepositoryPermission(namespace_name)
|
|
if permission.can():
|
|
repository_name = json['repository']
|
|
visibility = json['visibility']
|
|
|
|
existing = model.get_repository(namespace_name, repository_name)
|
|
if existing:
|
|
return make_response('Repository already exists', 400)
|
|
|
|
visibility = request.get_json()['visibility']
|
|
|
|
repo = model.create_repository(namespace_name, repository_name, owner,
|
|
visibility)
|
|
repo.description = json['description']
|
|
repo.save()
|
|
|
|
return jsonify({
|
|
'namespace': namespace_name,
|
|
'name': repository_name
|
|
})
|
|
|
|
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):
|
|
is_public = model.repository_is_public(repo_obj.namespace, repo_obj.name)
|
|
|
|
return {
|
|
'namespace': repo_obj.namespace,
|
|
'name': repo_obj.name,
|
|
'description': repo_obj.description,
|
|
'is_public': is_public
|
|
}
|
|
|
|
limit = request.args.get('limit', None)
|
|
namespace_filter = request.args.get('namespace', None)
|
|
include_public = request.args.get('public', 'true')
|
|
include_private = request.args.get('private', 'true')
|
|
sort = request.args.get('sort', 'false')
|
|
|
|
try:
|
|
limit = int(limit) if limit else None
|
|
except:
|
|
limit = None
|
|
|
|
include_public = include_public == 'true'
|
|
include_private = include_private == 'true'
|
|
sort = sort == 'true'
|
|
|
|
username = None
|
|
if current_user.is_authenticated() and include_private:
|
|
username = current_user.db_user().username
|
|
|
|
repo_query = model.get_visible_repositories(username, limit=limit,
|
|
include_public=include_public,
|
|
sort=sort, namespace=namespace_filter)
|
|
repos = [repo_view(repo) for repo in repo_query]
|
|
response = {
|
|
'repositories': repos
|
|
}
|
|
|
|
return jsonify(response)
|
|
|
|
|
|
@app.route('/api/repository/<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()
|
|
return jsonify({
|
|
'success': True
|
|
})
|
|
|
|
abort(404)
|
|
|
|
|
|
@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'])
|
|
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)
|
|
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:
|
|
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/filedrop/', methods=['POST'])
|
|
@api_login_required
|
|
@required_json_args('mimeType')
|
|
def get_filedrop_url():
|
|
mime_type = request.get_json()['mimeType']
|
|
(url, file_id) = user_files.prepare_for_drop(mime_type)
|
|
return jsonify({
|
|
'url': url,
|
|
'file_id': file_id
|
|
})
|
|
|
|
|
|
@app.route('/api/repository/<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}))
|
|
|
|
return jsonify({
|
|
'started': True
|
|
})
|
|
|
|
abort(403) # Permissions denied
|
|
|
|
|
|
def role_view(repo_perm_obj):
|
|
return {
|
|
'role': repo_perm_obj.role.name,
|
|
}
|
|
|
|
|
|
def wrap_role_view_org(role_json, org_member):
|
|
role_json['is_org_member'] = org_member
|
|
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():
|
|
# Determine how to wrap the permissions
|
|
role_view_func = role_view
|
|
try:
|
|
model.get_organization(namespace) # Will raise an error if not org
|
|
org_members = model.get_organization_member_set(namespace)
|
|
def wrapped_role_view(repo_perm):
|
|
unwrapped = role_view(repo_perm)
|
|
return wrap_role_view_org(unwrapped,
|
|
repo_perm.user.username in org_members)
|
|
|
|
role_view_func = wrapped_role_view
|
|
|
|
except model.InvalidOrganizationException:
|
|
# This repository isn't under an org
|
|
pass
|
|
|
|
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 = role_view(perm)
|
|
|
|
try:
|
|
model.get_organization(namespace)
|
|
org_members = model.get_organization_member_set(namespace)
|
|
perm_view = wrap_role_view_org(perm_view,
|
|
perm.user.username in 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(username, 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))
|
|
|
|
try:
|
|
perm = model.set_user_repo_permission(username, namespace, repository,
|
|
new_permission['role'])
|
|
except model.DataModelException:
|
|
logger.warning('User tried to remove themselves as admin.')
|
|
abort(409)
|
|
|
|
perm_view = role_view(perm)
|
|
|
|
try:
|
|
model.get_organization(namespace)
|
|
org_members = model.get_organization_member_set(namespace)
|
|
perm_view = wrap_role_view_org(perm_view,
|
|
perm.user.username in org_members)
|
|
except model.InvalidOrganizationException:
|
|
# This repository is not part of an organization
|
|
pass
|
|
|
|
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))
|
|
|
|
try:
|
|
perm = model.set_team_repo_permission(teamname, namespace, repository,
|
|
new_permission['role'])
|
|
except model.DataModelException:
|
|
logger.warning('User tried to remove themselves as admin.')
|
|
abort(409)
|
|
|
|
resp = jsonify(role_view(perm))
|
|
if request.method == 'POST':
|
|
resp.status_code = 201
|
|
return resp
|
|
|
|
abort(403) # Permission denied
|
|
|
|
|
|
@app.route('/api/repository/<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:
|
|
logger.warning('User tried to remove themselves as admin.')
|
|
abort(409)
|
|
|
|
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():
|
|
try:
|
|
model.delete_team_permission(teamname, namespace, repository)
|
|
except model.DataModelException:
|
|
logger.warning('User tried to remove themselves as admin.')
|
|
abort(409)
|
|
|
|
return make_response('Deleted', 204)
|
|
|
|
abort(403) # Permission denied
|
|
|
|
|
|
def token_view(token_obj):
|
|
return {
|
|
'friendlyName': token_obj.friendly_name,
|
|
'code': token_obj.code,
|
|
'role': token_obj.role.name,
|
|
}
|
|
|
|
|
|
@app.route('/api/repository/<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'])
|
|
|
|
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'])
|
|
|
|
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():
|
|
model.delete_delegate_token(namespace, repository, code)
|
|
return make_response('Deleted', 204)
|
|
|
|
abort(403) # Permission denied
|
|
|
|
|
|
def subscription_view(stripe_subscription, used_repos):
|
|
return {
|
|
'currentPeriodStart': stripe_subscription.current_period_start,
|
|
'currentPeriodEnd': stripe_subscription.current_period_end,
|
|
'plan': stripe_subscription.plan.id,
|
|
'usedPrivateRepos': used_repos,
|
|
}
|
|
|
|
|
|
@app.route('/api/user/plan', methods=['PUT'])
|
|
@api_login_required
|
|
def subscribe_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, USER_PLANS)
|
|
|
|
def subscribe(user, plan, token, accepted_plans):
|
|
plan_found = None
|
|
for plan_obj in accepted_plans:
|
|
if plan_obj['stripeId'] == plan:
|
|
plan_found = plan_obj
|
|
|
|
if not plan_found:
|
|
abort(404)
|
|
|
|
private_repos = model.get_private_repo_count(user.username)
|
|
|
|
if not user.stripe_id:
|
|
# Create the customer and plan simultaneously
|
|
card = token
|
|
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
|
|
user.stripe_id = cus.id
|
|
user.save()
|
|
|
|
resp = jsonify(subscription_view(cus.subscription, private_repos))
|
|
resp.status_code = 201
|
|
return resp
|
|
|
|
else:
|
|
# Change the plan
|
|
cus = stripe.Customer.retrieve(user.stripe_id)
|
|
|
|
if plan_found['price'] == 0:
|
|
cus.cancel_subscription()
|
|
cus.save()
|
|
|
|
response_json = {
|
|
'plan': plan,
|
|
'usedPrivateRepos': private_repos,
|
|
}
|
|
|
|
else:
|
|
cus.plan = plan
|
|
# User may have been a previous customer who is resubscribing
|
|
if token:
|
|
cus.card = token
|
|
|
|
cus.save()
|
|
response_json = subscription_view(cus.subscription, private_repos)
|
|
|
|
return jsonify(response_json)
|
|
|
|
|
|
@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, BUSINESS_PLANS)
|
|
|
|
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': 'bus-free',
|
|
'usedPrivateRepos': private_repos,
|
|
})
|
|
|
|
abort(403)
|