Merge remote-tracking branch 'origin/master' into tagyourit

Conflicts:
	static/css/quay.css
	static/js/graphing.js
	static/partials/view-repo.html
	test/data/test.db
This commit is contained in:
jakedt 2014-04-15 15:58:30 -04:00
commit 3f42d15335
132 changed files with 4266 additions and 1924 deletions

View file

@ -85,11 +85,32 @@ def handle_api_error(error):
def resource(*urls, **kwargs):
def wrapper(api_resource):
if not api_resource:
return None
api.add_resource(api_resource, *urls, **kwargs)
return api_resource
return wrapper
def show_if(value):
def f(inner):
if not value:
return None
return inner
return f
def hide_if(value):
def f(inner):
if value:
return None
return inner
return f
def truthy_bool(param):
return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'}
@ -103,6 +124,9 @@ def format_date(date):
def add_method_metadata(name, value):
def modifier(func):
if func is None:
return None
if '__api_metadata' not in dir(func):
func.__api_metadata = {}
func.__api_metadata[name] = value
@ -111,11 +135,15 @@ def add_method_metadata(name, value):
def method_metadata(func, name):
if func is None:
return None
if '__api_metadata' in dir(func):
return func.__api_metadata.get(name, None)
return None
nickname = partial(add_method_metadata, 'nickname')
related_user_resource = partial(add_method_metadata, 'related_user_resource')
internal_only = add_method_metadata('internal', True)
@ -274,6 +302,7 @@ import endpoints.api.repository
import endpoints.api.repotoken
import endpoints.api.robot
import endpoints.api.search
import endpoints.api.superuser
import endpoints.api.tag
import endpoints.api.team
import endpoints.api.trigger

View file

