useCheckPermissions.ts190 lines · main
| 1 | import { useIsLoggedIn, useParams } from 'common' |
| 2 | import jsonLogic from 'json-logic-js' |
| 3 | import { useMemo } from 'react' |
| 4 | |
| 5 | import { useSelectedOrganizationQuery } from './useSelectedOrganization' |
| 6 | import { useSelectedProjectQuery } from './useSelectedProject' |
| 7 | import { usePermissionsQuery } from '@/data/permissions/permissions-query' |
| 8 | import { IS_PLATFORM } from '@/lib/constants' |
| 9 | import type { Permission } from '@/types' |
| 10 | |
| 11 | const toRegexpString = (actionOrResource: string) => |
| 12 | `^${actionOrResource.replace('.', '\\.').replace('%', '.*')}$` |
| 13 | |
| 14 | function doPermissionConditionCheck(permissions: Permission[], data?: object) { |
| 15 | const isRestricted = permissions |
| 16 | .filter((permission) => permission.restrictive) |
| 17 | .some( |
| 18 | ({ condition }: { condition: jsonLogic.RulesLogic }) => |
| 19 | condition === null || jsonLogic.apply(condition, data) |
| 20 | ) |
| 21 | if (isRestricted) return false |
| 22 | |
| 23 | return permissions |
| 24 | .filter((permission) => !permission.restrictive) |
| 25 | .some( |
| 26 | ({ condition }: { condition: jsonLogic.RulesLogic }) => |
| 27 | condition === null || jsonLogic.apply(condition, data) |
| 28 | ) |
| 29 | } |
| 30 | |
| 31 | export function doPermissionsCheck( |
| 32 | permissions: Permission[] | undefined, |
| 33 | action: string, |
| 34 | resource: string, |
| 35 | data?: object, |
| 36 | organizationSlug?: string, |
| 37 | projectRef?: string |
| 38 | ) { |
| 39 | if (!permissions || !Array.isArray(permissions)) { |
| 40 | return false |
| 41 | } |
| 42 | |
| 43 | if (projectRef) { |
| 44 | const projectPermissions = permissions.filter( |
| 45 | (permission) => |
| 46 | permission.organization_slug === organizationSlug && |
| 47 | permission.actions.some((act) => (action ? action.match(toRegexpString(act)) : null)) && |
| 48 | permission.resources.some((res) => resource.match(toRegexpString(res))) && |
| 49 | permission.project_refs?.includes(projectRef) |
| 50 | ) |
| 51 | if (projectPermissions.length > 0) { |
| 52 | return doPermissionConditionCheck(projectPermissions, { resource_name: resource, ...data }) |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | const orgPermissions = permissions |
| 57 | // filter out org-level permission |
| 58 | .filter((permission) => !permission.project_refs || permission.project_refs.length === 0) |
| 59 | .filter( |
| 60 | (permission) => |
| 61 | permission.organization_slug === organizationSlug && |
| 62 | permission.actions.some((act) => (action ? action.match(toRegexpString(act)) : null)) && |
| 63 | permission.resources.some((res) => resource.match(toRegexpString(res))) |
| 64 | ) |
| 65 | return doPermissionConditionCheck(orgPermissions, { resource_name: resource, ...data }) |
| 66 | } |
| 67 | |
| 68 | export function useGetPermissions( |
| 69 | permissionsOverride?: Permission[], |
| 70 | organizationSlugOverride?: string, |
| 71 | enabled = true |
| 72 | ) { |
| 73 | return useGetProjectPermissions(permissionsOverride, organizationSlugOverride, undefined, enabled) |
| 74 | } |
| 75 | |
| 76 | function useGetProjectPermissions( |
| 77 | permissionsOverride?: Permission[], |
| 78 | organizationSlugOverride?: string, |
| 79 | projectRefOverride?: string, |
| 80 | enabled = true |
| 81 | ) { |
| 82 | const { |
| 83 | data, |
| 84 | isPending: isLoadingPermissions, |
| 85 | isSuccess: isSuccessPermissions, |
| 86 | } = usePermissionsQuery({ |
| 87 | enabled: permissionsOverride === undefined && enabled, |
| 88 | }) |
| 89 | const permissions = permissionsOverride === undefined ? data : permissionsOverride |
| 90 | |
| 91 | const getOrganizationDataFromParamsSlug = organizationSlugOverride === undefined && enabled |
| 92 | const { |
| 93 | data: organizationData, |
| 94 | isPending: isLoadingOrganization, |
| 95 | isSuccess: isSuccessOrganization, |
| 96 | } = useSelectedOrganizationQuery({ |
| 97 | enabled: getOrganizationDataFromParamsSlug, |
| 98 | }) |
| 99 | const organization = |
| 100 | organizationSlugOverride === undefined ? organizationData : { slug: organizationSlugOverride } |
| 101 | const organizationSlug = organization?.slug |
| 102 | |
| 103 | const { ref: urlProjectRef } = useParams() |
| 104 | const getProjectDataFromParamsRef = !!urlProjectRef && projectRefOverride === undefined && enabled |
| 105 | const { |
| 106 | data: projectData, |
| 107 | isPending: isLoadingProject, |
| 108 | isSuccess: isSuccessProject, |
| 109 | } = useSelectedProjectQuery({ |
| 110 | enabled: getProjectDataFromParamsRef, |
| 111 | }) |
| 112 | const project = |
| 113 | projectRefOverride === undefined || projectData?.parent_project_ref |
| 114 | ? projectData |
| 115 | : { ref: projectRefOverride, parent_project_ref: undefined } |
| 116 | |
| 117 | const projectRef = project?.parent_project_ref ? project.parent_project_ref : project?.ref |
| 118 | |
| 119 | const isLoading = |
| 120 | isLoadingPermissions || |
| 121 | (getOrganizationDataFromParamsSlug && isLoadingOrganization) || |
| 122 | (getProjectDataFromParamsRef && isLoadingProject) |
| 123 | const isSuccess = |
| 124 | isSuccessPermissions && |
| 125 | (!getOrganizationDataFromParamsSlug || isSuccessOrganization) && |
| 126 | (!getProjectDataFromParamsRef || isSuccessProject) |
| 127 | |
| 128 | return { |
| 129 | permissions, |
| 130 | organizationSlug, |
| 131 | projectRef, |
| 132 | isLoading, |
| 133 | isSuccess, |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | /** [Joshen] To be renamed to be useAsyncCheckPermissions, more generic as it covers both org and project perms */ |
| 138 | // Useful when you want to avoid layout changes while waiting for permissions to load |
| 139 | export function useAsyncCheckPermissions( |
| 140 | action: string, |
| 141 | resource: string, |
| 142 | data?: object, |
| 143 | overrides?: { |
| 144 | organizationSlug?: string |
| 145 | projectRef?: string |
| 146 | permissions?: Permission[] |
| 147 | } |
| 148 | ) { |
| 149 | const isLoggedIn = useIsLoggedIn() |
| 150 | const { organizationSlug, projectRef, permissions } = overrides ?? {} |
| 151 | |
| 152 | const { |
| 153 | permissions: allPermissions, |
| 154 | organizationSlug: _organizationSlug, |
| 155 | projectRef: _projectRef, |
| 156 | isLoading: isPermissionsLoading, |
| 157 | isSuccess: isPermissionsSuccess, |
| 158 | } = useGetProjectPermissions(permissions, organizationSlug, projectRef, isLoggedIn) |
| 159 | |
| 160 | const can = useMemo(() => { |
| 161 | if (!IS_PLATFORM) return true |
| 162 | if (!isLoggedIn) return false |
| 163 | if (!isPermissionsSuccess || !allPermissions) return false |
| 164 | |
| 165 | return doPermissionsCheck( |
| 166 | allPermissions, |
| 167 | action, |
| 168 | resource, |
| 169 | data, |
| 170 | _organizationSlug, |
| 171 | _projectRef |
| 172 | ) |
| 173 | }, [ |
| 174 | isLoggedIn, |
| 175 | isPermissionsSuccess, |
| 176 | allPermissions, |
| 177 | action, |
| 178 | resource, |
| 179 | data, |
| 180 | _organizationSlug, |
| 181 | _projectRef, |
| 182 | ]) |
| 183 | |
| 184 | // Derive loading/success consistently from the same branches |
| 185 | const isLoading = !IS_PLATFORM ? false : !isLoggedIn ? true : isPermissionsLoading |
| 186 | |
| 187 | const isSuccess = !IS_PLATFORM ? true : !isLoggedIn ? false : isPermissionsSuccess |
| 188 | |
| 189 | return { isLoading, isSuccess, can } |
| 190 | } |