מודל האבטחה¶
כל שאילתה עוברת את אותה ממשל, בכל שפה שבה היא כתובה (GraphQL, SQL, Cypher) ובכל דרך שבה היא מגיעה (REST, gRPC, Arrow Flight, JDBC, WebSocket). אין דרך לעקוף אותו.
הבדיקות רצות לפי הסדר. בקשה צריכה לעבור כל אחת לפני שבודקים את הבאה.
המודל השכבתי¶
שכבה 0 — סינון האינטרוספקציה¶
תפקיד רואה רק את הטבלאות שה-domain_access שלו מגיע אליהן ואת העמודות שכללי ה-visible_to שלו מתירים. כל השאר לא קיים מבחינת התפקיד: אי אפשר לשאול עליו, להשלים אותו אוטומטית או לנחש שהוא קיים. סכמת ה-GraphQL, קטלוג ה-SQL ודפדפן הסכמה של עורך השאילתות מציגים כולם את אותה תצוגה מצומצמת.
ראו נראות הסכמה.
שכבה 1 — גישה ציבורית¶
עמודה שפורסמה בהיקף public מוגשת לכל תפקיד בארגון, גם לתפקיד שה-domain_access שלו לא מגיע לדומיין של הטבלה. התפקיד הזה רואה את הטבלה עם העמודות הציבוריות שלה ותו לא. שום דבר אינו ציבורי עד שמי שרשם את הטבלה קובע זאת. ראו עמודות ציבוריות.
שכבה 2 — גישה לפי דומיין¶
לכל תפקיד יש רשימת domain_access של מזהי דומיינים, ושאילתה שנוגעת בטבלה מדומיין אחר נדחית לפני שהיא רצה. זהו גבול הבעלות הגס. תפקיד של משאבי אנוש לא מגיע לטבלאות הכספים, איך שלא ייכתב ה-SQL.
ראו מודל ההרשאות.
שכבה 3 — אבטחה ברמת השורה¶
אחרי שהגישה לפי דומיין הוסדרה, מסנן השורות של התפקיד לכל טבלה מתווסף לכל SELECT כפרדיקט WHERE ומחושב על הנתונים הגולמיים. מנהל אזורי שמריץ SELECT * על טבלת ההזמנות המשותפת עדיין מקבל רק את השורות של האזור שלו.
שכבה 4 — נראות עמודות והסוואה¶
עמודה שרשימת ה-visible_to שלה לא כוללת את התפקיד שלכם נשמטת מהתוצאה. בעמודה מוסווית הערכים מוחלפים לפני שהם יוצאים מהשרת, בביטוי רגולרי, בקבוע או בקיצוץ. זה חל בכל שפת שאילתה ובכל פורמט פלט.
ראו מודל ההרשאות לעמודות והסוואת עמודות.
שכבה 5 — שמירה על פרדיקטים¶
אי אפשר לסנן לפי עמודה מוסווית: WHERE ו-HAVING דוחים אותה כבר בניתוח השאילתה. אחרת מבקש יכול היה לשחזר את הערך האמיתי בחיפוש בינארי, מסנן אחר מסנן, אף שכל תוצאה שראה הייתה מוסווית.
ממשל קשרים (V002)¶
צירוף (join) ב-SQL חייב ללכת לפי קשר רשום ומאושר, וכל צירוף אחר נדחה. כל קשר נושא סיבה ותיאור שמסבירים לאנשים ולסוכנים למה הנתיב קיים. התייחסו לכך כמדיניות ממשל ולא כגבול אבטחה. שכבות 2 עד 5 תקפות איך שלא ייראו הצירופים, כך שעקיפת הבדיקה לא חושפת דבר שהתפקיד לא יכול היה לקרוא בשתי שאילתות נפרדות. ניסיונות עקיפה נרשמים ביומן.
יש שתי דרכים לעבור את V002. הראשונה היא יכולת: תפקיד שמחזיק ב-ignore_relationships יכול לצרף יחסים שהקטלוג לא מכסה. מבין התפקידים המובנים רק ל-modeler יש אותה, כי תפקידו לגבש את המודל; ל-analyst אין.
השנייה דורשת שני דברים בבת אחת:
- דגל בתפקיד:
relationship_guard: falseבהגדרת התפקיד (ברירת המחדל היאtrue). - ויתור ברמת השאילתה: ההערה
--relationship-guard=falseב-SQL.
כל אחד לבדו לא עושה דבר.
מצב אבטחה גבוהה נועל את הבדיקה. תחת security.mode: high אף אחת מהדרכים לא עובדת. גם ignore_relationships וגם relationship_guard: false מתעלמים מהם, וכל צירוף חייב להופיע בקטלוג המאושר. הכפילות מכוונת: תפקיד ייצור שקיבל את היכולת בטעות עדיין לא יכול לצאת מגבולות המודל.
GraphQL מדלג על V002. הקשרים שלו באים מהמודל, ולכן כבר מאושרים.
SQL ו-Cypher מחילים את V002 כברירת מחדל, עם הוויתור בשני התנאים שתואר למעלה.
pgwire מחיל את אותה בדיקה כמו SQL. ההערה --relationship-guard=false מוסרת לפני שהשאילתה מגיעה למסד הנתונים.
השכבות מצטברות. לתפקיד עם גישה לפי דומיין, מסנני שורות ועמודות מוסוות כל אלה בתוקף בו בזמן. הוספת מקור, עמודה או קשר לא מחייבת לגעת בכל כלל, כי כל שכבה מוגדרת בנפרד וחלה על כל שאילתה שמגיעה למה שהיא מסדירה.
מודל הזכויות¶
יכולות הניתנות להקצאה בנפרד, עם היררכיית תפקידים אופציונלית דרך parent_role_id. admin מעניק הכול.
| יכולת | תיאור |
|---|---|
source_registration |
רישום מקורות נתונים |
table_registration |
רישום טבלאות ועמודות |
create_relationship |
הגדרת קשרי FK |
access_config |
הגדרת RLS ומיסוך |
query_development |
הרצת שאילתות |
write |
הפעלת מוטציות רשומות (בקרה גסה; ראו הרשאת מוטציה) |
full_results |
עקיפת מגבלות הדגימה |
ignore_relationships |
עקיפת ממשל הקשרים (V002). מבין תפקידי המערכת רק ל-modeler יש אותה, ובמצב אבטחה גבוהה מתעלמים ממנה לחלוטין |
admin |
משתמש-על — מעניק הכול |
ירושת תפקידים¶
תפקידים יכולים לרשת מתפקיד הורה אחד דרך parent_role_id, שנקבע בקונפיגורציה או בעמוד האבטחה כ"יורש מ". השרשרת משוטחת בעת ההפעלה: הבן מקבל את האיחוד של היכולות וגישת התחום של אבותיו, כל הענקת עמודה, מדד, פונקציה ו-webhook שנוקבת באב קדמון, ואת כללי ה-RLS של האבות לכל טבלה כשהבן קודם. התפקיד הקרוב ביותר בשרשרת שיש לו כלל לטבלה נותן את הפרדיקט של אותה טבלה, וכלל שנשמר לבן מחליף את כלל ההורה לאותה טבלה.
roles:
- id: basic_user
description: Queries the public domain.
capabilities: [query_development]
domain_access: [public]
- id: analyst
description: Analyses sales; reads full results.
capabilities: [full_results]
domain_access: [sales, analytics]
parent_role_id: basic_user # inherits query_development + public domain
תפקידים: מוענקים, נגזרים ומטא¶
שלושה סוגי תפקידים פועלים יחד במודל הגישה של Provisa. דף התפקידים מציג את שני הסוגים הראשונים יחד; מטא-תפקיד לעולם אינו מוצג.
תפקידים מוענקים¶
תפקיד מוענק מוקצה למשתמש ישירות. הוא נושא זכויות: יכולות, גישת תחום, רשומות מסכה, מסנני שורות. ירושה (parent_role_id) מקפלת את זכויות האב הקדמון לתוך הבן בעת ההפעלה. זכויות רק מתווספות; אין חיסור. כדי להגביל קורא, הענו לרשומה מורכבת שנוקבת גם בתפקיד וגם בתנאי, או הציבו רשומת מסכה לפני רשומה מתירנית.
הקצאות שפוקעות והקצאות שאינן בשימוש¶
הקצאה ש-Provisa מחזיקה יכולה להסתיים. הקצאה שניתנה עם מועד פקיעה אינה מוחזקת עוד מאותו רגע, בכל בקשה. תפקיד יכול לציין inactivity_days: הקצאה של התפקיד שאף בקשה לא נזקקה לה זמן רב מזה, בספירה מהמאוחר מבין השימוש האחרון בה ויצירתה, מוסרת. תפקידים שמגיעים מהטענות (claims) או ממיפויי התפקידים של ספק זהות מוגבלים בזמן על ידי הספק; קבעו שם את הפקיעה שלהם. תפקידים נגזרים ומטא-תפקידים אינם פוקעים לעולם.
בקשה נזקקה לתפקיד כאשר התפקיד לבדו שירת אותה, או, כאשר הבקשה פועלת כמה תפקידים, כאשר לתפקיד הוענקה טבלה שההוראה קראה. פעולה כ„הכול“ אינה נחשבת לשימוש בכל תפקיד מוחזק. השימוש האחרון בהקצאה נרשם לכל היותר פעם ביום.
כל 15 דקות הסריקה של כל ארגון מוחקת את ההקצאות שפקעו ואת אלה שאינן בשימוש ורושמת כל אחת ביומן הניהול כ-role.expired או role.removed_inactive. היכן שמוגדר דואר יוצא, כולל בפריסה של ארגון יחיד, המחזיק ומנהלי הארגון מקבלים דוא״ל auth.role_grant_notice_days ימים (ברירת מחדל 7) לפני שהקצאה מסתיימת, והמנהלים מקבלים דוא״ל כאשר הקצאה מוסרת; היכן שלא מוגדר דואר, הרשומה ביומן היא התיעוד. הודעות אלה נשלחות ללא קשר להעדפת הדוא״ל של המשתמש.
תפקידים נגזרים¶
תפקיד נגזר בא מתכונות המשתמש. הארגון מצהיר אילו תכונות הופכות לתפקידים (derived_roles), כל אחת עם תיאור. לכל תכונה מוצהרת a שהתכונות של המשתמש נותנות לה ערך v, למשתמש יש את תפקיד ההורה a ואת תפקיד הערך שלו a__v: region = eu נותן region ו-region__eu. אין ערך — אין אף אחד מהתפקידים האלה.
פתרון התכונות חשוב. הרשומה של הארגון עצמו על החבר גוברת על מה שספק הזהות נותן: כששניהם נוקבים באותה תכונה, משתמשים בערך של הארגון, וערך הספק מסומן כמוחלף.
לתפקידים נגזרים אין זכויות והם לעולם אינם מוקצים ביד. רשימת הענקה, מסכה או כלל שורות נוקבים בהם בדיוק כמו בתפקיד מוענק; בקשה נוקבת בהם ב-X-Provisa-Role ופועלת בשמם.
בדף התפקידים כל תכונה מוצהרת היא תפקיד הורה המסומן נגזר, ותפקידי הערך שלה מוצגים תחתיה ככל שערכיהם נראים. + תפקיד ← נגזר ממאפיין מצהיר על אחד. החלונית שלו מציגה את התכונה, את התיאור שלה, את הערכים שנראו עד כה ואת כל מסכה, כלל שורות והענקה שנוקבים בה או באחד מתפקידי הערך שלה. אין לה פקדי הענקה. התיאור של תפקיד ערך נוצר אוטומטית: "מוחזק כאשר region של המשתמש הוא eu".
הצהירו עליהם בתצורה כרשימה:
derived_roles:
- attribute: region
description: The subsidiary a member works for; region__eu marks EU staff.
חשבו על JWT: ה-claims שלו הם עובדות על האדם. תפקידים נגזרים הופכים עובדות נבחרות לתפקידים, כדי שהענקה תוכל לפנות אליהן בלי ליצור תפקיד לכל צירוף.
מטא-תפקידים¶
כשבקשה נוקבת ביותר מתפקיד אחד, Provisa בונה מטא-תפקיד זמני עם המזהה meta:<ids sorted, joined by +>. הוא הבן היחיד של כל תפקיד בקבוצה הנקובה. הצינור כולו רץ אז איתו כמו עם כל תפקיד יחיד.
הזכויות הן האיחוד של זכויות כל חבר. מסנני השורות הם OR של המסננים המותרים של כל חבר; אם לחבר כלשהו שקורא טבלה אין מסנן, אין גם למטא-תפקיד. היעדר מסנן פירושו כל השורות. מטא-תפקיד לעולם אינו נשמר, מפורט או מיוצא. הוא נבנה מחדש מהחברים שלו כשהמודל משתנה.
העמודות של כל שורה מוסוות ומוצגות לפי התפקידים שמסנן השורות שלהם התיר את השורה: מבין תפקידים אלה חלה הקריאה המגבילה פחות של העמודה, ותפקיד שלא התיר את השורה אינו תורם דבר לאופן שבו היא נקראת. ההסוואות חלות במקום שבו הטבלה נקראת, לפני כל join או צבירה, כך שצבירה קוראת את הערכים המוסווים. כתיבות נבדקות באותו אופן: עמודה נכתבת בשורה רק היכן שתפקיד שמעניק את העמודה מתיר את השורה.
רשומת הביקורת נוקבת בכל תפקיד פועל, לא במזהה המטא-תפקיד. מכיוון שמטא-תפקיד לעולם אינו נשמר, אין לו תיאור משלו: בכל מקום שבו מוצג תיאור, התיאור שלו הוא של חבריו, כל מזהה עם התיאור שלו.
תיאורים¶
כל תפקיד וכל כלל שורות אומרים לשם מה הם קיימים. סקירת גישה קוראת מי רשאי לראות מה ולמה. תפקיד או כלל שאיש אינו יכול להסביר הם כאלה שאיש אינו יכול לסקור, ולכן התיאור חובה: תפקיד, תפקיד הורה נגזר או כלל שורות עם תיאור ריק נדחים בטעינת התצורה, ב-API הניהול וב-UI, עם הסיבה. הרשימות תפקידים וכללי שורות מציגות כל תיאור, וחלוניות הפרטים שלהן מציגות ועורכות אותו.
לתפקיד יש תיאור אחד, לא אחד לכל תחום. תפקיד כללי שאינו מתאר את אחריותו בתוך תחום מסוים פירושו שהתחום הזה צריך תפקיד משלו, בדרך כלל בן של התפקיד הכללי (parent_role_id), שיורש את זכויותיו ומתאר מה הוא עושה שם.
איך בקשה נוקבת בתפקידים¶
לקוח שולח תפקיד אחד, או קבוצה מופרדת בפסיקים, ב-X-Provisa-Role. כל תפקיד נקוב חייב להיות מוחזק על ידי המשתמש. תפקיד שאינו מוקצה לזהות נדחה בשמו.
זכויות ונתונים הולכים רק אחרי התפקידים שבשמם הבקשה פועלת: להחזיק תפקיד אינו לפעול בו. תפקיד אחד הוא אותו תפקיד; שניים או יותר פועלים כמטא-תפקיד שלהם. תפקיד פועל שהיה מביא איתו את הזכויות של כל תפקיד מוחזק לא היה אומר דבר, ולכן הבקשה מקבלת את ההרשאות המינימליות, ורשומת הביקורת נוקבת בדיוק במה שפעל.
תפקידים נגזרים נקובים נכנסים כמסייגים. למסייג אין זכויות, גישת תחום, מסכות או מסנני שורות משלו, ולכן הוא אינו חבר זכויות במטא-תפקיד: כחבר ללא מסכות הוא היה מסיר את המסכה מכל עמודה, וכמי שאינו קורא אף טבלה הוא היה סוגר כל OR של מסנני שורות. הוא נכנס רק להענקות: הענקה או רשומת מסכה שנוקבת ב-region__eu מגיעה לבקשה שנוקבת במסייג הזה יחד עם תפקיד מוענק.
סוכן MCP לעולם אינו פועל מעבר לתפקידים שניתנו לו. סוכן מרוחק מציין רק תפקידים שהישות של האסימון שלו מחזיקה. סוכן stdio מקומי מציין רק תפקידים בתוך הקבוצה שנקבעה ב-mcp.role (PROVISA_MCP_ROLE), ופועל כקבוצה זו כשאינו מציין אף אחד; כל תפקיד אחר נדחה בשמו.
סוכן שואל באיזה תפקיד הוא פועל באמצעות כלי ה-MCP describe_role. התשובה היא התפקיד הזה ולשם מה הוא קיים (מטא-תפקיד עם כל אחד מחבריו), והתפקידים שהסוכן רשאי לעבור אליהם, כל אחד עם התיאור שלו. היא לעולם אינה כוללת כלל שורות, מסכה, הענקה או תנאי. Provisa לעולם אינה אומרת לסוכן מדוע תוצאה צומצמה, ותוצאה אינה נושאת סימון "מסונן": הנתונים של תפקיד הם כל התצוגה שלו, וסיבה למה שחסר הייתה מזמינה גישוש אחריו.
מה + אומר¶
+ מופיע בשני מקומות, ובכל אחד משמעותו שונה.
ברשומת הענקה. analyst+region__eu ברשימת visible_to או to פירושו שהקורא מחזיק את כולם: analyst וגם region__eu בו-זמנית. רשומה שאינה מורכבת היא תפקיד אחד. בין רשומות הרשימה הכלל הוא כל אחת: לקורא יש את ההענקה אם יש לו רשומה כלשהי. AND בפנים, OR בין.
במזהה מטא-תפקיד. meta:analyst+engineer הוא המזהה בצד השרת של המטא-תפקיד של הקבוצה {analyst, engineer}. הזכויות שלו הן האיחוד של שניהם. לקוח לעולם אינו נוקב במזהה מטא-תפקיד ישירות; הוא נוקב בתפקידים שהוא מחזיק, והשרת בונה את המטא-תפקיד.
כללי עמודה של ABAC כ-RBAC פשוט¶
מדיניות מבוססת תכונות שואלת: "האם המשתמש הזה, עם התכונות האלה, יכול לראות את העמודה הזו?" RBAC שואל: "אילו תפקידים יכולים לראות את העמודה הזו?" הענקות מורכבות ותפקידים נגזרים הופכים את זה לשאלה אחת.
"הסירו את המסכה מהאימייל הזה רק לאנליסט באיחוד האירופי" היא רשומת המסכה {to: [analyst+region__eu], mask: null}. כל קורא שמחזיק גם את התפקיד analyst וגם את התפקיד הנגזר region__eu קורא את העמודה בלי מסכה. אין שפת מדיניות. אין תפקיד לכל צירוף.
הפשרה היא חישוב במקום מניה. קטלוג תפקידים קטן מוריד את עלות התחזוקה, אבל "מי יכול לראות את האימייל הזה מהאיחוד האירופי?" הופכת לתשובה מחושבת: הרשומות המורכבות מוערכות מול ההקצאות וה-claims של כל משתמש. Provisa יכולה לענות לשני הכיוונים, כי המודל הוא הגדרה אחת. בדיקת הגישה בעורך העמודות (POST /admin/masks/check) מריצה את ההערכה הזו עבור קבוצת תפקידים נבחרת.
יתרונות¶
- כלל עמודה של ABAC הופך לרשומת הענקה של RBAC.
- קטלוג תפקידים קטן במקום תפקיד לכל צירוף.
- הגדרה אחת שמתאמת את הכללים של כל אזור.
- הרשאות מינימליות ורשומת ביקורת מדויקת לכל בקשה.
הנחיות¶
- שמרו את קטלוג התפקידים קטן ככל שהארגון מאפשר.
- רשו בתוך ציר אחד (ותק, רמת סיווג). צרפו על פני צירים בלתי תלויים (תפקיד + אזור, תפקיד + מחלקה).
- הציבו את רשומות המסכה הספציפיות ביותר ראשונות. רשומה מוקדמת יותר גוברת גם אם לקורא יש רשומה מאוחרת יותר.
- בדקו את הגישה בפועל עם
POST /admin/masks/checkלפני שצירוף יוצא לייצור.
מודל הרשאות עמודה¶
לכל עמודה יש מודל הרשאות ששולט בקריאה, בכתיבה ובמיסוך לכל תפקיד.
נראות בשלוש רמות¶
| רמה | תנאי | תוצאה |
|---|---|---|
| מוסתרת | התפקיד אינו ב-visible_to |
העמודה אינה בסכמה ובקטלוג |
| ממוסכת | התפקיד ב-visible_to; לרשומת המסכה הראשונה שהוא מחזיק יש mask |
העמודה גלויה, הערך מוחלף בפלט |
| ללא מסכה | התפקיד ב-visible_to; לרשומת המסכה הראשונה שהוא מחזיק אין mask, או שאין לו אף אחת |
גישת קריאה מלאה |
עמודות ציבוריות¶
לכל עמודה יש scope, שנקבע בקובץ המודל, דרך API הניהול או בעמוד הטבלאות. שלושת ההיקפים מסודרים לפי מי שהם משרתים: restricted, אחר כך domain, אחר כך public. עורך הטבלה (table_registration בתחום הטבלה) יכול לצמצם את ההיקף של עמודה. הרחבתו — restricted ל-domain או ל-public, domain ל-public — מגישה את העמודה לתפקידים רבים יותר ודורשת את הזכות שההענקה שלה דורשת: column_grant או access_config בתחום הטבלה. עמודה נרשמת נמדדת מול domain, ולכן רישומה כ-public הוא הרחבה. לעמודה עם תג רגיש כל שינוי היקף דורש sensitive_data. שינוי שנדחה נוקב בעמודה ובזכות.
| היקף | מוגש ל |
|---|---|
domain (ברירת מחדל) |
תפקידים שמגיעים לתחום הטבלה, כפי ש-visible_to אומר. visible_to ריק הוא כל תפקיד כזה. |
public |
אותם תפקידים, וגם תפקידים שאינם מגיעים לתחום הטבלה. |
restricted |
רק התפקידים ש-visible_to נוקב בהם. visible_to ריק הוא אף אחד. |
פרסום עמודה מסיר את תנאי התחום לאותה עמודה ותו לא:
- לתפקיד מחוץ לתחום הטבלה מוגשת רק עם העמודות הציבוריות שלה. שאר עמודות הטבלה, וטבלאות שאינן מפרסמות דבר, נשארים מחוץ לסכמה, לקטלוג ול-SQL שלו.
- ה-
visible_toשל העמודה עצמה עדיין מחליט למי: עמודה ציבורית שהוענקה לתפקיד אחד ציבורית רק לאותו תפקיד. - מסכות, מסנני שורות, מגבלות שורות והגנת הקשרים חלים כמו בתוך התחום. ללא מסנן שורות לתפקיד, העמודות המפורסמות נקראות בלי מסנן; מסנן שורות על הטבלה או על התחום שלה עדיין חל.
- קשר מוגש כשהעמודות של שני קצותיו מוגשות.
- עמודת פרמטר של טבלת API (ארגומנט נתיב או שאילתה) אינה נתונים ואינה מפרסמת דבר.
- התחום
opsדורש הענקה מפורשת לכל עמודה; דבר בו אינו מוגש בין תחומים, יהיה ההיקף אשר יהיה.
הכלל זהה בכל משטח, כי כל אחד קורא את הסכמה המהודרת של התפקיד: GraphQL, SQL (HTTP, pgwire, Flight, JDBC), Cypher ו-Bolt, gRPC, REST ו-JSON:API, קטלוגי Flight ו-MCP ורשימות הניהול. היקף שאינו אחד משלושת הערכים נדחה בשמירת העמודה.
הרשאות כתיבה¶
קריאת עמודה נשלטת על ידי visible_to (ריק פירושו שכל התפקידים יכולים לקרוא). כתיבה נשלטת לכל טבלה על ידי write_grants, ולא לכל עמודה. כברירת מחדל אין לטבלה שום הרשאה, ולכן אף תפקיד אינו יכול לכתוב אליה.
הרשאת כתיבה מציינת מה תפקיד רשאי לכתוב לטבלה בפעולה אחת (insert, update או delete):
| שדה | מטרה |
|---|---|
columns |
העמודות ש-INSERT רשאי לרשום או ש-UPDATE רשאי לקבוע |
rows |
השורות ש-UPDATE או DELETE רשאי לגעת בהן (פרדיקט SQL) |
check |
מה שכל שורה ש-INSERT כותב, או ש-UPDATE משאיר, חייבת לקיים (פרדיקט SQL) |
presets |
עמודות שההרשאה קובעת בעצמה, ממשתנה הפעלה, מערך מילולי או מרגע הכתיבה. ההצהרה אינה רשאית לספק אותן. |
אם משמיטים את rows או את check, ההרשאה לוקחת את מסנן השורות של התפקיד לקריאה בטבלה, כך שתפקיד כותב את השורות שהוא קורא. "TRUE" פירושו כל שורה.
אישור יחיד מכריע בכל כתיבה בכל משטח, בסדר הזה: לתפקיד יש את הזכות write; יש לו הרשאה לפעולה על הטבלה; כל עמודה שההצהרה כותבת מופיעה בהרשאה; ה-presets מתווספים; rows מצורף ב-AND ל-WHERE; וה-check מוערך על השורות החדשות לפני שנשלח משהו למקור. דחייה נוקבת בטבלה, בתפקיד ובפעולה.
הצהרות רחבות יותר דורשות הרשאות רחבות יותר. TRUNCATE דורש הרשאת delete על כל שורה. MERGE דורש הרשאת insert והרשאת update על כל שורה. INSERT ... ON CONFLICT DO UPDATE דורש הרשאת update על כל שורה ללא presets. COPY דורש הרשאת insert ללא presets.
GraphQL מציע לכל תפקיד שדה מוטציה בדיוק עבור הפעולות שיש לו הרשאות עבורן, עם עמודות ההרשאה. תפקיד צאצא יורש את הרשאות ההורה, כשהשלו קודמות. תפקיד-על מחזיק את הקיפול של הרשאות חבריו: איחוד העמודות, ה-OR של rows ו-check, ורק ה-presets שכל החברים חולקים. בממשק הניהול, הסעיף כתיבות של טבלה מפרט ועורך את ההרשאות שלה.
דוגמה¶
tables:
- source_id: hr-pg
domain_id: people
schema: public
table: employees
columns:
- name: id
visible_to: [] # all can read
- name: email
visible_to: [admin, analyst, viewer]
masks:
- to: [admin] # admin sees raw value (no mask)
- to: ["*"] # everyone else sees it masked
mask: {type: regex, pattern: "(.).*@", replace: "$1***@"}
- name: salary
visible_to: [admin, hr]
masks:
- to: [admin, hr] # admin and hr see real value
- to: ["*"] # all other visible roles see 0
mask: {type: constant, value: 0}
- name: created_at
visible_to: [] # all can read
write_grants:
- role: admin
operation: update
columns: [email]
rows: "TRUE"
- role: hr
operation: update
columns: [salary]
- role: hr
operation: insert
columns: [email, salary]
presets:
- {column: created_at, source: now}
בדוגמה זו:
email: admin רואהalice@example.comויכול לעדכן אותו בכל שורה; analyst ו-viewer רואיםa***@example.comואינם יכולים לכתובsalary: admin ו-hr רואים את הערך האמיתי; hr יכול לעדכן אותו בשורות ש-hr יכול לקרוא, כי ההרשאה אינה קובעתrows; כל תפקיד גלוי אחר רואה0created_at: כולם יכולים לקרוא אותו, אף תפקיד אינו מספק אותו, וה-insert של hr קובע אותו לרגע הכתיבה
הרשאת מוטציה¶
מוטציה רשומה (GraphQL מרוחק, OpenAPI, gRPC, Hasura) עוברת שתי בדיקות. לתפקיד חייבת להיות היכולת הגלובלית write, והפקודה חייבת להיות מוקצית לו דרך visible_to; רשימה ריקה מקצה אותה לכל התפקידים. לפקודה אין רשימת writable_by, והתצורה דוחה רשימה כזו בשמה.
מה שקובע אם פעולה היא כתיבה הוא החוזה שלה, לא מה שהמבקש אומר. SELECT שקורא לפונקציה מסוג מוטציה נחשב כתיבה ועובר את אותן שתי בדיקות, כך שאי אפשר להריץ מוטציה בתחפושת של קריאה. סימון מוטציה כבטוחה לקריאה דורש את היכולת access_config ונרשם כהחלטת ממשל. אף בקשה לא יכולה לפטור את עצמה.
נראות סכמה¶
לכל תפקיד יש סכמת GraphQL משלו. מופיעות בה רק טבלאות מהדומיינים שב-domain_access שלו ("*" הם כולם), ועמודה שאינה ב-visible_to עבור התפקיד חסרה גם ב-SDL. מה שהתפקיד לא רשאי לקרוא, הוא לא רואה.
אבטחה ברמת השורה (RLS)¶
מסנן שורות הוא פרדיקט WHERE לטבלה אחת ולתפקיד אחד, שמתווסף אחרי ההידור ולפני הביצוע.
rls_rules:
- table_id: orders
role_id: analyst
filter: "region = current_setting('provisa.user_region')"
description: Analysts read only their own region's orders.
המסנן מצורף ב-AND לפסוקית ה-WHERE של השאילתה, בקריאה וגם ב-UPDATE וב-DELETE.
מסנן קורא את הבקשה דרך current_setting('provisa.<name>'). הבקשה קושרת את user_id לזהות המחוברת, את role לתפקיד הפועל, ואת כל טענה (claim) סקלרית באסימון, באותיות קטנות ועם קווים תחתונים במקום מקפים. provisa.user_id הוא תמיד הזהות המחוברת, ושום טענה לא יכולה להחליף אותו.
משתני הסשן של Hasura שומרים על שמם. X-Hasura-User-Id הוא hasura_user_id, נקרא מהטענה x-hasura-user-id באסימון (ברמה העליונה או בתוך אובייקט הטענות של Hasura, כמו https://hasura.io/jwt/claims) או מהתכונה hasura_user_id שנרשמה לחבר. לכן מסנן Hasura מיובא על X-Hasura-User-Id קורא את provisa.hasura_user_id.
מתחת לערכי הבקשה נמצאים ה-session_vars שהוגדרו לתפקיד, והבקשה דורסת אותם. תפקיד יורש מקבל את הקבועים של אבותיו מתחת לשלו, ולכל שם מנצח התפקיד הקרוב ביותר. user_id, role, region ו-org מגיעים מהבקשה או מהצומת ואי אפשר להגדיר אותם כקבועים. במצב לא מאובטח, כותרת x-provisa-session-<name> קושרת את <name>. משתנה שלא נקשר בשום מקום הוא NULL, ומסנן עליו לא מתאים לאף שורה.
קבוע של תפקיד פועל הפוך מתפקיד נגזר. תפקיד נגזר הופך ערך שיש לחבר לתפקיד. קבוע של תפקיד נותן ערך לכל בקשה שנעשית בשם התפקיד, וכלל שורות קורא אותו כמו שהוא קורא טענה. הקבוע יכול להיות הפניה ${env:NAME} או ${secret:NAME}, שנפתרת באותו מנגנון כמו כל הפניה אחרת כששאילתה משתמשת בה. המודל שומר את ההפניה, לעולם לא את הערך, והפניה שאינה נפתרת לשום דבר מכשילה את השאילתה ומציינת את הקבוע. אחרי שנפתר, הערך הוא חלק מהשאילתה: הרישומים של Provisa עצמו מכוסים בהשחרה המרכזית, אבל היומנים של מקור מאוחד עצמו אינם בהישג ידו של Provisa.
מיסוך ברמת עמודה¶
המיסוך מוגדר כרשימה מסודרת לכל עמודה. הרשומה הראשונה שהקורא מחזיק מחליטה מה הוא רואה.
| סוג מסכה | סוגי עמודה נתמכים | ביטוי SQL |
|---|---|---|
regex |
מחרוזת (varchar, char, text) | REGEXP_REPLACE(col, pattern, replace) |
constant |
כל סוג | ערך ליטרלי (NULL, 0, מחרוזת מותאמת) |
truncate |
תאריך/חותמת זמן | DATE_TRUNC(precision, col) |
לכל רשומה יש רשימת to שנוקבת בקוראים שהיא מכוונת אליהם, ו-mask אופציונלי. רשומה ללא mask פירושה "ללא מסכה" לאותם קוראים. * ב-to תואם כל קורא.
ביטוי המסכה נדחף להטלת ה-SQL SELECT. מסד הנתונים מחזיר את הערך הממוסך; נתונים לא ממוסכים לעולם אינם עוברים ברשת לתפקידים ממוסכים. עמודות ממוסכות חסומות גם בפסוקיות WHERE ו-HAVING (הגנת הפרדיקטים של שכבה 5) כדי למנוע הסקת ערכים דרך סינון.
מסכה חלה על כל שם של העמודה, לא על עמודה קשורה. עמודה הנקראת לפי הכינוי שלה או לפי שמה הפיזי ממוסכת באותו אופן. מסכה אינה מסתירה מפתח שקשר מאושר מצרף לעמודה שהתפקיד יכול לראות: הצירוף משווה את הערכים האמיתיים, כך שהעמודה הקשורה מציגה את מה שהמפתח הממוסך מכיל. מסכו גם את העמודה הקשורה, או אל תאשרו את הקשר עבור אותו תפקיד.
אין רשומה — אין מסכה. קורא שאינו מחזיק אף רשומה קורא את העמודה בלי מסכה. סיימו את הרשימה ברשומת * כשמסכה כלשהי צריכה לחול על כל קורא. עורך העמודות מזהיר כשברשימה אין רשומת * (unlisted_unmasked).
חסימה לפי סדר. רשומה מוקדמת יותר גוברת גם כשלקורא יש גם רשומה מאוחרת ומתירנית יותר. כדי שקבלנים יראו תמיד משכורת ממוסכת, לא משנה אילו תפקידים נוספים יש להם, הציבו את רשומת הקבלנים לפני הרשומה ללא מסכה. תפקיד מוענק לעולם אינו מאבד את מה שההורה נותן בירושה, אבל רשומת מסכה שמופיעה מוקדם יותר ברשימה חלה ראשונה. כך מגבילים כאן.
מטא-תפקידים. השרשרת של מטא-תפקיד כוללת את התפקידים של כל חבר, את אבותיהם וכל תפקיד נגזר נקוב. רשומה שהוענקה לחבר כלשהו מגיעה למטא-תפקיד. רשומה לפניה שחבר אחר מחזיק עדיין חלה ראשונה.
בדיקת גישה. POST /admin/masks/check מקבל רשימת מסכות וקבוצת תפקידים אופציונלית, ומחזיר איזו רשומה חלה על התפקידים האלה, וגם רשומות שאף קורא אינו יכול להגיע אליהן. מוגן ב-masking_config.
# email column: analyst+region__eu sees raw; everyone else sees masked
masks:
- to: [analyst+region__eu] # composite: analyst AND in EU region
- to: ["*"] # all others
mask: {type: regex, pattern: "(.).*@", replace: "$1***@"}
# hire_date column: hr sees raw; all other visible roles see year only
masks:
- to: [hr]
- to: ["*"]
mask: {type: truncate, precision: year}
ממשל תגובות של פעולות¶
מה שפונקציה במעקב או webhook מחזירים מנוהל כמו שורות של טבלה. Provisa קושר את השורות המוחזרות כיחס שעמודותיו הן הפלט המוצהר של הפעולה: output_columns לפונקציה, inline_return_type ל-webhook, או תכונות האובייקט במערך ה-return_schema של פונקציה. היחס הזה עובר אחר כך את אותו שלב ממשל כמו כל קריאה מטבלה, על VALUES CTE שמחזיק את השורות, כך שאבטחת שורות, הסוואה ונראות לא ממומשות מחדש עבור פעולות.
שלושה כללים חלים, באותו סדר כמו לטבלה:
- מסנן שורות. כלל ה-RLS של התפקיד שנשמר עבור הפעולה בשמה (
upsertRlsRuleעםactionName), או כלל הדומיין של הפעולה כשאין לה כלל משלה. הפרדיקט נבדק מול החוזה בעת השמירה, כמו כלל של טבלה. - נראות עמודות. עמודה בחוזה שמצהירה על
visible_toנשמטת לכל תפקיד שהרשימה לא כוללת. עמודה שלא מצהירה עליו היא חלק מהצורה הציבורית של הפעולה ונשארת. - הסוואות. עמודה בחוזה נושאת
masks, אותה רשימה מסודרת שעמודה בטבלה נושאת.
ירושת התפקידים פותרת את השרשרת לשלושתם. אם תגובה מחזירה עמודה מחוץ לחוזה המוצהר, הקריאה נכשלת עם 502 במקום להעביר אותה בלי ממשל. פעולה שלא מצהירה על עמודות מחזירה ערך סקלרי, שאין מה לקשור בו, והוא חוזר כפי שהוא. הפעלת הבדיקה בממשק הניהול מדווחת אילו מסננים, השמטות והסוואות הוחלו, והמשפט המנוהל נרשם ביומן הביקורת של השאילתות.
דגימה¶
כל תפקיד מקבל מדגם של התוצאות, 100 שורות כברירת מחדל, אלא אם יש לו היכולת full_results. את הגודל קובעים במשתנה הסביבה PROVISA_SAMPLE_SIZE.
יומן ביקורת¶
כל שאילתה שנוגעת בנכס של דומיין נכתבת ל-query_audit_log. היומן שייך לארגון, לא לפלטפורמה: המרשם של הפלטפורמה עצמה, עם ארגונים, משתמשים והזמנות, אינו מחזיק רשומות ביקורת. היומן של כל ארגון הוא טבלה בסכמה שלו, במאגר שהארגון מציין עבור הרישום שלו בכל אזור שהוא בוחר. ארגון שלא מצהיר על אזורים שומר אותו במסד הנתונים של מישור הבקרה של הפריסה, לצד המודל שלו.
שורה רושמת את tenant_id (הארגון), user_id, role_id, גיבוב SHA-256 של טקסט השאילתה, את הטקסט עצמו (מוצפן), table_ids ו-table_names, source, status_code, duration_ms, את הנתיב ומספר השורות, מה הוחל על המשפט, ואת logged_at. הטקסט מוצפן במפתח של הארגון אם הגדיר אחד, ואחרת במפתח של הפריסה. פריסה בלי ספק הצפנה שומרת אותו לא מוצפן. בכל מקרה הוא לא מופיע בטבלאות התפעול שאפשר לשאול עליהן, והוא מפוענח רק לקריאות ניהול מורשות.
ביומן אפשר רק להוסיף, ברמת מסד הנתונים עצמו: כללי PostgreSQL הופכים DELETE ו-UPDATE על query_audit_log לפעולות ריקות. שני אינדקסים, (tenant_id, logged_at) ו-(user_id, logged_at), שומרים על מהירות של שאילתות תאימות לפי ארגון ולפי משתמש בטווח זמן.
השורות נשמרות לצמיתות. ל-Provisa אין הגדרת שמירה והוא לעולם לא מנקה את היומן, ומכיוון ש-DELETE לא עושה דבר, מחיקת שורות ישנות פירושה שמפעיל מוחק או מעביר לארכיון את נתוני הטבלה מחוץ ל-Provisa.
השחרה¶
מה ש-Provisa מתעד על עבודתו עובר דרך רשימה של כללי השחרה לפני שהוא נכתב לאן שהוא. טקסט שכלל תואם לו מוחלף ב-[REDACTED:<rule>], שמציין את הכלל שתאם.
לרשימה ארבעה חלקים:
- תבניות מובנות. מספרי כרטיס שעוברים את בדיקת Luhn, מספרי ביטוח לאומי אמריקאיים, אסימוני bearer, אסימוני גישה של Provisa, מפתחות AWS, GCP, Azure, GitHub ו-Slack, ומפתחות פרטיים. הן ערכת פתיחה, לא הבטחה.
- כללי הפריסה, שחלים בכל ארגון. שינוים דורש את ההרשאה
platform_settings. - הכללים של הארגון עצמו. שינוים דורש את ההרשאה
org_settings. - הכלל האוטומטי על הסודות של הארגון: כל ערך שהמאגר של Provisa עצמו מחזיק עבור הארגון, כל
${secret:NAME}שהמודל וההגדרות של הארגון מזכירים (בכל backend של סודות), וכל ערך${secret:…}או${user:…}מרגע שהוא נפתר לראשונה. אין צורך שכלל כלשהו יזכיר אותם.
כלל הוא ביטוי רגולרי בעל שם או הפניה ${secret:NAME} / ${env:NAME} בעלת שם, שתואמת לערך שאליו ההפניה נפתרת. הפניה שאינה נפתרת נרשמת ביומן ברמת ERROR לפי שמה ומוצגת כלא נפתרת; שאר הכללים ממשיכים לחול. ערכים קצרים מארבעה תווים אינם מזוהים.
Provisa אינו אחראי להכיר כל פורמט רגיש. התבניות המובנות מכסות צורות נפוצות; ארגון מוסיף תבניות לפורמטים שלו, כמו מספרי עובד, מזהי חשבון או שמות מארחים פנימיים. ההשחרה נעשית במידת האפשר: ערך שנכתב בצורה שאף כלל אינו מתאר עובר כמו שהוא.
הכללים נמצאים בדף סודות הארגון, תחת כללי השחרה. הדף מציג את התבניות המובנות, את כללי הפריסה (ניתנים לעריכה עם platform_settings), את הכללים של הארגון, את הסודות של הכלל האוטומטי לפי שם בלבד, וכל הפניה שאינה נפתרת. הכללים נטענים כשהשרת עולה וכשסביבת הריצה של ארגון נבנית, ונטענים מחדש כשהמודל או ההגדרות משתנים וכשסוד של הארגון נשמר או מוסר.
הרשימה חלה בכל יציאה ש-Provisa כותב אליה:
- כל span, כעותק מושחר אחד שנמסר לכל exporter, ותצוגת ה-spans האחרונים;
- כל רשומת לוג בצינור הלוגים של OpenTelemetry ב-Provisa, וכל שורה שמטפל logging כותב, כולל הפלט הסטנדרטי ולוגי הקונטיינרים;
- טקסט השאילתה ביומן הביקורת, לפני שהוא עובר hash והצפנה, כך שלא הטקסט ולא ה-hash שלו חושפים ערך מזוהה; והפירוט ביומן הניהולי;
- הודעות השגיאה שלקוח מקבל דרך HTTP, pgwire, gRPC, Arrow Flight ו-MCP.
טלמטריה ושגיאות לקוח אינן קשורות לארגון אחד בזמן שהן נכתבות, ולכן הן מושחרות לפי הכללים של כל הארגונים יחד. שתי שגיאות עוברות בלי השחרה: גוף שגיאת HTTP גדול מ-1 MiB, והשגיאה של זרם Arrow Flight שנכשל אחרי שהתחיל לשלוח.
מקור מאוחד מנהל לוגים משלו, ש-Provisa אינו כותב ואינו יכול להגיע אליהם. רשומות שמקור שומר על המשפטים ש-Provisa שולח אליו הן באחריות המקור להגן עליהן.
סטטיסטיקה דרך MCP¶
כלי הסטטיסטיקה של MCP (describe, correlate, regress, compare, forecast) מריצים את ה-SQL שסוכן מגדיר דרך הצינור המנוהל בתפקיד של הסוכן, ולכן כללי שורות, מיסוך והשחרה חלים על מה שהם מנתחים. כלי הסטטיסטיקה מחזירים תוצאות בלבד, ולכן השורות שמאחורי ניתוח לעולם אינן נכנסות להקשר של המודל: מזעור נתונים כברירת מחדל.
בקרת הגישה היא שמגבילה את מה שסוכן יכול ללמוד. כדי להרחיק זהויות מהישג ידו של סוכן, העניקו לתפקיד שלו רק את העמודות שאינן מזהות איש; תפקיד שיכול לקרוא עמודה יכול גם לצבור אותה, ואגרגטים על קבוצות קטנות עדיין יכולים לזהות אנשים.
כיסוי עמודות רגישות¶
התצוגה sensitive_coverage בדומיין meta מפרטת כל עמודה רגישה בארגון עם הערך המדומה וכלל הסינתזה שהיא מצהירה עליהם, וסטטוס: covered, no_fake, או inadequate כשאחת מבדיקות ההלימה של Provisa נכשלה. הבדיקות מחפשות ערך מדומה שמשאיר ערכים אמיתיים ללא שינוי, ערך מדומה שסוגו אינו מתאים למה שהעמודה מכילה, וערך מדומה או כלל שקורא את הערך האמיתי של עמודה רגישה אחרת. אחראי נתונים רואה את העמודות שאין להן ערך מדומה בפרטי כל סביבה, וסביבת Test (fake) מסרבת להגיש נתונים עד שלכל עמודה רגישה יש ערך מדומה. הבדיקות והתצוגה מתוארות בבניית סביבות בדיקה.
שיקולי דעת בבדיקה¶
החלטות ממשל נשענות על שיקול דעת, ושיקול דעת יכול להיות שגוי או להתיישן כשהעובדות שעליהן נשען משתנות. כל שיקול דעת ממשלי יכול לשאת למה: תג על אובייקט, הערך המדומה וכלל הסינתזה של עמודה, רשומת מיסוך, כלל שורות והרשאת כתיבה. למה מכיל את הנימוק שעליו הסתמכו, את המטרה, את הבסיסים בעלי השם שצוטטו ותאריך לבדיקה, הכול אופציונלי. בהקצאת תג הנימוק הוא ה-reason של ההקצאה עצמה; בכלל שורות המטרה היא התיאור החובה שלו. Provisa רושם מי קיבל את ההחלטה, מתי, ועל סמך איזו ראיית פרופיל: הריצה האחרונה של פרופיל הטבלה ותקציר שלה. הוא רושם זאת בכל שמירה שמשנה את ההחלטה או את הלמה שלה. שמירה שמשמיטה את הלמה שומרת את הקיים, וקובץ מודל נושא כל למה בבלוק why:.
בסיסים בעלי שם הם ההקשר שהנתונים אינם יכולים להראות: רגולציה, מדיניות, אסטרטגיה. שיקול דעת מצטט אותם לפי מזהה מתוך רשימה קצרה (/admin/judgments/bases). מחזיק access_config מכריז על בסיס כמשתנה עם הערה, וכל שיקול דעת שמצטט אותו והוחלט לפני כן מתיישן מיד.
הבדיקה רצה כל שעה לכל ארגון, בסוף כל ריצת פרופיל עבור הטבלה שנבדקה, ומיד כשמוכרז על בסיס כמשתנה. היא מכניסה את מה שמצאה לתור אחד:
| קטגוריה | נמצא כאשר |
|---|---|
| שגוי | בדיקת הלימה של הערך המדומה של עמודה רגישה נכשלת, או שכלל הסינתזה של עמודה רגישה מעתיק ערכים אמיתיים |
| התיישן בגלל הנתונים | הפרופיל סטה על העמודה שהוחלט עליה (לפי הספים של הפרופיילר עצמו), הסוג הסביר, המשפחה או סוג הנתונים שלה השתנו, נוספה עמודה לטבלה, העמודה שהוחלט עליה הוסרה או שונה סוגה, או שמקבל ההחלטה אינו חבר עוד |
| התיישן בגלל ההקשר | בסיס שצוטט הוכרז כמשתנה אחרי ההחלטה, הגיע תאריך הבדיקה, או שה-expires_on של הקצאת תג חלף |
| לא הוחלט | לעמודה רגישה אין ערך מדומה, או שנוספה עמודה לטבלה שיש לה תג רגיש או ערך מדומה |
| שווה הצצה | אישור גישה של מקור נשמר כערך ולא כהפניה; מקור של סביבת בדיקה קורא את החיבור של prod; לא הוגדר אימות; מנהל האתחול לא נדרש; listener משרת בלי TLS; תפקיד או מקור מגיעים לכל הדומיינים; הקצאת תג שפג תוקפה עדיין בתוקף |
הקטגוריה האחרונה נצפית ואינה נאכפת: שום דבר בבדיקה אינו מסרב לדבר. GET /admin/judgments/findings מפרט את הממצאים הפתוחים שהקורא רשאי לראות; ממצא על עמודה רגישה דורש sensitive_data, וכל אחר view_governance. אישור מחדש (POST /admin/judgments/findings/{id}/reaffirm) רושם את שיקול הדעת מחדש עם ראיות היום ודורש את ההרשאה שעורכת את ההחלטה. ממצא עמדה נדחה עם הערה (.../dismiss, access_config). ממצא שהבדיקה כבר אינה רואה נסגר. כראיית ביקורת, התצוגה judgment_review בדומיין ops מחברת כל ממצא ללמה של שיקול הדעת שלו ונשאלת תחת גישה לפי תפקיד ודומיין.
הגבלת קצב¶
מגבלות לכל תפקיד מגדירים ב-provisa.yaml: בקשות לשנייה, מינויי SSE בו-זמניים וזרמי Arrow Flight בו-זמניים. הן נבדקות ב-API, לפני שמשהו מהודר או מורץ, ובקשה שחורגת מהמגבלה מקבלת HTTP 429 עם כותרת Retry-After.
לשאילתות בשפה טבעית (POST /query/nl) יש מגבלה משלהן, nl.rate_limit, בבקשות לדקה לכל תפקיד. בקשה שחורגת ממנה נדחית לפני כל קריאה ל-LLM.
הספירות נשמרות ב-Redis (cache.redis_url) כחלונות נעים, בלי מצב לכל מופע, כך שמגבלה חלה בלי קשר למספר מופעי Provisa שאתם מריצים.
מגבלת מורכבות שאילתה¶
מגבלת קצב מגבילה כמה בקשות תפקיד שולח. מגבלת מורכבות מגבילה מה הצהרה אחת יכולה לבקש.
לכל הצהרה יש ציון מורכבות: היחסים שהיא קוראת, ה-joins שלה, העמודות שהיא בוחרת (* נספר כעמודות שהוא מייצג) והשאילתות המקוננות בה. יחס שנקרא ממקור API מרוחק (OpenAPI, GraphQL מרוחק, gRPC מרוחק) נספר כ-10, כי כל קריאה מוציאה מתקציב שהמערכת המרוחקת קובעת לכל המשתמשים של אותו מקור.
המגבלה היא max_query_complexity ב-rate_limit של התפקיד. ההגדרה הארגונית limits.max_query_complexity היא תקרה לכל תפקיד: תפקיד יכול לקבוע מגבלה נמוכה יותר, ומגבלה גבוהה יותר אינה חלה. כברירת מחדל אף אחת אינה מוגדרת.
הציון נמדד אחרי ניתוח השאילתה ולפני הממשל או הביצוע שלה, כך שמגבלה אחת חלה על כל ממשק שדרכו יכולה להגיע הצהרה: GraphQL, SQL על HTTP, pgwire, Arrow Flight, gRPC, Cypher, JSON:API ו-MCP. הצהרה מעבר למגבלה נדחית עם הציון שלה, המגבלה ומה שהיא ביקשה. ממשקי HTTP עונים 413. הדחייה נרשמת כדחיית מדיניות.
אימות¶
ספקי אימות ניתנים לחיבור:
| ספק | אישור | תרחיש |
|---|---|---|
none |
כותרת X-Provisa-Role | פיתוח בלבד |
basic |
חשבונות מקומיים (bcrypt) + JWT של סשן | פריסות עצמאיות |
firebase |
ID token של Firebase | ייצור |
keycloak |
JWT של Keycloak | ארגוני |
oauth / oidc |
JWT של OIDC | PingFed, Okta, Azure AD, Auth0 |
ldap |
שם משתמש וסיסמה בספרייה (LDAP או Active Directory) | ספריות ארגוניות |
saml |
assertion חתום של SAML 2.0, ואז JWT של סשן | כניסה יחידה (SSO) ארגונית |
simple |
bcrypt + JWT | בדיקות |
ldap מכניס משתמש בשיטת search-then-bind: Provisa מתחבר כחשבון שירות, מוצא את הרשומה האחת ששם המשתמש מציין, ואז מתחבר כרשומה הזו עם הסיסמה שהוצגה, כך שהספרייה היא שבודקת אותה. הקבוצות של המשתמש הופכות לתפקידים שלו. החשבון והקבוצות שלו נקראים מחדש בכל פעם שטוקן סשן חוזר, ולכן משתמש שהוסר מהספרייה, או מקבוצה, מאבד את הגישה הזו לפני שהטוקן פג. גם ldaps:// וגם StartTLS נתמכים.
saml הוא Web Browser SSO ביוזמת ה-SP. חתימת ה-XML של התגובה מאומתת מול המטא-דאטה של ספק הזהות, וה-claims נקראים רק מהאלמנט החתום. Issuer, Audience, כתובת ה-ACS, התפוגה ו-InResponseTo נבדקים כולם. תגובות לא מבוקשות (ביוזמת ה-IdP), assertions מוצפנים, מסמכים עם DOCTYPE, מספר assertions וחתימות SHA-1 נדחים. סשן SAML אינו נבדק מחדש מול ספק הזהות: משתמש שהוסר שם שומר על גישה עד שטוקן הסשן פג.
מיפוי תפקידים: claims של זהות → תפקיד Provisa דרך כללים הניתנים להגדרה. השדה assignments_source שולט במקור הקצאות התפקידים: claims קורא אותן מ-claims של טוקן ה-JWT (ברירת מחדל), provisa מהמאגר הפנימי של הקצאות Provisa. פריסה מרובת דיירים (multitenancy: true) מקצה תפקידים בעצמה, לפי ארגון, ולכן חייבת להשתמש ב-assignments_source: provisa: עם claims היא אינה עולה, ושינוי הגדרות שהיה מביא לצירוף הזה נדחה. פריסה של דייר יחיד יכולה להשתמש בכל אחת מהאפשרויות.
משתמש-על שמוגדר ב-provisa.yaml (שם משתמש וסיסמה מסוד סביבה) תמיד מקבל את התפקיד admin ואת כל היכולות בלי קשר לספק המוגדר, נתיב האתחול להגדרה הראשונית.
משטחים ואישורים¶
כל ממשק מאמת את המבקשים דרך אותו ספק, כך שאישור שעובד באחד עובד בכולם, בכל מקום שבו לפרוטוקול יש היכן לשאת אותו.
| משטח | סיסמה | טוקן ספק | אסימון גישה אישי | תעודת לקוח (mTLS) |
|---|---|---|---|---|
| HTTP (REST, JSON:API, GraphQL) | Authorization: Basic |
Authorization: Bearer |
Authorization: Bearer |
דרך proxy מסיים |
| pgwire | שדה הסיסמה (טקסט גלוי או SCRAM) | שדה הסיסמה, פריסות OIDC | שדה הסיסמה | כן |
| Bolt | סכמת basic |
סכמת bearer |
סכמת bearer |
כן |
| Arrow Flight | — | token בלחיצת היד או במטען הכרטיס; כותרת הקריאה authorization: Bearer עבור list_flights, get_flight_info ו-get_schema |
אותו דבר | כן |
| gRPC | — | מטא-דאטה authorization |
מטא-דאטה authorization |
כן |
Airport (הרחבת DuckDB airport, לבחירה) |
— | authorization: Bearer (auth_token של הסוד) |
אותו דבר | — |
| MCP | — | Authorization: Bearer |
Authorization: Bearer |
דרך proxy מסיים |
ההרחבה airport של DuckDB יכולה לשלוח רק את ה-auth_token מסוג bearer של הסוד שלה. לקוח DuckDB מציב שם אסימון של הספק או אסימון גישה אישי, ופועל בשם התפקיד שהזהות שלו נפתרת אליו. כל לקוח שיכול להגדיר כותרות קריאה רשאי לבקש אחד מתפקידיו, או קבוצה מופרדת בפסיקים, ב-x-provisa-role.
כל ממשק אומר לכם איזה סוג של דחייה קיבלתם. אישור חסר, נדחה או לא שמיש הוא דחיית אימות: HTTP 401, gRPC UNAUTHENTICATED, Arrow Flight ו-Airport Unauthenticated. אישור תקף שמבקש לפעול בשם תפקיד שאין לזהות שלו הוא דחיית הרשאה, והיא נוקבת בתפקיד: HTTP 403, gRPC PERMISSION_DENIED, Arrow Flight ו-Airport Unauthorized. חריג אחד מגיע מספריית הלקוח של Arrow, לא מהשרת. כש-get_schema של Flight נדחה, לקוח Python רואה שגיאת Arrow כללית שנושאת את סיבת השרת אבל לא את הסוג; list_flights, get_flight_info ו-do_get כן מעבירים את הסוג.
— בטבלה פירושו שלפרוטוקול אין שדה שם משתמש לצמד אליו סיסמה, ותצורות האסימון מכסות זאת. pgwire הוא המקרה ההפוך. בחבילת הפתיחה שלו יש שדה סוד אחד ובלי סכמה, ולכן הסוד עצמו קובע את השיטה: אסימון גישה אישי מזוהה לפי הקידומת שלו, הסוד נקרא כאסימון bearer כשהספק המוגדר מנפיק אסימונים, וכל השאר הוא סיסמה. הבחירה נעשית פעם אחת, ואישור שהמאמת שנבחר דוחה אינו נבדק מול מאמת אחר.
אסימוני גישה אישיים¶
אסימון גישה אישי הוא סוד bearer ארוך חיים שאתם מנפיקים ללקוח שלא יכול להשלים כניסה אינטראקטיבית: סקריפט, כלי BI, מנהל התקן. הוא נושא ארגון ותפקיד משלו, וכל ממשק בודק אותו באותו מאמת, כך שאף ממשק לא צריך לדעת מהו.
ברשת הוא provisa_pat_ ואחריו 43 תווי base64 בטוחים ל-URL. הקידומת היא מה ששולח סוד שהוצג אל מאגר האסימונים במקום אל ספק הזהות, והיא הופכת אסימון שדלף לקל לאיתור ביומנים ובמאגרי קוד.
נשמר רק ה-SHA-256 של הסוד. את הסוד עצמו אתם רואים פעם אחת, ביצירתו, ואי אפשר לשחזר אותו אחר כך; רשימת האסימונים שלכם מציגה קידומת תצוגה ותאריכים, לעולם לא אישור שעובד. את האסימונים שלכם אתם יוצרים, מציגים ומבטלים עם POST /auth/tokens, GET /auth/tokens ו-DELETE /auth/tokens/{token_hash}, או באזור האסימונים בפרופיל שלכם בממשק הניהול.
אסימון תקף נפתר לחשבון של בעליו, עם מזהה משתמש, דוא"ל ושם תצוגה, כך שרשומת ביקורת או דוח שימוש שנכתבו תחת אסימון נוקבים באדם ולא באישור. איזה מהאסימונים שלו פעל נרשם בנפרד, ב-raw_claims["token_name"]. לאסימון יכול להיות תאריך תפוגה שאחריו הוא נדחה, והסרת החברות של משתמש מבטלת גם את האסימונים שלו.
SCRAM-SHA-256 על pgwire¶
עם הספק basic, ההגדרה auth.scram: true גורמת ל-pgwire להציע SASL (קוד אימות 10) עם המנגנון SCRAM-SHA-256, כך שהסיסמה מוכחת ולא נשלחת. קשירת ערוץ (SCRAM-SHA-256-PLUS) אינה מוצעת.
SCRAM צריך מאמת RFC 5802, שאי אפשר לגזור מגיבוב bcrypt. המאמת נכתב בכל פעם שסיסמה עוברת בטקסט גלוי (רישום, כניסה, שינוי סיסמה, איפוס על ידי מנהל) כך שפריסה שהפעילה SCRAM אוספת מאמתים כשהמשתמשים מתאמתים בפעם הבאה, וחיבור ה-SCRAM הראשון של כל משתמש בא אחרי הקלדת הסיסמה הבאה שלו. למשתמש שעדיין אין לו מאמת עונים בהחלפה מדומה שאינה ניתנת להבחנה מאמיתית, כדי שהרשת לא תחשוף מי כבר עבר.
TLS הדדי¶
אימות תעודת לקוח מעביר את הבדיקה הראשונה ללחיצת היד של TLS: קורא בלי תעודה שנחתמה על ידי ה-CA של הפריסה לעולם אינו מגיע לשכבת האישורים. הוא זמין ב-pgwire, ב-Bolt, ב-gRPC וב-Arrow Flight: ארבע התעבורות שמסיימות את ה-TLS שלהן בעצמן.
| משתנה | משמעות |
|---|---|
PROVISA_MTLS_CLIENT_CA |
חבילת PEM של ה-CA המורשים לחתום על תעודות לקוח |
PROVISA_MTLS_MODE |
required (ברירת מחדל ברגע שה-CA מוגדר) או optional |
PROVISA_MTLS_BIND_PRINCIPAL |
כש-true, השם המשותף בתעודה חייב להתאים לשם המשתמש שהחיבור מתאמת בו לאחר מכן |
דריסות לכל פרוטוקול עוקבות אחר אותה מוסכמת שמות כמו הגדרות ה-TLS. דבר אינו מוסק: מצב שהוגדר בלי CA מסרב לעלות, ומצב לא מזוהה מסרב לעלות במקום להיקרא כשכנו הבטוח ביותר. פריסה שמאמינה שהיא דורשת תעודות לקוח ואינה דורשת נמצאת במצב גרוע יותר מאחת שאינה עולה.
גרסאות TLS¶
כל listener ש-Provisa מסיים (ה-API של HTTP וה-UI, pgwire, Bolt, MCP, gRPC ו-Arrow Flight) מקבל TLS 1.2 ו-TLS 1.3 ודוחה TLS 1.0 ו-1.1. Provisa אינו קובע גרסת פרוטוקול או רשימת צפנים משלו. ה-listeners של HTTP, pgwire ו-Bolt משתמשים בברירות המחדל של השרת במודול ssl של Python (מינימום TLS 1.2). gRPC ו-Arrow Flight משתמשים בברירות המחדל של gRPC (מינימום TLS 1.2). הגרסאות אינן ניתנות להגדרה.
הגבלת ניסיונות כניסה¶
ניחוש סיסמאות אינו תלוי פרוטוקול: אפשר לתקוף את אותו חשבון על HTTP, pgwire ו-Bolt. לכן המונה נמצא בשכבת אימות האישורים, לא במשטח בודד, כך שנעילה שהושגה בכל מקום חלה בכל מקום.
היא פעילה כברירת מחדל (חמישה כשלים בחמש דקות נועלים את הנושא לחמש-עשרה דקות) וניתנת להגדרה ב-auth.login_throttle. נושא נעול נדחה עוד לפני בדיקת האישורים, ואימות מוצלח מנקה את ההיסטוריה של אותו נושא.
המפתח הוא ה-principal שהפרוטוקול נושא. משטח של bearer בלבד אינו נושא principal, ולכן המפתח הוא תקציר של האישורים עצמם; זה מונע חזרה בלתי מוגבלת על טוקן רע. המאגר הוא בתוך התהליך, ולכן פריסה עם כמה workers של API מאפשרת עד max_attempts לכל worker. ההגבלה מאטה ניחוש, אינה מכסה מבוזרת.
גישוש תפקידים¶
ציון תפקיד שהמבקש אינו מחזיק מטופל כאירוע אבטחה שזוהה, בחומרה גבוהה, בכל ממשק: ה-API של HTTP, MCP, pgwire, Bolt, Arrow Flight, שירות ה-Flight של airport ו-gRPC. לקוח לגיטימי מציין את התפקידים שניתנו לו; לקוח שמציין תפקיד אחר הוא תקול או מגשש.
הבקשה נדחית. המבקש מקבל רק "not permitted", בלי שום מידע על קיום התפקיד, והחיבור או ההפעלה מסתיימים היכן שיש כאלה: חיבור pgwire, הפעלת Bolt, קריאת gRPC, הפעלת MCP. החריג הוא ה-API של HTTP עם כל אישור גישה מלבד אסימון גישה אישי: שם הדפדפן מציג את ההפעלה שלו או את האסימון של ספק הזהות שלו, ולכן הדחייה ממשיכה לציין את התפקיד.
מה עוד קורה תלוי באישור הגישה ובערוץ:
| אישור גישה | ערוץ | מה מושעה |
|---|---|---|
| אסימון של מבקר בארגז חול של הדגמה | ה-API של HTTP | דבר |
| אסימון גישה אישי | כל ערוץ | האסימון |
| אסימון שחתם עליו ספק זהות חיצוני | MCP, pgwire, Bolt, Flight, gRPC | גישת האסימון של הישות לארגון |
| אסימון שחתם עליו ספק זהות חיצוני | ה-API של HTTP | דבר |
| הפעלה או סיסמה שהפריסה הזו בודקת בעצמה | כל ערוץ | דבר |
| הפעלת MCP מקומית (בלי bearer) | MCP | דבר; האירוע נרשם ביומן המפעיל |
ה-API של HTTP הוא החריג משום שהדפדפן מציג את אותו אסימון bearer שלקוח API מציג, כך שהשרת אינו יכול להבחין ביניהם, ולשונית שעדיין מציינת תפקיד שהמשתמש איבד (לאחר שמנהל שינה את תפקידיו, או מעבר ארגון) היא דבר שגרתי. MCP ושאר הערוצים לעולם אינם הפעלת דפדפן. שם האירוע נדחה, נרשם ומדווח כמו כל אירוע אחר; רק ההשעיה אינה מתבצעת. אסימון של מבקר בארגז חול של הדגמה הוא אסימון גישה אישי, אבל הוא אישור הגישה היחיד של לשונית הדפדפן של המבקר, ולכן ב-API של HTTP הוא מטופל כמו הפעלה: נדחה ונרשם, אך לעולם אינו מושעה.
אסימון מושעה נדחה כמו אסימון שבוטל. ישות שגישת האסימון שלה מושעית נדחית ב-MCP, pgwire, Bolt, Flight ו-gRPC בארגון זה, ועדיין יכולה להיכנס לממשק המשתמש. רק מנהל מסיר השעיה; שום דבר אינו מסיר אותה לאחר זמן. צוות ← גישה מושעית מציג כל השעיה עם האירוע שגרם לה ומסיר אותה (GET /admin/orgs/{org}/suspensions, POST /admin/orgs/{org}/suspensions/lift).
כל אירוע נכתב ביומן הניהול של הארגון (פעולה security.role_probe) עם הישות, מזהה אישור הגישה, כתובת הלקוח, הערוץ, נקודת הקצה או הכלי, התפקיד שצוין, התפקידים המוחזקים, הזמן ומזהי המשפטים האחרונים של הישות שנרשמו ביקורת. הטקסט שלהם נשאר ביומן הביקורת של השאילתות ונקרא תחת ההרשאה view_governance.
מנהלי הארגון מקבלים דוא"ל היכן שהפריסה שולחת דואר. האירוע נשלח גם כ-JSON ל-webhook האבטחה של המפעיל כאשר security.webhook_url מוגדר. מה שיוצא מהארגון מציין את הישות, מזהה אישור הגישה, הערוץ, נקודת הקצה, התפקיד שצוין, הזמן והחומרה, עם קישור לתצוגת הניהול; הוא אינו כולל משפט ואינו כולל תפקיד מוחזק. הודעה שלא ניתן למסור נרשמת ביומן ולעולם אינה מבטלת את הדחייה או את ההשעיה.
ציון הארגון¶
במצב מרובה דיירים כל בקשה נוקבת בארגון שהיא מיועדת לו. חברות בארגון אחד בדיוק אינה נוקבת בו: בקשה שאינה נוקבת באף ארגון נדחית, והדחייה אומרת מה לשלוח. כל משטח נוקב בארגון כך:
| משטח | איך הלקוח נוקב בארגון |
|---|---|
| HTTP (ממשק, REST, GraphQL) | שם המארח של הארגון עצמו, acme.provisa.dev, או הכותרת X-Org-Provisa על מארח מישור הבקרה |
| pgwire | שם מסד הנתונים (psql -d acme, שדה מסד הנתונים של כלי BI) או שם המארח של הארגון עצמו, שמחויג על TLS |
| Bolt | שם מסד הנתונים, <org>.provisa_<role> (או <org>.provisa_ops_<role>), או שם המארח של הארגון עצמו, שמחויג על TLS |
| Arrow Flight | "org" בכרטיס |
| gRPC | כותרת המטא-דאטה x-provisa-org |
| MCP (תעבורת HTTP) | שם המארח של הארגון עצמו או הכותרת X-Org-Provisa |
| לקוח Python | org= ב-ProvisaClient, connect() ו-adbc_connect(); ?org= בכתובת SQLAlchemy |
לקוח pgwire שמחייג שם מארח על TLS שולח אותו ב-TLS ClientHello, ו-Provisa קוראת את הארגון משם. כששם המארח ושם מסד הנתונים נוקבים שניהם בארגון והם שונים, החיבור נדחה; אף אחד אינו קודם. שם מסד הנתונים provisa, שמשמש כברירת מחדל, אינו נוקב בארגון, וכלי BI שמחובר לארגון מציג
את אותו ארגון כמסד הנתונים שלו. ב-Bolt, SHOW DATABASES מפרט <org>.provisa_<role> אחד לכל ארגון שהמשתמש
שייך אליו; provisa_<role> לבדו נדחה במצב מרובה דיירים, והסשן נשאר בארגון
שבו נקב ראשון.
השם הוא בקשה, לא הענקה. בכל מקום הוא מגיע לאותו פותר, שדוחה ארגון שה-principal המאומת אינו חבר בו ואין לו עליו זכות בין-ארגונית.
אסימון גישה אישי מונפק לארגון אחד ונוקב בו בעצמו. הוא פותח רק את אותו ארגון: בקשה שנוקבת בארגון אחר נדחית, גם אם בעל הטוקן שייך לשניהם.
לפריסה של דייר יחיד אין ארגון לנקוב בו. אף משטח שם אינו קורא לשם כך שם מארח, שם מסד נתונים או כותרת.
סודות בין ארגונים¶
את הסודות של ארגון קורא רק אותו ארגון. במאגר של Provisa עצמה כל סוד נשמר תחת הארגון שלו. בשירות מרכזי (Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) של פריסה עם ארגונים רבים, כל קריאה מתבצעת בתוך הקידומת של הארגון הקורא, והפניה שהייתה יוצאת ממנה נדחית. הקידומות והשלבים להעברת סודות קיימים תחתן מתוארים בסודות.
טלמטריה בין ארגונים¶
בפריסה שמשותפת לארגונים רבים, אותות התפעול ("ops"."traces", ובמנוע Trino גם המדדים והלוגים שלצידו) הם מאגר אחד שאליו מגיעה העבודה של כל ארגון. כל span רושם את הארגון שעבורו הוא התחיל, שנשמר כ-tenant_id, וכל קריאה של האותות מחזירה רק את השורות של הארגון הקורא, בכל תפקיד. span שהתחיל מחוץ לעבודה של ארגון כלשהו (הפעלת השרת, למשל) אינו שייך לאף ארגון ואף ארגון אינו קורא אותו. למנהל הפלטפורמה אין גישה לנתונים באף ארגון והוא אינו קורא אף אחד מהם דרך Provisa.
אותות התפעול נשמרים ללא הגבלת זמן בשני המנועים, כמו יומן הביקורת: הדחיסה כותבת מחדש את הקבצים שבהם הם מאוחסנים ואינה מסירה אף אחת מהשורות שלהם. ב-Trino הם טבלאות ה-Iceberg של הקטלוג otel. במנוע מקומי (DuckDB) השרת מניח את רשומות ה-trace שלו כקובצי Parquet בתיקיית הטלמטריה שלו, והמנוע קורא אותן תחת אותו שם, "ops"."traces", עם תצוגת השושלת של הרענונים "ops"."mv_refresh_lineage" מעליהן; שתיהן נקראות לפי אותו כלל לכל ארגון.
האותות נשמרים במאגר שלהם בטקסט גלוי. עומס עבודה שצריך יותר מגבול לכל ארגון בתוך פריסה אחת רץ בפריסה ייעודית; עומס רגיש מאוד או מוסדר מתארח באירוח עצמי.
עבודת רקע בין ארגונים¶
תהליך שמשרת ארגונים רבים מריץ את עבודת הרקע של כל אחד כארגון עצמו. עיבוד השינויים של כל ארגון (לולאת האירועים שמנחיתה שינויים ממקורות ומרעננת את התצוגות שמעליהם), בניית הרפליקות שלו וטריגרי הטבלה שהם מפעילים שייכים ל-runtime שלו: שינוי מעובד על ידי משימת הארגון שלו, קורא את המקור שלו בכתובת ובאישורי הגישה של אותו ארגון וכותב למאגר של אותו ארגון. כל סביבה של ארגון מחזיקה slot משלה לשכפול לוגי של PostgreSQL ו-publication משלה, הנקראים על שם הארגון, הסביבה והמקור, כך ששני ארגונים שקוראים למקור באותו שם על אותו מסד נתונים קוראים כל אחד את השינויים שלו.
מגורי נתונים¶
בפריסה עם אזורים, טבלה יכולה לציין אזור אחד כבית שלה, והנתונים האישיים שלה מאוחסנים ומעובדים שם.
- רק אזור הבית בונה ומרענן את הרפליקה של הטבלה. כל אזור אחר קורא את הרפליקה הזו במקומה, כשכללי השורה והעמודה של הקורא מוחלים באזור הבית, כך שרק מה שהקורא רשאי לראות יוצא ממנו.
- אזור בית שהרפליקה שלו לא נבנתה, או שהמאגרים שלו אינם זמינים, דוחה את הקריאה (
query.home_region_unavailable). המקור לעולם אינו נקרא במקום זאת בזמן אמת מאזור אחר. - טיוטה מוצגת בתצוגה מקדימה רק היכן שהיא חיה. טבלת טיוטה שמציינת אזור נקראת לתצוגה המקדימה של האחראי עליה רק בצמתים של אותו אזור ונדחית במקומות אחרים; העותק שהתצוגה המקדימה מנחיתה נשמר שם לקראת השחרור שלה. עותק שאזור כלשהו מחזיק של טבלה, טיוטה או מפורסמת, שהבית שלה הוא כעת אזור אחר, מוצא משירות ואז נמחק.
- הגדרה, שינוי או הסרה של אזור טבלה דורשים את ההרשאה
data_residencyעבור האזורים המעורבים, בנוסף לבעלות על הטבלה דרך הדומיין שלה. מעבר יכול לעבור דרך טיוטה, כך שהטבלה אינה מוגשת בשום מקום בינתיים.
כללי שורה ועמודה שנכתבו על האזור של הצומת העונה (current_setting('provisa.region')) ועל המאפיין region של חברים קובעים מה עובר בין אזורים. אזורים מתאר בהרחבה אזורים, שיוך בית וכללים אלה.
הקשחת פריסה¶
אישורי הגישה של המאגרים והכתובת שבה הם מאזינים¶
למאגרים המובנים אין אישורי גישה ברירת מחדל. מאגר הבקרה PostgreSQL, Redis, MinIO, חשבון המנהל של Grafana והסוד המשותף של אשכול Trino מקבלים כל אחד ערך שנוצר עבור הפריסה:
- המפעילים (
provisa start, מתקיני שולחן העבודה והשרת) מייצרים אותם פעם אחת ושומרים אותם ב-~/.provisa/store-credentials.env(מצב 600). ערך שכבר קיים בסביבה משמש כפי שהוא, ו-node משני, המשתמש במאגרי ה-primary, מסרב לעלות בלי הערכים של ה-primary. - מודולי Terraform מייצרים אותם באמצעות
random_passwordומוסרים את אותם ערכים לכל node. הם נשמרים ב-state של Terraform, ולכן הגנו על ה-state כפי שהייתם מגינים על הסיסמאות. - ה-Helm chart אינו קובע דבר. הוא דורש Secret שאתם יוצרים, בשם שמציין
credentials.existingSecret, ובוpg-password, redis-password, minio-access-key, minio-secret-key, grafana-admin-passwordו-trino-shared-secret.
Redis דורש את הסיסמה שלו, וסיסמת תפקיד ה-PostgreSQL נקבעת מחדש בכל הפעלה, כך שנפח שנוצר קודם עוקב אחר הסיסמה הנוכחית של הפריסה. המאגרים, מנוע הפדרציה ושירותי הטלמטריה מפרסמים את הפורטים שלהם על כתובת אחת, PROVISA_STORE_BIND_IP: 127.0.0.1 ב-node העומד בפני עצמו, או הכתובת הפרטית של ה-primary כאשר secondaries מתחברים אליו. חשבון המנהל של Grafana מקבל את הסיסמה שנוצרה; מחסנית ה-compose מאפשרת למבקרים אנונימיים לצפות ב-dashboards, וה-Helm chart אינו מאפשר מבקר אנונימי.
הצפנת תעבורה¶
כל מאזין ש-Provisa מסיימת מגיש TLS (ראו גרסאות TLS). node של שרת משתמש בתעודה שב-PROVISA_TLS_CERT/PROVISA_TLS_KEY, או מייצר תעודה בחתימה עצמית. ה-Helm chart מגיש TLS כברירת מחדל (tls.enabled: true) מתוך ה-Secret מסוג kubernetes.io/tls ש-tls.secretName מציין.
PostgreSQL המוטמע¶
שכבות שולחן העבודה והילידית מריצות PostgreSQL מוטמע. ב-macOS וב-Linux הוא מאזין על unix socket בתוך ספריית הנתונים שלו (מצב 700), שרק החשבון שבבעלותו הספרייה ניגש אליה. מעל TCP (Windows, שם הוא מאזין על 127.0.0.1) כל חיבור חייב לספק סיסמה שנוצרה עבור ספריית הנתונים ונשמרת בה (provisa.password, מצב 600). הוא אינו מופעל לעולם כ-root.
מנוע הפדרציה (Trino)¶
- בין nodes. coordinator וה-workers שלו מאמתים זה את זה באמצעות הסוד המשותף שנוצר לפריסה (
internal-communication.shared-secret). הצפנת התעבורה ביניהם היא אפשרות של המפעיל, כבויה כברירת מחדל: כשהיא כבויה, התעבורה נשארת בתוך הרשת הפרטית או האשכול של הפריסה. הפעילו אותה כאשר דרישת ציות או zero-trust מבקשת הצפנה בתוך הפריסה. Trino מתעדת שהיא מאטה שאילתות עתירות shuffle (joins מבוזרים, אגרגציות, פונקציות חלון) ב-10% ועד למעלה מ-100%. בפריסת compose זו ההגדרהengine.trino_internal_tls(הגדרות פריסה ← Engine), ולצדה רשת המנוע והכתובת הנעוצה של ה-coordinator, והיא נכנסת לתוקף בהפעלה הבאה. תחת ה-Helm chart זה הערךtrino.internalTls; ה-chart מייצר את ה-CA ואת התעודה שה-nodes משתמשים בהם, שומר אותם בין שדרוגים ומפנה לכל node לפי שם ה-DNS של ה-pod שלו. המנועים של הפלטפורמה המתארחת הם coordinators בעלי node יחיד, ללא תעבורה בין nodes, ומסרבים לה. - נתונים שנשפכו (spooled). ביצוע עמיד לתקלות מצפין את נתוני ה-exchange שהוא שופך באמצעות מפתח שנוצר לכל שאילתה ונזרק עם סיומה (
fault-tolerant-execution.exchange-encryption-enabled=true, מוגדר במפורש). spill ישן לדיסק אינו בשימוש. - ארגונים על מנוע אחד. הקטלוגים של כל ארגון הם שלו, נקראים על שמו, והפקודות של כל ארגון רצות ב-resource group משלו.
- הקטלוגים, ואישורי הגישה שבהם, נמצאים רק בזיכרון המנוע. Provisa יוצר כל קטלוג של Trino בזמן ריצה, וכל אחד מחזיק את מאפייני החיבור של המקור שלו, כולל סיסמאות. כל מנוע ש-Provisa מריץ שומר אותם בזיכרון (
catalog.store=memory): אף קטלוג אינו נכתב לדיסק של המנוע, ואף אחד אינו נטען מקובץ. coordinator שמופעל מחדש חוזר בלי קטלוגים; כל תהליך של Provisa מבחין בכך בשאילתה הבאה שלו, ומשגיח המנוע בתוך דקה, ויוצר מחדש את הקטלוגים שהוא משרת. מקור Kafka עם אישורי SASL צריך קובץ אחד: מחבר ה-Kafka של Trino קורא את הגדרות הלקוח שלו רק מקובץ, ולכן Provisa כותב אותן, כולל הסיסמה, ל-kafka-client/<catalog>.properties. הקובץ קריא למשתמש של Trino בלבד (מצב 0600 בתיקייה 0700). במחסנית הקונטיינרים התיקייה היא volume מבוסס זיכרון שמשותף ל-API ולמנוע. במנועים של הפלטפורמה המתארחת זהו ה-Secret של Kubernetes של המנוע עצמו, שמוחזק בזיכרון הצומת וקריא לקבוצה של Trino בלבד (מצב 0440). בכל מקרה, הקובץ לעולם אינו מגיע לדיסק. במנועים המתארחים, מקור Kafka חייב לציין מאגר סכימות: מקור שרק המודל מצהיר על עמודות ה-topics שלו נדחה בשמו בעת שמירה או החלה, משום שתיאורי הטבלאות שלו היו דורשים תיקייה שלא ניתן לתת למנוע בזמן ריצה. הפעלת המחסנית מסירה את קובצי הקטלוג ולקוח ה-Kafka שגרסאות קודמות השאירו על הדיסק.
מקורות בפריסה משותפת¶
בפריסה המחזיקה ארגונים רבים, המקורות של ארגון רשאים להגיע רק לכתובות ציבוריות: לעולם לא לרשת הפרטית של הפלטפורמה, שבה נמצאים מישור הבקרה, המטמון ומאגר האובייקטים שלה. הבדיקה חלה בעת שמירת מקור, בידי הממשק, ה-API של הניהול או החלת תצורה, ושוב בכל חיבור ובכל הפניה של HTTP. המקורות של ארגון דמו (sandbox) מוגבלים בנוסף לכתובת שאליה נבדקו. הארגון של הפריסה עצמה, שהמפעיל מגדיר את מקורותיו, אינו מוגבל.
הצפנה במנוחה והמפתחות של הארגון עצמו¶
הצפנה תחת מפתח של הארגון זמינה, והיא כבויה עד שהארגון מפעיל אותה. דבר אינו מוצפן תחת מפתח של הארגון אלא אם מנהל ארגון יוצר אחד.
מפתח ההצפנה של הארגון עוטף את סודות הארגון, את העמודות המוצפנות שלו ואת טקסט שאילתות הביקורת שלו. מנהל ארגון יוצר אותו תחת Admin → Org Secrets → Org Key, או באמצעות PUT /admin/org-encryption: ללא גוף Provisa מייצרת את המפתח, שאינו מוצג לעולם, וגוף key_b64 מספק 32 בתים משלכם. GET /admin/org-encryption מדווח רק על טביעת האצבע שלו. ללא מפתח ארגון, נתוני הארגון עטופים במפתח הפריסה (encryption.provider ב-provisa.yaml). פריסה שאינה מגדירה ספק הצפנה שומרת עמודות מוצפנות וטקסט שאילתות ביקורת בלי הצפנה; סודות אינם נשמרים לעולם בגלוי (ראו סודות).
תיקיית הארגון מחזיקה את הקבצים שהועלו ונטענו שלו, את קבצי ה-spill של המנוע שלו ואת הקבצים הזמניים שלו. מנהל ארגון מפעיל את הצפנתה תחת Admin → Org Secrets → File folder encryption, או באמצעות PUT /admin/org-files ({"encrypted": true, "key_reference": "${secret:NAME}"}). POST /admin/org-files/generate-key משיב במפתח חדש של 256 סיביות פעם אחת, כדי לשמור אותו בכספת. המפתח נשאר בכספת של הארגון עצמו (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault או HashiCorp Vault): Provisa שומרת רק את ההפניה, ו-gocryptfs שומר את קבצי התיקייה מוצפנים בדיסק תחת המפתח. נדרש FUSE, כלומר מארח או קונטיינר Linux עם /dev/fuse; ב-macOS ההגדרה נדחית.
בפריסה עצמית, שבדרך כלל מריצה ארגון אחד על מנועים משלו, הפעלת שניהם מצפינה את הסודות, הקבצים, קבצי ה-spill והקבצים הזמניים של אותו ארגון תחת המפתחות שלו:
- מנוע DuckDB שארגון מריץ שופך לתיקיית הארגון.
- שורות ש-row filter שולף וקוד שהועלה מועמדים בתיקיית הארגון.
- הביצוע העמיד לתקלות של Trino מצפין נתוני exchange שנשפכו באמצעות מפתח שנוצר לכל שאילתה ונזרק עם סיומה (
fault-tolerant-execution.exchange-encryption-enabled=true). Provisa אינה מפעילה את ה-spill הישן של Trino לדיסק. - Trino שומר את הקטלוגים של הארגון, עם אישורי הגישה שלהם, בזיכרון ואינו כותב אף אחד לדיסק (
catalog.store=memory); הגדרות לקוח ה-SASL של מקור Kafka נמצאות בקובץ שרק המשתמש של Trino קורא, בזיכרון במחסנית הקונטיינרים (ראו מנוע הפדרציה). - מנוע פדרציה PostgreSQL כותב את הקבצים הזמניים שלו במקום ששרת ה-PostgreSQL הזה שומר אותם, תחת הצפנת האחסון של השרת עצמו, לא תיקיית הארגון.
פלטפורמות מתארחות משותפות וייעודיות¶
הפלטפורמה המתארחת המשותפת מבודדת ארגונים באופן לוגי. פריסה אחת משרתת את כולם, ונתוני כל ארגון נשארים שלו באמצעות governance, הסכמות, הקטלוגים וה-resource groups שלו במנוע, המפתחות שלו, מפתחות לכל שאילתה לנתוני מנוע שנשפכו, ודחיית מקורותיו בכתובות הפרטיות של הפלטפורמה. היא מומלצת לניסויים ולעסקים קטנים. פלטפורמה מתארחת ייעודית מופרדת פיזית: מכונות, רשת ומאגרים משלה, המשרתים לקוח אחד. לעומסי עבודה רגישים מאוד או מוסדרים, ארחו את Provisa בעצמכם בתוך הגבול שלכם, שבו המפעילים שלכם מחזיקים בכל מפתח ובכל מארח. לפלטפורמות המתארחות אין דוח SOC 2. אם אתם זקוקים ל-SOC 2, אנו ממליצים על אירוח עצמי.
Python בדפדפן¶
הלשוניות Python ו-Notebook של Explore מריצות Python בדפדפן של המבקר. כל קריאה שהן מבצעות עוברת לשרת כמשתמש המחובר, עם התפקיד הפעיל, הארגון והסביבה שלו, ומנוהלת בדיוק כמו אותו משפט מכל לקוח אחר.
מצב אבטחה גבוהה¶
security.mode: high ב-provisa.yaml מבטיח ערובה אחת: ה-backend של Provisa לעולם אינו מעבד נתונים בטקסט גלוי. כל עמודה משמעותית מוצפנת במקור, ורק לקוח שמחזיק את מפתח הפענוח יכול לקרוא אותה. לערובה הזו יש השלכות שפריסה חייבת לתכנן עבורן.
מה המצב עושה:
- נקודות הקצה של הנתונים דורשות הוכחה לפענוח בצד הלקוח. כל מה שתחת
/data/מחזיר 403 אלא אם הקורא מציג את הכותרתX-Provisa-KMS-Key, הסימן ללקוח JDBC או Python שהוגדר לפענח מקומית. דפדפן או צרכן REST של טקסט גלוי אינו נושא מפתח כזה ונדחה. הבקרה היא דחייה כברירת מחדל על העץ כולו: נתיב שנוסף מחר מוגן ביום שהוא יוצא, וחריגה צריכה הצדקה. - נקודות הקצה של מטא-דאטת הסכמה נשארות פתוחות.
/data/sdl,/data/introspection,/data/schema-version,/data/domains,/data/protoו-/data/compileאינם מחזירים נתוני שורות, ולקוח חייב לקרוא את הסכמה (כולל אילו שדות@encrypted) לפני שהוא יכול להתחבר. - gRPC ו-Arrow Flight ממשיכים לשרת, עם אותה הוכחה. אלה התעבורות שלקוחות מצפינים משתמשים בהן בפועל; סגירתן הייתה משאירה פריסת אבטחה גבוהה בלי פרוטוקול תעבורה. קריאת נתונים בכל אחת מהן חייבת לשאת את אותו מפתח KMS כמטא-דאטת קריאה.
- pgwire, Bolt ו-MCP אינם עולים. לאף אחד משלושתם אין לחיצת יד לכל חיבור שיכולה לשאת הקשר פענוח: קבוצת השורות של pgwire ותוצאת Cypher עוברות בטקסט גלוי, וקריאת כלי של MCP מעבירה את התוצאות שלה למודל כטקסט. פורט מוגדר של כל אחד מהם נדחה בעלייה, ולא מוגש.
- אי אפשר לעקוף את הגנת הקשרים. גם
ignore_relationshipsוגםrelationship_guard: falseמתעלמים מהם; ראו ממשל קשרים.
איך לוודא שפריסה במצב הזה: יומן העלייה נוקב בו, שאילתת /data/sql בלי מפתח KMS עונה 403 עם הודעה שנוקבת במצב האבטחה הגבוהה, ופורטי pgwire, Bolt ו-MCP אינם מאזינים.
Hook אישור ABAC¶
hook מדיניות חיצוני אופציונלי שמופעל לפני ביצוע השאילתה. כשהוא מוגדר, Provisa פונה למנוע המדיניות שלכם עם זהות המשתמש, התפקידים, הטבלאות, העמודות והפעולה. התשובה קובעת אם השאילתה ממשיכה. פקודה שנרשמה עם דורשת אישור עוברת לאותו hook לפני כל קריאה; ראו קריאות לפקודה.
היקף¶
ה-hook מופעל רק כששאילתה נוגעת בטבלה או במקור שהוא מופעל עליהם, ולכן כל שאילתה אחרת רצה בלי עלות נוספת.
| קונפיגורציה | השפעה |
|---|---|
auth.approval_hook.scope: all |
כל שאילתה מפעילה את ה-hook |
sources[].approval_hook: true |
כל הטבלאות של המקור הזה מפעילות את ה-hook |
tables[].approval_hook: true |
הטבלה הזו מפעילה את ה-hook |
פרוטוקולים¶
שלוש תעבורות נתמכות:
| סוג | תרחיש | שדה קונפיגורציה |
|---|---|---|
webhook |
כל שירות מדיניות עם HTTP (OPA, מותאם) | url |
unix_socket |
OPA או sidecar מדיניות באותו מארח | socket_path + url |
grpc |
שירות מדיניות קרוב בעל ביצועים גבוהים | url (host:port) |
תעבורת gRPC משתמשת בחוזה provisa.auth.ApprovalService המוגדר ב-provisa/auth/approval.proto. ממשו את השירות הזה במנוע המדיניות שלכם:
service ApprovalService {
rpc Evaluate (ApprovalRequest) returns (ApprovalResponse);
}
message ApprovalRequest {
string user = 1;
repeated string roles = 2;
repeated string tables = 3;
repeated string columns = 4;
string operation = 5;
map<string, string> session_vars = 6;
string command = 7; // a command call's name; empty for a query
string arguments_json = 8; // a command call's arguments as a JSON object
}
message ApprovalResponse {
bool approved = 1;
string reason = 2;
}
ערוץ ה-gRPC קבוע: ערוץ אחד לכל מופע Provisa, המשמש שוב לכל הקריאות לנקודת הקצה של ה-hook.
בקשה / תגובה¶
כל שלוש התעבורות נושאות את אותו מטען:
| שדה | סוג | תיאור |
|---|---|---|
user |
string | זהות המשתמש המאומת |
roles |
string[] | תפקידי ה-Provisa של המשתמש |
tables |
string[] | מזהי הטבלאות שהשאילתה מפנה אליהן |
columns |
string[] | העמודות שנבחרו בשאילתה |
operation |
string | "query" או "mutation"; "command" לקריאה לפקודה |
command |
string | שם הפקודה בקריאה לפקודה; ריק לשאילתה |
arguments |
object | הארגומנטים שהפקודה נקראת איתם; ריק לשאילתה |
תעבורות ה-webhook וה-Unix socket מחליפות JSON, עם command ו-arguments כמפתחות. ב-gRPC הארגומנטים עוברים כטקסט JSON של arguments_json. התגובה חייבת להכיל approved (bool) ובאופן אופציונלי reason (string).
קריאות לפקודה¶
פקודה שמוגדר בה requires_approval (המתג דורשת אישור בטופס הפקודה) עוברת ל-hook לפני כל קריאה, בכל משטח. היא רצה רק אם ה-hook מאשר. הבקשה נושאת operation: "command", את שם הפקודה ואת הארגומנטים שלה; tables ו-columns ריקים.
{
"user": "analyst",
"roles": ["analyst"],
"tables": [],
"columns": [],
"operation": "command",
"command": "create_issue",
"arguments": {"input": {"title": "Crash on save"}}
}
שתי דחיות, שתיהן 403:
functions.approval_unavailable: ה-hook אינו מוגדר. בשונה משאילתה, פקודה שצריכה אישור לעולם אינה עוברת בלעדיו.functions.approval_denied: ה-hook ענהapproved: false. הסיבה בהודעה.
Timeout ונפילה חוזרת¶
auth:
approval_hook:
type: grpc # webhook | grpc | unix_socket
url: "localhost:50051"
timeout_ms: 500 # default 5000
fallback: deny # allow | deny — applied on timeout or error
scope: "" # "" = use per-table/per-source flags; "all" = every query
ב-timeout או בשגיאת תעבורה חלה מדיניות ה-fallback. מפסק זרם (ברירת מחדל: נפתח אחרי 5 שגיאות רצופות, חצי-פתוח אחרי 30 ש') מונע כשלים מתגלגלים מנקודת קצה איטית של ה-hook.
דוגמת קונפיגורציה¶
auth:
approval_hook:
type: webhook
url: "http://opa.internal:8181/v1/data/provisa/allow"
timeout_ms: 300
fallback: deny
sources:
- id: analytics_pg
approval_hook: true # all tables on this source require hook approval
tables:
- id: salary_data
approval_hook: true # this table always requires hook approval
סודות¶
אישורים משתמשים בתחביר ${env:VAR_NAME}, שמתפענח בזמן ריצה. סיסמאות לעולם אינן נשמרות במסד הנתונים של הקונפיגורציה.
לתיאור המלא של שירות הסודות (מאגרים, תחביר הפניות וספקים) ראו סודות.