@ -1,16 +1,17 @@
import stripe
from flask import request
from app import billing
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin)
require_user_admin, show_if, hide_if)
from endpoints.api.subscribe import subscribe, subscription_view
from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from data import model
from data.plans import PLANS
from data.billing import PLANS
import features
def carderror_response(e):
return {'carderror': e.message}, 402
@ -22,7 +23,7 @@ def get_card(user):
}
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
cus = billing.Customer.retrieve(user.stripe_id)
if cus and cus.default_card:
# Find the default card.
default_card = None
@ -43,7 +44,7 @@ def get_card(user):
def set_card(user, token):
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
cus = billing.Customer.retrieve(user.stripe_id)
if cus:
try:
cus.card = token
@ -72,13 +73,14 @@ def get_invoices(customer_id):
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
}
invoices = stripe.Invoice.all(customer=customer_id, count=12)
invoices = billing.Invoice.all(customer=customer_id, count=12)
return {
'invoices': [invoice_view(i) for i in invoices.data]
}
@resource('/v1/plans/')
@show_if(features.BILLING)
class ListPlans(ApiResource):
""" Resource for listing the available plans. """
@nickname('listPlans')
@ -91,6 +93,7 @@ class ListPlans(ApiResource):
@resource('/v1/user/card')
@internal_only
@show_if(features.BILLING)
class UserCard(ApiResource):
""" Resource for managing a user's credit card. """
schemas = {
@ -132,6 +135,7 @@ class UserCard(ApiResource):
@resource('/v1/organization/<orgname>/card')
@internal_only
@related_user_resource(UserCard)
@show_if(features.BILLING)
class OrganizationCard(ApiResource):
""" Resource for managing an organization's credit card. """
schemas = {
@ -178,6 +182,7 @@ class OrganizationCard(ApiResource):
@resource('/v1/user/plan')
@internal_only
@show_if(features.BILLING)
class UserPlan(ApiResource):
""" Resource for managing a user's subscription. """
schemas = {
@ -220,7 +225,7 @@ class UserPlan(ApiResource):
private_repos = model.get_private_repo_count(user.username)
if user.stripe_id:
cus = stripe.Customer.retrieve(user.stripe_id)
cus = billing.Customer.retrieve(user.stripe_id)
if cus.subscription:
return subscription_view(cus.subscription, private_repos)
@ -234,6 +239,7 @@ class UserPlan(ApiResource):
@resource('/v1/organization/<orgname>/plan')
@internal_only
@related_user_resource(UserPlan)
@show_if(features.BILLING)
class OrganizationPlan(ApiResource):
""" Resource for managing a org's subscription. """
schemas = {
@ -279,7 +285,7 @@ class OrganizationPlan(ApiResource):
private_repos = model.get_private_repo_count(orgname)
organization = model.get_organization(orgname)
if organization.stripe_id:
cus = stripe.Customer.retrieve(organization.stripe_id)
cus = billing.Customer.retrieve(organization.stripe_id)
if cus.subscription:
return subscription_view(cus.subscription, private_repos)
@ -294,6 +300,7 @@ class OrganizationPlan(ApiResource):
@resource('/v1/user/invoices')
@internal_only
@show_if(features.BILLING)
class UserInvoiceList(ApiResource):
""" Resource for listing a user's invoices. """
@require_user_admin
@ -310,6 +317,7 @@ class UserInvoiceList(ApiResource):
@resource('/v1/organization/<orgname>/invoices')
@internal_only
@related_user_resource(UserInvoiceList)
@show_if(features.BILLING)
class OrgnaizationInvoiceList(ApiResource):
""" Resource for listing an orgnaization's invoices. """
@nickname('listOrgInvoices')
@ -323,4 +331,4 @@ class OrgnaizationInvoiceList(ApiResource):
return get_invoices(organization.stripe_id)
raise Unauthorized()
raise Unauthorized()

View file

@ -3,19 +3,20 @@ import json
from flask import request
from app import app
from app import app, userfiles as user_files
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
require_repo_read, require_repo_write, validate_json_request,
ApiResource, internal_only, format_date, api, Unauthorized, NotFound)
from endpoints.common import start_build
from endpoints.trigger import BuildTrigger
from data import model
from auth.permissions import ModifyRepositoryPermission
from auth.auth_context import get_authenticated_user
from auth.permissions import ModifyRepositoryPermission, AdministerOrganizationPermission
from data.buildlogs import BuildStatusRetrievalError
from util.names import parse_robot_username
logger = logging.getLogger(__name__)
user_files = app.config['USERFILES']
build_logs = app.config['BUILDLOGS']
@ -33,7 +34,15 @@ def get_job_config(build_obj):
return None
def user_view(user):
return {
'name': user.username,
'kind': 'user',
'is_robot': user.robot,
}
def trigger_view(trigger):
if trigger and trigger.uuid:
config_dict = get_trigger_config(trigger)
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
@ -42,7 +51,8 @@ def trigger_view(trigger):
'config': config_dict,
'id': trigger.uuid,
'connected_user': trigger.connected_user.username,
'is_active': build_trigger.is_active(config_dict)
'is_active': build_trigger.is_active(config_dict),
'pull_robot': user_view(trigger.pull_robot) if trigger.pull_robot else None
}
return None
@ -67,6 +77,7 @@ def build_status_view(build_obj, can_write=False):
'is_writer': can_write,
'trigger': trigger_view(build_obj.trigger),
'resource_key': build_obj.resource_key,
'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None,
}
if can_write:
@ -95,6 +106,10 @@ class RepositoryBuildList(RepositoryParamResource):
'type': 'string',
'description': 'Subdirectory in which the Dockerfile can be found',
},
'pull_robot': {
'type': 'string',
'description': 'Username of a Quay robot account to use as pull credentials',
}
},
},
}
@ -123,6 +138,22 @@ class RepositoryBuildList(RepositoryParamResource):
dockerfile_id = request_json['file_id']
subdir = request_json['subdirectory'] if 'subdirectory' in request_json else ''
pull_robot_name = request_json.get('pull_robot', None)
# Verify the security behind the pull robot.
if pull_robot_name:
result = parse_robot_username(pull_robot_name)
if result:
pull_robot = model.lookup_robot(pull_robot_name)
if not pull_robot:
raise NotFound()
# Make sure the user has administer permissions for the robot's namespace.
(robot_namespace, shortname) = result
if not AdministerOrganizationPermission(robot_namespace).can():
raise Unauthorized()
else:
raise Unauthorized()
# Check if the dockerfile resource has already been used. If so, then it
# can only be reused if the user has access to the repository for which it
@ -137,7 +168,8 @@ class RepositoryBuildList(RepositoryParamResource):
repo = model.get_repository(namespace, repository)
display_name = user_files.get_file_checksum(dockerfile_id)
build_request = start_build(repo, dockerfile_id, ['latest'], display_name, subdir, True)
build_request = start_build(repo, dockerfile_id, ['latest'], display_name, subdir, True,
pull_robot_name=pull_robot_name)
resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository)

