UNPKG

screwdriver-api

Version:

API server for the Screwdriver.cd service

219 lines (180 loc) • 8.4 kB
'use strict'; const boom = require('@hapi/boom'); const joi = require('joi'); const schema = require('screwdriver-data-schema'); const logger = require('screwdriver-logger'); const idSchema = schema.models.pipeline.base.extract('id'); const { formatCheckoutUrl, sanitizeRootDir } = require('./helper'); const { getUserPermissions } = require('../helper'); const ANNOTATION_USE_DEPLOY_KEY = 'screwdriver.cd/useDeployKey'; /** * Get user permissions on old pipeline * @method getPermissionsForOldPipeline * @param {Array} scmContexts An array of scmContext * @param {Object} pipeline Pipeline to check against * @param {Object} user User to check for * @return {Promise} */ function getPermissionsForOldPipeline({ scmContexts, pipeline, user }) { const isPipelineSCMContextObsolete = !scmContexts.includes(pipeline.scmContext); const isUserFromAnotherSCMContext = user.scmContext !== pipeline.scmContext; // this pipeline's scmContext has been removed, allow current admin to change it // also allow pipeline admins from other scmContexts to change it if (isPipelineSCMContextObsolete || isUserFromAnotherSCMContext) { const isUserIdInAdminList = pipeline.adminUserIds.includes(user.id); const isSCMUsernameInAdminsObject = !!pipeline.admins[user.username]; const isAdmin = isUserIdInAdminList || (isPipelineSCMContextObsolete && isSCMUsernameInAdminsObject); return Promise.resolve({ admin: isAdmin }); } return user.getPermissions(pipeline.scmUri); } module.exports = () => ({ method: 'PUT', path: '/pipelines/{id}', options: { description: 'Update a pipeline', notes: 'Update a specific pipeline', tags: ['api', 'pipelines'], auth: { strategies: ['token'], scope: ['user', '!guest', 'pipeline'] }, handler: async (request, h) => { const { checkoutUrl, rootDir, settings, badges } = request.payload; const { id } = request.params; const { pipelineFactory, userFactory, secretFactory } = request.server.app; const { scmContext, username } = request.auth.credentials; const scmContexts = pipelineFactory.scm.getScmContexts(); const { isValidToken } = request.server.plugins.pipelines; const deployKeySecret = 'SD_SCM_DEPLOY_KEY'; if (!isValidToken(id, request.auth.credentials)) { return boom.unauthorized('Token does not have permission to this pipeline'); } // get the pipeline given its ID and the user const oldPipeline = await pipelineFactory.get({ id }); const user = await userFactory.get({ username, scmContext }); // Handle pipeline permissions // if the pipeline ID is invalid, reject if (!oldPipeline) { throw boom.notFound(`Pipeline ${id} does not exist`); } if (oldPipeline.configPipelineId) { throw boom.forbidden( `Child pipeline can only be modified by config pipeline ${oldPipeline.configPipelineId}` ); } // get the user permissions for the repo let oldPermissions; try { oldPermissions = await getPermissionsForOldPipeline({ scmContexts, pipeline: oldPipeline, user }); } catch (err) { throw boom.forbidden(`User ${user.getFullDisplayName()} does not have admin permission for this repo`); } let token; let formattedCheckoutUrl; const oldPipelineConfig = { ...oldPipeline }; if (checkoutUrl || rootDir) { formattedCheckoutUrl = formatCheckoutUrl(request.payload.checkoutUrl); const sanitizedRootDir = sanitizeRootDir(request.payload.rootDir); // get the user token token = await user.unsealToken(); // get the scm URI const scmUri = await pipelineFactory.scm.parseUrl({ scmContext, checkoutUrl: formattedCheckoutUrl, rootDir: sanitizedRootDir, token }); // get the user permissions for the repo await getUserPermissions({ user, scmUri }); // check if there is already a pipeline with the new checkoutUrl const newPipeline = await pipelineFactory.get({ scmUri }); // reject if pipeline already exists with new checkoutUrl if (newPipeline) { throw boom.conflict(`Pipeline already exists with the ID: ${newPipeline.id}`); } const scmRepo = await pipelineFactory.scm.decorateUrl({ scmUri, scmContext, token }); // update keys oldPipeline.scmContext = scmContext; oldPipeline.scmUri = scmUri; oldPipeline.scmRepo = scmRepo; oldPipeline.name = scmRepo.name; } if (!oldPermissions.admin) { throw boom.forbidden(`User ${username} is not an admin of these repos`); } oldPipeline.admins = { [username]: true }; if (!oldPipeline.adminUserIds.includes(user.id)) { oldPipeline.adminUserIds.push(user.id); } if (settings) { oldPipeline.settings = { ...oldPipeline.settings, ...settings }; } if (checkoutUrl || rootDir) { logger.info( `[Audit] user ${user.username}:${scmContext} updates the scmUri for pipelineID:${id} to ${oldPipeline.scmUri} from ${oldPipelineConfig.scmUri}.` ); } if (badges) { if (!oldPipeline.badges) { oldPipeline.badges = badges; } else { const newBadges = {}; Object.keys(oldPipeline.badges).forEach(badgeKey => { newBadges[badgeKey] = { ...oldPipeline.badges[badgeKey], ...badges[badgeKey] }; }); oldPipeline.badges = newBadges; } } // update pipeline const updatedPipeline = await oldPipeline.update(); await updatedPipeline.addWebhooks(`${request.server.info.uri}/v4/webhooks`); const result = await updatedPipeline.sync(); // check if pipeline has deploy key annotation then create secrets // sync needs to happen before checking annotations const deployKeyAnnotation = updatedPipeline.annotations && updatedPipeline.annotations[ANNOTATION_USE_DEPLOY_KEY]; if (deployKeyAnnotation) { const deploySecret = await secretFactory.get({ pipelineId: updatedPipeline.id, name: deployKeySecret }); // create only secret doesn't exist already if (!deploySecret) { const privateDeployKey = await pipelineFactory.scm.addDeployKey({ scmContext: updatedPipeline.scmContext, checkoutUrl: formattedCheckoutUrl, token }); const privateDeployKeyB64 = Buffer.from(privateDeployKey).toString('base64'); await secretFactory.create({ pipelineId: updatedPipeline.id, name: deployKeySecret, value: privateDeployKeyB64, allowInPR: true }); } } return h.response(result.toJson()).code(200); }, validate: { params: joi.object({ id: idSchema }), payload: schema.models.pipeline.update } } });