• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

screwdriver-cd / screwdriver / #3075

25 Mar 2025 04:42PM UTC coverage: 94.685% (-0.6%) from 95.284%
#3075

Pull #3314

screwdriver

sagar1312
fix(3304): Relax permission check to allow admins from other SCMs to update pipeline
Pull Request #3314: feat(3304): Relax permission check to allow admins from other SCMs to update pipeline

1949 of 2122 branches covered (91.85%)

Branch coverage included in aggregate %.

4 of 8 new or added lines in 1 file covered. (50.0%)

18 existing lines in 1 file now uncovered.

4909 of 5121 relevant lines covered (95.86%)

102.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.98
/plugins/pipelines/update.js
1
'use strict';
2

3
const boom = require('@hapi/boom');
1✔
4
const joi = require('joi');
1✔
5
const schema = require('screwdriver-data-schema');
1✔
6
const logger = require('screwdriver-logger');
1✔
7
const idSchema = schema.models.pipeline.base.extract('id');
1✔
8
const { formatCheckoutUrl, sanitizeRootDir } = require('./helper');
1✔
9
const { getUserPermissions } = require('../helper');
1✔
10
const ANNOTATION_USE_DEPLOY_KEY = 'screwdriver.cd/useDeployKey';
1✔
11

12
/**
13
 * Get user permissions on old pipeline
14
 * @method getPermissionsForOldPipeline
15
 * @param  {Array}                     scmContexts  An array of scmContext
16
 * @param  {Object}                    pipeline     Pipeline to check against
17
 * @param  {Object}                    user         User to check for
18
 * @return {Promise}
19
 */
20
function getPermissionsForOldPipeline({ scmContexts, pipeline, user }) {
21
    const isPipelineSCMContextObsolete = !scmContexts.includes(pipeline.scmContext);
30✔
22

23
    // this pipeline's scmContext has been removed, allow current admin to change it
24
    // also allow pipeline admins from other scmContexts to change it
25
    if (isPipelineSCMContextObsolete || user.scmContext !== pipeline.scmContext) {
30✔
26
        let isAdmin = pipeline.adminUserIds.includes(user.id) || !!pipeline.admins[user.username];
4!
27

NEW
28
        if (!isAdmin && isPipelineSCMContextObsolete && pipeline.admins[user.username]) {
×
NEW
29
            isAdmin = true;
×
30
        }
31

NEW
32
        return Promise.resolve({ admin: isAdmin });
×
33
    }
34

35
    return user.getPermissions(pipeline.scmUri);
26✔
36
}
37