View file

@ -23,13 +23,12 @@ TYPE_CONVERTER = {
int: 'integer',
}
URL_SCHEME = app.config['URL_SCHEME']
URL_HOST = app.config['URL_HOST']
PREFERRED_URL_SCHEME = app.config['PREFERRED_URL_SCHEME']
SERVER_HOSTNAME = app.config['SERVER_HOSTNAME']
def fully_qualified_name(method_view_class):
inst = method_view_class()
return '%s.%s' % (inst.__module__, inst.__class__.__name__)
return '%s.%s' % (method_view_class.__module__, method_view_class.__name__)
def swagger_route_data(include_internal=False, compact=False):
@ -143,7 +142,7 @@ def swagger_route_data(include_internal=False, compact=False):
swagger_data = {
'apiVersion': 'v1',
'swaggerVersion': '1.2',
'basePath': '%s://%s' % (URL_SCHEME, URL_HOST),
'basePath': '%s://%s' % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
'resourcePath': '/',
'info': {
'title': 'Quay.io API',
@ -160,7 +159,7 @@ def swagger_route_data(include_internal=False, compact=False):
"implicit": {
"tokenName": "access_token",
"loginEndpoint": {
"url": "%s://%s/oauth/authorize" % (URL_SCHEME, URL_HOST),
"url": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME),
},
},
},

View file

@ -2,16 +2,13 @@ import json
from collections import defaultdict
from app import app
from app import storage as store
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
format_date, NotFound)
from data import model
from util.cache import cache_control_flask_restful
store = app.config['STORAGE']
def image_view(image):
extended_props = image
if image.storage and image.storage.id:

View file

@ -29,8 +29,7 @@ def log_view(log):
return view
def get_logs(namespace, start_time, end_time, performer_name=None,
repository=None):
def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None):
performer = None
if performer_name:
performer = model.get_user(performer_name)
@ -54,8 +53,8 @@ def get_logs(namespace, start_time, end_time, performer_name=None,
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(start_time, end_time, performer=performer, repository=repository,
namespace=namespace)
return {
'start_time': format_date(start_time),
'end_time': format_date(end_time),
@ -80,7 +79,7 @@ class RepositoryLogs(RepositoryParamResource):
start_time = args['starttime']
end_time = args['endtime']
return get_logs(namespace, start_time, end_time, repository=repo)
return get_logs(start_time, end_time, repository=repo, namespace=namespace)
@resource('/v1/user/logs')
@ -100,7 +99,7 @@ class UserLogs(ApiResource):
end_time = args['endtime']
user = get_authenticated_user()
return get_logs(user.username, start_time, end_time, performer_name=performer_name)
return get_logs(start_time, end_time, performer_name=performer_name, namespace=user.username)
@resource('/v1/organization/<orgname>/logs')
@ -121,6 +120,6 @@ class OrgLogs(ApiResource):
start_time = args['starttime']
end_time = args['endtime']
return get_logs(orgname, start_time, end_time, performer_name=performer_name)
return get_logs(start_time, end_time, namespace=orgname, performer_name=performer_name)
raise Unauthorized()
raise Unauthorized()

View file

@ -1,20 +1,22 @@
import logging
import stripe
from flask import request
from app import billing as stripe
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin, log_action)
require_user_admin, log_action, show_if)
from endpoints.api.team import team_view
from endpoints.api.user import User, PrivateRepositories
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
CreateRepositoryPermission)
from auth.auth_context import get_authenticated_user
from data import model
from data.plans import get_plan
from data.billing import get_plan
from util.gravatar import compute_hash
import features
logger = logging.getLogger(__name__)
@ -163,6 +165,7 @@ class Organization(ApiResource):
@resource('/v1/organization/<orgname>/private')
@internal_only
@related_user_resource(PrivateRepositories)
@show_if(features.BILLING)
class OrgPrivateRepositories(ApiResource):
""" Custom verb to compute whether additional private repositories are available. """
@nickname('getOrganizationPrivateAllowed')

View file

