Change to store the pull robot on the repository build and only add the credentials to the queue item. This prevents the credentials from being exposed to the end user. Also fixes the restart build option

This commit is contained in:
Joseph Schorr 2014-04-01 21:49:06 -04:00
parent 2a72e91bdb
commit 9a79d1562a
13 changed files with 110 additions and 68 deletions

View file

@ -176,7 +176,7 @@ class RepositoryBuildTrigger(BaseModel):
auth_token = CharField() auth_token = CharField()
config = TextField(default='{}') config = TextField(default='{}')
write_token = ForeignKeyField(AccessToken, null=True) write_token = ForeignKeyField(AccessToken, null=True)
pull_user = ForeignKeyField(User, null=True, related_name='pulluser') pull_robot = ForeignKeyField(User, null=True, related_name='triggerpullrobot')
class EmailConfirmation(BaseModel): class EmailConfirmation(BaseModel):
@ -245,6 +245,7 @@ class RepositoryBuild(BaseModel):
started = DateTimeField(default=datetime.now) started = DateTimeField(default=datetime.now)
display_name = CharField() display_name = CharField()
trigger = ForeignKeyField(RepositoryBuildTrigger, null=True, index=True) trigger = ForeignKeyField(RepositoryBuildTrigger, null=True, index=True)
pull_robot = ForeignKeyField(User, null=True, related_name='buildpullrobot')
class QueueItem(BaseModel): class QueueItem(BaseModel):

View file

@ -1453,27 +1453,42 @@ def get_recent_repository_build(namespace_name, repository_name):
def create_repository_build(repo, access_token, job_config_obj, dockerfile_id, def create_repository_build(repo, access_token, job_config_obj, dockerfile_id,
display_name, trigger=None): display_name, trigger=None, pull_robot_name=None):
pull_robot = None
if pull_robot_name:
pull_robot = lookup_robot(pull_robot_name)
return RepositoryBuild.create(repository=repo, access_token=access_token, return RepositoryBuild.create(repository=repo, access_token=access_token,
job_config=json.dumps(job_config_obj), job_config=json.dumps(job_config_obj),
display_name=display_name, trigger=trigger, display_name=display_name, trigger=trigger,
resource_key=dockerfile_id) resource_key=dockerfile_id,
pull_robot=pull_robot)
def get_pull_credentials(trigger):
if not trigger.pull_user: def get_pull_robot_name(trigger):
if not trigger.pull_robot:
return None
return trigger.pull_robot.username
def get_pull_credentials(robotname):
robot = lookup_robot(robotname)
if not robot:
return None return None
try: try:
login_info = FederatedLogin.get(user=trigger.pull_user) login_info = FederatedLogin.get(user=robot)
except FederatedLogin.DoesNotExist: except FederatedLogin.DoesNotExist:
return None return None
return { return {
'username': trigger.pull_user.username, 'username': robot.username,
'password': login_info.service_ident, 'password': login_info.service_ident,
'registry': '%s://%s/v1/' % (app.config['URL_SCHEME'], app.config['URL_HOST']), 'registry': '%s://%s/v1/' % (app.config['URL_SCHEME'], app.config['URL_HOST']),
} }
def create_webhook(repo, params_obj): def create_webhook(repo, params_obj):
return Webhook.create(repository=repo, parameters=json.dumps(params_obj)) return Webhook.create(repository=repo, parameters=json.dumps(params_obj))
@ -1530,12 +1545,12 @@ def log_action(kind_name, user_or_organization_name, performer=None,
metadata_json=json.dumps(metadata), datetime=timestamp) metadata_json=json.dumps(metadata), datetime=timestamp)
def create_build_trigger(repo, service_name, auth_token, user, pull_user=None): def create_build_trigger(repo, service_name, auth_token, user, pull_robot=None):
service = BuildTriggerService.get(name=service_name) service = BuildTriggerService.get(name=service_name)
trigger = RepositoryBuildTrigger.create(repository=repo, service=service, trigger = RepositoryBuildTrigger.create(repository=repo, service=service,
auth_token=auth_token, auth_token=auth_token,
connected_user=user, connected_user=user,
pull_user=None) pull_robot=pull_robot)
return trigger return trigger

View file

