diff --git a/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx b/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx index 3cacf12808d..6cbdd654104 100644 --- a/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx +++ b/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx @@ -1,6 +1,6 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { checkPermissions } from 'hooks' +import { useCheckPermissions } from 'hooks' import { IS_PLATFORM } from 'lib/constants' import { useState } from 'react' import semver from 'semver' @@ -21,8 +21,8 @@ const AddUserDropdown = ({ projectKpsVersion }: AddUserDropdownProps) => { ) : true - const canInviteUsers = checkPermissions(PermissionAction.AUTH_EXECUTE, 'invite_user') - const canCreateUsers = checkPermissions(PermissionAction.AUTH_EXECUTE, 'create_user') + const canInviteUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'invite_user') + const canCreateUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'create_user') const [inviteVisible, setInviteVisible] = useState(false) const [createVisible, setCreateVisible] = useState(false) diff --git a/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx b/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx index 099999fe0a6..0038fdf50ea 100644 --- a/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx +++ b/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx @@ -2,18 +2,18 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { observer } from 'mobx-react-lite' import { FC } from 'react' -import { Button, Dropdown, IconMoreHorizontal, IconTrash } from 'ui' -import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' -import { checkPermissions, useStore } from 'hooks' import { useParams } from 'common/hooks' -import { Member, Role } from 'types' - +import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' import { useOrganizationMemberDeleteMutation } from 'data/organizations/organization-member-delete-mutation' import { useOrganizationMemberInviteCreateMutation } from 'data/organizations/organization-member-invite-create-mutation' import { useOrganizationMemberInviteDeleteMutation } from 'data/organizations/organization-member-invite-delete-mutation' +import { usePermissionsQuery } from 'data/permissions/permissions-query' +import { checkPermissions, useSelectedOrganization, useStore } from 'hooks' +import { Member, Role } from 'types' +import { Button, Dropdown, IconMoreHorizontal, IconTrash } from 'ui' import { isInviteExpired } from '../Organization.utils' -import { getRolesManagementPermissions } from './TeamSettings.utils' +import { useGetRolesManagementPermissions } from './TeamSettings.utils' interface Props { member: Member @@ -23,7 +23,13 @@ interface Props { const MemberActions: FC = ({ member, roles }) => { const { ui } = useStore() const { slug } = useParams() - const { rolesRemovable } = getRolesManagementPermissions(roles) + const selectedOrganization = useSelectedOrganization() + const { data: permissions } = usePermissionsQuery() + const { rolesRemovable } = useGetRolesManagementPermissions( + selectedOrganization?.id, + roles, + permissions ?? [] + ) const isExpired = isInviteExpired(member?.invited_at ?? '') const isPendingInviteAcceptance = member.invited_id diff --git a/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx b/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx index 1727cb59eaa..89549bcc9c6 100644 --- a/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx +++ b/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx @@ -1,23 +1,23 @@ -import Image from 'next/image' import * as Tooltip from '@radix-ui/react-tooltip' -import { useState, Fragment } from 'react' import { observer } from 'mobx-react-lite' -import { Badge, Button, Loading, Listbox, IconUser, Modal, IconAlertCircle, IconLoader } from 'ui' +import Image from 'next/image' +import { Fragment, useState } from 'react' -import { Member } from 'types' -import { useStore } from 'hooks' import { useParams } from 'common/hooks' - -import ShimmeringLoader from 'components/ui/ShimmeringLoader' import Table from 'components/to-be-cleaned/Table' -import MemberActions from './MemberActions' -import RolesHelperModal from './RolesHelperModal/RolesHelperModal' -import { getRolesManagementPermissions } from './TeamSettings.utils' -import { isInviteExpired, getUserDisplayName } from '../Organization.utils' -import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query' import { useOrganizationMemberUpdateMutation } from 'data/organizations/organization-member-update-mutation' +import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query' +import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useProfileQuery } from 'data/profile/profile-query' +import { useSelectedOrganization, useStore } from 'hooks' +import { Member } from 'types' +import { Badge, Button, IconAlertCircle, IconLoader, IconUser, Listbox, Loading, Modal } from 'ui' +import { getUserDisplayName, isInviteExpired } from '../Organization.utils' +import MemberActions from './MemberActions' +import RolesHelperModal from './RolesHelperModal/RolesHelperModal' +import { useGetRolesManagementPermissions } from './TeamSettings.utils' interface SelectedMember extends Member { oldRoleId: number @@ -31,8 +31,10 @@ export interface MembersViewProps { const MembersView = ({ searchString }: MembersViewProps) => { const { ui } = useStore() const { slug } = useParams() + const selectedOrganization = useSelectedOrganization() const { data: profile } = useProfileQuery() + const { data: permissions } = usePermissionsQuery() const { data: detailData, isLoading: isLoadingOrgDetails } = useOrganizationDetailQuery({ slug }) const { data: rolesData, isLoading: isLoadingRoles } = useOrganizationRolesQuery({ slug }) const { mutate: updateOrganizationMember, isLoading } = useOrganizationMemberUpdateMutation({ @@ -52,7 +54,11 @@ const MembersView = ({ searchString }: MembersViewProps) => { const roles = rolesData?.roles ?? [] const members = detailData?.members ?? [] - const { rolesAddable, rolesRemovable } = getRolesManagementPermissions(roles) + const { rolesAddable, rolesRemovable } = useGetRolesManagementPermissions( + selectedOrganization?.id, + roles, + permissions ?? [] + ) const [selectedMember, setSelectedMember] = useState() const [userRoleChangeModalVisible, setUserRoleChangeModalVisible] = useState(false) diff --git a/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx b/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx index fcb76dfc531..600ce662774 100644 --- a/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx +++ b/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx @@ -6,13 +6,14 @@ import { useParams } from 'common/hooks' import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal' import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query' import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query' +import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useProfileQuery } from 'data/profile/profile-query' import { useSelectedOrganization, useStore } from 'hooks' import { delete_ } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import InviteMemberButton from './InviteMemberButton' import MembersView from './MembersView' -import { getRolesManagementPermissions, hasMultipleOwners } from './TeamSettings.utils' +import { hasMultipleOwners, useGetRolesManagementPermissions } from './TeamSettings.utils' const TeamSettings = () => { const { ui } = useStore() @@ -22,13 +23,18 @@ const TeamSettings = () => { const selectedOrganization = useSelectedOrganization() const isOwner = selectedOrganization?.is_owner + const { data: permissions } = usePermissionsQuery() const { data: detailData } = useOrganizationDetailQuery({ slug }) const { data: rolesData } = useOrganizationRolesQuery({ slug }) const members = detailData?.members ?? [] const roles = rolesData?.roles ?? [] - const { rolesAddable } = getRolesManagementPermissions(roles) + const { rolesAddable } = useGetRolesManagementPermissions( + selectedOrganization?.id, + roles, + permissions ?? [] + ) const [isLeaving, setIsLeaving] = useState(false) const [searchString, setSearchString] = useState('') diff --git a/studio/components/interfaces/Organization/TeamSettings/TeamSettings.utils.ts b/studio/components/interfaces/Organization/TeamSettings/TeamSettings.utils.ts index 9651dacc86c..a245ffacce2 100644 --- a/studio/components/interfaces/Organization/TeamSettings/TeamSettings.utils.ts +++ b/studio/components/interfaces/Organization/TeamSettings/TeamSettings.utils.ts @@ -1,23 +1,43 @@ -import { Member, Role } from 'types' -import { checkPermissions } from 'hooks' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { doPermissionsCheck, useGetPermissions } from 'hooks' +import { Member, Permission, Role } from 'types' -export const getRolesManagementPermissions = ( - roles: Role[] +export const useGetRolesManagementPermissions = ( + orgId?: number, + roles?: Role[], + permissions?: Permission[] ): { rolesAddable: Number[]; rolesRemovable: Number[] } => { + const { permissions: allPermissions, organizationId } = useGetPermissions( + permissions, + orgId, + permissions !== undefined && orgId !== undefined + ) + const rolesAddable: Number[] = [] const rolesRemovable: Number[] = [] - if (!roles) return { rolesAddable, rolesRemovable } + if (!roles || !orgId) return { rolesAddable, rolesRemovable } roles.forEach((role: Role) => { - const canAdd = checkPermissions(PermissionAction.CREATE, 'auth.subject_roles', { - resource: { role_id: role.id }, - }) + const canAdd = doPermissionsCheck( + allPermissions, + PermissionAction.CREATE, + 'auth.subject_roles', + { + resource: { role_id: role.id }, + }, + organizationId + ) if (canAdd) rolesAddable.push(role.id) - const canRemove = checkPermissions(PermissionAction.DELETE, 'auth.subject_roles', { - resource: { role_id: role.id }, - }) + const canRemove = doPermissionsCheck( + allPermissions, + PermissionAction.DELETE, + 'auth.subject_roles', + { + resource: { role_id: role.id }, + }, + organizationId + ) if (canRemove) rolesRemovable.push(role.id) }) diff --git a/studio/data/permissions/permissions-query.ts b/studio/data/permissions/permissions-query.ts index 0dff6750374..fa7d16d5492 100644 --- a/studio/data/permissions/permissions-query.ts +++ b/studio/data/permissions/permissions-query.ts @@ -1,6 +1,6 @@ import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' import { get } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' +import { API_URL, IS_PLATFORM } from 'lib/constants' import { useCallback } from 'react' import { Permission } from 'types' import { permissionKeys } from './keys' @@ -27,7 +27,7 @@ export const usePermissionsQuery = ( useQuery( permissionKeys.list(), ({ signal }) => getPermissions(signal), - options + { ...options, enabled: IS_PLATFORM && options.enabled } ) export const usePermissionsPrefetch = () => { diff --git a/studio/hooks/misc/useCheckPermissions.ts b/studio/hooks/misc/useCheckPermissions.ts index 300285def31..c54c0a1717f 100644 --- a/studio/hooks/misc/useCheckPermissions.ts +++ b/studio/hooks/misc/useCheckPermissions.ts @@ -2,30 +2,23 @@ import jsonLogic from 'json-logic-js' import { usePermissionsQuery } from 'data/permissions/permissions-query' import { IS_PLATFORM } from 'lib/constants' +import { Permission } from 'types' import { useSelectedOrganization } from './useSelectedOrganization' const toRegexpString = (actionOrResource: string) => `^${actionOrResource.replace('.', '\\.').replace('%', '.*')}$` -export function useCheckPermissions( +export function doPermissionsCheck( + permissions: Permission[] | undefined, action: string, resource: string, data?: object, organizationId?: number ) { - const { data: permissions } = usePermissionsQuery({ - enabled: IS_PLATFORM, - }) - const organization = useSelectedOrganization() - - if (!IS_PLATFORM) return true - - const orgId = organizationId ?? organization?.id - return (permissions ?? []) .filter( (permission) => - permission.organization_id === orgId && + permission.organization_id === organizationId && permission.actions.some((act) => (action ? action.match(toRegexpString(act)) : null)) && permission.resources.some((res) => resource.match(toRegexpString(res))) ) @@ -35,6 +28,51 @@ export function useCheckPermissions( ) } +export function useGetPermissions( + permissionsOverride?: Permission[], + organizationIdOverride?: number, + enabled = true +) { + const permissionsResult = usePermissionsQuery({ + enabled: permissionsOverride === undefined && enabled, + }) + + const permissions = + permissionsOverride === undefined ? permissionsResult.data : permissionsOverride + + const organizationResult = useSelectedOrganization({ + enabled: organizationIdOverride === undefined && enabled, + }) + + const organization = + organizationIdOverride === undefined ? organizationResult : { id: organizationIdOverride } + const organizationId = organization?.id + + return { + permissions, + organizationId, + } +} + +export function useCheckPermissions( + action: string, + resource: string, + data?: object, + // [Joshen] Pass the variables if you want to avoid hooks in this + // e.g If you want to use useCheckPermissions in a loop like organization settings + organizationId?: number, + permissions?: Permission[] +) { + const { permissions: allPermissions, organizationId: orgId } = useGetPermissions( + permissions, + organizationId + ) + + if (!IS_PLATFORM) return true + + return doPermissionsCheck(allPermissions, action, resource, data, orgId) +} + /** * @deprecated use useCheckPermissions instead */ diff --git a/studio/hooks/misc/useSelectedOrganization.ts b/studio/hooks/misc/useSelectedOrganization.ts index b63927fd14b..c2446380fc5 100644 --- a/studio/hooks/misc/useSelectedOrganization.ts +++ b/studio/hooks/misc/useSelectedOrganization.ts @@ -4,9 +4,9 @@ import { useMemo } from 'react' import { useSelectedProject } from './useSelectedProject' -export function useSelectedOrganization() { +export function useSelectedOrganization({ enabled = true } = {}) { const { slug } = useParams() - const { data } = useOrganizationsQuery() + const { data } = useOrganizationsQuery({ enabled }) const selectedProject = useSelectedProject() return useMemo(() => { diff --git a/studio/hooks/misc/withAuth.tsx b/studio/hooks/misc/withAuth.tsx index 3c0216cc2b8..7cf5706b7ff 100644 --- a/studio/hooks/misc/withAuth.tsx +++ b/studio/hooks/misc/withAuth.tsx @@ -41,7 +41,6 @@ export function withAuth( const redirectIfFound = options?.redirectIfFound usePermissionsQuery({ - enabled: IS_PLATFORM, onSuccess(permissions) { ui.setPermissions(permissions) },