StoragePoliciesEditPolicyModal.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import { noop, pull } from 'lodash'
  2. import { ChevronLeft } from 'lucide-react'
  3. import { useEffect, useState } from 'react'
  4. import { toast } from 'sonner'
  5. import { Modal } from 'ui'
  6. import {
  7. applyBucketIdToTemplateDefinition,
  8. createPayloadsForAddPolicy,
  9. createSQLPolicies,
  10. } from '../Storage.utils'
  11. import { STORAGE_POLICY_TEMPLATES } from './StoragePolicies.constants'
  12. import StoragePoliciesEditor from './StoragePoliciesEditor'
  13. import StoragePoliciesReview from './StoragePoliciesReview'
  14. import { POLICY_MODAL_VIEWS } from '@/components/interfaces/Auth/Policies/Policies.constants'
  15. import PolicySelection from '@/components/interfaces/Auth/Policies/PolicySelection'
  16. import PolicyTemplates from '@/components/interfaces/Auth/Policies/PolicyTemplates'
  17. import { DocsButton } from '@/components/ui/DocsButton'
  18. import { DOCS_URL } from '@/lib/constants'
  19. const newPolicyTemplate: any = {
  20. name: '',
  21. roles: [],
  22. policyIds: [],
  23. definition: '',
  24. allowedOperations: [],
  25. }
  26. export const StoragePoliciesEditPolicyModal = ({
  27. visible = false,
  28. bucketName = '',
  29. onSelectCancel = () => {},
  30. onCreatePolicies = () => {},
  31. onSaveSuccess = () => {},
  32. }: any) => {
  33. const [previousView, setPreviousView] = useState('') // Mainly to decide which view to show when back from templates
  34. const [view, setView] = useState('')
  35. const [policyFormFields, setPolicyFormFields] = useState(newPolicyTemplate)
  36. const [policyStatementsForReview, setPolicyStatementsForReview] = useState<any[]>([])
  37. useEffect(() => {
  38. if (visible) {
  39. onViewIntro()
  40. setPolicyFormFields(newPolicyTemplate)
  41. }
  42. }, [visible])
  43. /* Methods to determine which step to show */
  44. const onViewIntro = () => setView(POLICY_MODAL_VIEWS.SELECTION)
  45. const onViewEditor = (state?: any) => {
  46. if (state === 'new') {
  47. setPolicyFormFields({
  48. ...policyFormFields,
  49. definition: `bucket_id = '${bucketName}'`,
  50. })
  51. }
  52. setView(POLICY_MODAL_VIEWS.EDITOR)
  53. }
  54. const onViewTemplates = () => {
  55. setPreviousView(view)
  56. setView(POLICY_MODAL_VIEWS.TEMPLATES)
  57. }
  58. const onReviewPolicy = () => setView(POLICY_MODAL_VIEWS.REVIEW)
  59. /* Methods for policy templates */
  60. const onSelectBackFromTemplates = () => setView(previousView)
  61. const onUseTemplate = (template: any) => {
  62. // Each template has an id as a unique identifier to refresh the SQL editor
  63. // but we don't need this property to be in the policyFormField
  64. const { id, ...templateFields } = template
  65. const definition = applyBucketIdToTemplateDefinition(templateFields.definition, bucketName)
  66. setPolicyFormFields({
  67. ...policyFormFields,
  68. ...templateFields,
  69. definition: definition,
  70. })
  71. onViewEditor()
  72. }
  73. /* Methods for policy editor form fields */
  74. const onUpdatePolicyName = (name: string) => {
  75. if (name.length <= 50) {
  76. setPolicyFormFields({
  77. ...policyFormFields,
  78. name,
  79. })
  80. }
  81. }
  82. const onUpdatePolicyDefinition = (definition: any) => {
  83. setPolicyFormFields({
  84. ...policyFormFields,
  85. definition,
  86. })
  87. }
  88. const onToggleOperation = (operation: any, isSingleOperation = false) => {
  89. if (isSingleOperation) {
  90. return setPolicyFormFields({
  91. ...policyFormFields,
  92. allowedOperations: [operation],
  93. })
  94. }
  95. const currentOps = policyFormFields.allowedOperations
  96. const isRemoving = currentOps.includes(operation)
  97. let updatedAllowedOperations = isRemoving
  98. ? pull(currentOps.slice(), operation)
  99. : currentOps.concat([operation])
  100. if (!isRemoving && (operation === 'UPDATE' || operation === 'DELETE')) {
  101. if (!updatedAllowedOperations.includes('SELECT')) {
  102. updatedAllowedOperations = updatedAllowedOperations.concat(['SELECT'])
  103. }
  104. }
  105. if (isRemoving && operation === 'SELECT') {
  106. updatedAllowedOperations = updatedAllowedOperations.filter(
  107. (op: string) => op !== 'UPDATE' && op !== 'DELETE'
  108. )
  109. }
  110. return setPolicyFormFields({
  111. ...policyFormFields,
  112. allowedOperations: updatedAllowedOperations,
  113. })
  114. }
  115. const onUpdatePolicyRoles = (roles: any) => {
  116. setPolicyFormFields({
  117. ...policyFormFields,
  118. roles,
  119. })
  120. }
  121. const validatePolicyEditorFormFields = () => {
  122. const { name, definition, allowedOperations } = policyFormFields
  123. if (name.length === 0) {
  124. return toast.error('Please provide a name for your policy')
  125. }
  126. if (definition.length === 0) {
  127. // Will need to figure out how to strip away comments or something
  128. return toast.error('Please provide a definition for your policy')
  129. }
  130. if (allowedOperations.length === 0) {
  131. return toast.error('Please allow at least one operation in your policy')
  132. }
  133. const policySQLStatements = createSQLPolicies(bucketName, policyFormFields)
  134. setPolicyStatementsForReview(policySQLStatements)
  135. onReviewPolicy()
  136. }
  137. /* Create policy payloads to be sent upstream to API endpoint */
  138. const onReviewSave = () => {
  139. const payloads = createPayloadsForAddPolicy(bucketName, policyFormFields)
  140. onSavePolicy(payloads)
  141. }
  142. const onSavePolicy = async (payloads: any) => {
  143. const errors = await onCreatePolicies(payloads)
  144. const hasErrors = errors.indexOf(true) !== -1
  145. if (hasErrors) {
  146. onViewEditor()
  147. } else {
  148. onSaveSuccess()
  149. }
  150. }
  151. /* Misc components */
  152. const StoragePolicyEditorModalTitle = ({
  153. view,
  154. bucketName,
  155. onSelectBackFromTemplates = noop,
  156. }: any) => {
  157. const getTitle = () => {
  158. if (view === POLICY_MODAL_VIEWS.EDITOR || view === POLICY_MODAL_VIEWS.SELECTION) {
  159. return `Adding new policy to ${bucketName}`
  160. }
  161. if (view === POLICY_MODAL_VIEWS.REVIEW) {
  162. return `Reviewing policies to be created for ${bucketName}`
  163. }
  164. }
  165. if (view === POLICY_MODAL_VIEWS.TEMPLATES) {
  166. return (
  167. <div>
  168. <div className="flex items-center space-x-3">
  169. <span
  170. onClick={onSelectBackFromTemplates}
  171. className="cursor-pointer text-foreground-lighter transition-colors hover:text-foreground"
  172. >
  173. <ChevronLeft strokeWidth={2} size={14} />
  174. </span>
  175. <h4 className="textlg m-0">Select a template to use for your new policy</h4>
  176. </div>
  177. </div>
  178. )
  179. }
  180. return (
  181. <div className="w-full flex items-center justify-between gap-x-2 pr-6">
  182. <h4 className="m-0 truncate">{getTitle()}</h4>
  183. <DocsButton href={`${DOCS_URL}/learn/auth-deep-dive/auth-policies`} />
  184. </div>
  185. )
  186. }
  187. return (
  188. <Modal
  189. hideFooter
  190. className="[&>div:first-child]:py-3"
  191. size={view === POLICY_MODAL_VIEWS.SELECTION ? 'medium' : 'xxlarge'}
  192. visible={visible}
  193. contentStyle={{ padding: 0 }}
  194. header={[
  195. <StoragePolicyEditorModalTitle
  196. key="0"
  197. view={view}
  198. bucketName={bucketName}
  199. onSelectBackFromTemplates={onSelectBackFromTemplates}
  200. />,
  201. ]}
  202. onCancel={onSelectCancel}
  203. >
  204. <div className="w-full">
  205. {view === POLICY_MODAL_VIEWS.SELECTION ? (
  206. <PolicySelection
  207. description="PostgreSQL policies control access to your files and folders"
  208. onViewTemplates={onViewTemplates}
  209. onViewEditor={() => onViewEditor('new')}
  210. showAssistantPreview={false}
  211. />
  212. ) : view === POLICY_MODAL_VIEWS.EDITOR ? (
  213. <StoragePoliciesEditor
  214. policyFormFields={policyFormFields}
  215. onViewTemplates={onViewTemplates}
  216. onUpdatePolicyName={onUpdatePolicyName}
  217. onUpdatePolicyDefinition={onUpdatePolicyDefinition}
  218. onToggleOperation={onToggleOperation}
  219. onUpdatePolicyRoles={onUpdatePolicyRoles}
  220. onReviewPolicy={validatePolicyEditorFormFields}
  221. />
  222. ) : view === POLICY_MODAL_VIEWS.TEMPLATES ? (
  223. <PolicyTemplates
  224. templates={STORAGE_POLICY_TEMPLATES as any[]}
  225. onUseTemplate={onUseTemplate}
  226. templatesNote={''}
  227. />
  228. ) : view === POLICY_MODAL_VIEWS.REVIEW ? (
  229. <StoragePoliciesReview
  230. policyStatements={policyStatementsForReview}
  231. onSelectBack={onViewEditor}
  232. onSelectSave={onReviewSave}
  233. />
  234. ) : (
  235. <div />
  236. )}
  237. </div>
  238. </Modal>
  239. )
  240. }