@ -10,8 +10,10 @@ from endpoints.api import (RepositoryParamResource, parse_args, query_param, nic
from endpoints.common import start_build from endpoints.common import start_build
from endpoints.trigger import BuildTrigger from endpoints.trigger import BuildTrigger
from data import model 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 data.buildlogs import BuildStatusRetrievalError
from util.names import parse_robot_username
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,14 +35,15 @@ def get_job_config(build_obj):
return None return None
def trigger_view(trigger): def user_view(user):
def user_view(user):
return { return {
'name': user.username, 'name': user.username,
'kind': 'user', 'kind': 'user',
'is_robot': user.robot, 'is_robot': user.robot,
} }
def trigger_view(trigger):
if trigger and trigger.uuid: if trigger and trigger.uuid:
config_dict = get_trigger_config(trigger) config_dict = get_trigger_config(trigger)
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name) build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
@ -50,7 +53,7 @@ def trigger_view(trigger):
'id': trigger.uuid, 'id': trigger.uuid,
'connected_user': trigger.connected_user.username, 'connected_user': trigger.connected_user.username,
'is_active': build_trigger.is_active(config_dict), 'is_active': build_trigger.is_active(config_dict),
'pull_user': user_view(trigger.pull_user) if trigger.pull_user else None 'pull_robot': user_view(trigger.pull_robot) if trigger.pull_robot else None
} }
return None return None
@ -75,6 +78,7 @@ def build_status_view(build_obj, can_write=False):
'is_writer': can_write, 'is_writer': can_write,
'trigger': trigger_view(build_obj.trigger), 'trigger': trigger_view(build_obj.trigger),
'resource_key': build_obj.resource_key, 'resource_key': build_obj.resource_key,
'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None,
} }
if can_write: if can_write:
@ -103,28 +107,9 @@ class RepositoryBuildList(RepositoryParamResource):
'type': 'string', 'type': 'string',
'description': 'Subdirectory in which the Dockerfile can be found', 'description': 'Subdirectory in which the Dockerfile can be found',
}, },
'pull_credentials': { 'pull_robot': {
'type': 'object',
'description': 'Credentials used by the builder when pulling images',
'required': [
'username',
'password',
'registry'
],
'properties': {
'username': {
'type': 'string', 'type': 'string',
'description': 'The username for the pull' 'description': 'Username of a Quay robot account to use as pull credentials',
},
'password': {
'type': 'string',
'description': 'The password for the pull'
},
'registry': {
'type': 'string',
'description': 'The registry hostname for the pull'
},
}
} }
}, },
}, },
@ -154,7 +139,22 @@ class RepositoryBuildList(RepositoryParamResource):
dockerfile_id = request_json['file_id'] dockerfile_id = request_json['file_id']
subdir = request_json['subdirectory'] if 'subdirectory' in request_json else '' subdir = request_json['subdirectory'] if 'subdirectory' in request_json else ''
pull_credentials = request_json.get('pull_credentials', None) 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 # 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 # can only be reused if the user has access to the repository for which it
@ -170,7 +170,7 @@ class RepositoryBuildList(RepositoryParamResource):
display_name = user_files.get_file_checksum(dockerfile_id) 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_credentials=pull_credentials) pull_robot_name=pull_robot_name)
resp = build_status_view(build_request, True) resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository) repo_string = '%s/%s' % (namespace, repository)

View file

@ -190,7 +190,7 @@ class BuildTriggerActivate(RepositoryParamResource):
raise Unauthorized() raise Unauthorized()
# Set the pull robot. # Set the pull robot.
trigger.pull_user = pull_robot trigger.pull_robot = pull_robot
# Update the config. # Update the config.
new_config_dict = request.get_json()['config'] new_config_dict = request.get_json()['config']
@ -224,7 +224,7 @@ class BuildTriggerActivate(RepositoryParamResource):
log_action('setup_repo_trigger', namespace, log_action('setup_repo_trigger', namespace,
{'repo': repository, 'namespace': namespace, {'repo': repository, 'namespace': namespace,
'trigger_id': trigger.uuid, 'service': trigger.service.name, 'trigger_id': trigger.uuid, 'service': trigger.service.name,
'pull_user': trigger.pull_user.username if trigger.pull_user else None, 'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
'config': final_config}, repo=repo) 'config': final_config}, repo=repo)
return trigger_view(trigger) return trigger_view(trigger)
@ -254,10 +254,10 @@ class ActivateBuildTrigger(RepositoryParamResource):
dockerfile_id, tags, name, subdir = specs dockerfile_id, tags, name, subdir = specs
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
pull_credentials = model.get_pull_credentials(trigger) 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_credentials=pull_credentials) pull_robot_name=pull_robot_name)
resp = build_status_view(build_request, True) resp = build_status_view(build_request, True)
repo_string = '%s/%s' % (namespace, repository) repo_string = '%s/%s' % (namespace, repository)

View file

