UNPKG

screwdriver-api

Version:

API server for the Screwdriver.cd service

335 lines (279 loc) • 13 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; // for mysql backward compatibility if (!pipeline.adminUserIds) { pipeline.adminUserIds = []; } // 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); } /** * Apply state change fields to a pipeline * @method applyStateChange * @param {Object} pipeline Pipeline object to mutate * @param {String} newState New state ('ACTIVE' or 'DISABLED') * @param {String} username Username making the change * @param {String} stateChangeMessage Optional message for state change */ function applyStateChange(pipeline, newState, username, stateChangeMessage) { const currentState = pipeline.state; if ( (currentState === 'ACTIVE' && newState === 'DISABLED') || (currentState === 'DISABLED' && newState === 'ACTIVE') ) { pipeline.state = newState; pipeline.stateChanger = username; pipeline.stateChangeTime = new Date().toISOString(); pipeline.stateChangeMessage = stateChangeMessage || null; } else { throw boom.conflict(`Pipeline state cannot be transitioned from ${currentState} to ${newState}`); } } 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, state, stateChangeMessage } = request.payload; const hasNonStateFields = !!( checkoutUrl || rootDir || (settings && Object.keys(settings).length > 0) || badges ); const { id } = request.params; const { pipelineFactory, userFactory, secretFactory, bannerFactory } = request.server.app; const { scmContext, username, scmUserId } = 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.state === 'DELETING') { throw boom.conflict('This pipeline is being deleted.'); } // for mysql backward compatibility if (!oldPipeline.adminUserIds) { oldPipeline.adminUserIds = []; } // Detect if user is a screwdriver cluster admin const scmDisplayName = bannerFactory.scm.getDisplayName({ scmContext }); const adminDetails = request.server.plugins.banners.screwdriverAdminDetails( username, scmDisplayName, scmUserId ); const isScrewdriverAdmin = adminDetails.isAdmin; // If the pipeline is a child pipeline, only allow state/stateChangeMessage updates if (oldPipeline.configPipelineId) { // get pipeline admin permissions for child pipeline check let childPermissions; try { childPermissions = await getPermissionsForOldPipeline({ scmContexts, pipeline: oldPipeline, user }); } catch (err) { childPermissions = { admin: false }; } const isPipelineAdmin = childPermissions.admin; if (!isPipelineAdmin && !isScrewdriverAdmin) { throw boom.forbidden( `Child pipeline can only be modified by config pipeline ${oldPipeline.configPipelineId}` ); } if (hasNonStateFields) { throw boom.forbidden('Only state fields can be updated for a child pipeline'); } if (!state) { throw boom.forbidden( `Child pipeline can only be modified by config pipeline ${oldPipeline.configPipelineId}` ); } // Apply state change and return early applyStateChange(oldPipeline, state, username, stateChangeMessage); const updatedPipeline = await oldPipeline.update(); return h.response(updatedPipeline.toJson()).code(200); } // get the user permissions for the repo let oldPermissions; try { oldPermissions = await getPermissionsForOldPipeline({ scmContexts, pipeline: oldPipeline, user }); } catch (err) { if (isScrewdriverAdmin) { oldPermissions = { admin: false }; } else { throw boom.forbidden( `User ${user.getFullDisplayName()} does not have admin permission for this repo` ); } } const isPipelineAdmin = oldPermissions.admin; // Screwdriver admin without pipeline admin access: only allow state updates if (!isPipelineAdmin && isScrewdriverAdmin) { if (hasNonStateFields) { throw boom.forbidden(`User ${username} is only allowed to update the state of this pipeline`); } if (!state) { throw boom.forbidden(`User ${username} is not an admin of these repos`); } applyStateChange(oldPipeline, state, username, stateChangeMessage); const updatedPipeline = await oldPipeline.update(); return h.response(updatedPipeline.toJson()).code(200); } 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 (!isPipelineAdmin) { 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 (state) { applyStateChange(oldPipeline, state, username, stateChangeMessage); } if (checkoutUrl || rootDir) { logger.info( `[Audit] user ${user.username}:${scmContext} updates the scmUri for pipelineID:${id} to ${oldPipeline.scmUri} from ${oldPipelineConfig.scmUri}.` ); } if (badges) { const updatedBadges = { ...oldPipeline.badges }; Object.keys(badges).forEach(badgeKey => { if (Object.keys(badges[badgeKey]).length > 0) { updatedBadges[badgeKey] = badges[badgeKey]; } else { updatedBadges[badgeKey] = {}; } }); if (Object.keys(updatedBadges).length === 0) { oldPipeline.badges = {}; } else { oldPipeline.badges = updatedBadges; } } // 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 } } });