This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/endpoints/api.py

1968 lines
57 KiB
Python

import logging
import stripe
import requests
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 data import model
from data.queue import dockerfile_build_queue
from data.plans import PLANS, get_plan
from app import app
from util.email import send_confirmation_email, send_recovery_email
from util.names import parse_repository_name, format_robot_username
from util.gravatar import compute_hash
from auth.permissions import (ReadRepositoryPermission,
ModifyRepositoryPermission,
AdministerRepositoryPermission,
CreateRepositoryPermission,
AdministerOrganizationPermission,
OrganizationMemberPermission,
ViewTeamPermission)
from endpoints import registry
from endpoints.common import common_login
from util.cache import cache_control
from datetime import datetime, timedelta
store = app.config['STORAGE']
user_files = app.config['USERFILES']
logger = logging.getLogger(__name__)
route_data = None
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:
abort(403)
def get_route_data():
global route_data
if route_data:
return route_data
routes = []
for rule in app.url_map.iter_rules():
if rule.endpoint.startswith('api.'):
endpoint_method = globals()[rule.endpoint[4:]] # Remove api.
is_internal = '__internal_call' in dir(endpoint_method)
is_org_api = '__user_call' in dir(endpoint_method)
methods = list(rule.methods.difference(['HEAD', 'OPTIONS']))
route = {
'name': rule.endpoint[4:],
'methods': methods,
'path': rule.rule,
'parameters': list(rule.arguments)
}
if is_org_api:
route['user_method'] = endpoint_method.__user_call
routes.append(route)
route_data = {
'endpoints': routes
}
return route_data
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 make_response('welcome', 200)
@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()
}
organizations = model.get_user_organizations(user.username)
return {
'verified': user.verified,
'anonymous': False,
'username': user.username,
'email': user.email,
'gravatar': compute_hash(user.email),
'askForPassword': user.password_hash is None,
'organizations': [org_view(o) for o in organizations],
'can_create_repo': True,
'invoice_email': user.invoice_email
}
@api.route('/user/', methods=['GET'])
@internal_api_call
def get_logged_in_user():
if current_user.is_anonymous():
return jsonify({'anonymous': True})
user = current_user.db_user()
if not user or user.organization:
return jsonify({'anonymous': True})
return jsonify(user_view(user))
@api.route('/user/private', methods=['GET'])
@api_login_required
@internal_api_call
def get_user_private_count():
user = current_user.db_user()
private_repos = model.get_private_repo_count(user.username)
repos_allowed = 0
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
if cus.subscription:
plan = get_plan(cus.subscription.plan.id)
if plan:
repos_allowed = plan['privateRepos']
return jsonify({
'privateCount': private_repos,
'reposAllowed': repos_allowed
})
@api.route('/user/convert', methods=['POST'])
@api_login_required
@internal_api_call
def convert_user_to_organization():
user = current_user.db_user()
convert_data = request.get_json()
# Ensure that the new admin user is the not user being converted.
admin_username = convert_data['adminUser']
if admin_username == user.username:
error_resp = jsonify({
'reason': 'invaliduser'
})
error_resp.status_code = 400
return error_resp
# Ensure that the sign in credentials work.
admin_password = convert_data['adminPassword']
if not model.verify_user(admin_username, admin_password):
error_resp = jsonify({
'reason': 'invaliduser'
})
error_resp.status_code = 400
return error_resp
# Subscribe the organization to the new plan.
plan = convert_data['plan']
subscribe(user, plan, None, True) # Require business plans
# Convert the user to an organization.
model.convert_user_to_organization(user, model.get_user(admin_username))
log_action('account_convert', user.username)
# And finally login with the admin credentials.
return conduct_signin(admin_username, admin_password)
@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'])
except model.InvalidPasswordException, ex:
error_resp = jsonify({
'message': ex.message,
})
error_resp.status_code = 400
return error_resp
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:
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
@api.route('/signin', methods=['POST'])
@internal_api_call
def signin_user():
signin_data = request.get_json()
username = signin_data['username']
password = signin_data['password']
return conduct_signin(username, password)
def conduct_signin(username, password):
#TODO Allow email login
needs_email_verification = False
invalid_credentials = False
verified = model.verify_user(username, password)
if verified:
if common_login(verified):
return make_response('Success', 200)
else:
needs_email_verification = True
else:
invalid_credentials = True
response = jsonify({
'needsEmailVerification': needs_email_verification,
'invalidCredentials': invalid_credentials,
})
response.status_code = 403
return response
@api.route("/signout", methods=['POST'])
@api_login_required
@internal_api_call
def logout():
logout_user()
identity_changed.send(app, identity=AnonymousIdentity())
return make_response('Success', 200)
@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 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']) or
model.get_user(org_data['name']))
except model.InvalidOrganizationException:
pass
if existing:
error_resp = jsonify({
'message': 'A user or organization with this name already exists'
})
error_resp.status_code = 400
return error_resp
try:
model.create_organization(org_data['name'], org_data['email'],
current_user.db_user())
return make_response('Created', 201)
except model.DataModelException as ex:
error_resp = jsonify({
'message': ex.message,
})
error_resp.status_code = 400
return error_resp
def org_view(o, teams):
admin_org = AdministerOrganizationPermission(o.username)
is_admin = admin_org.can()
view = {
'name': o.username,
'email': o.email if is_admin else '',
'gravatar': compute_hash(o.email),
'teams': {t.name : team_view(o.username, t) for t in teams},
'is_admin': is_admin
}
if is_admin:
view['invoice_email'] = o.invoice_email
return view
@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'])
teams = model.get_teams_within_org(org)
return jsonify(org_view(org, teams))
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] = {'username': member.user.username,
'is_robot': member.user.robot,
'teams': []}
members_dict[member.user.username]['teams'].append(member.team.name)
return jsonify({'members': members_dict})
abort(403)
@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 = {'username': member.user.username,
'is_robot': member.user.robot,
'teams': []}
member_dict['teams'].append(member.team.name)
if not member_dict:
abort(404)
return jsonify({'member': member_dict})
abort(403)
@api.route('/organization/<orgname>/private', methods=['GET'])
@api_login_required
@internal_api_call
def get_organization_private_allowed(orgname):
permission = CreateRepositoryPermission(orgname)
if permission.can():
organization = model.get_organization(orgname)
private_repos = model.get_private_repo_count(organization.username)
if organization.stripe_id:
cus = stripe.Customer.retrieve(organization.stripe_id)
if cus.subscription:
repos_allowed = 0
plan = get_plan(cus.subscription.plan.id)
if plan:
repos_allowed = plan['privateRepos']
return jsonify({
'privateAllowed': (private_repos < repos_allowed)
})
return jsonify({
'privateAllowed': False
})
abort(403)
def member_view(member):
return {
'username': member.username,
'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:
abort(400)
# Add the user to the team.
model.add_user_to_team(user, team)
log_action('org_add_team_member', orgname,
{'member': membername, 'team': teamname})
return jsonify(member_view(user))
abort(403)
@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 make_response('Repository already exists', 400)
visibility = req['visibility']
repo = model.create_repository(namespace_name, repository_name, owner,
visibility)
repo.description = req['description']
repo.save()
log_action('create_repo', namespace_name,
{'repo': repository_name, 'namespace': namespace_name},
repo=repo)
return jsonify({
'namespace': namespace_name,
'name': repository_name
})
abort(403)
@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',
}
limit = request.args.get('limit', None)
namespace_filter = request.args.get('namespace', None)
include_public = request.args.get('public', 'true')
include_private = request.args.get('private', 'true')
sort = request.args.get('sort', 'false')
try:
limit = int(limit) if limit else None
except TypeError:
limit = None
include_public = include_public == 'true'
include_private = include_private == 'true'
sort = sort == 'true'
username = None
if current_user.is_authenticated() and include_private:
username = current_user.db_user().username
repo_query = model.get_visible_repositories(username, limit=limit,
include_public=include_public,
sort=sort,
namespace=namespace_filter)
repos = [repo_view(repo) for repo in repo_query]
response = {
'repositories': repos
}
return jsonify(response)
@api.route('/repository/<path: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/<path: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/<path:repository>', methods=['DELETE'])
@api_login_required
@parse_repository_name
def delete_repository(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
model.purge_repository(namespace, repository)
registry.delete_repository_storage(namespace, repository)
log_action('delete_repo', namespace,
{'repo': repository, 'namespace': namespace})
return make_response('Deleted', 204)
abort(403)
def image_view(image):
return {
'id': image.docker_image_id,
'created': image.created,
'comment': image.comment,
'ancestors': image.ancestors,
'dbid': image.id,
}
@api.route('/repository/<path:repository>', methods=['GET'])
@parse_repository_name
def get_repo(namespace, repository):
logger.debug('Get repo: %s/%s' % (namespace, repository))
def tag_view(tag):
image = model.get_tag_image(namespace, repository, tag.name)
if not image:
return {}
return {
'name': tag.name,
'image': image_view(image),
}
organization = None
try:
organization = model.get_organization(namespace)
except model.InvalidOrganizationException:
pass
permission = ReadRepositoryPermission(namespace, repository)
is_public = model.repository_is_public(namespace, repository)
if permission.can() or is_public:
repo = model.get_repository(namespace, repository)
if repo:
tags = model.list_repository_tags(namespace, repository)
tag_dict = {tag.name: tag_view(tag) for tag in tags}
can_write = ModifyRepositoryPermission(namespace, repository).can()
can_admin = AdministerRepositoryPermission(namespace, repository).can()
active_builds = model.list_repository_builds(namespace, repository,
include_inactive=False)
return jsonify({
'namespace': namespace,
'name': repository,
'description': repo.description,
'tags': tag_dict,
'can_write': can_write,
'can_admin': can_admin,
'is_public': is_public,
'is_building': len(active_builds) > 0,
'is_organization': bool(organization)
})
abort(404) # Not found
abort(403) # Permission denied
@api.route('/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
@api.route('/repository/<path:repository>/build/', methods=['POST'])
@api_login_required
@parse_repository_name
def request_repo_build(namespace, repository):
permission = ModifyRepositoryPermission(namespace, repository)
if permission.can():
logger.debug('User requested repository initialization.')
dockerfile_id = request.get_json()['file_id']
repo = model.get_repository(namespace, repository)
token = model.create_access_token(repo, 'write')
host = urlparse.urlparse(request.url).netloc
tag = '%s/%s/%s' % (host, repo.namespace, repo.name)
build_request = model.create_repository_build(repo, token, dockerfile_id,
tag)
dockerfile_build_queue.put(json.dumps({'build_id': build_request.id}))
log_action('build_dockerfile', namespace,
{'repo': repository, 'namespace': namespace,
'fileid': dockerfile_id}, repo=repo)
resp = jsonify({
'started': True
})
resp.status_code = 201
return resp
abort(403) # Permissions denied
def webhook_view(webhook):
return {
'public_id': webhook.public_id,
'parameters': json.loads(webhook.parameters),
}
@api.route('/repository/<path:repository>/webhook/', methods=['POST'])
@api_login_required
@parse_repository_name
def create_webhook(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
repo = model.get_repository(namespace, repository)
webhook = model.create_webhook(repo, request.get_json())
resp = jsonify(webhook_view(webhook))
repo_string = '%s/%s' % (namespace, repository)
resp.headers['Location'] = url_for('get_webhook', repository=repo_string,
public_id=webhook.public_id)
log_action('add_repo_webhook', namespace,
{'repo': repository, 'webhook_id': webhook.public_id},
repo=repo)
return resp
abort(403) # Permissions denied
@api.route('/repository/<path:repository>/webhook/<public_id>',
methods=['GET'])
@api_login_required
@parse_repository_name
def get_webhook(namespace, repository, public_id):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
webhook = model.get_webhook(namespace, repository, public_id)
return jsonify(webhook_view(webhook))
abort(403) # Permission denied
@api.route('/repository/<path:repository>/webhook/', methods=['GET'])
@api_login_required
@parse_repository_name
def list_webhooks(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
webhooks = model.list_webhooks(namespace, repository)
return jsonify({
'webhooks': [webhook_view(webhook) for webhook in webhooks]
})
abort(403) # Permission denied
@api.route('/repository/<path:repository>/webhook/<public_id>',
methods=['DELETE'])
@api_login_required
@parse_repository_name
def delete_webhook(namespace, repository, public_id):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
model.delete_webhook(namespace, repository, public_id)
log_action('delete_repo_webhook', namespace,
{'repo': repository, 'webhook_id': public_id},
repo=model.get_repository(namespace, repository))
return make_response('No Content', 204)
abort(403) # Permission denied
@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/<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)
@api.route('/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)
@api.route('/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)
@api.route('/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
@api.route('/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
@api.route('/repository/<path:repository>/permissions/user/',
methods=['GET'])
@api_login_required
@parse_repository_name
def list_repo_user_permissions(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
# Lookup the organization (if any).
org = None
try:
org = model.get_organization(namespace) # Will raise an error if not org
except model.InvalidOrganizationException:
# This repository isn't under an org
pass
# Determine how to wrap the role(s).
def wrapped_role_view(repo_perm):
return wrap_role_view_user(role_view(repo_perm), repo_perm.user)
role_view_func = wrapped_role_view
if org:
org_members = model.get_organization_member_set(namespace)
current_func = role_view_func
def wrapped_role_org_view(repo_perm):
return wrap_role_view_org(current_func(repo_perm), repo_perm.user,
org_members)
role_view_func = wrapped_role_org_view
# Load and return the permissions.
repo_perms = model.get_all_repo_users(namespace, repository)
return jsonify({
'permissions': {perm.user.username: role_view_func(perm)
for perm in repo_perms}
})
abort(403) # Permission denied
@api.route('/repository/<path:repository>/permissions/user/<username>',
methods=['GET'])
@api_login_required
@parse_repository_name
def get_user_permissions(namespace, repository, username):
logger.debug('Get repo: %s/%s permissions for user %s' %
(namespace, repository, username))
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
perm = model.get_user_reponame_permission(username, namespace, repository)
perm_view = wrap_role_view_user(role_view(perm), perm.user)
try:
model.get_organization(namespace)
org_members = model.get_organization_member_set(namespace)
perm_view = wrap_role_view_org(perm_view, perm.user, org_members)
except model.InvalidOrganizationException:
# This repository is not part of an organization
pass
return jsonify(perm_view)
abort(403) # Permission denied
@api.route('/repository/<path:repository>/permissions/team/<teamname>',
methods=['GET'])
@api_login_required
@parse_repository_name
def get_team_permissions(namespace, repository, teamname):
logger.debug('Get repo: %s/%s permissions for team %s' %
(namespace, repository, teamname))
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
perm = model.get_team_reponame_permission(teamname, namespace, repository)
return jsonify(role_view(perm))
abort(403) # Permission denied
@api.route('/repository/<path:repository>/permissions/user/<username>',
methods=['PUT', 'POST'])
@api_login_required
@parse_repository_name
def change_user_permissions(namespace, repository, username):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
new_permission = request.get_json()
logger.debug('Setting permission to: %s for user %s' %
(new_permission['role'], username))
perm = model.set_user_repo_permission(username, namespace, repository,
new_permission['role'])
perm_view = wrap_role_view_user(role_view(perm), perm.user)
try:
model.get_organization(namespace)
org_members = model.get_organization_member_set(namespace)
perm_view = wrap_role_view_org(perm_view, perm.user, org_members)
except model.InvalidOrganizationException:
# This repository is not part of an organization
pass
except model.DataModelException as ex:
error_resp = jsonify({
'message': ex.message,
})
error_resp.status_code = 400
return error_resp
log_action('change_repo_permission', namespace,
{'username': username, 'repo': repository,
'role': new_permission['role']},
repo=model.get_repository(namespace, repository))
resp = jsonify(perm_view)
if request.method == 'POST':
resp.status_code = 201
return resp
abort(403) # Permission denied
@api.route('/repository/<path:repository>/permissions/team/<teamname>',
methods=['PUT', 'POST'])
@api_login_required
@parse_repository_name
def change_team_permissions(namespace, repository, teamname):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
new_permission = request.get_json()
logger.debug('Setting permission to: %s for team %s' %
(new_permission['role'], teamname))
perm = model.set_team_repo_permission(teamname, namespace, repository,
new_permission['role'])
log_action('change_repo_permission', namespace,
{'team': teamname, 'repo': repository,
'role': new_permission['role']},
repo=model.get_repository(namespace, repository))
resp = jsonify(role_view(perm))
if request.method == 'POST':
resp.status_code = 201
return resp
abort(403) # Permission denied
@api.route('/repository/<path:repository>/permissions/user/<username>',
methods=['DELETE'])
@api_login_required
@parse_repository_name
def delete_user_permissions(namespace, repository, username):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
try:
model.delete_user_permission(username, namespace, repository)
except model.DataModelException as ex:
error_resp = jsonify({
'message': ex.message,
})
error_resp.status_code = 400
return error_resp
log_action('delete_repo_permission', namespace,
{'username': username, 'repo': repository},
repo=model.get_repository(namespace, repository))
return make_response('Deleted', 204)
abort(403) # Permission denied
@api.route('/repository/<path:repository>/permissions/team/<teamname>',
methods=['DELETE'])
@api_login_required
@parse_repository_name
def delete_team_permissions(namespace, repository, teamname):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
model.delete_team_permission(teamname, namespace, repository)
log_action('delete_repo_permission', namespace,
{'team': teamname, 'repo': repository},
repo=model.get_repository(namespace, repository))
return make_response('Deleted', 204)
abort(403) # Permission denied
def token_view(token_obj):
return {
'friendlyName': token_obj.friendly_name,
'code': token_obj.code,
'role': token_obj.role.name,
}
@api.route('/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
@api.route('/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
@api.route('/repository/<path:repository>/tokens/', methods=['POST'])
@api_login_required
@parse_repository_name
def create_token(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
token_params = request.get_json()
token = model.create_delegate_token(namespace, repository,
token_params['friendlyName'])
log_action('add_repo_accesstoken', namespace,
{'repo': repository, 'token': token_params['friendlyName']},
repo = model.get_repository(namespace, repository))
resp = jsonify(token_view(token))
resp.status_code = 201
return resp
abort(403) # Permission denied
@api.route('/repository/<path:repository>/tokens/<code>', methods=['PUT'])
@api_login_required
@parse_repository_name
def change_token(namespace, repository, code):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
new_permission = request.get_json()
logger.debug('Setting permission to: %s for code %s' %
(new_permission['role'], code))
token = model.set_repo_delegate_token_role(namespace, repository, code,
new_permission['role'])
log_action('change_repo_permission', namespace,
{'repo': repository, 'token': token.friendly_name, 'code': code,
'role': new_permission['role']},
repo = model.get_repository(namespace, repository))
resp = jsonify(token_view(token))
return resp
abort(403) # Permission denied
@api.route('/repository/<path:repository>/tokens/<code>',
methods=['DELETE'])
@api_login_required
@parse_repository_name
def delete_token(namespace, repository, code):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
token = model.delete_delegate_token(namespace, repository, code)
log_action('delete_repo_accesstoken', namespace,
{'repo': repository, 'token': token.friendly_name,
'code': code},
repo = model.get_repository(namespace, repository))
return make_response('Deleted', 204)
abort(403) # Permission denied
def subscription_view(stripe_subscription, used_repos):
return {
'currentPeriodStart': stripe_subscription.current_period_start,
'currentPeriodEnd': stripe_subscription.current_period_end,
'plan': stripe_subscription.plan.id,
'usedPrivateRepos': used_repos,
}
@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)
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)
abort(400)
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('No Content', 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('No Content', 204)
abort(403)
def log_view(log):
view = {
'kind': log.kind.name,
'metadata': json.loads(log.metadata_json),
'ip': log.ip,
'datetime': log.datetime,
}
if log.performer:
view['performer'] = {
'username': log.performer.username,
'is_robot': log.performer.robot,
}
return view
@api.route('/repository/<path: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]
})