Skip to content

Commit 88d1497

Browse files
Merge pull request #428 from devtron-labs/feat/approval-policy
feat: add global approval policy and enforcement
2 parents 2d96122 + ca12b59 commit 88d1497

File tree

24 files changed

+339
-244
lines changed

24 files changed

+339
-244
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "1.3.8",
3+
"version": "1.3.9",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",
@@ -10,7 +10,7 @@
1010
],
1111
"repository": {
1212
"type": "git",
13-
"url": "https://github.com/devtron-labs/devtron-fe-common-lib.git"
13+
"url": "git+https://github.com/devtron-labs/devtron-fe-common-lib.git"
1414
},
1515
"author": "Devtron",
1616
"license": "ISC",

src/Assets/Icon/ic-arrow-square-out.svg

Lines changed: 1 addition & 1 deletion
Loading
Lines changed: 4 additions & 0 deletions
Loading

src/Assets/Icon/ic-edit-file.svg

Lines changed: 3 additions & 3 deletions
Loading

src/Assets/Icon/ic-stamp.svg

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/Common/Common.service.ts

Lines changed: 29 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
import { MutableRefObject } from 'react'
1818
import moment from 'moment'
19+
import { sanitizeApprovalConfigData, sanitizeUserApprovalList } from '@Shared/Helpers'
1920
import { PolicyBlockInfo, RuntimeParamsAPIResponseType, RuntimePluginVariables } from '@Shared/types'
20-
import { getIsManualApprovalSpecific, sanitizeUserApprovalConfig, stringComparatorBySortOrder } from '@Shared/Helpers'
2121
import { get, post } from './Api'
2222
import { GitProviderType, ROUTES } from './Constants'
2323
import { getUrlWithSearchParams, showError, sortCallback } from './Helper'
@@ -35,19 +35,13 @@ import {
3535
CDMaterialFilterQuery,
3636
ImagePromotionMaterialInfo,
3737
EnvironmentListHelmResponse,
38-
UserGroupApproverType,
39-
ImageApprovalPolicyUserGroupDataType,
40-
ImageApprovalPolicyType,
41-
ImageApprovalUsersInfoDTO,
4238
UserApprovalMetadataType,
43-
UserApprovalConfigType,
4439
CDMaterialListModalServiceUtilProps,
40+
CDMaterialType,
4541
GlobalVariableDTO,
4642
GlobalVariableOptionType,
4743
} from './Types'
4844
import { ApiResourceType } from '../Pages'
49-
import { API_TOKEN_PREFIX } from '@Shared/constants'
50-
import { DefaultUserKey } from '@Shared/types'
5145
import { RefVariableType } from './CIPipeline.Types'
5246