@ -1,11 +1,13 @@
import logging
import stripe
from app import billing
from endpoints.api import request_error, log_action, NotFound
from endpoints.common import check_repository_usage
from data import model
from data.plans import PLANS
from data.billing import PLANS
import features
logger = logging.getLogger(__name__)
@ -24,6 +26,9 @@ def subscription_view(stripe_subscription, used_repos):
def subscribe(user, plan, token, require_business_plan):
if not features.BILLING:
return
plan_found = None
for plan_obj in PLANS:
if plan_obj['stripeId'] == plan:
@ -56,7 +61,7 @@ def subscribe(user, plan, token, require_business_plan):
card = token
try:
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
cus = billing.Customer.create(email=user.email, plan=plan, card=card)
user.stripe_id = cus.id
user.save()
check_repository_usage(user, plan_found)
@ -69,7 +74,7 @@ def subscribe(user, plan, token, require_business_plan):
else:
# Change the plan
cus = stripe.Customer.retrieve(user.stripe_id)
cus = billing.Customer.retrieve(user.stripe_id)
if plan_found['price'] == 0:
if cus.subscription is not None:

160
endpoints/api/superuser.py Normal file
View file

@ -0,0 +1,160 @@
import logging
import json
from app import app
from flask import request
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin, format_date,
InvalidToken, require_scope, format_date, hide_if, show_if, parse_args,
query_param, abort)
from endpoints.api.logs import get_logs
from data import model
from auth.permissions import SuperUserPermission
from auth.auth_context import get_authenticated_user
import features
logger = logging.getLogger(__name__)
@resource('/v1/superuser/logs')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserLogs(ApiResource):
""" Resource for fetching all logs in the system. """
@nickname('listAllLogs')
@parse_args
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
@query_param('performer', 'Username for which to filter logs.', type=str)
def get(self, args):
""" List the logs for the current system. """
if SuperUserPermission().can():
performer_name = args['performer']
start_time = args['starttime']
end_time = args['endtime']
return get_logs(start_time, end_time)
abort(403)
@resource('/v1/superuser/seats')
@internal_only
@show_if(features.SUPER_USERS)
@hide_if(features.BILLING)
class SeatUsage(ApiResource):
""" Resource for managing the seats granted in the license for the system. """
@nickname('getSeatCount')
def get(self):
""" Returns the current number of seats being used in the system. """
if SuperUserPermission().can():
return {
'count': model.get_active_user_count(),
'allowed': app.config.get('LICENSE_SEAT_COUNT', 0)
}
abort(403)
def user_view(user):
return {
'username': user.username,
'email': user.email,
'verified': user.verified,
'super_user': user.username in app.config['SUPER_USERS']
}
@resource('/v1/superuser/users/')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserList(ApiResource):
""" Resource for listing users in the system. """
@nickname('listAllUsers')
def get(self):
""" Returns a list of all users in the system. """
if SuperUserPermission().can():
users = model.get_active_users()
return {
'users': [user_view(user) for user in users]
}
abort(403)
@resource('/v1/superuser/users/<username>')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserManagement(ApiResource):
""" Resource for managing users in the system. """
schemas = {
'UpdateUser': {
'id': 'UpdateUser',
'type': 'object',
'description': 'Description of updates for a user',
'properties': {
'password': {
'type': 'string',
'description': 'The new password for the user',
},
'email': {
'type': 'string',
'description': 'The new e-mail address for the user',
}
},
},
}
@nickname('getInstallUser')
def get(self, username):
""" Returns information about the specified user. """
if SuperUserPermission().can():
user = model.get_user(username)
if not user or user.organization or user.robot:
abort(404)
return user_view(user)
abort(403)
@nickname('deleteInstallUser')
def delete(self, username):
""" Deletes the specified user. """
if SuperUserPermission().can():
user = model.get_user(username)
if not user or user.organization or user.robot:
abort(404)
if username in app.config['SUPER_USERS']:
abort(403)
model.delete_user(user)
return 'Deleted', 204
abort(403)
@nickname('changeInstallUser')
@validate_json_request('UpdateUser')
def put(self, username):
""" Updates information about the specified user. """
if SuperUserPermission().can():
user = model.get_user(username)
if not user or user.organization or user.robot:
abort(404)
if username in app.config['SUPER_USERS']:
abort(403)
user_data = request.get_json()
if 'password' in user_data:
model.change_password(user, user_data['password'])
if 'email' in user_data:
model.update_email(user, user_data['email'])
return user_view(user)
abort(403)

View file

