Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tag based permissions #4853

Merged
merged 29 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
83b3b2e
Add tag based permissions
kyle-ssg Sep 18, 2024
c8d4532
Merge branch 'refs/heads/main' into feat/tag-based-permissions
kyle-ssg Oct 8, 2024
58e6478
Merge
kyle-ssg Oct 8, 2024
dcef5ae
Tag based permissions flag
kyle-ssg Nov 5, 2024
82519f2
Add tag based permissions ui
kyle-ssg Nov 5, 2024
716706b
tag based role ui
kyle-ssg Nov 6, 2024
2c71cfa
Add tag_based_permissions logic to frontend
kyle-ssg Nov 6, 2024
3634609
Merge branch 'refs/heads/main' into feat/tag-based-permissions-valida…
kyle-ssg Nov 12, 2024
985cceb
Adjust tag base permissions data structure
kyle-ssg Nov 12, 2024
c09d39b
Adjust tag base permissions data structure
kyle-ssg Nov 12, 2024
a940e9a
Better permissions view for create feature
kyle-ssg Nov 12, 2024
750054b
Merge branch 'refs/heads/main' into feat/tag-based-permissions
kyle-ssg Nov 12, 2024
1837651
Merge branch 'refs/heads/main' into feat/tag-based-permissions
kyle-ssg Nov 12, 2024
48d54c5
Remove static tag_based variable
kyle-ssg Nov 12, 2024
f962c1c
Remove incorrect function call
kyle-ssg Nov 12, 2024
243c7a4
Adjust doc wording
kyle-ssg Nov 12, 2024
9ad625f
Merge branch 'refs/heads/main' into feat/tag-based-permissions
kyle-ssg Nov 13, 2024
bf5d177
Permissions v2
kyle-ssg Nov 13, 2024
72e5d8c
Tag based permissions UI
kyle-ssg Nov 19, 2024
195c400
Merge branch 'refs/heads/main' into feat/tag-based-permissions-v2
kyle-ssg Nov 19, 2024
d2b7011
Interop edit permissions / role-based permissions
kyle-ssg Nov 19, 2024
8482f89
Adjust UI for limited permissions
kyle-ssg Nov 19, 2024
29051be
LIMITED > GRANTED_FOR_TAGS
kyle-ssg Nov 19, 2024
23f5d18
Merge branch 'refs/heads/main' into feat/tag-based-permissions-v2
kyle-ssg Nov 27, 2024
7b2f458
Fix
kyle-ssg Nov 27, 2024
050f305
Integrate new API
kyle-ssg Dec 3, 2024
ae40fbc
Merge branch 'refs/heads/main' into feat/tag-based-permissions-v2
kyle-ssg Dec 3, 2024
f347559
Update docs
kyle-ssg Dec 3, 2024
104da33
Add test ids / fix environment and project settings pages
kyle-ssg Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 26 additions & 19 deletions docs/docs/system-administration/rbac.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ Assigning roles to groups has several benefits over assigning permissions direct

Permissions can be assigned at four levels: user group, organisation, project, and environment.

## Tagged Permissions

When creating a role, some permissions allow you to grant access when features have specific tags. For example, you can
configure a role to create change requests only for features tagged with "marketing".

### User group

| Permission | Ability |
Expand All @@ -149,25 +154,27 @@ Permissions can be assigned at four levels: user group, organisation, project, a

### Project

| Permission | Ability |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Administrator | Grants full read and write access to all environments, features and segments. |
| View Project | Allows viewing this project. The project is hidden from users without this permission. |
| Create Environment | Allows creating new environments in this project. Users are automatically granted Administrator permissions on any environments they create. |
| Create Feature | Allows creating new features in all environments. |
| Delete Feature | Allows deleting features from all environments. |
| Manage Segments | Grants write access to segments in this project. |
| View audit log | Allows viewing all audit log entries for this project. |
### Project