5347
export const getTeamListMin = (): Promise<TeamList> => {
@@ -95,26 +89,12 @@ export function setImageTags(request, pipelineId: number, artifactId: number) {
9589
return post(`${ROUTES.IMAGE_TAGGING}/${pipelineId}/${artifactId}`, request)
9690
}
9791

98-
const sanitizeApprovalConfigFromApprovalMetadata = (
99-
approvalMetadata: UserApprovalMetadataType,
100-
userApprovalConfig: UserApprovalConfigType,
101-
): UserApprovalMetadataType => {
102-
if (!approvalMetadata) {
103-
return null
104-
}
105-
106-
const approvedUsersData = approvalMetadata.approvedUsersData || []
107-
const unsanitizedApprovalConfig = approvalMetadata.approvalConfig || userApprovalConfig
108-
109-
return {
110-
...approvalMetadata,
111-
approvedUsersData: approvedUsersData.map((userData) => ({
112-
...userData,
113-
userGroups: userData.userGroups?.filter((group) => !!group?.identifier && !!group?.name) ?? [],
114-
})),
115-
approvalConfig: sanitizeUserApprovalConfig(unsanitizedApprovalConfig),
116-
}
117-
}
92+
export const sanitizeUserApprovalMetadata = (userApprovalMetadata: UserApprovalMetadataType): UserApprovalMetadataType => ({
93+
...userApprovalMetadata,
94+
hasCurrentUserApproved: userApprovalMetadata?.hasCurrentUserApproved ?? false,
95+
canCurrentUserApprove: userApprovalMetadata?.canCurrentUserApprove ?? false,
96+
approvalConfigData: sanitizeApprovalConfigData(userApprovalMetadata?.approvalConfigData),
97+
})
11898

11999
const sanitizeDeploymentBlockedState = (deploymentBlockedState: PolicyBlockInfo) => {
120100
if (!deploymentBlockedState) {
@@ -137,15 +117,14 @@ const cdMaterialListModal = ({
137117
artifactId,
138118
artifactStatus,
139119
disableDefaultSelection,
140-
userApprovalConfig,
141120
}: CDMaterialListModalServiceUtilProps) => {
142121
if (!artifacts || !artifacts.length) return []
143122

144123
const markFirstSelected = offset === 0
145124
const startIndex = offset
146125
let isImageMarked = disableDefaultSelection
147126

148-
const materials = artifacts.map((material, index) => {
127+
const materials = artifacts.map<CDMaterialType>((material, index) => {
149128
let artifactStatusValue = ''
150129
const filterState = material.filterState ?? FilterStates.ALLOWED
151130

@@ -182,9 +161,8 @@ const cdMaterialListModal = ({
182161
vulnerable: material.vulnerable,
183162
runningOnParentCd: material.runningOnParentCd,
184163
artifactStatus: artifactStatusValue,
185-
userApprovalMetadata: sanitizeApprovalConfigFromApprovalMetadata(
164+
userApprovalMetadata: sanitizeUserApprovalMetadata(
186165
material.userApprovalMetadata,
187-
userApprovalConfig,
188166
),
189167
triggeredBy: material.triggeredBy,
190168
isVirtualEnvironment: material.isVirtualEnvironment,
@@ -231,110 +209,38 @@ const cdMaterialListModal = ({
231209
return materials
232210
}
233211

234-
const getImageApprovalPolicyDetailsFromMaterialResult = (cdMaterialsResult): ImageApprovalPolicyType => {
235-
const approvalUsers: string[] = cdMaterialsResult.approvalUsers || []
236-
const userApprovalConfig = sanitizeUserApprovalConfig(cdMaterialsResult.userApprovalConfig)
237-
const isPolicyConfigured = getIsManualApprovalSpecific(userApprovalConfig)
238-
const imageApprovalUsersInfo: ImageApprovalUsersInfoDTO = cdMaterialsResult.imageApprovalUsersInfo || {}
239-
240-
const approvalUsersMap = approvalUsers.reduce(
241-
(acc, user) => {
242-
acc[user] = true
243-
return acc
212+
const sanitizeDeploymentApprovalInfo = (
213+
deploymentApprovalInfo: CDMaterialsApprovalInfo['deploymentApprovalInfo'],
214+
): CDMaterialsApprovalInfo['deploymentApprovalInfo'] => ({
215+
eligibleApprovers: {
216+
anyUsers: {
217+
approverList: sanitizeUserApprovalList(deploymentApprovalInfo?.eligibleApprovers?.anyUsers?.approverList),
244218
},
245-
{} as Record<string, true>,
246-
)
247-
248-
const specificUsersAPIToken = userApprovalConfig.specificUsers.identifiers
249-
.filter((user) => user.startsWith(API_TOKEN_PREFIX))
250-
.sort(stringComparatorBySortOrder)
251-
const specificUsersEmails = userApprovalConfig.specificUsers.identifiers
252-
.filter((user) => !user.startsWith(API_TOKEN_PREFIX) && user !== DefaultUserKey.system)
253-
.sort(stringComparatorBySortOrder)
254-
255-
const specificUsersData: ImageApprovalPolicyType['specificUsersData'] = {
256-
dataStore: userApprovalConfig.specificUsers.identifiers.reduce(
257-
(acc, email) => {
258-
acc[email] = {
259-
email,
260-
hasAccess: approvalUsersMap[email] ?? false,
261-
}
262-
return acc
263-
},
264-
{} as Record<string, UserGroupApproverType>,
265-
),
266-
requiredCount: userApprovalConfig.specificUsers.requiredCount,
267-
emails: specificUsersEmails.concat(specificUsersAPIToken),
268-
}
269-
270-
const validGroups = userApprovalConfig.userGroups.map((group) => group.identifier)
271-
272-
// Have moved from Object.keys(imageApprovalUsersInfo) to approvalUsers since backend is not filtering out the users without approval
273-
// TODO: This check should be on BE. Need to remove this once BE is updated
274-
const usersList = approvalUsers.filter((user) => user !== DefaultUserKey.system)
275-
const groupIdentifierToUsersMap = usersList.reduce(
276-
(acc, user) => {
277-
const userGroups = imageApprovalUsersInfo[user] || []
278-
userGroups.forEach((group) => {
279-
if (!acc[group.identifier]) {
280-
acc[group.identifier] = {}
281-
}
282-
acc[group.identifier][user] = true
283-
})
284-
return acc
219+
specificUsers: {
220+
approverList: sanitizeUserApprovalList(deploymentApprovalInfo?.eligibleApprovers?.specificUsers?.approverList),
285221
},
286-
{} as Record<string, Record<string, true>>,
287-
)
288-
289-
return {
290-
isPolicyConfigured,
291-
specificUsersData,
292-
userGroupData: userApprovalConfig.userGroups.reduce(
293-
(acc, group) => {
294-
const identifier = group.identifier
295-
// No need of handling api tokens here since they are not part of user groups
296-
const users = Object.keys(groupIdentifierToUsersMap[identifier] || {}).sort(stringComparatorBySortOrder)
297-
298-
acc[identifier] = {
299-
dataStore: users.reduce(
300-
(acc, user) => {
301-
acc[user] = {
302-
email: user,
303-
// As of now it will always be true, but UI has handled it in a way that can support false as well
304-
hasAccess: approvalUsersMap[user] ?? false,
305-
}
306-
return acc
307-
},
308-
{} as Record<string, UserGroupApproverType>,
309-
),
310-
requiredCount: group.requiredCount,
311-
emails: users,
312-
}
313-
314-
return acc
315-
},
316-
{} as Record<string, ImageApprovalPolicyUserGroupDataType>,
222+
userGroups: (deploymentApprovalInfo?.eligibleApprovers?.userGroups ?? []).map(
223+
({ groupName, groupIdentifier, approverList }) => ({
224+
groupIdentifier,
225+
groupName,
226+
approverList: sanitizeUserApprovalList(approverList),
227+
}),
317228
),
318-
// Not sorting since would change them in approval info modal to name
319-
validGroups,
320-
}
321-
}
229+
},
230+
approvalConfigData: sanitizeApprovalConfigData(deploymentApprovalInfo?.approvalConfigData),
231+
})
322232

323233
const processCDMaterialsApprovalInfo = (enableApproval: boolean, cdMaterialsResult): CDMaterialsApprovalInfo => {
324234
if (!enableApproval || !cdMaterialsResult) {
325235
return {
326-
approvalUsers: [],
327-
userApprovalConfig: null,
328236
canApproverDeploy: cdMaterialsResult?.canApproverDeploy ?? false,
329-
imageApprovalPolicyDetails: null,
237+
deploymentApprovalInfo: null,
330238
}
331239
}
332240

333241
return {
334-
approvalUsers: cdMaterialsResult.approvalUsers,
335-
userApprovalConfig: sanitizeUserApprovalConfig(cdMaterialsResult.userApprovalConfig),
336242
canApproverDeploy: cdMaterialsResult.canApproverDeploy ?? false,
337-
imageApprovalPolicyDetails: getImageApprovalPolicyDetailsFromMaterialResult(cdMaterialsResult),
243+
deploymentApprovalInfo: sanitizeDeploymentApprovalInfo(cdMaterialsResult.deploymentApprovalInfo),
338244
}
339245
}
340246

@@ -401,7 +307,6 @@ export const processCDMaterialServiceResponse = (
401307
artifactId: cdMaterialsResult.latest_wf_artifact_id,
402308
artifactStatus: cdMaterialsResult.latest_wf_artifact_status,
403309
disableDefaultSelection,
404-
userApprovalConfig: cdMaterialsResult.userApprovalConfig,
405310
})
406311
const approvalInfo = processCDMaterialsApprovalInfo(
407312
stage === DeploymentNodeType.CD || stage === DeploymentNodeType.APPROVAL,

src/Common/Helper.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,12 @@ import { getIsRequestAborted } from './Api'
5656
export function showError(serverError, showToastOnUnknownError = true, hideAccessError = false) {
5757
if (serverError instanceof ServerErrors && Array.isArray(serverError.errors)) {
5858
serverError.errors.map(({ userMessage, internalMessage }) => {
59+
const userMessageInLowercase = userMessage?.toLowerCase()
60+
5961
if (
6062
serverError.code === 403 &&
61-
(userMessage === ERROR_EMPTY_SCREEN.UNAUTHORIZED || userMessage === ERROR_EMPTY_SCREEN.FORBIDDEN)
63+
(userMessageInLowercase === ERROR_EMPTY_SCREEN.UNAUTHORIZED.toLowerCase() ||
64+
userMessageInLowercase === ERROR_EMPTY_SCREEN.FORBIDDEN.toLowerCase())
6265
) {
6366
if (!hideAccessError) {
6467
ToastManager.showToast({
@@ -172,7 +175,7 @@ export const getAlphabetIcon = (str: string, rootClassName: string = '') => {
172175
if (!str) return null
173176
return (
174177
<span
175-
className={`${rootClassName} alphabet-icon__initial fs-13 icon-dim-20 flex cn-0 mr-8`}
178+
className={`${rootClassName} alphabet-icon__initial fs-13 icon-dim-20 flex cn-0 mr-8 dc__no-shrink`}
176179
style={{ backgroundColor: getRandomColor(str) }}
177180
>
178181
{str[0]}
@@ -495,6 +498,7 @@ export const processDeployedTime = (lastDeployed, isArgoInstalled) => {
495498
*/
496499
export const getUrlWithSearchParams = <T extends string | number = string | number>(
497500
url: string,
501+
// FIXME: Need to fix this as the generic typing is incorrect
498502
params = {} as Partial<Record<T, any>>,
499503
) => {
500504
const searchParams = new URLSearchParams()

0 commit comments

Comments
 (0)