screwdriver-api 8.0.132 → 8.0.133

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screwdriver-api",
3
- "version": "8.0.132",
3
+ "version": "8.0.133",
4
4
  "description": "API server for the Screwdriver.cd service",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -114,7 +114,7 @@
114
114
  "screwdriver-config-parser": "^12.2.3",
115
115
  "screwdriver-coverage-bookend": "^3.0.0",
116
116
  "screwdriver-coverage-sonar": "^5.0.0",
117
- "screwdriver-data-schema": "^25.12.0",
117
+ "screwdriver-data-schema": "^26.1.0",
118
118
  "screwdriver-datastore-sequelize": "^10.0.0",
119
119
  "screwdriver-executor-base": "^11.0.0",
120
120
  "screwdriver-executor-docker": "^8.0.1",
@@ -123,7 +123,7 @@
123
123
  "screwdriver-executor-queue": "^6.0.0",
124
124
  "screwdriver-executor-router": "^5.0.0",
125
125
  "screwdriver-logger": "^3.0.0",
126
- "screwdriver-models": "^33.2.0",
126
+ "screwdriver-models": "^34.1.0",
127
127
  "screwdriver-notifications-email": "^5.0.0",
128
128
  "screwdriver-notifications-slack": "^7.0.0",
129
129
  "screwdriver-request": "^3.0.0",
