Merge branch 'master' into contact
Conflicts: static/js/controllers.js templates/base.html
This commit is contained in:
commit
82c4c8a28b
78 changed files with 5071 additions and 1953 deletions
408
endpoints/api.py
408
endpoints/api.py
|
@ -12,7 +12,7 @@ from collections import defaultdict
|
|||
|
||||
from data import model
|
||||
from data.queue import dockerfile_build_queue
|
||||
from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan
|
||||
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
|
||||
|
@ -25,20 +25,53 @@ from auth.permissions import (ReadRepositoryPermission,
|
|||
AdministerOrganizationPermission,
|
||||
OrganizationMemberPermission,
|
||||
ViewTeamPermission)
|
||||
from endpoints import registry
|
||||
from endpoints.web import common_login
|
||||
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
|
||||
|
||||
def get_route_data():
|
||||
global route_data
|
||||
if route_data:
|
||||
return route_data
|
||||
|
||||
routes = []
|
||||
for rule in app.url_map.iter_rules():
|
||||
if rule.rule.startswith('/api/'):
|
||||
endpoint_method = globals()[rule.endpoint]
|
||||
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,
|
||||
'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)
|
||||
model.log_action(kind, user_or_orgname, performer=performer,
|
||||
ip=request.remote_addr, metadata=metadata, repository=repo)
|
||||
|
||||
|
||||
def api_login_required(f):
|
||||
@wraps(f)
|
||||
|
@ -58,26 +91,51 @@ def api_login_required(f):
|
|||
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
|
||||
|
||||
@app.errorhandler(model.DataModelException)
|
||||
def handle_dme(ex):
|
||||
return make_response(ex.message, 400)
|
||||
|
||||
|
||||
@app.errorhandler(KeyError)
|
||||
def handle_dme(ex):
|
||||
def handle_dme_key_error(ex):
|
||||
return make_response(ex.message, 400)
|
||||
|
||||
|
||||
@app.route('/api/discovery')
|
||||
def discovery():
|
||||
return jsonify(get_route_data())
|
||||
|
||||
|
||||
@app.route('/api/')
|
||||
@internal_api_call
|
||||
def welcome():
|
||||
return make_response('welcome', 200)
|
||||
|
||||
|
||||
@app.route('/api/plans/')
|
||||
def plans_list():
|
||||
def list_plans():
|
||||
return jsonify({
|
||||
'user': USER_PLANS,
|
||||
'business': BUSINESS_PLANS,
|
||||
'plans': PLANS,
|
||||
})
|
||||
|
||||
|
||||
|
@ -93,6 +151,14 @@ def user_view(user):
|
|||
|
||||
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,
|
||||
|
@ -101,12 +167,14 @@ def user_view(user):
|
|||
'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
|
||||
}
|
||||
|
||||
|
||||
@app.route('/api/user/', methods=['GET'])
|
||||
@internal_api_call
|
||||
def get_logged_in_user():
|
||||
if current_user.is_anonymous():
|
||||
return jsonify({'anonymous': True})
|
||||
|
@ -118,8 +186,30 @@ def get_logged_in_user():
|
|||
return jsonify(user_view(user))
|
||||
|
||||
|
||||
@app.route('/api/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
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/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()
|
||||
|
@ -144,7 +234,7 @@ def convert_user_to_organization():
|
|||
|
||||
# Subscribe the organization to the new plan.
|
||||
plan = convert_data['plan']
|
||||
subscribe(user, plan, None, BUSINESS_PLANS)
|
||||
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))
|
||||
|
@ -152,11 +242,11 @@ def convert_user_to_organization():
|
|||
|
||||
# And finally login with the admin credentials.
|
||||
return conduct_signin(admin_username, admin_password)
|
||||
|
||||
|
||||
|
||||
@app.route('/api/user/', methods=['PUT'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
def change_user_details():
|
||||
user = current_user.db_user()
|
||||
|
||||
|
@ -183,7 +273,8 @@ def change_user_details():
|
|||
|
||||
|
||||
@app.route('/api/user/', methods=['POST'])
|
||||
def create_user_api():
|
||||
@internal_api_call
|
||||
def create_new_user():
|
||||
user_data = request.get_json()
|
||||
|
||||
existing_user = model.get_user(user_data['username'])
|
||||
|
@ -209,7 +300,8 @@ def create_user_api():
|
|||
|
||||
|
||||
@app.route('/api/signin', methods=['POST'])
|
||||
def signin_api():
|
||||
@internal_api_call
|
||||
def signin_user():
|
||||
signin_data = request.get_json()
|
||||
|
||||
username = signin_data['username']
|
||||
|
@ -243,6 +335,7 @@ def conduct_signin(username, password):
|
|||
|
||||
@app.route("/api/signout", methods=['POST'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
def logout():
|
||||
logout_user()
|
||||
identity_changed.send(app, identity=AnonymousIdentity())
|
||||
|
@ -250,7 +343,8 @@ def logout():
|
|||
|
||||
|
||||
@app.route("/api/recovery", methods=['POST'])
|
||||
def send_recovery():
|
||||
@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)
|
||||
|
@ -272,9 +366,10 @@ def get_matching_users(prefix):
|
|||
def get_matching_entities(prefix):
|
||||
teams = []
|
||||
|
||||
namespace_name = request.args.get('namespace', None)
|
||||
namespace_name = request.args.get('namespace', '')
|
||||
robot_namespace = None
|
||||
organization = None
|
||||
|
||||
try:
|
||||
organization = model.get_organization(namespace_name)
|
||||
|
||||
|
@ -308,7 +403,7 @@ def get_matching_entities(prefix):
|
|||
'is_robot': user.is_robot,
|
||||
}
|
||||
|
||||
if user.is_org_member is not None:
|
||||
if organization is not None:
|
||||
user_json['is_org_member'] = user.is_robot or user.is_org_member
|
||||
|
||||
return user_json
|
||||
|
@ -334,7 +429,8 @@ def team_view(orgname, team):
|
|||
|
||||
@app.route('/api/organization/', methods=['POST'])
|
||||
@api_login_required
|
||||
def create_organization_api():
|
||||
@internal_api_call
|
||||
def create_organization():
|
||||
org_data = request.get_json()
|
||||
existing = None
|
||||
|
||||
|
@ -398,6 +494,7 @@ def get_organization(orgname):
|
|||
|
||||
@app.route('/api/organization/<orgname>', methods=['PUT'])
|
||||
@api_login_required
|
||||
@org_api_call('change_user_details')
|
||||
def change_organization_details(orgname):
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
|
@ -406,7 +503,7 @@ def change_organization_details(orgname):
|
|||
except model.InvalidOrganizationException:
|
||||
abort(404)
|
||||
|
||||
org_data = request.get_json();
|
||||
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'])
|
||||
|
@ -456,7 +553,7 @@ def get_organization_member(orgname, membername):
|
|||
abort(404)
|
||||
|
||||
member_dict = None
|
||||
member_teams = model.get_organization_members_with_teams(org, membername = membername)
|
||||
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,
|
||||
|
@ -475,6 +572,7 @@ def get_organization_member(orgname, membername):
|
|||
|
||||
@app.route('/api/organization/<orgname>/private', methods=['GET'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
def get_organization_private_allowed(orgname):
|
||||
permission = CreateRepositoryPermission(orgname)
|
||||
if permission.can():
|
||||
|
@ -484,7 +582,11 @@ def get_organization_private_allowed(orgname):
|
|||
if organization.stripe_id:
|
||||
cus = stripe.Customer.retrieve(organization.stripe_id)
|
||||
if cus.subscription:
|
||||
repos_allowed = get_plan(cus.subscription.plan.id)
|
||||
repos_allowed = 0
|
||||
plan = get_plan(cus.subscription.plan.id)
|
||||
if plan:
|
||||
repos_allowed = plan['privateRepos']
|
||||
|
||||
return jsonify({
|
||||
'privateAllowed': (private_repos < repos_allowed)
|
||||
})
|
||||
|
@ -526,17 +628,20 @@ def update_organization_team(orgname, teamname):
|
|||
log_action('org_create_team', orgname, {'team': teamname})
|
||||
|
||||
if is_existing:
|
||||
if 'description' in details and team.description != details['description']:
|
||||
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})
|
||||
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']})
|
||||
log_action('org_set_team_role', orgname,
|
||||
{'team': teamname, 'role': details['role']})
|
||||
|
||||
resp = jsonify(team_view(orgname, team))
|
||||
if not is_existing:
|
||||
|
@ -604,7 +709,8 @@ def update_organization_team_member(orgname, teamname, membername):
|
|||
|
||||
# Add the user to the team.
|
||||
model.add_user_to_team(user, team)
|
||||
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
|
||||
log_action('org_add_team_member', orgname,
|
||||
{'member': membername, 'team': teamname})
|
||||
return jsonify(member_view(user))
|
||||
|
||||
abort(403)
|
||||
|
@ -619,7 +725,8 @@ def delete_organization_team_member(orgname, teamname, membername):
|
|||
# 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})
|
||||
log_action('org_remove_team_member', orgname,
|
||||
{'member': membername, 'team': teamname})
|
||||
return make_response('Deleted', 204)
|
||||
|
||||
abort(403)
|
||||
|
@ -627,7 +734,7 @@ def delete_organization_team_member(orgname, teamname, membername):
|
|||
|
||||
@app.route('/api/repository', methods=['POST'])
|
||||
@api_login_required
|
||||
def create_repo_api():
|
||||
def create_repo():
|
||||
owner = current_user.db_user()
|
||||
req = request.get_json()
|
||||
namespace_name = req['namespace'] if 'namespace' in req else owner.username
|
||||
|
@ -648,7 +755,9 @@ def create_repo_api():
|
|||
repo.description = req['description']
|
||||
repo.save()
|
||||
|
||||
log_action('create_repo', namespace_name, {'repo': repository_name, 'namespace': namespace_name}, repo=repo)
|
||||
log_action('create_repo', namespace_name,
|
||||
{'repo': repository_name, 'namespace': namespace_name},
|
||||
repo=repo)
|
||||
return jsonify({
|
||||
'namespace': namespace_name,
|
||||
'name': repository_name
|
||||
|
@ -658,7 +767,7 @@ def create_repo_api():
|
|||
|
||||
|
||||
@app.route('/api/find/repository', methods=['GET'])
|
||||
def match_repos_api():
|
||||
def find_repos():
|
||||
prefix = request.args.get('query', '')
|
||||
|
||||
def repo_view(repo):
|
||||
|
@ -681,7 +790,7 @@ def match_repos_api():
|
|||
|
||||
|
||||
@app.route('/api/repository/', methods=['GET'])
|
||||
def list_repos_api():
|
||||
def list_repos():
|
||||
def repo_view(repo_obj):
|
||||
return {
|
||||
'namespace': repo_obj.namespace,
|
||||
|
@ -690,11 +799,13 @@ def list_repos_api():
|
|||
'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 = request.args.get('public', 'true')
|
||||
include_private = request.args.get('private', 'true')
|
||||
sort = request.args.get('sort', 'false')
|
||||
include_count = request.args.get('count', 'false')
|
||||
|
||||
try:
|
||||
limit = int(limit) if limit else None
|
||||
|
@ -703,28 +814,45 @@ def list_repos_api():
|
|||
|
||||
include_public = include_public == 'true'
|
||||
include_private = include_private == 'true'
|
||||
include_count = include_count == 'true'
|
||||
sort = sort == 'true'
|
||||
if page:
|
||||
try:
|
||||
page = int(page)
|
||||
except:
|
||||
page = None
|
||||
|
||||
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,
|
||||
repo_count = None
|
||||
if include_count:
|
||||
repo_count = model.get_visible_repository_count(username,
|
||||
include_public=include_public,
|
||||
sort=sort,
|
||||
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)
|
||||
|
||||
|
||||
@app.route('/api/repository/<path:repository>', methods=['PUT'])
|
||||
@api_login_required
|
||||
@parse_repository_name
|
||||
def update_repo_api(namespace, repository):
|
||||
def update_repo(namespace, repository):
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
repo = model.get_repository(namespace, repository)
|
||||
|
@ -733,7 +861,8 @@ def update_repo_api(namespace, repository):
|
|||
repo.description = values['description']
|
||||
repo.save()
|
||||
|
||||
log_action('set_repo_description', namespace, {'repo': repository, 'description': values['description']},
|
||||
log_action('set_repo_description', namespace,
|
||||
{'repo': repository, 'description': values['description']},
|
||||
repo=repo)
|
||||
return jsonify({
|
||||
'success': True
|
||||
|
@ -746,14 +875,15 @@ def update_repo_api(namespace, repository):
|
|||
methods=['POST'])
|
||||
@api_login_required
|
||||
@parse_repository_name
|
||||
def change_repo_visibility_api(namespace, repository):
|
||||
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']},
|
||||
log_action('change_repo_visibility', namespace,
|
||||
{'repo': repository, 'visibility': values['visibility']},
|
||||
repo=repo)
|
||||
return jsonify({
|
||||
'success': True
|
||||
|
@ -769,8 +899,8 @@ 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})
|
||||
log_action('delete_repo', namespace,
|
||||
{'repo': repository, 'namespace': namespace})
|
||||
return make_response('Deleted', 204)
|
||||
|
||||
abort(403)
|
||||
|
@ -781,14 +911,16 @@ def image_view(image):
|
|||
'id': image.docker_image_id,
|
||||
'created': image.created,
|
||||
'comment': image.comment,
|
||||
'command': json.loads(image.command) if image.command else None,
|
||||
'ancestors': image.ancestors,
|
||||
'dbid': image.id,
|
||||
'size': image.image_size,
|
||||
}
|
||||
|
||||
|
||||
@app.route('/api/repository/<path:repository>', methods=['GET'])
|
||||
@parse_repository_name
|
||||
def get_repo_api(namespace, repository):
|
||||
def get_repo(namespace, repository):
|
||||
logger.debug('Get repo: %s/%s' % (namespace, repository))
|
||||
|
||||
def tag_view(tag):
|
||||
|
@ -849,15 +981,15 @@ def get_repo_builds(namespace, repository):
|
|||
return node_status
|
||||
|
||||
# If there was no status url, do the best we can
|
||||
# The format of this block should mirror that of the buildserver.
|
||||
return {
|
||||
'id': build_obj.id,
|
||||
'total_commands': None,
|
||||
'total_images': None,
|
||||
'current_command': None,
|
||||
'current_image': None,
|
||||
'image_completion_percent': None,
|
||||
'push_completion': 0.0,
|
||||
'status': build_obj.phase,
|
||||
'message': None,
|
||||
'image_completion': {},
|
||||
}
|
||||
|
||||
builds = model.list_repository_builds(namespace, repository)
|
||||
|
@ -887,7 +1019,8 @@ def request_repo_build(namespace, repository):
|
|||
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)
|
||||
{'repo': repository, 'namespace': namespace,
|
||||
'fileid': dockerfile_id}, repo=repo)
|
||||
|
||||
resp = jsonify({
|
||||
'started': True
|
||||
|
@ -918,7 +1051,8 @@ def create_webhook(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)
|
||||
{'repo': repository, 'webhook_id': webhook.public_id},
|
||||
repo=repo)
|
||||
return resp
|
||||
|
||||
abort(403) # Permissions denied
|
||||
|
@ -969,6 +1103,7 @@ def delete_webhook(namespace, repository, public_id):
|
|||
|
||||
@app.route('/api/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)
|
||||
|
@ -1051,6 +1186,24 @@ def get_image_changes(namespace, repository, image_id):
|
|||
abort(403)
|
||||
|
||||
|
||||
@app.route('/api/repository/<path: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_and_images(namespace, repository, tag)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@app.route('/api/repository/<path:repository>/tag/<tag>/images',
|
||||
methods=['GET'])
|
||||
@parse_repository_name
|
||||
|
@ -1118,7 +1271,8 @@ def list_repo_user_permissions(namespace, repository):
|
|||
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)
|
||||
return wrap_role_view_org(current_func(repo_perm), repo_perm.user,
|
||||
org_members)
|
||||
|
||||
role_view_func = wrapped_role_org_view
|
||||
|
||||
|
@ -1203,7 +1357,8 @@ def change_user_permissions(namespace, repository, username):
|
|||
return error_resp
|
||||
|
||||
log_action('change_repo_permission', namespace,
|
||||
{'username': username, 'repo': repository, 'role': new_permission['role']},
|
||||
{'username': username, 'repo': repository,
|
||||
'role': new_permission['role']},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
resp = jsonify(perm_view)
|
||||
|
@ -1230,7 +1385,8 @@ def change_team_permissions(namespace, repository, teamname):
|
|||
new_permission['role'])
|
||||
|
||||
log_action('change_repo_permission', namespace,
|
||||
{'team': teamname, 'repo': repository, 'role': new_permission['role']},
|
||||
{'team': teamname, 'repo': repository,
|
||||
'role': new_permission['role']},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
resp = jsonify(role_view(perm))
|
||||
|
@ -1257,7 +1413,8 @@ def delete_user_permissions(namespace, repository, username):
|
|||
error_resp.status_code = 400
|
||||
return error_resp
|
||||
|
||||
log_action('delete_repo_permission', namespace, {'username': username, 'repo': repository},
|
||||
log_action('delete_repo_permission', namespace,
|
||||
{'username': username, 'repo': repository},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
return make_response('Deleted', 204)
|
||||
|
@ -1274,7 +1431,8 @@ def delete_team_permissions(namespace, repository, teamname):
|
|||
if permission.can():
|
||||
model.delete_team_permission(teamname, namespace, repository)
|
||||
|
||||
log_action('delete_repo_permission', namespace, {'team': teamname, 'repo': repository},
|
||||
log_action('delete_repo_permission', namespace,
|
||||
{'team': teamname, 'repo': repository},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
return make_response('Deleted', 204)
|
||||
|
@ -1328,7 +1486,8 @@ def create_token(namespace, repository):
|
|||
token = model.create_delegate_token(namespace, repository,
|
||||
token_params['friendlyName'])
|
||||
|
||||
log_action('add_repo_accesstoken', namespace, {'repo': repository, 'token': 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))
|
||||
|
@ -1353,7 +1512,8 @@ def change_token(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': repository, 'token': token.friendly_name, 'code': code,
|
||||
'role': new_permission['role']},
|
||||
repo = model.get_repository(namespace, repository))
|
||||
|
||||
resp = jsonify(token_view(token))
|
||||
|
@ -1372,7 +1532,8 @@ def delete_token(namespace, repository, code):
|
|||
token = model.delete_delegate_token(namespace, repository, code)
|
||||
|
||||
log_action('delete_repo_accesstoken', namespace,
|
||||
{'repo': repository, 'token': token.friendly_name, 'code': code},
|
||||
{'repo': repository, 'token': token.friendly_name,
|
||||
'code': code},
|
||||
repo = model.get_repository(namespace, repository))
|
||||
|
||||
return make_response('Deleted', 204)
|
||||
|
@ -1391,14 +1552,17 @@ def subscription_view(stripe_subscription, used_repos):
|
|||
|
||||
@app.route('/api/user/card', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_user_card_api():
|
||||
@internal_api_call
|
||||
def get_user_card():
|
||||
user = current_user.db_user()
|
||||
return get_card(user)
|
||||
|
||||
|
||||
@app.route('/api/organization/<orgname>/card', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_org_card_api(orgname):
|
||||
@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)
|
||||
|
@ -1409,7 +1573,8 @@ def get_org_card_api(orgname):
|
|||
|
||||
@app.route('/api/user/card', methods=['POST'])
|
||||
@api_login_required
|
||||
def set_user_card_api():
|
||||
@internal_api_call
|
||||
def set_user_card():
|
||||
user = current_user.db_user()
|
||||
token = request.get_json()['token']
|
||||
response = set_card(user, token)
|
||||
|
@ -1419,7 +1584,8 @@ def set_user_card_api():
|
|||
|
||||
@app.route('/api/organization/<orgname>/card', methods=['POST'])
|
||||
@api_login_required
|
||||
def set_org_card_api(orgname):
|
||||
@org_api_call('set_user_card')
|
||||
def set_org_card(orgname):
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
organization = model.get_organization(orgname)
|
||||
|
@ -1461,21 +1627,22 @@ def get_card(user):
|
|||
|
||||
if default_card:
|
||||
card_info = {
|
||||
'owner': card.name,
|
||||
'type': card.type,
|
||||
'last4': card.last4
|
||||
'owner': default_card.name,
|
||||
'type': default_card.type,
|
||||
'last4': default_card.last4
|
||||
}
|
||||
|
||||
return jsonify({'card': card_info})
|
||||
|
||||
@app.route('/api/user/plan', methods=['PUT'])
|
||||
@api_login_required
|
||||
def subscribe_api():
|
||||
@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, USER_PLANS)
|
||||
return subscribe(user, plan, token, False) # Business features not required
|
||||
|
||||
|
||||
def carderror_response(e):
|
||||
|
@ -1486,15 +1653,22 @@ def carderror_response(e):
|
|||
return resp
|
||||
|
||||
|
||||
def subscribe(user, plan, token, accepted_plans):
|
||||
def subscribe(user, plan, token, require_business_plan):
|
||||
plan_found = None
|
||||
for plan_obj in accepted_plans:
|
||||
for plan_obj in PLANS:
|
||||
if plan_obj['stripeId'] == plan:
|
||||
plan_found = plan_obj
|
||||
|
||||
if not plan_found:
|
||||
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
|
||||
|
@ -1553,9 +1727,32 @@ def subscribe(user, plan, token, accepted_plans):
|
|||
return resp
|
||||
|
||||
|
||||
@app.route('/api/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)
|
||||
|
||||
|
||||
@app.route('/api/organization/<orgname>/invoices', methods=['GET'])
|
||||
@api_login_required
|
||||
def org_invoices_api(orgname):
|
||||
@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,
|
||||
|
@ -1571,37 +1768,32 @@ def org_invoices_api(orgname):
|
|||
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
|
||||
}
|
||||
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
organization = model.get_organization(orgname)
|
||||
if not organization.stripe_id:
|
||||
abort(404)
|
||||
|
||||
invoices = stripe.Invoice.all(customer=organization.stripe_id, count=12)
|
||||
return jsonify({
|
||||
'invoices': [invoice_view(i) for i in invoices.data]
|
||||
})
|
||||
|
||||
abort(403)
|
||||
invoices = stripe.Invoice.all(customer=customer_id, count=12)
|
||||
return jsonify({
|
||||
'invoices': [invoice_view(i) for i in invoices.data]
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/organization/<orgname>/plan', methods=['PUT'])
|
||||
@api_login_required
|
||||
def subscribe_org_api(orgname):
|
||||
@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, BUSINESS_PLANS)
|
||||
return subscribe(organization, plan, token, True) # Business plan required
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@app.route('/api/user/plan', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_subscription():
|
||||
@internal_api_call
|
||||
def get_user_subscription():
|
||||
user = current_user.db_user()
|
||||
private_repos = model.get_private_repo_count(user.username)
|
||||
|
||||
|
@ -1619,6 +1811,8 @@ def get_subscription():
|
|||
|
||||
@app.route('/api/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():
|
||||
|
@ -1631,7 +1825,7 @@ def get_org_subscription(orgname):
|
|||
return jsonify(subscription_view(cus.subscription, private_repos))
|
||||
|
||||
return jsonify({
|
||||
'plan': 'bus-free',
|
||||
'plan': 'free',
|
||||
'usedPrivateRepos': private_repos,
|
||||
})
|
||||
|
||||
|
@ -1657,6 +1851,7 @@ def get_user_robots():
|
|||
|
||||
@app.route('/api/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():
|
||||
|
@ -1670,7 +1865,7 @@ def get_org_robots(orgname):
|
|||
|
||||
@app.route('/api/user/robots/<robot_shortname>', methods=['PUT'])
|
||||
@api_login_required
|
||||
def create_robot(robot_shortname):
|
||||
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))
|
||||
|
@ -1682,6 +1877,7 @@ def create_robot(robot_shortname):
|
|||
@app.route('/api/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():
|
||||
|
@ -1697,7 +1893,7 @@ def create_org_robot(orgname, robot_shortname):
|
|||
|
||||
@app.route('/api/user/robots/<robot_shortname>', methods=['DELETE'])
|
||||
@api_login_required
|
||||
def delete_robot(robot_shortname):
|
||||
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})
|
||||
|
@ -1707,6 +1903,7 @@ def delete_robot(robot_shortname):
|
|||
@app.route('/api/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():
|
||||
|
@ -1718,27 +1915,27 @@ def delete_org_robot(orgname, robot_shortname):
|
|||
|
||||
|
||||
def log_view(log):
|
||||
view = {
|
||||
'kind': log.kind.name,
|
||||
'metadata': json.loads(log.metadata_json),
|
||||
'ip': log.ip,
|
||||
'datetime': log.datetime,
|
||||
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,
|
||||
}
|
||||
|
||||
if log.performer:
|
||||
view['performer'] = {
|
||||
'username': log.performer.username,
|
||||
'is_robot': log.performer.robot,
|
||||
}
|
||||
|
||||
return view
|
||||
return view
|
||||
|
||||
|
||||
|
||||
@app.route('/api/repository/<path:repository>/logs', methods=['GET'])
|
||||
@api_login_required
|
||||
@parse_repository_name
|
||||
def repo_logs_api(namespace, repository):
|
||||
def list_repo_logs(namespace, repository):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
repo = model.get_repository(namespace, repository)
|
||||
|
@ -1754,19 +1951,33 @@ def repo_logs_api(namespace, repository):
|
|||
|
||||
@app.route('/api/organization/<orgname>/logs', methods=['GET'])
|
||||
@api_login_required
|
||||
def org_logs_api(orgname):
|
||||
@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)
|
||||
return get_logs(orgname, start_time, end_time,
|
||||
performer_name=performer_name)
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
def get_logs(namespace, start_time, end_time, performer_name=None, repository=None):
|
||||
@app.route('/api/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)
|
||||
|
@ -1790,7 +2001,8 @@ def get_logs(namespace, start_time, end_time, performer_name=None, repository=No
|
|||
if not end_time:
|
||||
end_time = datetime.today()
|
||||
|
||||
logs = model.list_logs(namespace, start_time, end_time, performer = performer, repository=repository)
|
||||
logs = model.list_logs(namespace, start_time, end_time, performer=performer,
|
||||
repository=repository)
|
||||
return jsonify({
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
|
|
48
endpoints/common.py
Normal file
48
endpoints/common.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
import logging
|
||||
|
||||
from flask.ext.login import login_user, UserMixin
|
||||
from flask.ext.principal import identity_changed
|
||||
|
||||
from data import model
|
||||
from app import app, login_manager
|
||||
from auth.permissions import QuayDeferredPermissionUser
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(username):
|
||||
logger.debug('Loading user: %s' % username)
|
||||
return _LoginWrappedDBUser(username)
|
||||
|
||||
class _LoginWrappedDBUser(UserMixin):
|
||||
def __init__(self, db_username, db_user=None):
|
||||
|
||||
self._db_username = db_username
|
||||
self._db_user = db_user
|
||||
|
||||
def db_user(self):
|
||||
if not self._db_user:
|
||||
self._db_user = model.get_user(self._db_username)
|
||||
return self._db_user
|
||||
|
||||
def is_authenticated(self):
|
||||
return self.db_user() is not None
|
||||
|
||||
def is_active(self):
|
||||
return self.db_user().verified
|
||||
|
||||
def get_id(self):
|
||||
return unicode(self._db_username)
|
||||
|
||||
|
||||
def common_login(db_user):
|
||||
if login_user(_LoginWrappedDBUser(db_user.username, db_user)):
|
||||
logger.debug('Successfully signed in as: %s' % db_user.username)
|
||||
new_identity = QuayDeferredPermissionUser(db_user.username, 'username')
|
||||
identity_changed.send(app, identity=new_identity)
|
||||
return True
|
||||
else:
|
||||
logger.debug('User could not be logged in, inactive?.')
|
||||
return False
|
|
@ -63,14 +63,14 @@ def create_user():
|
|||
model.load_token_data(password)
|
||||
return make_response('Verified', 201)
|
||||
except model.InvalidTokenException:
|
||||
abort(401)
|
||||
return make_response('Invalid access token.', 400)
|
||||
|
||||
elif '+' in username:
|
||||
try:
|
||||
model.verify_robot(username, password)
|
||||
return make_response('Verified', 201)
|
||||
except model.InvalidRobotException:
|
||||
abort(401)
|
||||
return make_response('Invalid robot account or password.', 400)
|
||||
|
||||
existing_user = model.get_user(username)
|
||||
if existing_user:
|
||||
|
@ -78,7 +78,7 @@ def create_user():
|
|||
if verified:
|
||||
return make_response('Verified', 201)
|
||||
else:
|
||||
abort(401)
|
||||
return make_response('Invalid password.', 400)
|
||||
else:
|
||||
# New user case
|
||||
new_user = model.create_user(username, password, user_data['email'])
|
||||
|
@ -134,8 +134,8 @@ def create_repository(namespace, repository):
|
|||
repo = model.get_repository(namespace, repository)
|
||||
|
||||
if not repo and get_authenticated_user() is None:
|
||||
logger.debug('Attempt to create new repository with token auth.')
|
||||
abort(400)
|
||||
logger.debug('Attempt to create new repository without user auth.')
|
||||
abort(401)
|
||||
|
||||
elif repo:
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
|
@ -158,10 +158,6 @@ def create_repository(namespace, repository):
|
|||
for existing in model.get_repository_images(namespace, repository):
|
||||
if existing.docker_image_id in new_repo_images:
|
||||
added_images.pop(existing.docker_image_id)
|
||||
else:
|
||||
logger.debug('Deleting existing image with id: %s' %
|
||||
existing.docker_image_id)
|
||||
existing.delete_instance(recursive=True)
|
||||
|
||||
for image_description in added_images.values():
|
||||
model.create_image(image_description['id'], repo)
|
||||
|
|
|
@ -43,6 +43,7 @@ def require_completion(f):
|
|||
def wrapper(namespace, repository, *args, **kwargs):
|
||||
if store.exists(store.image_mark_path(namespace, repository,
|
||||
kwargs['image_id'])):
|
||||
logger.warning('Image is already being uploaded: %s', kwargs['image_id'])
|
||||
abort(400) # 'Image is being uploaded, retry later')
|
||||
return f(namespace, repository, *args, **kwargs)
|
||||
return wrapper
|
||||
|
@ -87,6 +88,7 @@ def get_image_layer(namespace, repository, image_id, headers):
|
|||
try:
|
||||
return Response(store.stream_read(path), headers=headers)
|
||||
except IOError:
|
||||
logger.warning('Image not found: %s', image_id)
|
||||
abort(404) # 'Image not found', 404)
|
||||
|
||||
abort(403)
|
||||
|
@ -124,6 +126,11 @@ def put_image_layer(namespace, repository, image_id):
|
|||
store.stream_write(layer_path, sr)
|
||||
csums.append('sha256:{0}'.format(h.hexdigest()))
|
||||
try:
|
||||
image_size = tmp.tell()
|
||||
|
||||
# Save the size of the image.
|
||||
model.set_image_size(image_id, namespace, repository, image_size)
|
||||
|
||||
tmp.seek(0)
|
||||
csums.append(checksums.compute_tarsum(tmp, json_data))
|
||||
tmp.close()
|
||||
|
@ -141,7 +148,7 @@ def put_image_layer(namespace, repository, image_id):
|
|||
return make_response('true', 200)
|
||||
# We check if the checksums provided matches one the one we computed
|
||||
if checksum not in csums:
|
||||
logger.debug('put_image_layer: Wrong checksum')
|
||||
logger.warning('put_image_layer: Wrong checksum')
|
||||
abort(400) # 'Checksum mismatch, ignoring the layer')
|
||||
# Checksum is ok, we remove the marker
|
||||
store.remove(mark_path)
|
||||
|
@ -168,8 +175,10 @@ def put_image_checksum(namespace, repository, image_id):
|
|||
|
||||
checksum = request.headers.get('X-Docker-Checksum')
|
||||
if not checksum:
|
||||
logger.warning('Missing Image\'s checksum: %s', image_id)
|
||||
abort(400) # 'Missing Image\'s checksum')
|
||||
if not session.get('checksum'):
|
||||
logger.warning('Checksum not found in Cookie for image: %s', image_id)
|
||||
abort(400) # 'Checksum not found in Cookie')
|
||||
if not store.exists(store.image_json_path(namespace, repository, image_id)):
|
||||
abort(404) # 'Image not found', 404)
|
||||
|
@ -287,8 +296,11 @@ def put_image_json(namespace, repository, image_id):
|
|||
except json.JSONDecodeError:
|
||||
pass
|
||||
if not data or not isinstance(data, dict):
|
||||
logger.warning('Invalid JSON for image: %s json: %s', image_id,
|
||||
request.data)
|
||||
abort(400) # 'Invalid JSON')
|
||||
if 'id' not in data:
|
||||
logger.warning('Missing key `id\' in JSON for image: %s', image_id)
|
||||
abort(400) # 'Missing key `id\' in JSON')
|
||||
# Read the checksum
|
||||
checksum = request.headers.get('X-Docker-Checksum')
|
||||
|
@ -301,11 +313,14 @@ def put_image_json(namespace, repository, image_id):
|
|||
# We cleanup any old checksum in case it's a retry after a fail
|
||||
store.remove(store.image_checksum_path(namespace, repository, image_id))
|
||||
if image_id != data['id']:
|
||||
logger.warning('JSON data contains invalid id for image: %s', image_id)
|
||||
abort(400) # 'JSON data contains invalid id')
|
||||
parent_id = data.get('parent')
|
||||
if parent_id and not store.exists(store.image_json_path(namespace,
|
||||
repository,
|
||||
data['parent'])):
|
||||
logger.warning('Image depends on a non existing parent image: %s',
|
||||
image_id)
|
||||
abort(400) # 'Image depends on a non existing parent')
|
||||
json_path = store.image_json_path(namespace, repository, image_id)
|
||||
mark_path = store.image_mark_path(namespace, repository, image_id)
|
||||
|
@ -319,8 +334,10 @@ def put_image_json(namespace, repository, image_id):
|
|||
else:
|
||||
parent_obj = None
|
||||
|
||||
command_list = data.get('container_config', {}).get('Cmd', None)
|
||||
command = json.dumps(command_list) if command_list else None
|
||||
model.set_image_metadata(image_id, namespace, repository,
|
||||
data.get('created'), data.get('comment'),
|
||||
data.get('created'), data.get('comment'), command,
|
||||
parent_obj)
|
||||
store.put_content(mark_path, 'true')
|
||||
store.put_content(json_path, request.data)
|
||||
|
@ -328,14 +345,6 @@ def put_image_json(namespace, repository, image_id):
|
|||
return make_response('true', 200)
|
||||
|
||||
|
||||
def delete_repository_storage(namespace, repository):
|
||||
""" Caller should have already verified proper permissions. """
|
||||
repository_path = store.repository_namespace_path(namespace, repository)
|
||||
|
||||
logger.debug('Recursively deleting path: %s' % repository_path)
|
||||
store.remove(repository_path)
|
||||
|
||||
|
||||
def process_image_changes(namespace, repository, image_id):
|
||||
logger.debug('Generating diffs for image: %s' % image_id)
|
||||
|
||||
|
|
|
@ -1,19 +1,33 @@
|
|||
import math
|
||||
|
||||
from random import SystemRandom
|
||||
from flask import jsonify, send_file
|
||||
from flask import jsonify
|
||||
from app import app
|
||||
|
||||
|
||||
def generate_image_completion(rand_func):
|
||||
images = {}
|
||||
for image_id in range(rand_func.randint(1, 11)):
|
||||
total = int(math.pow(abs(rand_func.gauss(0, 1000)), 2))
|
||||
current = rand_func.randint(0, total)
|
||||
image_id = 'image_id_%s' % image_id
|
||||
images[image_id] = {
|
||||
'total': total,
|
||||
'current': current,
|
||||
}
|
||||
return images
|
||||
|
||||
|
||||
@app.route('/test/build/status', methods=['GET'])
|
||||
def generate_random_build_status():
|
||||
response = {
|
||||
'id': 1,
|
||||
'total_commands': None,
|
||||
'total_images': None,
|
||||
'current_command': None,
|
||||
'current_image': None,
|
||||
'image_completion_percent': None,
|
||||
'push_completion': 0.0,
|
||||
'status': None,
|
||||
'message': None,
|
||||
'image_completion': {},
|
||||
}
|
||||
|
||||
random = SystemRandom()
|
||||
|
@ -35,9 +49,8 @@ def generate_random_build_status():
|
|||
'pushing': {
|
||||
'total_commands': 7,
|
||||
'current_command': 7,
|
||||
'total_images': 11,
|
||||
'current_image': random.randint(1, 11),
|
||||
'image_completion_percent': random.randint(0, 100),
|
||||
'push_completion': random.random(),
|
||||
'image_completion': generate_image_completion(random),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
151
endpoints/web.py
151
endpoints/web.py
|
@ -4,53 +4,33 @@ import stripe
|
|||
|
||||
from flask import (abort, redirect, request, url_for, render_template,
|
||||
make_response, Response)
|
||||
from flask.ext.login import login_user, UserMixin
|
||||
from flask.ext.principal import identity_changed
|
||||
from flask.ext.login import login_required, current_user
|
||||
from urlparse import urlparse
|
||||
|
||||
from data import model
|
||||
from app import app, login_manager, mixpanel
|
||||
from auth.permissions import (QuayDeferredPermissionUser,
|
||||
AdministerOrganizationPermission)
|
||||
from app import app, mixpanel
|
||||
from auth.permissions import AdministerOrganizationPermission
|
||||
from util.invoice import renderInvoiceToPdf
|
||||
from util.seo import render_snapshot
|
||||
from util.cache import no_cache
|
||||
from endpoints.api import get_route_data
|
||||
from endpoints.common import common_login
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _LoginWrappedDBUser(UserMixin):
|
||||
def __init__(self, db_username, db_user=None):
|
||||
|
||||
self._db_username = db_username
|
||||
self._db_user = db_user
|
||||
|
||||
def db_user(self):
|
||||
if not self._db_user:
|
||||
self._db_user = model.get_user(self._db_username)
|
||||
return self._db_user
|
||||
|
||||
def is_authenticated(self):
|
||||
return self.db_user() is not None
|
||||
|
||||
def is_active(self):
|
||||
return self.db_user().verified
|
||||
|
||||
def get_id(self):
|
||||
return unicode(self._db_username)
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(username):
|
||||
logger.debug('Loading user: %s' % username)
|
||||
return _LoginWrappedDBUser(username)
|
||||
def render_page_template(name, **kwargs):
|
||||
return make_response(render_template(name, route_data=get_route_data(),
|
||||
**kwargs))
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'], defaults={'path': ''})
|
||||
@app.route('/repository/<path:path>', methods=['GET'])
|
||||
@app.route('/organization/<path:path>', methods=['GET'])
|
||||
@no_cache
|
||||
def index(path):
|
||||
return render_template('index.html')
|
||||
return render_page_template('index.html')
|
||||
|
||||
|
||||
@app.route('/snapshot', methods=['GET'])
|
||||
|
@ -67,26 +47,32 @@ def snapshot(path = ''):
|
|||
|
||||
|
||||
@app.route('/plans/')
|
||||
@no_cache
|
||||
def plans():
|
||||
return index('')
|
||||
|
||||
|
||||
@app.route('/guide/')
|
||||
@no_cache
|
||||
def guide():
|
||||
return index('')
|
||||
|
||||
|
||||
@app.route('/organizations/')
|
||||
@app.route('/organizations/new/')
|
||||
@no_cache
|
||||
def organizations():
|
||||
return index('')
|
||||
|
||||
|
||||
@app.route('/user/')
|
||||
@no_cache
|
||||
def user():
|
||||
return index('')
|
||||
|
||||
|
||||
@app.route('/signin/')
|
||||
@no_cache
|
||||
def signin():
|
||||
return index('')
|
||||
|
||||
|
@ -97,76 +83,85 @@ def contact():
|
|||
|
||||
|
||||
@app.route('/new/')
|
||||
@no_cache
|
||||
def new():
|
||||
return index('')
|
||||
|
||||
|
||||
@app.route('/repository/')
|
||||
@no_cache
|
||||
def repository():
|
||||
return index('')
|
||||
|
||||
|
||||
@app.route('/security/')
|
||||
@no_cache
|
||||
def security():
|
||||
return index('')
|
||||
|
||||
|
||||
@app.route('/v1')
|
||||
@app.route('/v1/')
|
||||
@no_cache
|
||||
def v1():
|
||||
return index('')
|
||||
|
||||
|
||||
@app.route('/status', methods=['GET'])
|
||||
@no_cache
|
||||
def status():
|
||||
return make_response('Healthy')
|
||||
|
||||
|
||||
@app.route('/tos', methods=['GET'])
|
||||
@no_cache
|
||||
def tos():
|
||||
return render_template('tos.html')
|
||||
return render_page_template('tos.html')
|
||||
|
||||
|
||||
@app.route('/disclaimer', methods=['GET'])
|
||||
@no_cache
|
||||
def disclaimer():
|
||||
return render_template('disclaimer.html')
|
||||
return render_page_template('disclaimer.html')
|
||||
|
||||
|
||||
@app.route('/privacy', methods=['GET'])
|
||||
@no_cache
|
||||
def privacy():
|
||||
return render_template('privacy.html')
|
||||
return render_page_template('privacy.html')
|
||||
|
||||
|
||||
@app.route('/receipt', methods=['GET'])
|
||||
def receipt():
|
||||
if not current_user.is_authenticated():
|
||||
abort(401)
|
||||
return
|
||||
|
||||
id = request.args.get('id')
|
||||
if id:
|
||||
invoice = stripe.Invoice.retrieve(id)
|
||||
if invoice:
|
||||
org = model.get_user_or_org_by_customer_id(invoice.customer)
|
||||
if org and org.organization:
|
||||
admin_org = AdministerOrganizationPermission(org.username)
|
||||
if admin_org.can():
|
||||
file_data = renderInvoiceToPdf(invoice, org)
|
||||
return Response(file_data,
|
||||
mimetype="application/pdf",
|
||||
headers={"Content-Disposition":
|
||||
"attachment;filename=receipt.pdf"})
|
||||
user_or_org = model.get_user_or_org_by_customer_id(invoice.customer)
|
||||
|
||||
if user_or_org:
|
||||
if user_or_org.organization:
|
||||
admin_org = AdministerOrganizationPermission(user_or_org.username)
|
||||
if not admin_org.can():
|
||||
abort(404)
|
||||
return
|
||||
else:
|
||||
if not user_or_org.username == current_user.db_user().username:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
file_data = renderInvoiceToPdf(invoice, user_or_org)
|
||||
return Response(file_data,
|
||||
mimetype="application/pdf",
|
||||
headers={"Content-Disposition": "attachment;filename=receipt.pdf"})
|
||||
abort(404)
|
||||
|
||||
def common_login(db_user):
|
||||
if login_user(_LoginWrappedDBUser(db_user.username, db_user)):
|
||||
logger.debug('Successfully signed in as: %s' % db_user.username)
|
||||
new_identity = QuayDeferredPermissionUser(db_user.username, 'username')
|
||||
identity_changed.send(app, identity=new_identity)
|
||||
return True
|
||||
else:
|
||||
logger.debug('User could not be logged in, inactive?.')
|
||||
return False
|
||||
|
||||
|
||||
@app.route('/oauth2/github/callback', methods=['GET'])
|
||||
def github_oauth_callback():
|
||||
def exchange_github_code_for_token(code):
|
||||
code = request.args.get('code')
|
||||
payload = {
|
||||
'client_id': app.config['GITHUB_CLIENT_ID'],
|
||||
|
@ -181,19 +176,37 @@ def github_oauth_callback():
|
|||
params=payload, headers=headers)
|
||||
|
||||
token = get_access_token.json()['access_token']
|
||||
return token
|
||||
|
||||
|
||||
def get_github_user(token):
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param)
|
||||
|
||||
user_data = get_user.json()
|
||||
return get_user.json()
|
||||
|
||||
|
||||
@app.route('/oauth2/github/callback', methods=['GET'])
|
||||
def github_oauth_callback():
|
||||
error = request.args.get('error', None)
|
||||
if error:
|
||||
return render_page_template('githuberror.html', error_message=error)
|
||||
|
||||
token = exchange_github_code_for_token(request.args.get('code'))
|
||||
user_data = get_github_user(token)
|
||||
|
||||
username = user_data['login']
|
||||
github_id = user_data['id']
|
||||
|
||||
v3_media_type = {
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
}
|
||||
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
get_email = requests.get(app.config['GITHUB_USER_EMAILS'],
|
||||
params=token_param, headers=v3_media_type)
|
||||
|
||||
|
@ -220,18 +233,33 @@ def github_oauth_callback():
|
|||
mixpanel.alias(to_login.username, state)
|
||||
|
||||
except model.DataModelException, ex:
|
||||
return render_template('githuberror.html', error_message=ex.message)
|
||||
return render_page_template('githuberror.html', error_message=ex.message)
|
||||
|
||||
if common_login(to_login):
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return render_template('githuberror.html')
|
||||
return render_page_template('githuberror.html')
|
||||
|
||||
|
||||
@app.route('/oauth2/github/callback/attach', methods=['GET'])
|
||||
@login_required
|
||||
def github_oauth_attach():
|
||||
token = exchange_github_code_for_token(request.args.get('code'))
|
||||
user_data = get_github_user(token)
|
||||
github_id = user_data['id']
|
||||
user_obj = current_user.db_user()
|
||||
model.attach_federated_login(user_obj, 'github', github_id)
|
||||
return redirect(url_for('user'))
|
||||
|
||||
|
||||
@app.route('/confirm', methods=['GET'])
|
||||
def confirm_email():
|
||||
code = request.values['code']
|
||||
user = model.confirm_user_email(code)
|
||||
|
||||
try:
|
||||
user = model.confirm_user_email(code)
|
||||
except model.DataModelException as ex:
|
||||
return redirect(url_for('signin'))
|
||||
|
||||
common_login(user)
|
||||
|
||||
|
@ -248,8 +276,3 @@ def confirm_recovery():
|
|||
return redirect(url_for('user'))
|
||||
else:
|
||||
abort(403)
|
||||
|
||||
|
||||
@app.route('/reset', methods=['GET'])
|
||||
def password_reset():
|
||||
pass
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
import logging
|
||||
import requests
|
||||
import stripe
|
||||
|
||||
from flask import (abort, redirect, request, url_for, render_template,
|
||||
make_response)
|
||||
from flask.ext.login import login_user, UserMixin, login_required
|
||||
from flask.ext.principal import identity_changed, Identity, AnonymousIdentity
|
||||
from flask import request, make_response
|
||||
|
||||
from data import model
|
||||
from app import app, login_manager, mixpanel
|
||||
from auth.permissions import QuayDeferredPermissionUser
|
||||
from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan
|
||||
from app import app
|
||||
from util.invoice import renderInvoiceToHtml
|
||||
from util.email import send_invoice_email
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -33,10 +28,10 @@ def stripe_webhook():
|
|||
# Find the user associated with the customer ID.
|
||||
user = model.get_user_or_org_by_customer_id(customer_id)
|
||||
if user and user.invoice_email:
|
||||
# Lookup the invoice.
|
||||
invoice = stripe.Invoice.retrieve(invoice_id)
|
||||
if invoice:
|
||||
invoice_html = renderInvoiceToHtml(invoice, user)
|
||||
send_invoice_email(user.email, invoice_html)
|
||||
# Lookup the invoice.
|
||||
invoice = stripe.Invoice.retrieve(invoice_id)
|
||||
if invoice:
|
||||
invoice_html = renderInvoiceToHtml(invoice, user)
|
||||
send_invoice_email(user.email, invoice_html)
|
||||
|
||||
return make_response('Okay')
|
||||
|
|
Reference in a new issue