@ -107,7 +107,7 @@ def check_repository_usage(user_or_org, plan_found):
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
trigger=None, pull_credentials=None): trigger=None, pull_robot_name=None):
host = urlparse.urlparse(request.url).netloc host = urlparse.urlparse(request.url).netloc
repo_path = '%s/%s/%s' % (host, repository.namespace, repository.name) repo_path = '%s/%s/%s' % (host, repository.namespace, repository.name)
@ -118,18 +118,18 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
job_config = { job_config = {
'docker_tags': tags, 'docker_tags': tags,
'repository': repo_path, 'repository': repo_path,
'build_subdir': subdir, 'build_subdir': subdir
'pull_credentials': pull_credentials,
} }
build_request = model.create_repository_build(repository, token, job_config, build_request = model.create_repository_build(repository, token, job_config,
dockerfile_id, build_name, dockerfile_id, build_name,
trigger) trigger, pull_robot_name = pull_robot_name)
dockerfile_build_queue.put(json.dumps({ dockerfile_build_queue.put(json.dumps({
'build_uuid': build_request.uuid, 'build_uuid': build_request.uuid,
'namespace': repository.namespace, 'namespace': repository.namespace,
'repository': repository.name, 'repository': repository.name,
'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None
}), retries_remaining=1) }), retries_remaining=1)
metadata = { metadata = {

View file

@ -73,10 +73,10 @@ def build_trigger_webhook(namespace, repository, trigger_uuid):
# This was just a validation request, we don't need to build anything # This was just a validation request, we don't need to build anything
return make_response('Okay') return make_response('Okay')
pull_credentials = model.get_pull_credentials(trigger) pull_robot_name = model.get_pull_robot_name(trigger)
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
start_build(repo, dockerfile_id, tags, name, subdir, False, trigger, start_build(repo, dockerfile_id, tags, name, subdir, False, trigger,
pull_credentials=pull_credentials) pull_robot_name=pull_robot_name)
return make_response('Okay') return make_response('Okay')

View file

@ -332,7 +332,7 @@ def populate_database():
token = model.create_access_token(building, 'write') token = model.create_access_token(building, 'write')
trigger = model.create_build_trigger(building, 'github', '123authtoken', trigger = model.create_build_trigger(building, 'github', '123authtoken',
new_user_1, pull_user = dtrobot) new_user_1, pull_robot=dtrobot[0])
trigger.config = json.dumps({ trigger.config = json.dumps({
'build_source': 'jakedt/testconnect', 'build_source': 'jakedt/testconnect',
'subdir': '', 'subdir': '',

View file

@ -962,9 +962,13 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
var data = { var data = {
'file_id': build['resource_key'], 'file_id': build['resource_key'],
'subdirectory': subdirectory 'subdirectory': subdirectory,
}; };
if (build['pull_robot']) {
data['pull_robot'] = build['pull_robot']['name'];
}
var params = { var params = {
'repository': namespace + '/' + name 'repository': namespace + '/' + name
}; };
@ -1487,6 +1491,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
ApiService.activateBuildTrigger(data, params).then(function(resp) { ApiService.activateBuildTrigger(data, params).then(function(resp) {
trigger['is_active'] = true; trigger['is_active'] = true;
trigger['pull_robot'] = resp['pull_robot'];
}, function(resp) { }, function(resp) {
$scope.triggers.splice($scope.triggers.indexOf(trigger), 1); $scope.triggers.splice($scope.triggers.indexOf(trigger), 1);
bootbox.dialog({ bootbox.dialog({

View file

@ -270,11 +270,11 @@
Setting up trigger Setting up trigger
</div> </div>
<div ng-show="trigger.is_active" class="trigger-description" trigger="trigger"></div> <div ng-show="trigger.is_active" class="trigger-description" trigger="trigger"></div>
<div class="trigger-pull-credentials" ng-if="trigger.is_active && trigger.pull_user"> <div class="trigger-pull-credentials" ng-if="trigger.is_active && trigger.pull_robot">
<span class="context-tooltip" title="The credentials used by the builder when pulling images" bs-tooltip> <span class="context-tooltip" title="The credentials used by the builder when pulling images" bs-tooltip>
Pull Credentials: Pull Credentials:
</span> </span>
<span class="entity-reference" entity="trigger.pull_user"></span> <span class="entity-reference" entity="trigger.pull_robot"></span>
</div> </div>
</td> </td>
<td style="white-space: nowrap;"> <td style="white-space: nowrap;">

Binary file not shown.

View file

@ -964,7 +964,7 @@ class TestRequestRepoBuild(ApiTestCase):
def test_requestrepobuild(self): def test_requestrepobuild(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
# Ensure where not yet building. # Ensure we are not yet building.
json = self.getJsonResponse(RepositoryBuildList, json = self.getJsonResponse(RepositoryBuildList,
params=dict(repository=ADMIN_ACCESS_USER + '/simple')) params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
@ -982,24 +982,20 @@ class TestRequestRepoBuild(ApiTestCase):
assert len(json['builds']) > 0 assert len(json['builds']) > 0
def test_requestrepobuild_with_credentials(self): def test_requestrepobuild_with_robot(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
# Ensure where not yet building. # Ensure we are not yet building.
json = self.getJsonResponse(RepositoryBuildList, json = self.getJsonResponse(RepositoryBuildList,
params=dict(repository=ADMIN_ACCESS_USER + '/simple')) params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
assert len(json['builds']) == 0 assert len(json['builds']) == 0
# Request a (fake) build. # Request a (fake) build.
pull_creds = { pull_robot = ADMIN_ACCESS_USER + '+dtrobot'
'username': 'foo',
'password': 'bar',
'registry': 'baz'
}
self.postResponse(RepositoryBuildList, self.postResponse(RepositoryBuildList,
params=dict(repository=ADMIN_ACCESS_USER + '/simple'), params=dict(repository=ADMIN_ACCESS_USER + '/simple'),
data=dict(file_id='foobarbaz', pull_credentials=pull_creds), data=dict(file_id='foobarbaz', pull_robot=pull_robot),
expected_code=201) expected_code=201)
# Check for the build. # Check for the build.
@ -1009,6 +1005,27 @@ class TestRequestRepoBuild(ApiTestCase):
assert len(json['builds']) > 0 assert len(json['builds']) > 0
def test_requestrepobuild_with_invalid_robot(self):
self.login(ADMIN_ACCESS_USER)
# Request a (fake) build.
pull_robot = ADMIN_ACCESS_USER + '+invalidrobot'
self.postResponse(RepositoryBuildList,
params=dict(repository=ADMIN_ACCESS_USER + '/simple'),
data=dict(file_id='foobarbaz', pull_robot=pull_robot),
expected_code=404)
def test_requestrepobuild_with_unauthorized_robot(self):
self.login(ADMIN_ACCESS_USER)
# Request a (fake) build.
pull_robot = 'freshuser+anotherrobot'
self.postResponse(RepositoryBuildList,
params=dict(repository=ADMIN_ACCESS_USER + '/simple'),
data=dict(file_id='foobarbaz', pull_robot=pull_robot),
expected_code=403)
class TestWebhooks(ApiTestCase): class TestWebhooks(ApiTestCase):
def test_webhooks(self): def test_webhooks(self):
@ -1746,7 +1763,7 @@ class TestBuildTriggers(ApiTestCase):
# Verify that the robot was saved. # Verify that the robot was saved.
self.assertEquals(True, activate_json['is_active']) self.assertEquals(True, activate_json['is_active'])
self.assertEquals(ADMIN_ACCESS_USER + '+dtrobot', activate_json['pull_user']['name']) self.assertEquals(ADMIN_ACCESS_USER + '+dtrobot', activate_json['pull_robot']['name'])
# Start a manual build. # Start a manual build.
start_json = self.postJsonResponse(ActivateBuildTrigger, start_json = self.postJsonResponse(ActivateBuildTrigger,

View file

@ -26,4 +26,7 @@ def format_robot_username(parent_username, robot_shortname):
return '%s+%s' % (parent_username, robot_shortname) return '%s+%s' % (parent_username, robot_shortname)
def parse_robot_username(robot_username): def parse_robot_username(robot_username):
if not '+' in robot_username:
return None
return robot_username.split('+', 2) return robot_username.split('+', 2)

View file

@ -352,13 +352,14 @@ class DockerfileBuildWorker(Worker):
job_details['repository'], job_details['repository'],
job_details['build_uuid']) job_details['build_uuid'])
pull_credentials = job_details.get('pull_credentials', None)
job_config = json.loads(repository_build.job_config) job_config = json.loads(repository_build.job_config)
resource_url = user_files.get_file_url(repository_build.resource_key) resource_url = user_files.get_file_url(repository_build.resource_key)
tag_names = job_config['docker_tags'] tag_names = job_config['docker_tags']
build_subdir = job_config['build_subdir'] build_subdir = job_config['build_subdir']
repo = job_config['repository'] repo = job_config['repository']
pull_credentials = job_config.get('pull_credentials', None)
access_token = repository_build.access_token.code access_token = repository_build.access_token.code