38
module.exports = () => ({
685✔
39
    method: 'PUT',
40
    path: '/pipelines/{id}',
41
    options: {
42
        description: 'Update a pipeline',
43
        notes: 'Update a specific pipeline',
44
        tags: ['api', 'pipelines'],
45
        auth: {
46
            strategies: ['token'],
47
            scope: ['user', '!guest', 'pipeline']
48
        },
49

50
        handler: async (request, h) => {
51
            const { checkoutUrl, rootDir, settings, badges } = request.payload;
34✔
52
            const { id } = request.params;
34✔
53
            const { pipelineFactory, userFactory, secretFactory } = request.server.app;
34✔
54
            const { scmContext, username } = request.auth.credentials;
34✔
55
            const scmContexts = pipelineFactory.scm.getScmContexts();
34✔
56
            const { isValidToken } = request.server.plugins.pipelines;
34✔
57
            const deployKeySecret = 'SD_SCM_DEPLOY_KEY';
34✔
58

59
            if (!isValidToken(id, request.auth.credentials)) {
34✔
60
                return boom.unauthorized('Token does not have permission to this pipeline');
1✔
61
            }
62

63
            // get the pipeline given its ID and the user
64
            const oldPipeline = await pipelineFactory.get({ id });
33✔
65
            const user = await userFactory.get({ username, scmContext });
32✔
66

67
            // Handle pipeline permissions
68
            // if the pipeline ID is invalid, reject
69
            if (!oldPipeline) {
32✔
70
                throw boom.notFound(`Pipeline ${id} does not exist`);
1✔
71
            }
72

73
            if (oldPipeline.configPipelineId) {
31✔
74
                throw boom.forbidden(
1✔
75
                    `Child pipeline can only be modified by config pipeline ${oldPipeline.configPipelineId}`
76
                );
77
            }
78

79
            // get the user permissions for the repo
80
            let oldPermissions;
81

82
            try {
30✔
83
                oldPermissions = await getPermissionsForOldPipeline({
30✔
84
                    scmContexts,
85
                    pipeline: oldPipeline,
86
                    user
87
                });
88
            } catch (err) {
89
                throw boom.forbidden(`User ${user.getFullDisplayName()} does not have admin permission for this repo`);
5✔
90
            }
91

92
            let token;
93
            let formattedCheckoutUrl;
94
            const oldPipelineConfig = { ...oldPipeline };
25✔
95

96
            if (checkoutUrl || rootDir) {
25✔
97
                formattedCheckoutUrl = formatCheckoutUrl(request.payload.checkoutUrl);
23✔
98
                const sanitizedRootDir = sanitizeRootDir(request.payload.rootDir);
23✔
99

100
                // get the user token
101
                token = await user.unsealToken();
23✔
102
                // get the scm URI
103
                const scmUri = await pipelineFactory.scm.parseUrl({
23✔
104
                    scmContext,
105
                    checkoutUrl: formattedCheckoutUrl,
106
                    rootDir: sanitizedRootDir,
107
                    token
108
                });
109

110
                // get the user permissions for the repo
111
                await getUserPermissions({ user, scmUri });
23✔
112

113
                // check if there is already a pipeline with the new checkoutUrl
114
                const newPipeline = await pipelineFactory.get({ scmUri });
19✔
115

116
                // reject if pipeline already exists with new checkoutUrl
117
                if (newPipeline) {
19✔
118
                    throw boom.conflict(`Pipeline already exists with the ID: ${newPipeline.id}`);
1✔
119
                }
120

121
                const scmRepo = await pipelineFactory.scm.decorateUrl({
18✔
122
                    scmUri,
123
                    scmContext,
124
                    token
125
                });
126

127
                // update keys
128
                oldPipeline.scmContext = scmContext;
18✔
129
                oldPipeline.scmUri = scmUri;
18✔
130
                oldPipeline.scmRepo = scmRepo;
18✔
131
                oldPipeline.name = scmRepo.name;
18✔
132
            }
133

134
            if (!oldPermissions.admin) {
20✔
135
                throw boom.forbidden(`User ${username} is not an admin of these repos`);
1✔
136
            }
137

138
            oldPipeline.admins = {
19✔
139
                [username]: true
140
            };
141

142
            if (!oldPipeline.adminUserIds.includes(user.id)) {
19!
NEW
143
                oldPipeline.adminUserIds.push(user.id);
×
144
            }
145

UNCOV
146
            if (settings) {
×
UNCOV
147
                oldPipeline.settings = { ...oldPipeline.settings, ...settings };
×
148
            }
149

UNCOV
150
            if (checkoutUrl || rootDir) {
×
UNCOV
151
                logger.info(
×
152
                    `[Audit] user ${user.username}:${scmContext} updates the scmUri for pipelineID:${id} to ${oldPipeline.scmUri} from ${oldPipelineConfig.scmUri}.`
153
                );
154
            }
155

UNCOV
156
            if (badges) {
×
UNCOV
157
                if (!oldPipeline.badges) {
×
UNCOV
158
                    oldPipeline.badges = badges;
×
159
                } else {
160
                    const newBadges = {};
×
161

162
                    Object.keys(oldPipeline.badges).forEach(badgeKey => {
×
163
                        newBadges[badgeKey] = {
×
164
                            ...oldPipeline.badges[badgeKey],
165
                            ...badges[badgeKey]
166
                        };
167
                    });
168

169
                    oldPipeline.badges = newBadges;
×
170
                }
171
            }
172

173
            // update pipeline
UNCOV
174
            const updatedPipeline = await oldPipeline.update();
×
175

UNCOV
176
            await updatedPipeline.addWebhooks(`${request.server.info.uri}/v4/webhooks`);
×
177

UNCOV
178
            const result = await updatedPipeline.sync();
×
179

180
            // check if pipeline has deploy key annotation then create secrets
181
            // sync needs to happen before checking annotations
182
            const deployKeyAnnotation =
UNCOV
183
                updatedPipeline.annotations && updatedPipeline.annotations[ANNOTATION_USE_DEPLOY_KEY];
×
184

UNCOV
185
            if (deployKeyAnnotation) {
×
UNCOV
186
                const deploySecret = await secretFactory.get({
×
187
                    pipelineId: updatedPipeline.id,
188
                    name: deployKeySecret
189
                });
190
                // create only secret doesn't exist already
191

UNCOV
192
                if (!deploySecret) {
×
UNCOV
193
                    const privateDeployKey = await pipelineFactory.scm.addDeployKey({
×
194
                        scmContext: updatedPipeline.scmContext,
195
                        checkoutUrl: formattedCheckoutUrl,
196
                        token
197
                    });
UNCOV
198
                    const privateDeployKeyB64 = Buffer.from(privateDeployKey).toString('base64');
×
199

UNCOV
200
                    await secretFactory.create({
×
201
                        pipelineId: updatedPipeline.id,
202
                        name: deployKeySecret,
203
                        value: privateDeployKeyB64,
204
                        allowInPR: true
205
                    });
206
                }
207
            }
208

UNCOV
209
            return h.response(result.toJson()).code(200);
×
210
        },
211
        validate: {
212
            params: joi.object({
213
                id: idSchema
214
            }),
215
            payload: schema.models.pipeline.update
216
        }
217
    }
218
});
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc