StoragePoliciesEditPolicyModal.tsx262 lines · main
1import { noop, pull } from 'lodash'
2import { ChevronLeft } from 'lucide-react'
3import { useEffect, useState } from 'react'
4import { toast } from 'sonner'
5import { Modal } from 'ui'
6
7import {
8 applyBucketIdToTemplateDefinition,
9 createPayloadsForAddPolicy,
10 createSQLPolicies,
11} from '../Storage.utils'
12import { STORAGE_POLICY_TEMPLATES } from './StoragePolicies.constants'
13import StoragePoliciesEditor from './StoragePoliciesEditor'
14import StoragePoliciesReview from './StoragePoliciesReview'
15import { POLICY_MODAL_VIEWS } from '@/components/interfaces/Auth/Policies/Policies.constants'
16import PolicySelection from '@/components/interfaces/Auth/Policies/PolicySelection'
17import PolicyTemplates from '@/components/interfaces/Auth/Policies/PolicyTemplates'
18import { DocsButton } from '@/components/ui/DocsButton'
19import { DOCS_URL } from '@/lib/constants'
20
21const newPolicyTemplate: any = {
22 name: '',
23 roles: [],
24 policyIds: [],
25 definition: '',
26 allowedOperations: [],
27}
28
29export 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}