@@ -129,7 +129,7 @@ module.exports = () => ({
129
129
  if (!pipeline) {
130
130
  throw boom.notFound();
131
131
  } else if (pipeline.state !== 'ACTIVE') {
132
- // INACTIVE or DELETING pipeline
132
+ // INACTIVE, DELETING, or DISABLED pipeline
133
133
  throw boom.badRequest(`Cannot create an event for a(n) ${pipeline.state} pipeline`);
134
134
  }
135
135
 
@@ -39,6 +39,30 @@ function getPermissionsForOldPipeline({ scmContexts, pipeline, user }) {
39
39
  return user.getPermissions(pipeline.scmUri);
40
40
  }
41
41
 
42
+ /**
43
+ * Apply state change fields to a pipeline
44
+ * @method applyStateChange
45
+ * @param {Object} pipeline Pipeline object to mutate
46
+ * @param {String} newState New state ('ACTIVE' or 'DISABLED')
47
+ * @param {String} username Username making the change
48
+ * @param {String} stateChangeMessage Optional message for state change
49
+ */
50
+ function applyStateChange(pipeline, newState, username, stateChangeMessage) {
51
+ const currentState = pipeline.state;
52
+
53
+ if (
54
+ (currentState === 'ACTIVE' && newState === 'DISABLED') ||
55
+ (currentState === 'DISABLED' && newState === 'ACTIVE')
56
+ ) {
57
+ pipeline.state = newState;
58
+ pipeline.stateChanger = username;
59
+ pipeline.stateChangeTime = new Date().toISOString();
60
+ pipeline.stateChangeMessage = stateChangeMessage || null;
61
+ } else {
62
+ throw boom.conflict(`Pipeline state cannot be transitioned from ${currentState} to ${newState}`);
63
+ }
64
+ }
65
+
42
66
  module.exports = () => ({
43
67
  method: 'PUT',
44
68
  path: '/pipelines/{id}',
@@ -52,10 +76,16 @@ module.exports = () => ({
52
76
  },
53
77
 
54
78
  handler: async (request, h) => {
55
- const { checkoutUrl, rootDir, settings, badges } = request.payload;
79
+ const { checkoutUrl, rootDir, settings, badges, state, stateChangeMessage } = request.payload;
80
+ const hasNonStateFields = !!(
81
+ checkoutUrl ||
82
+ rootDir ||
83
+ (settings && Object.keys(settings).length > 0) ||
84
+ badges
85
+ );
56
86
  const { id } = request.params;
57
- const { pipelineFactory, userFactory, secretFactory } = request.server.app;
58
- const { scmContext, username } = request.auth.credentials;
87
+ const { pipelineFactory, userFactory, secretFactory, bannerFactory } = request.server.app;
88
+ const { scmContext, username, scmUserId } = request.auth.credentials;
59
89
  const scmContexts = pipelineFactory.scm.getScmContexts();
60
90
  const { isValidToken } = request.server.plugins.pipelines;
61
91
  const deployKeySecret = 'SD_SCM_DEPLOY_KEY';
@@ -82,10 +112,53 @@ module.exports = () => ({
82
112
  oldPipeline.adminUserIds = [];
83
113
  }
84
114
 
115
+ // Detect if user is a screwdriver cluster admin
116
+ const scmDisplayName = bannerFactory.scm.getDisplayName({ scmContext });
117
+ const adminDetails = request.server.plugins.banners.screwdriverAdminDetails(
118
+ username,
119
+ scmDisplayName,
120
+ scmUserId
121
+ );
122
+ const isScrewdriverAdmin = adminDetails.isAdmin;
123
+
124
+ // If the pipeline is a child pipeline, only allow state/stateChangeMessage updates
85
125
  if (oldPipeline.configPipelineId) {
86
- throw boom.forbidden(
87
- `Child pipeline can only be modified by config pipeline ${oldPipeline.configPipelineId}`
88
- );
126
+ // get pipeline admin permissions for child pipeline check
127
+ let childPermissions;
128
+
129
+ try {
130
+ childPermissions = await getPermissionsForOldPipeline({
131
+ scmContexts,
132
+ pipeline: oldPipeline,
133
+ user
134
+ });
135
+ } catch (err) {
136
+ childPermissions = { admin: false };
137
+ }
138
+
139
+ const isPipelineAdmin = childPermissions.admin;
140
+
141
+ if (!isPipelineAdmin && !isScrewdriverAdmin) {
142
+ throw boom.forbidden(
143
+ `Child pipeline can only be modified by config pipeline ${oldPipeline.configPipelineId}`
144
+ );
145
+ }
146
+
147
+ if (hasNonStateFields) {
148
+ throw boom.forbidden('Only state fields can be updated for a child pipeline');
149
+ }
150
+
151
+ if (!state) {
152
+ throw boom.forbidden(
153
+ `Child pipeline can only be modified by config pipeline ${oldPipeline.configPipelineId}`
154
+ );
155
+ }
156
+
157
+ // Apply state change and return early
158
+ applyStateChange(oldPipeline, state, username, stateChangeMessage);
159
+ const updatedPipeline = await oldPipeline.update();
160
+
161
+ return h.response(updatedPipeline.toJson()).code(200);
89
162
  }
90
163
 
91
164
  // get the user permissions for the repo
@@ -98,7 +171,31 @@ module.exports = () => ({
98
171
  user
99
172
  });
100
173
  } catch (err) {
101
- throw boom.forbidden(`User ${user.getFullDisplayName()} does not have admin permission for this repo`);
174
+ if (isScrewdriverAdmin) {
175
+ oldPermissions = { admin: false };
176
+ } else {
177
+ throw boom.forbidden(
178
+ `User ${user.getFullDisplayName()} does not have admin permission for this repo`
179
+ );
180
+ }
181
+ }
182
+
183
+ const isPipelineAdmin = oldPermissions.admin;
184
+
185
+ // Screwdriver admin without pipeline admin access: only allow state updates
186
+ if (!isPipelineAdmin && isScrewdriverAdmin) {
187
+ if (hasNonStateFields) {
188
+ throw boom.forbidden(`User ${username} is only allowed to update the state of this pipeline`);
189
+ }
190
+
191
+ if (!state) {
192
+ throw boom.forbidden(`User ${username} is not an admin of these repos`);
193
+ }
194
+
195
+ applyStateChange(oldPipeline, state, username, stateChangeMessage);
196
+ const updatedPipeline = await oldPipeline.update();
197
+
198
+ return h.response(updatedPipeline.toJson()).code(200);
102
199
  }
103
200
 
104
201
  let token;
@@ -143,7 +240,7 @@ module.exports = () => ({
143
240
  oldPipeline.name = scmRepo.name;
144
241
  }
145
242
 
146
- if (!oldPermissions.admin) {
243
+ if (!isPipelineAdmin) {
147
244
  throw boom.forbidden(`User ${username} is not an admin of these repos`);
148
245
  }
149
246
 
@@ -159,6 +256,10 @@ module.exports = () => ({
159
256
  oldPipeline.settings = { ...oldPipeline.settings, ...settings };
160
257
  }
161
258
 
259
+ if (state) {
260
+ applyStateChange(oldPipeline, state, username, stateChangeMessage);
261
+ }
262
+
162
263
  if (checkoutUrl || rootDir) {
163
264
  logger.info(
164
265
  `[Audit] user ${user.username}:${scmContext} updates the scmUri for pipelineID:${id} to ${oldPipeline.scmUri} from ${oldPipelineConfig.scmUri}.`