@ -16,7 +16,9 @@ from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactiva
TriggerActivationException, EmptyRepositoryException,
RepositoryReadException)
from data import model
from auth.permissions import UserAdminPermission
from auth.permissions import UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission
from util.names import parse_robot_username
from util.dockerfileparse import parse_dockerfile
logger = logging.getLogger(__name__)
@ -139,7 +141,19 @@ class BuildTriggerActivate(RepositoryParamResource):
'BuildTriggerActivateRequest': {
'id': 'BuildTriggerActivateRequest',
'type': 'object',
'description': 'Arbitrary json.',
'required': [
'config'
],
'properties': {
'config': {
'type': 'object',
'description': 'Arbitrary json.',
},
'pull_robot': {
'type': 'string',
'description': 'The name of the robot that will be used to pull images.'
}
}
},
}
@ -160,7 +174,27 @@ class BuildTriggerActivate(RepositoryParamResource):
user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can():
new_config_dict = request.get_json()
# Update the pull robot (if any).
pull_robot_name = request.get_json().get('pull_robot', None)
if pull_robot_name:
pull_robot = model.lookup_robot(pull_robot_name)
if not pull_robot:
raise NotFound()
# Make sure the user has administer permissions for the robot's namespace.
(robot_namespace, shortname) = parse_robot_username(pull_robot_name)
if not AdministerOrganizationPermission(robot_namespace).can():
raise Unauthorized()
# Make sure the namespace matches that of the trigger.
if robot_namespace != namespace:
raise Unauthorized()
# Set the pull robot.
trigger.pull_robot = pull_robot
# Update the config.
new_config_dict = request.get_json()['config']
token_name = 'Build Trigger: %s' % trigger.service.name
token = model.create_delegate_token(namespace, repository, token_name,
@ -171,9 +205,8 @@ class BuildTriggerActivate(RepositoryParamResource):
trigger.repository.name)
path = url_for('webhooks.build_trigger_webhook',
repository=repository_path, trigger_uuid=trigger.uuid)
authed_url = _prepare_webhook_url(app.config['URL_SCHEME'], '$token',
token.code, app.config['URL_HOST'],
path)
authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'], '$token', token.code,
app.config['SERVER_HOSTNAME'], path)
final_config = handler.activate(trigger.uuid, authed_url,
trigger.auth_token, new_config_dict)
@ -191,6 +224,7 @@ class BuildTriggerActivate(RepositoryParamResource):
log_action('setup_repo_trigger', namespace,
{'repo': repository, 'namespace': namespace,
'trigger_id': trigger.uuid, 'service': trigger.service.name,
'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
'config': final_config}, repo=repo)
return trigger_view(trigger)
@ -198,6 +232,141 @@ class BuildTriggerActivate(RepositoryParamResource):
raise Unauthorized()
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/analyze')
@internal_only
class BuildTriggerAnalyze(RepositoryParamResource):
""" Custom verb for analyzing the config for a build trigger and suggesting various changes
(such as a robot account to use for pulling)
"""
schemas = {
'BuildTriggerAnalyzeRequest': {
'id': 'BuildTriggerAnalyzeRequest',
'type': 'object',
'required': [
'config'
],
'properties': {
'config': {
'type': 'object',
'description': 'Arbitrary json.',
}
}
},
}
@require_repo_admin
@nickname('analyzeBuildTrigger')
@validate_json_request('BuildTriggerAnalyzeRequest')
def post(self, namespace, repository, trigger_uuid):
""" Analyze the specified build trigger configuration. """
try:
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
except model.InvalidBuildTriggerException:
raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
new_config_dict = request.get_json()['config']
try:
# Load the contents of the Dockerfile.
contents = handler.load_dockerfile_contents(trigger.auth_token, new_config_dict)
if not contents:
return {
'status': 'error',
'message': 'Could not read the Dockerfile for the trigger'
}
# Parse the contents of the Dockerfile.
parsed = parse_dockerfile(contents)
if not parsed:
return {
'status': 'error',
'message': 'Could not parse the Dockerfile specified'
}
# Determine the base image (i.e. the FROM) for the Dockerfile.
base_image = parsed.get_base_image()
if not base_image:
return {
'status': 'warning',
'message': 'No FROM line found in the Dockerfile'
}
# Check to see if the base image lives in Quay.
quay_registry_prefix = '%s/' % (app.config['SERVER_HOSTNAME'])
if not base_image.startswith(quay_registry_prefix):
return {
'status': 'publicbase'
}
# Lookup the repository in Quay.
result = base_image[len(quay_registry_prefix):].split('/', 2)
if len(result) != 2:
return {
'status': 'warning',
'message': '"%s" is not a valid Quay repository path' % (base_image)
}
(base_namespace, base_repository) = result
found_repository = model.get_repository(base_namespace, base_repository)
if not found_repository:
return {
'status': 'error',
'message': 'Repository "%s" was not found' % (base_image)
}
# If the repository is private and the user cannot see that repo, then
# mark it as not found.
can_read = ReadRepositoryPermission(base_namespace, base_repository)
if found_repository.visibility.name != 'public' and not can_read:
return {
'status': 'error',
'message': 'Repository "%s" was not found' % (base_image)
}
# Check to see if the repository is public. If not, we suggest the
# usage of a robot account to conduct the pull.
read_robots = []
if AdministerOrganizationPermission(base_namespace).can():
def robot_view(robot):
return {
'name': robot.username,
'kind': 'user',
'is_robot': True
}
def is_valid_robot(user):
# Make sure the user is a robot.
if not user.robot:
return False
# Make sure the current user can see/administer the robot.
(robot_namespace, shortname) = parse_robot_username(user.username)
return AdministerOrganizationPermission(robot_namespace).can()
repo_perms = model.get_all_repo_users(base_namespace, base_repository)
read_robots = [robot_view(perm.user) for perm in repo_perms if is_valid_robot(perm.user)]
return {
'namespace': base_namespace,
'name': base_repository,
'is_public': found_repository.visibility.name == 'public',
'robots': read_robots,
'status': 'analyzed',
'dockerfile_url': handler.dockerfile_url(trigger.auth_token, new_config_dict)
}
except RepositoryReadException as rre:
return {
'status': 'error',
'message': rre.message
}
raise NotFound()
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
class ActivateBuildTrigger(RepositoryParamResource):
""" Custom verb to manually activate a build trigger. """
@ -220,8 +389,10 @@ class ActivateBuildTrigger(RepositoryParamResource):
dockerfile_id, tags, name, subdir = specs
repo = model.get_repository(namespace, repository)
pull_robot_name = model.get_pull_robot_name(trigger)
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True)
build_request = start_build(repo, dockerfile_id, tags, name, subdir, True,
pull_robot_name=pull_robot_name)
resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository)

View file

@ -1,27 +1,27 @@
import logging
import stripe
import json
from flask import request
from flask.ext.login import logout_user
from flask.ext.principal import identity_changed, AnonymousIdentity
from app import app
from app import app, billing as stripe
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin,
InvalidToken, require_scope, format_date)
InvalidToken, require_scope, format_date, hide_if, show_if)
from endpoints.api.subscribe import subscribe
from endpoints.common import common_login
from data import model
from data.plans import get_plan
from data.billing import get_plan
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
UserAdminPermission, UserReadPermission)
UserAdminPermission, UserReadPermission, SuperUserPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes
from util.gravatar import compute_hash
from util.email import (send_confirmation_email, send_recovery_email,
send_change_email)
import features
logger = logging.getLogger(__name__)
@ -65,6 +65,11 @@ def user_view(user):
'preferred_namespace': not (user.stripe_id is None),
})
if features.SUPER_USERS:
user_response.update({
'super_user': user and user == get_authenticated_user() and SuperUserPermission().can()
})
return user_response
@ -193,6 +198,7 @@ class User(ApiResource):
@resource('/v1/user/private')
@internal_only
@show_if(features.BILLING)
class PrivateRepositories(ApiResource):
""" Operations dealing with the available count of private repositories. """
@require_user_admin
@ -248,8 +254,7 @@ class ConvertToOrganization(ApiResource):
'description': 'Information required to convert a user to an organization.',
'required': [
'adminUser',
'adminPassword',
'plan',
'adminPassword'
],
'properties': {
'adminUser': {
@ -262,7 +267,7 @@ class ConvertToOrganization(ApiResource):
},
'plan': {
'type': 'string',
'description': 'The plan to which the organizatino should be subscribed',
'description': 'The plan to which the organization should be subscribed',
},
},
},
@ -289,8 +294,9 @@ class ConvertToOrganization(ApiResource):
message='The admin user credentials are not valid')
# Subscribe the organization to the new plan.
plan = convert_data['plan']
subscribe(user, plan, None, True) # Require business plans
if features.BILLING:
plan = convert_data.get('plan', 'free')
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))