| Permission | Ability | Supports Tags |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| Administrator | Grants full read and write access to all environments, features, and segments. | |
| View Project | Allows viewing this project. The project is hidden from users without this permission. | |
| Create Environment | Allows creating new environments in this project. Users are automatically granted Administrator permissions on any environments they create. | |
| Create Feature | Allows creating new features in all environments. | |
| Delete Feature | Allows deleting features from all environments. | Yes |
| Manage Segments | Grants write access to segments in this project. | |
| View audit log | Allows viewing all audit log entries for this project. | |

### Environment

| Permission | Ability |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| Administrator | Grants full read and write access to all feature states, overrides, identities and change requests in this environment. |
| View Environment | Allows viewing this environment. The environment is hidden from users without this permission. |
| Update Feature State | Allows updating updating any feature state or values in this environment. |
| Manage Identities | Grants read and write access to identities in this environment. |
| Manage Segment Overrides | Grants write access to segment overrides in this environment. |
| Create Change Request | Allows creating change requests for features in this environment. |
| Approve Change Request | Allows approving or denying change requests in this environment. |
| View Identities | Grants read-only access to identities in this environment. |
| Permission | Ability | Supports Tags |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------------- |
| Administrator | Grants full read and write access to all feature states, overrides, identities, and change requests in this environment. | |
| View Environment | Allows viewing this environment. The environment is hidden from users without this permission. | |
| Update Feature State | Allows updating any feature state or values in this environment. | Yes |
| Manage Identities | Grants read and write access to identities in this environment. | |
| Manage Segment Overrides | Grants write access to segment overrides in this environment. | |
| Create Change Request | Allows creating change requests for features in this environment. | Yes |
| Approve Change Request | Allows approving or denying change requests in this environment. | Yes |
| View Identities | Grants read-only access to identities in this environment. | |
4 changes: 4 additions & 0 deletions frontend/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ app.get('/config/project-overrides', (req, res) => {
name: 'githubAppURL',
value: process.env.GITHUB_APP_URL,
},
{
name: 'e2eToken',
value: process.env.E2E_TEST_TOKEN || '',
},
]
let output = values.map(getVariable).join('')
let dynatrace = ''
Expand Down
33 changes: 27 additions & 6 deletions frontend/common/providers/Permission.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import React, { FC, ReactNode } from 'react'
import React, { FC, ReactNode, useMemo } from 'react'
import { useGetPermissionQuery } from 'common/services/usePermission'
import { PermissionLevel } from 'common/types/requests'
import AccountStore from 'common/stores/account-store' // we need this to make JSX compile
import AccountStore from 'common/stores/account-store'
import intersection from 'lodash/intersection'
import { add } from 'ionicons/icons';
import { cloneDeep } from 'lodash'; // we need this to make JSX compile

