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: Add tag based permissions #4643

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions docs/docs/system-administration/rbac.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,12 @@ Permissions can be assigned at four levels: user group, organisation, project, a
| 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. |

### Tags

When tags are applied to a role, the following permissions apply:

- If the role does not have project admin permissions, users will only be able to delete features that have a matching
tag.
- For all environments within the project where the role is not set as admin, users will only be able to update Feature
States, Segment Overrides, and Change Requests for features with a matching tag.
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
6 changes: 3 additions & 3 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
ProjectFlag,
Environment,
UserGroup,
AttributeName,
} from './responses'
AttributeName, Role,
} from './responses';

export type PagedRequest<T> = T & {
page?: number
Expand Down Expand Up @@ -157,7 +157,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 Down
1 change: 1 addition & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ export type Account = {
}
export type Role = {
id: number
tags: number[]
name: string
description?: string
organisation: number
Expand Down
46 changes: 25 additions & 21 deletions frontend/web/components/EditPermissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
UserGroupSummary,
UserPermission,
} from 'common/types/responses'
import Utils from 'common/utils/utils'
import AccountStore from 'common/stores/account-store'
import Format from 'common/utils/format'
import PanelSearch from './PanelSearch'
Expand Down Expand Up @@ -131,24 +130,7 @@ const withAdminPermissions = (InnerComponent: any) => {
return WrappedComponent
}
const _EditPermissionsModal: FC<EditPermissionModalType> = withAdminPermissions(
forwardRef((props) => {
const [entityPermissions, setEntityPermissions] =
useState<EntityPermissions>({ admin: false, permissions: [] })
const [parentError, setParentError] = useState(false)
const [saving, setSaving] = useState(false)
const [showRoles, setShowRoles] = useState<boolean>(false)
const [valueChanged, setValueChanged] = useState(false)

const [permissionWasCreated, setPermissionWasCreated] =
useState<boolean>(false)
const [rolesSelected, setRolesSelected] = useState<
{
role: number
user_role_id?: number
group_role_id?: number
}[]
>([])

forwardRef((props: EditPermissionModalType) => {
const {
className,
envId,
Expand All @@ -170,6 +152,30 @@ const _EditPermissionsModal: FC<EditPermissionModalType> = withAdminPermissions(
user,
} = props

const [entityPermissions, setEntityPermissions] =
useState<EntityPermissions>({ admin: false, permissions: [] })
const [parentError, setParentError] = useState(false)
const [saving, setSaving] = useState(false)
const [showRoles, setShowRoles] = useState<boolean>(false)
const [valueChanged, setValueChanged] = useState(false)

const projectId =
props.level === 'project'
? props.id
: props.level === 'environment'
? props.parentId
: undefined

const [permissionWasCreated, setPermissionWasCreated] =
useState<boolean>(false)
const [rolesSelected, setRolesSelected] = useState<
{
role: number
user_role_id?: number
group_role_id?: number
}[]
>([])

const { data: permissions } = useGetAvailablePermissionsQuery({ level })
const { data: userWithRolesData, isSuccess: userWithRolesDataSuccesfull } =
useGetUserWithRolesQuery(
Expand Down Expand Up @@ -670,8 +676,6 @@ const _EditPermissionsModal: FC<EditPermissionModalType> = withAdminPermissions(

const isAdmin = admin()

const [search, setSearch] = useState()

return !permissions || !entityPermissions ? (
<div className='modal-body text-center'>
<Loader />
Expand Down
42 changes: 24 additions & 18 deletions frontend/web/components/PermissionsTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ProjectFilter from './ProjectFilter'
import OrganisationStore from 'common/stores/organisation-store'
import PlanBasedAccess from './PlanBasedAccess'
import WarningMessage from './WarningMessage'
import TagBasedPermissions from './TagBasedPermissions'

type PermissionsTabsType = {
orgId?: number
Expand Down Expand Up @@ -120,7 +121,7 @@ const PermissionsTabs: FC<PermissionsTabsType> = ({
group={group}
orgId={orgId}
filter={searchProject}
mainItems={projectData}
mainItems={projectData.map((v) => ({ ...v, projectId: v.id }))}
role={role}
level={'project'}
ref={tabRef}
Expand Down Expand Up @@ -150,23 +151,28 @@ const PermissionsTabs: FC<PermissionsTabsType> = ({
value={project}
/>
</div>
{environments.length > 0 && (
<RolePermissionsList
user={user}
orgId={orgId}
group={group}
filter={searchEnv}
mainItems={(environments || [])?.map((v) => {
return {
id: role ? v.id : v.api_key,
name: v.name,
}
})}
role={role}
level={'environment'}
ref={tabRef}
/>
)}

<TagBasedPermissions projectId={project} role={role} />
<div className='mt-2'>
{environments.length > 0 && (
<RolePermissionsList
user={user}
orgId={orgId}
group={group}
filter={searchEnv}
mainItems={(environments || [])?.map((v) => {
return {
id: role ? v.id : v.api_key,
name: v.name,
parentId: v.project,
}
})}
role={role}
level={'environment'}
ref={tabRef}
/>
)}
</div>
</TabItem>
</Tabs>
</PlanBasedAccess>
Expand Down
30 changes: 21 additions & 9 deletions frontend/web/components/RolePermissionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { PermissionLevel } from 'common/types/requests'
import { Role, User, UserGroup, UserGroupSummary } from 'common/types/responses'
import PanelSearch from './PanelSearch'
import PermissionsSummaryList from './PermissionsSummaryList'
import TagBasedPermissions from './TagBasedPermissions'

type NameAndId = {
name: string
Expand Down Expand Up @@ -145,15 +146,26 @@ const RolePermissionsList: React.FC<RolePermissionsListProps> = forwardRef(
</Row>
<div>
{expandedItems.includes(mainItem.id) && (
<EditPermissionsModal
id={mainItem.id}
level={level}
role={role}
className='mt-2 px-3'
isGroup={!!group}
group={group}
user={user}
/>
<>
<div className='px-3'>
{level === 'project' && role && (
<TagBasedPermissions
projectId={`${mainItem.id}`}
role={role}
/>
)}
</div>
<EditPermissionsModal
id={mainItem.id}
level={level}
role={role}
className='mt-2 px-3'
isGroup={!!group}
parentId={mainItem.parentId}
group={group}
user={user}
/>
</>
)}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/RolesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const RolesTable: FC<RolesTableType> = ({ organisationId, users }) => {
<CreateRole
organisationId={role.organisation}
isEdit
role={role}
role={role.id}
onComplete={() => {
toast('Role Updated')
}}
Expand Down
109 changes: 109 additions & 0 deletions frontend/web/components/TagBasedPermissions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { FC, useMemo, useState } from 'react'
import InfoMessage from './InfoMessage'
import { useUpdateRoleMutation } from 'common/services/useRole'
import Utils from 'common/utils/utils'
import { useGetTagsQuery } from 'common/services/useTag'
import { Role } from 'common/types/responses'
import Switch from './Switch'
import AddEditTags from './tags/AddEditTags'
import Tooltip from './Tooltip'

type TaggedPermissionsType = {
role: Role | undefined
projectId: string
}

const TagBasedPermissions: FC<TaggedPermissionsType> = ({
projectId,
role,
}) => {
const [updateRole, { isLoading: roleUpdating }] = useUpdateRoleMutation({})
const tag_based_permissions = Utils.getFlagsmithHasFeature(
'tag_based_permissions',
)
const { data: tags } = useGetTagsQuery(
{ projectId: `${projectId}` },
{ skip: !projectId || !role },
)
const showTagBasedPermissions = projectId && tag_based_permissions && !!role
const [tagBasedPermissionsEnabled, setTagBasedPermissionsEnabled] =
useState<boolean>(!!role?.tags?.length)
const matchingTags = useMemo(() => {
if (!role?.tags || !tags?.length) return []
return role.tags.filter((id) => tags?.find((tag) => tag.id === id))
}, [tags, role?.tags])
if (!showTagBasedPermissions) return null
return (
<>
{!!showTagBasedPermissions && !!role && (
<div className='mt-4 mb-2'>
<div className='d-flex align-items-center gap-2 fw-semibold'>
<Switch
disabled={!!matchingTags?.length || roleUpdating}
checked={tagBasedPermissionsEnabled}
onChange={(v: boolean) => {
if (!v) {
setTagBasedPermissionsEnabled(false)
updateRole({
body: {
...role,
tags: role.tags.filter((v) => !matchingTags.includes(v)),
},
organisation_id: role.organisation,
role_id: role.id,
})
} else {
setTagBasedPermissionsEnabled(true)
}
}}
/>
<Tooltip
tooltipClassName='fw-normal'
title={'Restrict permissions to tagged features'}
>
{`This will restrict <strong>Delete Feature</strong>,
<strong>Update Feature State</strong>,
<strong>Segment Overrides</strong> and <strong>Change Requests</strong>
to only features with the assigned tags.
<br /> <br />
<strong>
This will apply across all environments within the project where the
environment admin permission is not enabled.
</strong>`}
</Tooltip>
<a
target='_blank'
href='https://docs.flagsmith.com/system-administration/rbac#tags'
className='fw-normal text-primary'
rel='noreferrer'
>
View Docs
</a>
</div>
</div>
)}
{tagBasedPermissionsEnabled && role && projectId && (
<div className='mb-2'>
<AddEditTags
projectId={projectId}
value={matchingTags}
onChange={(newTags) => {
updateRole({
body: {
...role,
tags: role.tags
.filter((v) => !matchingTags.includes(v))
.concat(newTags),
},
organisation_id: role.organisation,
role_id: role.id,
})
}}
/>
</div>
)}
</>
)
}

export default TagBasedPermissions
Loading
Loading