Skip to content

Commit 2d9f0fa

Browse files
kyle-ssgZaimwa9matthewelwellpre-commit-ci[bot]
authored
refactor: permission types (#6417)
Co-authored-by: Zaimwa9 <wadii.zaim@flagsmith.com> Co-authored-by: Matthew Elwell <matthew.elwell@flagsmith.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 13ada3b commit 2d9f0fa

41 files changed

Lines changed: 381 additions & 180 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/common/constants.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { OAuthType } from './types/requests'
22
import { SegmentCondition } from './types/responses'
33
import Utils from './utils/utils'
4-
54
import Project from './project'
65
import { integrationCategories } from 'components/pages/IntegrationsPage'
6+
import {
7+
EnvironmentPermission,
8+
EnvironmentPermissionDescriptions,
9+
OrganisationPermission,
10+
OrganisationPermissionDescriptions,
11+
ProjectPermission,
12+
ProjectPermissionDescriptions,
13+
} from './types/permissions.types'
14+
715
const keywords = {
816
FEATURE_FUNCTION: 'myCoolFeature',
917
FEATURE_NAME: 'my_cool_feature',
@@ -252,8 +260,9 @@ const Constants = {
252260
value: '',
253261
} as SegmentCondition,
254262
defaultTagColor: '#3d4db6',
255-
environmentPermissions: (perm: string) =>
256-
`To manage this feature you need the <i>${perm}</i> permission for this environment.<br/>Please contact a member of this environment who has administrator privileges.`,
263+
environmentPermissions: (perm: EnvironmentPermission) => {
264+
return `To manage this feature you need the <i>${EnvironmentPermissionDescriptions[perm]}</i> permission for this environment.<br/>Please contact a member of this environment who has administrator privileges.`
265+
},
257266
events: {
258267
'ACCEPT_INVITE': (org: any) => ({
259268
'category': 'Invite',
@@ -626,8 +635,9 @@ const Constants = {
626635
modals: {
627636
'PAYMENT': 'Payment Modal',
628637
},
629-
organisationPermissions: (perm: string) =>
630-
`To manage this feature you need the <i>${perm}</i> permission for this organisastion.<br/>Please contact a member of this organisation who has administrator privileges.`,
638+
organisationPermissions: (perm: OrganisationPermission) => {
639+
return `To manage this feature you need the <i>${OrganisationPermissionDescriptions[perm]}</i> permission for this organisation.<br/>Please contact a member of this organisation who has administrator privileges.`
640+
},
631641
pages: {
632642
'ACCOUNT': 'Account Page',
633643
'AUDIT_LOG': 'Audit Log Page',
@@ -659,8 +669,9 @@ const Constants = {
659669
'#FFBE71',
660670
'#F57C78',
661671
],
662-
projectPermissions: (perm: string) =>
663-
`To use this feature you need the <i>${perm}</i> permission for this project.<br/>Please contact a member of this project who has administrator privileges.`,
672+
projectPermissions: (perm: ProjectPermission) => {
673+
return `To use this feature you need the <i>${ProjectPermissionDescriptions[perm]}</i> permission for this project.<br/>Please contact a member of this project who has administrator privileges.`
674+
},
664675
resourceTypes: {
665676
GITHUB_ISSUE: {
666677
id: 1,

frontend/common/providers/Permission.tsx

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,22 @@
11
import React, { FC, ReactNode, useMemo } from 'react'
22
import { useGetPermissionQuery } from 'common/services/usePermission'
3-
import { PermissionLevel } from 'common/types/requests'
43
import AccountStore from 'common/stores/account-store'
54
import intersection from 'lodash/intersection'
65
import { cloneDeep } from 'lodash'
76
import Utils from 'common/utils/utils'
87
import Constants from 'common/constants'
8+
import {
9+
EnvironmentPermission,
10+
OrganisationPermission,
11+
ProjectPermission,
12+
} from 'common/types/permissions.types'
913

1014
/**
11-
* Props for the Permission component
12-
*
13-
* @property {number | string} id - The ID of the resource (projectId, organisationId, environmentId, etc.)
14-
* @property {string} permission - The permission key to check (e.g., 'CREATE_FEATURE', 'UPDATE_FEATURE')
15-
* @property {PermissionLevel} level - The permission level ('project', 'organisation', 'environment')
16-
* @property {number[]} [tags] - Optional tag IDs for tag-based permission checking
17-
* @property {ReactNode | ((data: { permission: boolean; isLoading: boolean }) => ReactNode)} children - Content to render or render function
18-
* @property {ReactNode} [fallback] - Optional content to render when permission is denied
19-
* @property {string} [permissionName] - Optional custom permission name for tooltip display
20-
* @property {boolean} [showTooltip=false] - Whether to show a tooltip when permission is denied
15+
* Base props shared across all permission levels
2116
*/
22-
type PermissionType = {
17+
type BasePermissionProps = {
2318
id: number | string
24-
permission: string
2519
tags?: number[]
26-
level: PermissionLevel
2720
children:
2821
| ReactNode
2922
| ((data: { permission: boolean; isLoading: boolean }) => ReactNode)
@@ -32,6 +25,37 @@ type PermissionType = {
3225
showTooltip?: boolean
3326
}
3427

28+
/**
29+
* Discriminated union types for each permission level
30+
* This means we can detect a mismatch between level and permission
31+
*/
32+
type OrganisationLevelProps = BasePermissionProps & {
33+
level: 'organisation'
34+
permission: OrganisationPermission
35+
}
36+
37+
type ProjectLevelProps = BasePermissionProps & {
38+
level: 'project'
39+
permission: ProjectPermission
40+
}
41+
42+
type EnvironmentLevelProps = BasePermissionProps & {
43+
level: 'environment'
44+
permission: EnvironmentPermission
45+
}
46+
47+
type PermissionType =
48+
| OrganisationLevelProps
49+
| ProjectLevelProps
50+
| EnvironmentLevelProps
51+
52+
type UseHasPermissionParams = {
53+
id: number | string
54+
level: 'organisation' | 'project' | 'environment'
55+
permission: OrganisationPermission | ProjectPermission | EnvironmentPermission
56+
tags?: number[]
57+
}
58+
3559
/**
3660
* Hook to check if the current user has a specific permission
3761
*
@@ -54,12 +78,15 @@ export const useHasPermission = ({
5478
level,
5579
permission,
5680
tags,
57-
}: Omit<PermissionType, 'children'>) => {
81+
}: UseHasPermissionParams) => {
5882
const {
5983
data: permissionsData,
6084
isLoading,
6185
isSuccess,
62-
} = useGetPermissionQuery({ id: `${id}`, level }, { skip: !id || !level })
86+
} = useGetPermissionQuery(
87+
{ id: id as number, level },
88+
{ skip: !id || !level },
89+
)
6390
const data = useMemo(() => {
6491
if (!tags?.length || !permissionsData?.tag_based_permissions)
6592
return permissionsData
@@ -154,6 +181,23 @@ const Permission: FC<PermissionType> = ({
154181

155182
const finalPermission = hasPermission || AccountStore.isAdmin()
156183

184+
const getPermissionDescription = (): string => {
185+
switch (level) {
186+
case 'environment':
187+
return Constants.environmentPermissions(
188+
permission as EnvironmentPermission,
189+
)
190+
case 'project':
191+
return Constants.projectPermissions(permission as ProjectPermission)
192+
default:
193+
return Constants.organisationPermissions(
194+
permission as OrganisationPermission,
195+
)
196+
}
197+
}
198+
199+
const tooltipMessage = permissionName || getPermissionDescription()
200+
157201
if (typeof children === 'function') {
158202
const renderedChildren = children({
159203
isLoading,
@@ -166,7 +210,7 @@ const Permission: FC<PermissionType> = ({
166210

167211
return Utils.renderWithPermission(
168212
finalPermission,
169-
permissionName || Constants.projectPermissions(permission),
213+
tooltipMessage,
170214
renderedChildren,
171215
)
172216
}
@@ -176,11 +220,7 @@ const Permission: FC<PermissionType> = ({
176220
}
177221

178222
if (showTooltip) {
179-
return Utils.renderWithPermission(
180-
finalPermission,
181-
permissionName || Constants.projectPermissions(permission),
182-
children,
183-
)
223+
return Utils.renderWithPermission(finalPermission, tooltipMessage, children)
184224
}
185225

186226
return <>{fallback || null}</>

frontend/common/stores/config-store.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ flagsmith
4040
AsyncStorage,
4141
api: Project.flagsmithClientAPI,
4242
cacheFlags: true,
43-
enableAnalytics: Project.flagsmithAnalytics,
43+
enableAnalytics: window.E2E ? false : Project.flagsmithAnalytics,
4444
environmentID: Project.flagsmith,
4545
onChange: controller.loaded,
4646
realtime: window.E2E ? false : Project.flagsmithRealtime,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Organization Permissions
2+
export enum OrganisationPermission {
3+
ADMIN = 'ADMIN',
4+
CREATE_PROJECT = 'CREATE_PROJECT',
5+
MANAGE_USERS = 'MANAGE_USERS',
6+
MANAGE_USER_GROUPS = 'MANAGE_USER_GROUPS',
7+
}
8+
export const OrganisationPermissionDescriptions: Record<
9+
OrganisationPermission,
10+
string
11+
> = {
12+
[OrganisationPermission.ADMIN]: 'Administrator',
13+
[OrganisationPermission.CREATE_PROJECT]: 'Create project',
14+
[OrganisationPermission.MANAGE_USERS]: 'Manage users',
15+
[OrganisationPermission.MANAGE_USER_GROUPS]: 'Manage user groups',
16+
} as const
17+
18+
// Project Permissions
19+
export enum ProjectPermission {
20+
ADMIN = 'ADMIN',
21+
VIEW_PROJECT = 'VIEW_PROJECT',
22+
CREATE_ENVIRONMENT = 'CREATE_ENVIRONMENT',
23+
DELETE_FEATURE = 'DELETE_FEATURE',
24+
CREATE_FEATURE = 'CREATE_FEATURE',
25+
MANAGE_SEGMENTS = 'MANAGE_SEGMENTS',
26+
VIEW_AUDIT_LOG = 'VIEW_AUDIT_LOG',
27+
MANAGE_TAGS = 'MANAGE_TAGS',
28+
MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS = 'MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS',
29+
APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS = 'APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS',
30+
CREATE_PROJECT_LEVEL_CHANGE_REQUESTS = 'CREATE_PROJECT_LEVEL_CHANGE_REQUESTS',
31+
}
32+
export const ProjectPermissionDescriptions: Record<ProjectPermission, string> =
33+
{
34+
[ProjectPermission.ADMIN]: 'Administrator',
35+
[ProjectPermission.VIEW_PROJECT]: 'View project',
36+
[ProjectPermission.CREATE_ENVIRONMENT]: 'Create environment',
37+
[ProjectPermission.DELETE_FEATURE]: 'Delete feature',
38+
[ProjectPermission.CREATE_FEATURE]: 'Create feature',
39+
[ProjectPermission.MANAGE_SEGMENTS]: 'Manage segments',
40+
[ProjectPermission.VIEW_AUDIT_LOG]: 'View audit log',
41+
[ProjectPermission.MANAGE_TAGS]: 'Manage tags',
42+
[ProjectPermission.MANAGE_PROJECT_LEVEL_CHANGE_REQUESTS]:
43+
'Manage project level change requests',
44+
[ProjectPermission.APPROVE_PROJECT_LEVEL_CHANGE_REQUESTS]:
45+
'Approve project level change requests',
46+
[ProjectPermission.CREATE_PROJECT_LEVEL_CHANGE_REQUESTS]:
47+
'Create project level change requests',
48+
} as const
49+
50+
// Environment Permissions
51+
export enum EnvironmentPermission {
52+
ADMIN = 'ADMIN',
53+
VIEW_ENVIRONMENT = 'VIEW_ENVIRONMENT',
54+
UPDATE_FEATURE_STATE = 'UPDATE_FEATURE_STATE',
55+
MANAGE_IDENTITIES = 'MANAGE_IDENTITIES',
56+
CREATE_CHANGE_REQUEST = 'CREATE_CHANGE_REQUEST',
57+
APPROVE_CHANGE_REQUEST = 'APPROVE_CHANGE_REQUEST',
58+
VIEW_IDENTITIES = 'VIEW_IDENTITIES',
59+
MANAGE_SEGMENT_OVERRIDES = 'MANAGE_SEGMENT_OVERRIDES',
60+
}
61+
export const EnvironmentPermissionDescriptions: Record<
62+
EnvironmentPermission,
63+
string
64+
> = {
65+
[EnvironmentPermission.ADMIN]: 'Administrator',
66+
[EnvironmentPermission.VIEW_ENVIRONMENT]: 'View environment',
67+
[EnvironmentPermission.UPDATE_FEATURE_STATE]: 'Update feature state',
68+
[EnvironmentPermission.MANAGE_IDENTITIES]: 'Manage identities',
69+
[EnvironmentPermission.CREATE_CHANGE_REQUEST]: 'Create change request',
70+
[EnvironmentPermission.APPROVE_CHANGE_REQUEST]: 'Approve change request',
71+
[EnvironmentPermission.VIEW_IDENTITIES]: 'View identities',
72+
[EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES]: 'Manage segment overrides',
73+
} as const
74+
75+
export type Permission =
76+
| OrganisationPermission
77+
| ProjectPermission
78+
| EnvironmentPermission
79+
80+
// Combined permission descriptions record
81+
export const PermissionDescriptions: Record<Permission, string> = {
82+
...OrganisationPermissionDescriptions,
83+
...ProjectPermissionDescriptions,
84+
...EnvironmentPermissionDescriptions,
85+
} as const

frontend/common/utils/utils.tsx

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import {
99
MultivariateFeatureStateValue,
1010
MultivariateOption,
1111
Organisation,
12+
PConfidence,
1213
Project as ProjectType,
1314
ProjectFlag,
1415
SegmentCondition,
1516
Tag,
16-
PConfidence,
1717
UserPermissions,
1818
} from 'common/types/responses'
1919
import flagsmith from '@flagsmith/flagsmith'
@@ -28,6 +28,12 @@ import { selectBuildVersion } from 'common/services/useBuildVersion'
2828
import { getStore } from 'common/store'
2929
import { TRACKED_UTMS, UtmsType } from 'common/types/utms'
3030
import { TimeUnit } from 'components/release-pipelines/constants'
31+
import {
32+
EnvironmentPermission,
33+
EnvironmentPermissionDescriptions,
34+
OrganisationPermission,
35+
OrganisationPermissionDescriptions,
36+
} from 'common/types/permissions.types'
3137

3238
const semver = require('semver')
3339

@@ -226,15 +232,17 @@ const Utils = Object.assign({}, require('./base/_utils'), {
226232
},
227233
getCreateProjectPermission(organisation: Organisation) {
228234
if (organisation?.restrict_project_create_to_admin) {
229-
return 'ADMIN'
235+
return OrganisationPermission.ADMIN
230236
}
231-
return 'CREATE_PROJECT'
237+
return OrganisationPermission.CREATE_PROJECT
232238
},
233239
getCreateProjectPermissionDescription(organisation: Organisation) {
234240
if (organisation?.restrict_project_create_to_admin) {
235-
return 'Administrator'
241+
return OrganisationPermissionDescriptions[OrganisationPermission.ADMIN]
236242
}
237-
return 'Create Project'
243+
return OrganisationPermissionDescriptions[
244+
OrganisationPermission.CREATE_PROJECT
245+
]
238246
},
239247
getExistingWaitForTime: (
240248
waitFor: string | undefined,
@@ -384,22 +392,15 @@ const Utils = Object.assign({}, require('./base/_utils'), {
384392
},
385393
getManageFeaturePermission(isChangeRequest: boolean) {
386394
if (isChangeRequest) {
387-
return 'CREATE_CHANGE_REQUEST'
395+
return EnvironmentPermission.CREATE_CHANGE_REQUEST
388396
}
389-
return 'UPDATE_FEATURE_STATE'
397+
return EnvironmentPermission.UPDATE_FEATURE_STATE
390398
},
391399
getManageFeaturePermissionDescription(isChangeRequest: boolean) {
392400
if (isChangeRequest) {
393-
return 'Create Change Request'
401+
return EnvironmentPermissionDescriptions.CREATE_CHANGE_REQUEST
394402
}
395-
return 'Update Feature State'
396-
},
397-
398-
getManageUserPermission() {
399-
return 'MANAGE_IDENTITIES'
400-
},
401-
getManageUserPermissionDescription() {
402-
return 'Manage Identities'
403+
return EnvironmentPermissionDescriptions.UPDATE_FEATURE_STATE
403404
},
404405

405406
getNextPlan: (skipFree?: boolean) => {
@@ -431,7 +432,12 @@ const Utils = Object.assign({}, require('./base/_utils'), {
431432
const organisationId = match?.params?.organisationId
432433
return organisationId ? parseInt(organisationId) : null
433434
},
434-
getOverridePermission: (level: 'identity' | 'segment') => {
435+
getOverridePermission: (
436+
level: 'identity' | 'segment',
437+
): {
438+
permission: EnvironmentPermission
439+
permissionDescription: string
440+
} => {
435441
switch (level) {
436442
case 'identity':
437443
return {
@@ -441,8 +447,9 @@ const Utils = Object.assign({}, require('./base/_utils'), {
441447
}
442448
default:
443449
return {
444-
permission: 'MANAGE_SEGMENT_OVERRIDES',
445-
permissionDescription: 'Manage Segment Overrides',
450+
permission: EnvironmentPermission.MANAGE_SEGMENT_OVERRIDES,
451+
permissionDescription:
452+
EnvironmentPermissionDescriptions.MANAGE_SEGMENT_OVERRIDES,
446453
}
447454
}
448455
},
@@ -632,9 +639,6 @@ const Utils = Object.assign({}, require('./base/_utils'), {
632639
return utms
633640
}, {} as UtmsType)
634641
},
635-
getViewIdentitiesPermission() {
636-
return 'VIEW_IDENTITIES'
637-
},
638642

639643
hasEntityPermission(key: string, entityPermissions: UserPermissions) {
640644
if (entityPermissions?.admin) return true

0 commit comments

Comments
 (0)