תקציר
כל מוצר שמשרת יותר מלקוח אחד מאותה מערכת חייב לענות על שאלה אחת לפני כל השאר: האם לקוח אחד יכול לראות אי פעם נתונים של לקוח אחר? הפוסט הזה עוסק בשאלה איפה שמים את הגבול הזה. בקצרה: לא בקוד האפליקציה.
המקום המקובל, ולמה הוא דולף
הגישה הנפוצה היא עמודה. כל טבלה מקבלת tenant_id, וכל שאילתה מקבלת WHERE tenant_id = ... שמתאים לה. זה עובד, וזו התחלה קלה.
הבעיה היא שזה צריך להמשיך לעבוד, בכל שאילתה, לנצח. דוח חדש, מסך אדמין זריז, משימת רקע שנכתבה בסוף יום ארוך, join שמכניס טבלה שנייה. כל אחד מהם הוא עוד הזדמנות להשאיר את הסינון בחוץ, והכשל שקט: השאילתה מחזירה שורות, העמוד מוצג, ואף אחד לא שם לב עד שלקוח רואה שם שלא היה צריך לראות.
Code review תופס את רוב המקרים. “רוב” זו הבעיה. הגבול שמגן על הלקוחות שלכם אחד מהשני לא צריך להיות תלוי בזה שכולם יזכרו משהו בכל פעם.
להוריד את הגבול שכבה אחת למטה
Postgres יכול לאכוף את זה בעצמו, עם Row-Level Security. מצמידים לטבלה policy שאומרת אילו שורות החיבור הנוכחי רשאי לראות, החיבור מכריז עבור איזה לקוח הוא פועל, ומכאן בסיס הנתונים מסנן כל שאילתה, גם אם האפליקציה שכחה.
שלושה פרטים חשובים יותר מהמנגנון עצמו.
לאכוף בכוח. כברירת מחדל, הבעלים של טבלה פטור מה-policy שלה, ובפועל זה אומר שהמשתמש הראשי של האפליקציה שלכם עובר לידה. הפעלת Row-Level Security במצב FORCE מבטלת את הפטור, כך שלא נשאר נתיב מיוחס שפתוח בטעות.
לקבוע את הלקוח על החיבור, פעם אחת. זהות הלקוח מגיעה מה-session המאומת, נקבעת על החיבור בתחילת הבקשה, ולעולם לא נקראת ממשהו שהמשתמש שלח. אם היא יכולה להגיע מ-URL או מגוף הבקשה, בניתם מחדש את הבעיה המקורית עם צעדים נוספים.
שזו תהיה ברירת המחדל לטבלאות חדשות. policy על תשע טבלאות מתוך עשר הוא לא גבול. בקוד שלנו טבלה חדשה לא נחשבת גמורה עד שיש לה עמודת לקוח, policy, והשם שלה ברשימה שהבדיקות בודקות, הכול באותו שינוי. החלק האחרון הוא מה ששומר על זה נכון גם בעוד שנה.
מה זה עולה
זה לא בחינם, וכדאי להיות כנים לגבי זה.
מוותרים על נוחות במיגרציות ובכלי התחזוקה, כי סקריפט תחזוקה צריך עכשיו להגיד עבור מי הוא פועל. ל-debug נוספת שאלה: השאילתה מחזירה ריק כי הנתונים חסרים, או כי ה-policy עושה את העבודה שלו? וה-policy עצמו הוא קוד, כלומר הוא צריך בדיקות, כולל בדיקה שמנסה בכוונה לקרוא שורה של לקוח אחר ומוודאת שחוזר ריק.
מול כל זה: סוג הבאג האחד שאי אפשר לשחרר הוא זה שבסיס הנתונים מסרב לבצע.
צ’קליסט קצר
- לכל טבלה שמחזיקה נתוני לקוחות יש עמודת לקוח, עם foreign key.
- לכל טבלה כזו יש policy, ו-Row-Level Security במצב FORCE, לא רק מופעל.
- זהות הלקוח נקבעת מה-session המאומת, על החיבור, ולא מקלט שהמשתמש שלח.
- יש בדיקה שמוודאת שקריאה חוצת-לקוחות חוזרת ריקה.
- הוספת טבלה והוספת ה-policy שלה קורות באותו שינוי, לא בהמשך.
אנחנו מריצים את זה בעצמנו
זה לא תרגיל תיאורטי אצלנו. Pipelix הוא מוצר שלנו, פלטפורמה שמשרתת כמה חברות מאותה מערכת, והוא בנוי בדיוק כך: הפרדה נאכפת ב-Postgres, זהות הלקוח נגזרת מה-session, ורשימת הטבלאות נבדקת בבדיקות כך שטבלה חדשה לא יכולה להגיע בשקט בלי policy.

סביבת עבודה אחת ב-Pipelix. אילו שורות המסך הזה רשאי להציג נקבע בבסיס הנתונים, מתחת לקוד שבונה את העמוד.
זו גם החלטה שזולה ביום הראשון ויקרה בשנה השנייה. אם אתם בונים משהו שיהיה לו לקוח שני, כדאי לסדר את זה לפני שיש אחד.