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