type PermissionType = {
id: any
permission: string
tags?: number[]
level: PermissionLevel
children: (data: { permission: boolean; isLoading: boolean }) => ReactNode
}
Expand All @@ -14,11 +18,26 @@ export const useHasPermission = ({
id,
level,
permission,
tags,
}: Omit<PermissionType, 'children'>) => {
const { data, isLoading, isSuccess } = useGetPermissionQuery(
{ id: `${id}`, level },
{ skip: !id || !level },
)
const {
data: permissionsData,
isLoading,
isSuccess,
} = useGetPermissionQuery({ id: `${id}`, level }, { skip: !id || !level })
const data = useMemo(() => {
if (!tags?.length || !permissionsData?.tag_based_permissions)
return permissionsData
const addedPermissions = cloneDeep(permissionsData)
permissionsData.tag_based_permissions.forEach((tagBasedPermission) => {
if (intersection(tagBasedPermission.tags, tags).length) {
tagBasedPermission.permissions.forEach((key) => {
addedPermissions[key] = true
})
}
})
return addedPermissions
}, [permissionsData, tags])
const hasPermission = !!data?.[permission] || !!data?.ADMIN
return {
isLoading,
Expand All @@ -32,11 +51,13 @@ const Permission: FC<PermissionType> = ({
id,
level,
permission,
tags,
}) => {
const { isLoading, permission: hasPermission } = useHasPermission({
id,
level,
permission,
tags,
})
return (
<>
Expand Down
17 changes: 13 additions & 4 deletions frontend/common/services/usePermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,25 @@ export const permissionService = service
query: ({ id, level }: Req['getPermission']) => ({
url: `${level}s/${id}/my-permissions/`,
}),
transformResponse(baseQueryReturnValue: {
admin: boolean
permissions: string[]
}) {
transformResponse(
baseQueryReturnValue: {
admin: boolean
permissions: string[]
tag_based_permissions?: Res['permission']['tag_based_permissions']
},
_,
) {
const res: Res['permission'] = {
ADMIN: baseQueryReturnValue.admin,
}
if (baseQueryReturnValue.tag_based_permissions) {
res.tag_based_permissions =
baseQueryReturnValue.tag_based_permissions
}
baseQueryReturnValue.permissions.forEach((v) => {
res[v] = true
})

return res
},
}),
Expand Down
2 changes: 1 addition & 1 deletion frontend/common/services/useProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const projectService = service
query: (data) => ({
url: `projects/?organisation=${data.organisationId}`,
}),
transformResponse: (res) => sortBy(res, 'name'),
transformResponse: (res) => sortBy(res, (v) => v.name.toLowerCase()),
}),
// END OF ENDPOINTS
}),
Expand Down
6 changes: 5 additions & 1 deletion frontend/common/services/useRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const roleService = service
}),
}),
getRole: builder.query<Res['role'], Req['getRole']>({
providesTags: (res) => [{ id: res?.id, type: 'Role' }],
query: (query: Req['getRole']) => ({
url: `organisations/${query.organisation_id}/roles/${query.role_id}/`,
}),
Expand All @@ -33,7 +34,10 @@ export const roleService = service
}),
}),
updateRole: builder.mutation<Res['roles'], Req['updateRole']>({
invalidatesTags: (res) => [{ id: 'LIST', type: 'Role' }],
invalidatesTags: (res, _, req) => [
{ id: 'LIST', type: 'Role' },
{ id: req.role_id, type: 'Role' },
],
query: (query: Req['updateRole']) => ({
body: query.body,
method: 'PUT',
Expand Down
10 changes: 2 additions & 8 deletions frontend/common/services/useRolePermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,12 @@ export async function getRoleProjectPermissions(
typeof rolePermissionService.endpoints.getRoleProjectPermissions.initiate
>[1],
) {
store.dispatch(
return store.dispatch(
rolePermissionService.endpoints.getRoleProjectPermissions.initiate(
data,
options,
),
)
return Promise.all(
store.dispatch(rolePermissionService.util.getRunningQueriesThunk()),
)
}

export async function getRoleEnvironmentPermissions(
Expand All @@ -139,15 +136,12 @@ export async function getRoleEnvironmentPermissions(
typeof rolePermissionService.endpoints.getRoleEnvironmentPermissions.initiate
>[1],
) {
store.dispatch(
return store.dispatch(
rolePermissionService.endpoints.getRoleEnvironmentPermissions.initiate(
data,
options,
),
)
return Promise.all(
store.dispatch(rolePermissionService.util.getRunningQueriesThunk()),
)
}

export async function createRolePermissions(
Expand Down
3 changes: 3 additions & 0 deletions frontend/common/stores/account-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import Constants from 'common/constants'
import dataRelay from 'data-relay'
import { sortBy } from 'lodash'
import Project from 'common/project'
import { getStore } from 'common/store'
import { service } from "common/service";

const controller = {
acceptInvite: (id) => {
Expand Down Expand Up @@ -341,6 +343,7 @@ const controller = {
API.reset().finally(() => {
store.model = user
store.organisation = null
getStore().dispatch(service.util.resetApiState())
store.trigger('logout')
})
})
Expand Down
6 changes: 5 additions & 1 deletion frontend/common/stores/organisation-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ const controller = {
)
: ['Development', 'Production']
data
.post(`${Project.api}projects/`, { name, organisation: store.id })
.post(
`${Project.api}projects/`,
{ name, organisation: store.id },
E2E ? { 'X-E2E-Test-Auth-Token': Project.e2eToken } : {},
)
.then((project) => {
Promise.all(
defaultEnvironmentNames.map((envName) => {
Expand Down
6 changes: 4 additions & 2 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
UserGroup,
AttributeName,
Identity,
Role,
RolePermission,
} from './responses'

export type PagedRequest<T> = T & {
Expand Down Expand Up @@ -158,7 +160,7 @@ export type Req = {
updateRole: {
organisation_id: number
role_id: number
body: { description: string | null; name: string }
body: Role
}
deleteRole: { organisation_id: number; role_id: number }
getRolePermissionEnvironment: {
Expand All @@ -179,7 +181,7 @@ export type Req = {
level: PermissionLevel
body: {
admin?: boolean
permissions: string[]
permissions: RolePermission['permissions']
project: number
environment: number
}
Expand Down
14 changes: 11 additions & 3 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,12 @@ export type UserPermission = {
id: number
role?: number
}

export type RolePermission = Omit<UserPermission, 'permissions'> & {
permissions: { permission_key: string; tags: number[] }[]
}
export type GroupPermission = Omit<UserPermission, 'user'> & {
group: UserGroup
group: UserGroupSummary
}

export type AuditLogItem = {
Expand Down Expand Up @@ -328,6 +332,7 @@ export type Identity = {
export type AvailablePermission = {
key: string
description: string
supports_tag: boolean
}

export type APIKey = {
Expand Down Expand Up @@ -657,7 +662,10 @@ export type Res = {
}
identity: { id: string } //todo: we don't consider this until we migrate identity-store
identities: EdgePagedResponse<Identity>
permission: Record<string, boolean>
permission: Record<string, boolean> & {
ADMIN: boolean
tag_based_permissions?: { permissions: string[]; tags: number[] }[]
}
availablePermissions: AvailablePermission[]
tag: Tag
tags: Tag[]
Expand Down Expand Up @@ -695,7 +703,7 @@ export type Res = {
versionFeatureState: FeatureState[]
role: Role
roles: PagedResponse<Role>
rolePermission: PagedResponse<UserPermission>
rolePermission: PagedResponse<RolePermission>
projectFlags: PagedResponse<ProjectFlag>
projectFlag: ProjectFlag
identityFeatureStatesAll: IdentityFeatureState[]
Expand Down
30 changes: 0 additions & 30 deletions frontend/common/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -341,36 +341,6 @@ const Utils = Object.assign({}, require('./base/_utils'), {
return `/organisation/${orgId}/projects`
},

getPermissionList(
isAdmin: boolean,
permissions: string[] | undefined | null,
numberToTruncate = 3,
): {
items: string[]
truncatedItems: string[]
} {
if (isAdmin) {
return {
items: ['Administrator'],
truncatedItems: [],
}
}
if (!permissions) return { items: [], truncatedItems: [] }

const items =
permissions && permissions.length
? permissions
.slice(0, numberToTruncate)
.map((item) => `${Format.enumeration.get(item)}`)
: []

return {
items,
truncatedItems: (permissions || [])
.slice(numberToTruncate)
.map((item) => `${Format.enumeration.get(item)}`),
}
},
getPlanName: (plan: string) => {
if (plan && plan.includes('free')) {
return planNames.free
Expand Down
3 changes: 3 additions & 0 deletions frontend/web/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ const App = class extends Component {
>
<NavLink
id='org-settings-link'
data-test='org-settings-link'
activeClassName='active'
className={classNames(
'breadcrumb-link',
Expand Down Expand Up @@ -571,6 +572,7 @@ const App = class extends Component {
icon={<AuditLogIcon />}
id='audit-log-link'
to={`/project/${projectId}/audit-log`}
data-test='audit-log-link'
>
Audit Log
</NavSubLink>
Expand Down Expand Up @@ -661,6 +663,7 @@ const App = class extends Component {
<NavSubLink
icon={<SettingsIcon />}
id='org-settings-link'
data-test='org-settings-link'
to={`/organisation/${
AccountStore.getOrganisation().id
}/settings`}
Expand Down
Loading
Loading