לדלג לתוכן

מודל אבטחה

Provisa אוכפת מודל אבטחה בשכבות על פני כל שפות השאילתה (GraphQL, SQL, Cypher) וכל התעבורות (REST, gRPC, Arrow Flight, JDBC, WebSocket). (REQ-001, REQ-266) הממשל מוחל באופן אחיד — אין נתיב שאילתה שעוקף אותו. (REQ-002, REQ-266)

השכבות מוחלות לפי הסדר. בקשה חייבת לעבור כל שכבה לפני שהשכבה הבאה מוערכת.

המודל בשכבות

שכבה 0 — סינון Introspection

הסכמה והקטלוג שמוצגים לתפקיד מכילים רק טבלאות מרשימת domain_access שלו ועמודות שעוברות את כללי visible_to של כל עמודה. (REQ-039) אובייקטים מחוץ לגישת התפקיד בלתי נראים בגילוי — אי אפשר לשאול עליהם, להשלים אותם אוטומטית או להסיק את קיומם. (REQ-039) הדבר חל על סכמת GraphQL, על קטלוג SQL ועל דפדפן הסכמה בעורך השאילתות. (REQ-039, REQ-363)

ראו נראות סכמה.

שכבה 1 — גישה ציבורית

עמודה שפורסמה בהיקף public מוגשת לכל תפקיד בארגון, כולל תפקידים ש-domain_access שלהם אינו מגיע לתחום הטבלה; תפקיד כזה רואה את הטבלה עם העמודות הציבוריות שלה בלבד. דבר אינו ציבורי עד שמי שרושם את הטבלה אומר זאת. (REQ-1959) ראו עמודות ציבוריות.

שכבה 2 — גישת תחום

כל תפקיד נושא רשימת domain_access של מזהי תחומים. שאילתה שנוגעת בטבלה מחוץ לתחומים אלה נדחית לפני הביצוע. (REQ-038, REQ-039) זהו גבול הבעלות הגס — תפקיד HR אינו מגיע לטבלאות כספים, לא משנה איך נכתב ה-SQL. (REQ-002)

ראו מודל הזכויות.

שכבה 3 — אבטחה ברמת השורה

לאחר אישור גישת התחום, פרדיקטי WHERE לכל טבלה ולכל תפקיד מוזרקים לכל SELECT בזמן הביצוע. (REQ-041, REQ-263) הפרדיקטים מוערכים מול נתוני המקור. מנהל אזורי ששואל טבלת הזמנות משותפת רואה רק שורות של האזור שלו, גם עם SELECT *. (REQ-264)

ראו אבטחה ברמת השורה (RLS).

שכבה 4 — נראות עמודה ומיסוך

עמודות עם רשימת visible_to שאינה כוללת את התפקיד המבקש מוסרות מפלט השאילתה. (REQ-040, REQ-263) ערכי עמודות עם כלל מיסוך מוחלפים — בעריכה לפי regex, בהחלפה בקבוע או בקיטום — לפני שהתוצאות עוזבות את השרת. (REQ-263) המיסוך מוחל בכל שפות השאילתה ופורמטי הפלט. (REQ-263)

ראו מודל הרשאות עמודה ומיסוך ברמת עמודה.

שכבה 5 — הגנת פרדיקטים

עמודות ממוסכות נדחות בפסוקיות WHERE ו-HAVING. (REQ-263) בלי זה, קורא יכול היה להסיק את הערך הלא-ממוסך בחיפוש בינארי בתוך מסנן, גם כשהפלט ממוסך. הדחייה מתבצעת בניתוח השאילתה, לפני הביצוע. (REQ-531)

ממשל קשרים (V002)

תנאי JOIN ב-SQL חייבים להתאים לקשר רשום ומאושר בין הטבלאות. (REQ-001) joins לא מאושרים נדחים. כל קשר נושא סיבה ותיאור קריאים לאדם — הכוונה למשתמשים ולסוכנים אוטונומיים מדוע קיים נתיב המעבר. זוהי מדיניות ממשל, לא גבול אבטחה קשיח: שכבות 2–5 נאכפות בלי קשר למבנה ה-join, כך שעקיפה מכוונת אינה חושפת נתונים שהתפקיד לא היה יכול לקבל בשתי שאילתות נפרדות. ניסיונות עקיפה נרשמים ביומן וניתנים לביקורת.

מנגנוני עקיפה — אפשר לעקוף את V002 בשתי דרכים. הראשונה היא יכולת: תפקיד שמחזיק ignore_relationships מצרף יחסים שהקטלוג אינו מכסה. מבין תפקידי המערכת המוגדרים מראש רק ל-modeler יש אותה — תפקיד חקירה שתפקידו להגדיר את המודל, לא לאכוף אותו. (REQ-1297) ל-analyst אין.

השנייה היא חריגה דו-תנאית, ששני התנאים בה חייבים להתקיים:

  1. דגל תפקיד — relationship_guard: false בהגדרת התפקיד (ברירת מחדל: true).
  2. חריגה בשאילתה — ה-SQL מכיל את ההערה --relationship-guard=false.

דגל התפקיד לבדו אינו עוקף את V002; ההערה לבדה אינה עוקפת את V002.

מצב אבטחה גבוהה נועל את ההגנה. עם security.mode: high אף עקיפה אינה פועלת: ignore_relationships מתעלמים ממנו, relationship_guard: false מתעלמים ממנו, וכל join חייב להופיע בקטלוג הקשרים המאושרים. (REQ-693) זוהי יתירות מכוונת — תפקיד ייצור שקיבל את היכולת בטעות עדיין אינו יכול לצאת מהמודל.

נתיב GraphQL — V002 תמיד מדולג לשאילתות GraphQL. קשרים שמוגדרים ב-SDL מאושרים מראש מעצם טבעם; הבדיקה מיותרת ואינה מוחלת.

נתיבי SQL ו-Cypher — V002 פעיל כברירת מחדל. הן endpoint_dev.py והן cypher_router.py מחילים את הבדיקה הדו-תנאית לפני קריאה ל-validate_sql.

נתיב pgwire — אותה בדיקה דו-תנאית כמו ב-SQL. ההערה --relationship-guard=false מוסרת מהשאילתה לפני הביצוע; היא אינה מגיעה למסד הנתונים.


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


מודל הזכויות

יכולות הניתנות להקצאה בנפרד, עם היררכיית תפקידים אופציונלית דרך parent_role_id. admin מעניק הכול. (REQ-042)

יכולת תיאור
source_registration רישום מקורות נתונים
table_registration רישום טבלאות ועמודות
create_relationship הגדרת קשרי FK
access_config הגדרת RLS ומיסוך
query_development הרצת שאילתות
write הפעלת מוטציות רשומות (בקרה גסה; ראו הרשאת מוטציה)
full_results עקיפת מגבלות הדגימה
ignore_relationships עקיפת ממשל הקשרים (V002). מבין תפקידי המערכת רק ל-modeler יש אותה, ובמצב אבטחה גבוהה מתעלמים ממנה לחלוטין
admin משתמש-על — מעניק הכול

ירושת תפקידים

תפקידים יכולים לרשת מתפקיד הורה אחד דרך parent_role_id, שנקבע בקונפיגורציה או בעמוד האבטחה כ"יורש מ". (REQ-215) השרשרת משוטחת בעת ההפעלה: הבן מקבל את האיחוד של היכולות וגישת התחום של אבותיו, כל הענקת עמודה, מדד, פונקציה ו-webhook שנוקבת באב קדמון, ואת כללי ה-RLS של האבות לכל טבלה כשהבן קודם — התפקיד הקרוב ביותר בשרשרת שיש לו כלל לטבלה נותן את הפרדיקט של אותה טבלה, וכלל שנשמר לבן מחליף את כלל ההורה לאותה טבלה. (REQ-1677)

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) מקפלת את זכויות האב הקדמון לתוך הבן בעת ההפעלה. זכויות רק מתווספות; אין חיסור. כדי להגביל קורא, הענו לרשומה מורכבת שנוקבת גם בתפקיד וגם בתנאי, או הציבו רשומת מסכה לפני רשומה מתירנית. (REQ-215, REQ-1677)

הקצאות שפוקעות והקצאות שאינן בשימוש

הקצאה ש-Provisa מחזיקה יכולה להסתיים. הקצאה שניתנה עם מועד פקיעה אינה מוחזקת עוד מאותו רגע, בכל בקשה. תפקיד יכול לציין inactivity_days: הקצאה של התפקיד שאף בקשה לא נזקקה לה זמן רב מזה, בספירה מהמאוחר מבין השימוש האחרון בה ויצירתה, מוסרת. תפקידים שמגיעים מהטענות (claims) או ממיפויי התפקידים של ספק זהות מוגבלים בזמן על ידי הספק; קבעו שם את הפקיעה שלהם. תפקידים נגזרים ומטא-תפקידים אינם פוקעים לעולם. (REQ-2077)

בקשה נזקקה לתפקיד כאשר התפקיד לבדו שירת אותה, או, כאשר הבקשה פועלת כמה תפקידים, כאשר לתפקיד הוענקה טבלה שההוראה קראה. פעולה כ„הכול“ אינה נחשבת לשימוש בכל תפקיד מוחזק. השימוש האחרון בהקצאה נרשם לכל היותר פעם ביום.

כל 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 ופועלת בשמם. (REQ-2057)

בדף התפקידים כל תכונה מוצהרת היא תפקיד הורה המסומן נגזר, ותפקידי הערך שלה מוצגים תחתיה ככל שערכיהם נראים. + תפקיד ← נגזר ממאפיין מצהיר על אחד. החלונית שלו מציגה את התכונה, את התיאור שלה, את הערכים שנראו עד כה ואת כל מסכה, כלל שורות והענקה שנוקבים בה או באחד מתפקידי הערך שלה. אין לה פקדי הענקה. התיאור של תפקיד ערך נוצר אוטומטית: "מוחזק כאשר region של המשתמש הוא eu". (REQ-2078, REQ-2079)

הצהירו עליהם בתצורה כרשימה:

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 של המסננים המותרים של כל חבר; אם לחבר כלשהו שקורא טבלה אין מסנן, אין גם למטא-תפקיד — היעדר מסנן פירושו כל השורות. מטא-תפקיד לעולם אינו נשמר, מפורט או מיוצא. הוא נבנה מחדש מהחברים שלו כשהמודל משתנה. (REQ-1620)

העמודות של כל שורה מוסוות ומוצגות לפי התפקידים שמסנן השורות שלהם התיר את השורה: מבין תפקידים אלה חלה הקריאה המגבילה פחות של העמודה, ותפקיד שלא התיר את השורה אינו תורם דבר לאופן שבו היא נקראת. ההסוואות חלות במקום שבו הטבלה נקראת, לפני כל join או צבירה, כך שצבירה קוראת את הערכים המוסווים. כתיבות נבדקות באותו אופן: עמודה נכתבת בשורה רק היכן שתפקיד שמעניק את העמודה מתיר את השורה. (REQ-1620)

רשומת הביקורת נוקבת בכל תפקיד פועל, לא במזהה המטא-תפקיד. מכיוון שמטא-תפקיד לעולם אינו נשמר, אין לו תיאור משלו: בכל מקום שבו מוצג תיאור, התיאור שלו הוא של חבריו, כל מזהה עם התיאור שלו. (REQ-2080)

תיאורים

כל תפקיד וכל כלל שורות אומרים לשם מה הם קיימים. סקירת גישה קוראת מי רשאי לראות מה ולמה. תפקיד או כלל שאיש אינו יכול להסביר הם כאלה שאיש אינו יכול לסקור, ולכן התיאור חובה: תפקיד, תפקיד הורה נגזר או כלל שורות עם תיאור ריק נדחים בטעינת התצורה, ב-API הניהול וב-UI, עם הסיבה. הרשימות תפקידים וכללי שורות מציגות כל תיאור, וחלוניות הפרטים שלהן מציגות ועורכות אותו. (REQ-2080, REQ-2081)

לתפקיד יש תיאור אחד, לא אחד לכל תחום. תפקיד כללי שאינו מתאר את אחריותו בתוך תחום מסוים פירושו שהתחום הזה צריך תפקיד משלו, בדרך כלל בן של התפקיד הכללי (parent_role_id), שיורש את זכויותיו ומתאר מה הוא עושה שם.

איך בקשה נוקבת בתפקידים

לקוח שולח תפקיד אחד, או קבוצה מופרדת בפסיקים, ב-X-Provisa-Role. כל תפקיד נקוב חייב להיות מוחזק על ידי המשתמש. תפקיד שאינו מוקצה לזהות נדחה בשמו.

זכויות ונתונים הולכים רק אחרי התפקידים שבשמם הבקשה פועלת: להחזיק תפקיד אינו לפעול בו. תפקיד אחד הוא אותו תפקיד; שניים או יותר פועלים כמטא-תפקיד שלהם. תפקיד פועל שהיה מביא איתו את הזכויות של כל תפקיד מוחזק לא היה אומר דבר, ולכן הבקשה מקבלת את ההרשאות המינימליות, ורשומת הביקורת נוקבת בדיוק במה שפעל.

תפקידים נגזרים נקובים נכנסים כמסייגים. למסייג אין זכויות, גישת תחום, מסכות או מסנני שורות משלו, ולכן הוא אינו חבר זכויות במטא-תפקיד: כחבר ללא מסכות הוא היה מסיר את המסכה מכל עמודה, וכמי שאינו קורא אף טבלה הוא היה סוגר כל OR של מסנני שורות. הוא נכנס רק להענקות: הענקה או רשומת מסכה שנוקבת ב-region__eu מגיעה לבקשה שנוקבת במסייג הזה יחד עם תפקיד מוענק. (REQ-2057)

סוכן MCP לעולם אינו פועל מעבר לתפקידים שניתנו לו. סוכן מרוחק מציין רק תפקידים שהישות של האסימון שלו מחזיקה. סוכן stdio מקומי מציין רק תפקידים בתוך הקבוצה שנקבעה ב-mcp.role (PROVISA_MCP_ROLE), ופועל כקבוצה זו כשאינו מציין אף אחד; כל תפקיד אחר נדחה בשמו. (REQ-2075)

סוכן שואל באיזה תפקיד הוא פועל באמצעות כלי ה-MCP describe_role. התשובה היא התפקיד הזה ולשם מה הוא קיים (מטא-תפקיד עם כל אחד מחבריו), והתפקידים שהסוכן רשאי לעבור אליהם, כל אחד עם התיאור שלו. היא לעולם אינה כוללת כלל שורות, מסכה, הענקה או תנאי. Provisa לעולם אינה אומרת לסוכן מדוע תוצאה צומצמה, ותוצאה אינה נושאת סימון "מסונן": הנתונים של תפקיד הם כל התצוגה שלו, וסיבה למה שחסר הייתה מזמינה גישוש אחריו. (REQ-2082)

מה + אומר

+ מופיע בשני מקומות, ובכל אחד משמעותו שונה.

ברשומת הענקה. 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 לפני שצירוף יוצא לייצור.

מודל הרשאות עמודה

לכל עמודה יש מודל הרשאות ששולט בקריאה, בכתיבה ובמיסוך לכל תפקיד. (REQ-042, REQ-249)

נראות בשלוש רמות

רמה תנאי תוצאה
מוסתרת התפקיד אינו ב-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. שינוי שנדחה נוקב בעמודה ובזכות. (REQ-1959, REQ-1943, REQ-1944)

היקף מוגש ל
domain (ברירת מחדל) תפקידים שמגיעים לתחום הטבלה, כפי ש-visible_to אומר. visible_to ריק הוא כל תפקיד כזה.
public אותם תפקידים, וגם תפקידים שאינם מגיעים לתחום הטבלה.
restricted רק התפקידים ש-visible_to נוקב בהם. visible_to ריק הוא אף אחד.

פרסום עמודה מסיר את תנאי התחום לאותה עמודה ותו לא:

  • לתפקיד מחוץ לתחום הטבלה מוגשת רק עם העמודות הציבוריות שלה. שאר עמודות הטבלה, וטבלאות שאינן מפרסמות דבר, נשארים מחוץ לסכמה, לקטלוג ול-SQL שלו.
  • ה-visible_to של העמודה עצמה עדיין מחליט למי: עמודה ציבורית שהוענקה לתפקיד אחד ציבורית רק לאותו תפקיד.
  • מסכות, מסנני שורות, מגבלות שורות והגנת הקשרים חלים כמו בתוך התחום. ללא מסנן שורות לתפקיד, העמודות המפורסמות נקראות בלי מסנן; מסנן שורות על הטבלה או על התחום שלה עדיין חל.
  • קשר מוגש כשהעמודות של שני קצותיו מוגשות.
  • עמודת פרמטר של טבלת API (ארגומנט נתיב או שאילתה) אינה נתונים ואינה מפרסמת דבר.
  • התחום ops דורש הענקה מפורשת לכל עמודה (REQ-1133); דבר בו אינו מוגש בין תחומים, יהיה ההיקף אשר יהיה.

הכלל זהה בכל משטח, כי כל אחד קורא את הסכמה המהודרת של התפקיד: GraphQL, SQL (HTTP, pgwire, Flight, JDBC), Cypher ו-Bolt, gRPC, REST ו-JSON:API, קטלוגי Flight ו-MCP ורשימות הניהול. היקף שאינו אחד משלושת הערכים נדחה בשמירת העמודה.

הרשאות כתיבה

קריאת עמודה נשלטת על ידי visible_to (ריק פירושו שכל התפקידים יכולים לקרוא). כתיבה נשלטת לכל טבלה על ידי write_grants, ולא לכל עמודה. כברירת מחדל אין לטבלה שום הרשאה, ולכן אף תפקיד אינו יכול לכתוב אליה. (REQ-2074)

הרשאת כתיבה מציינת מה תפקיד רשאי לכתוב לטבלה בפעולה אחת (insert, update או delete):

שדה מטרה
columns העמודות ש-INSERT רשאי לרשום או ש-UPDATE רשאי לקבוע
rows השורות ש-UPDATE או DELETE רשאי לגעת בהן (פרדיקט SQL)
check מה שכל שורה ש-INSERT כותב, או ש-UPDATE משאיר, חייבת לקיים (פרדיקט SQL)
presets עמודות שההרשאה קובעת בעצמה, ממשתנה הפעלה, מערך מילולי או מרגע הכתיבה. ההצהרה אינה רשאית לספק אותן.

אם משמיטים את rows או את check, ההרשאה לוקחת את מסנן השורות של התפקיד לקריאה בטבלה, כך שתפקיד כותב את השורות שהוא קורא. "TRUE" פירושו כל שורה.

אישור יחיד מכריע בכל כתיבה בכל משטח, בסדר הזה: לתפקיד יש את הזכות write; יש לו הרשאה לפעולה על הטבלה; כל עמודה שההצהרה כותבת מופיעה בהרשאה; ה-presets מתווספים; rows מצורף ב-AND ל-WHERE; וה-check מוערך על השורות החדשות לפני שנשלח משהו למקור. דחייה נוקבת בטבלה, בתפקיד ובפעולה. (REQ-2074)

הצהרות רחבות יותר דורשות הרשאות רחבות יותר. 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; כל תפקיד גלוי אחר רואה 0
  • created_at: כולם יכולים לקרוא אותו, אף תפקיד אינו מספק אותו, וה-insert של hr קובע אותו לרגע הכתיבה

הרשאת מוטציה

מוטציות רשומות (GraphQL מרוחק, OpenAPI, gRPC, Hasura) מוגנות בשתי בדיקות בלתי תלויות. (REQ-868) תפקיד רשאי להפעיל מוטציה רק אם יש לו את היכולת הגלובלית write וגם ה-command משויך אליו דרך visible_to (רשימה ריקה משייכת אותו לכל תפקיד). (REQ-868) ל-command אין רשימת writable_by; ההגדרות דוחות רשימה כזו בשמה. [tool-verified: provisa/security/mutation_authz.py command_assigned, admit_command; provisa/core/models.py _one_role_list]

מוטציות מסווגות ככתיבות לפי החוזה, לא לפי הצהרת הקורא. (REQ-869) SELECT שמפנה לפונקציה מסוג מוטציה מועלה לכתיבה וכפוף לאותה בדיקה דו-שערית, כך שקורא אינו יכול להפעיל מוטציה כשהוא מציג אותה כקריאה. (REQ-869) סיווג מחדש של מוטציה כבטוחה לקריאה דורש את היכולת access_config ונרשם כהחלטת ממשל; אין חריגה לבקשה בודדת. (REQ-870)

נראות סכמה

סכמות GraphQL לכל תפקיד מסתירות תוכן לא מורשה: (REQ-039)

  • גישת תחום: התפקיד רואה רק טבלאות בתחומי ה-domain_access שלו ("*" = הכול) (REQ-039)
  • נראות עמודה: עמודות שאינן ב-visible_to של התפקיד מוחרגות מה-SDL (REQ-039)
  • טבלאות/עמודות לא מורשות אינן מופיעות בסכמה (REQ-039)

אבטחה ברמת השורה (RLS)

הזרקת פסוקיות SQL WHERE לכל טבלה ולכל תפקיד. מוחלת אחרי ההידור, לפני הביצוע. (REQ-041, REQ-263)

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). (REQ-035, REQ-041)

איבר current_setting('provisa.<name>') מתפענח ממשתני הסשן שהבקשה קושרת: user_id מהזהות המאומתת, role מהתפקיד הפועל, וכל claim סקלרי בטוקן תחת שמו באותיות קטנות, מקפים כקווים תחתונים. משתנה הפעלה של Hasura שומר על שמו: X-Hasura-User-Id הוא hasura_user_id, הנקרא מה-claim‏ x-hasura-user-id של הטוקן (ברמה העליונה או בתוך אובייקט ה-claims של Hasura, כגון https://hasura.io/jwt/claims) או מתועד עבור החבר כתכונה hasura_user_id. לכן מסנן Hasura מיובא על X-Hasura-User-Id קורא provisa.hasura_user_id. provisa.user_id הוא תמיד הזהות המחוברת; שום claim אינו מחליף אותו. ה-session_vars המוגדרים של תפקיד הם קבועי בסיס; קישורי הבקשה מונחים מעליהם. תפקיד שיורש מתפקיד אחר מקבל את הקבועים של אבותיו מתחת לשלו, ולכל שם את הערך של התפקיד הקרוב ביותר. user_id, role, region ו-org נקשרים על ידי הבקשה או הצומת ולא ניתן להגדיר אותם כקבועים. במצב לא מאובטח הכותרת x-provisa-session-<name> קושרת את <name>. משתנה שאינו קשור בשום מקום מתפענח ל-NULL ואינו תואם אף שורה. (REQ-1682)

קבוע תפקיד הוא ההפך של תפקיד נגזר. תפקיד נגזר הופך ערך שיש לחבר לתפקיד; קבוע תפקיד נותן לכל בקשה שפועלת כתפקיד ערך, שכלל שורה קורא כפי שהוא קורא claim. קבוע תפקיד יכול להיות הפניה ${env:NAME} או ${secret:NAME}, שנפתרת באותו מנגנון פתרון כמו כל הפניה אחרת כששאילתה משתמשת בה; המודל שומר את ההפניה, לעולם לא את הערך שאליו נפתרה, והפניה שאינה נפתרת לדבר מכשילה את השאילתה ומציינת את הקבוע. קבוע שנפתר הוא חלק מהשאילתה. הרישומים של Provisa עצמו לגבי השאילתה מכוסים בהשחרה המרכזית; היומנים של מקור מאוחד עצמו מחוץ להישג ידו של Provisa. (REQ-2086)

מיסוך ברמת עמודה

המיסוך מוגדר כרשימה מסודרת לכל עמודה. הרשומה הראשונה שהקורא מחזיק מחליטה מה הוא רואה. (REQ-2059, REQ-263)

סוג מסכה סוגי עמודה נתמכים ביטוי SQL
regex מחרוזת (varchar, char, text) REGEXP_REPLACE(col, pattern, replace)
constant כל סוג ערך ליטרלי (NULL, 0, מחרוזת מותאמת)
truncate תאריך/חותמת זמן DATE_TRUNC(precision, col)

לכל רשומה יש רשימת to שנוקבת בקוראים שהיא מכוונת אליהם, ו-mask אופציונלי. רשומה ללא mask פירושה "ללא מסכה" לאותם קוראים. * ב-to תואם כל קורא.

ביטוי המסכה נדחף להטלת ה-SQL SELECT. מסד הנתונים מחזיר את הערך הממוסך; נתונים לא ממוסכים לעולם אינם עוברים ברשת לתפקידים ממוסכים. (REQ-263) עמודות ממוסכות חסומות גם בפסוקיות WHERE ו-HAVING (הגנת הפרדיקטים של שכבה 5) כדי למנוע הסקת ערכים דרך סינון. (REQ-263, REQ-531)

מסכה חלה על כל שם של העמודה, לא על עמודה קשורה. עמודה הנקראת לפי הכינוי שלה או לפי שמה הפיזי ממוסכת באותו אופן. מסכה אינה מסתירה מפתח שקשר מאושר מצרף לעמודה שהתפקיד יכול לראות: הצירוף משווה את הערכים האמיתיים, כך שהעמודה הקשורה מציגה את מה שהמפתח הממוסך מכיל. מסכו גם את העמודה הקשורה, או אל תאשרו את הקשר עבור אותו תפקיד. (REQ-263, REQ-019)

אין רשומה — אין מסכה. קורא שאינו מחזיק אף רשומה קורא את העמודה בלי מסכה. סיימו את הרשימה ברשומת * כשמסכה כלשהי צריכה לחול על כל קורא. עורך העמודות מזהיר כשברשימה אין רשומת * (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 מנוהלת כמו שורות של טבלה. (REQ-1679) השורות שהפעולה מחזירה נקשרות כיחס שהעמודות שלו הן חוזה הפלט המוצהר של הפעולה (output_columns לפונקציה, inline_return_type ל-webhook, או מאפייני האובייקט של return_schema מסוג מערך של פונקציה), והיחס הזה עובר באותו שלב ממשל כמו כל קריאת טבלה, דרך CTE של VALUES עם השורות. (REQ-1679) דבר מ-RLS, מיסוך או נראות אינו מיושם מחדש לפעולות.

שלושה דברים חלים, באותו סדר כמו לטבלה. (REQ-1679)

  • מסנן שורות: כלל ה-RLS של התפקיד שנשמר לפעולה בשמה (upsertRlsRule עם actionName), או, כשלפעולה אין כלל משלה, כלל התחום של תחום הפעולה. הפרדיקט נבדק מול החוזה בשמירה כמו כלל טבלה. (REQ-1676)
  • נראות עמודה: עמודת חוזה שמצהירה visible_to מוסרת לתפקיד שהרשימה אינה נוקבת בו; עמודה שאינה מצהירה דבר היא חלק מהצורה הציבורית של הפעולה ונשארת.
  • מסכות: עמודת חוזה נושאת masks, אותה רשימת מסכות מסודרת כמו עמודת טבלה. (REQ-2059)

ירושת תפקידים פותרת את השרשרת לכל שלושתם. (REQ-1677) תגובה שמחזירה עמודה מחוץ לחוזה המוצהר נדחית עם 502, ולא עוברת בלי ממשל. פעולה שאינה מצהירה חוזה עמודות מחזירה סקלר, ואין מה לקשור; היא מוחזרת כמות שהיא. קריאת הבדיקה של הניהול מדווחת על המסננים, ההחרגות והמסכות שהוחלו, וההצהרה המנוהלת נרשמת ביומן ביקורת השאילתות.

דגימה

כל התפקידים רואים תוצאות מדוגמות (ברירת מחדל: 100 שורות) אלא אם יש להם את היכולת full_results. (REQ-554) נשלט על ידי משתנה הסביבה PROVISA_SAMPLE_SIZE. (REQ-554)

יומן ביקורת

כל שאילתה שנוגעת בנכס של תחום נרשמת ב-query_audit_log. (REQ-596, REQ-613) יומן הביקורת שייך לארגון, לא לפלטפורמה: הרישום של הפלטפורמה עצמה של ארגונים, משתמשים והזמנות אינו מכיל רשומות ביקורת. (REQ-1922) ה-query_audit_log של כל ארגון הוא טבלה בסכמה של הארגון עצמו, ב-store שהארגון מציין עבור הרשומה שלו בכל אזור שהוא בוחר. ארגון שאינו מצהיר על אזורים משאיר את הסכמה הזו במסד הנתונים של ה-control plane של הפריסה, לצד המודל שלו.

כל שורה לוכדת את tenant_id (הארגון), user_id, role_id, גיבוב SHA-256 של טקסט השאילתה, את טקסט השאילתה עצמו מוצפן (REQ-689), table_ids ו-table_names, source, status_code, duration_ms, את המסלול ומספר השורות, מה נאכף על ההצהרה, ו-logged_at. טקסט השאילתה מוצפן במפתח של הארגון כשהארגון הגדיר אחד, ואחרת במפתח של הפריסה (REQ-1574). פריסה שאינה מגדירה ספק הצפנה שומרת אותו לא מוצפן. הטקסט לעולם אינו נחשף בטבלאות ה-ops הניתנות לשאילתה, ומפוענח רק בקריאות ניהול מורשות.

היומן הוא להוספה בלבד ברמת מסד הנתונים: כללי PostgreSQL הופכים DELETE ו-UPDATE על query_audit_log לפעולות ריקות. (REQ-596, REQ-613) שני אינדקסים, (tenant_id, logged_at) ו-(user_id, logged_at), תומכים בשאילתות תאימות לפי ארגון ולפי משתמש לאורך טווחי זמן. (REQ-596, REQ-613)

שמירה: שורות נשמרות ללא הגבלת זמן. ל-Provisa אין הגדרת שמירה והוא לעולם אינו מוחק את יומן הביקורת; מכיוון ש-DELETE היא פעולה ריקה, הסרת שורות ישנות פירושה שמפעיל מוחק או מאכסן בארכיון את נתוני הטבלה מחוץ ל-Provisa.

השחרה

מה ש-Provisa מתעד על עבודתו עובר דרך רשימה אחת של כללי השחרה לכל ארגון לפני שהוא נכתב לאן שהוא. הכללים המובנים של Provisa הם ערכת הפתיחה של הרשימה; ארגון מוסיף כללים משלו. כל סוד ש-Provisa מחזיק, וכל סוד שהמודל מפנה אליו, מושחר בלי שכלל ינקוב בו: ערך שנפתר מ-${secret:…} או מהמאגר של Provisa עצמו לעולם אינו מופיע ברשומה בטקסט גלוי.

הרשימה חלה בכל יציאה ש-Provisa כותב אליה:

  • כל span וכל רשומת לוג, ב-SDK של OpenTelemetry של Provisa, לפני שמייצא כלשהו רואה אותם; כולל הפלט הסטנדרטי;
  • טבלאות הביקורת, בעת כתיבת כל שורה;
  • הודעות השגיאה שלקוח מקבל.

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

הגבלת קצב

מגבלות קצב לכל תפקיד מוגדרות ב-provisa.yaml: מקסימום שאילתות לשנייה, מקסימום מינויי SSE בו-זמניים ומקסימום זרמי Arrow Flight בו-זמניים. (REQ-369) המגבלות נאכפות בשכבת ה-API לפני הידור או ביצוע; בקשות מעבר למגבלה נדחות עם HTTP 429 וכותרת Retry-After. (REQ-369)

לשירות שאילתות ה-NL (POST /query/nl) יש מגבלה נפרדת דרך nl.rate_limit (בקשות לדקה לתפקיד). בקשות מעבר למגבלה נדחות לפני כל קריאה ל-LLM. (REQ-370)

מצב מגבלות הקצב נשמר ב-Redis (cache.redis_url) כמונה חלון מחליק — ללא מצב בתוך מופע בודד — כך שהמגבלות נאכפות על פני כל המופעים האופקיים של Provisa. (REQ-371)

מגבלת מורכבות שאילתה

מגבלת קצב מגבילה כמה בקשות תפקיד שולח. מגבלת מורכבות מגבילה מה הצהרה אחת יכולה לבקש. (REQ-1174)

לכל הצהרה יש ציון מורכבות: היחסים שהיא קוראת, ה-joins שלה, העמודות שהיא בוחרת (* נספר כעמודות שהוא מייצג) והשאילתות המקוננות בה. יחס שנקרא ממקור API מרוחק (OpenAPI, GraphQL מרוחק, gRPC מרוחק) נספר כ-10, כי כל קריאה מוציאה מתקציב שהמערכת המרוחקת קובעת לכל המשתמשים של אותו מקור.

המגבלה היא max_query_complexity ב-rate_limit של התפקיד. ההגדרה הארגונית limits.max_query_complexity היא תקרה לכל תפקיד: תפקיד יכול לקבוע מגבלה נמוכה יותר, ומגבלה גבוהה יותר אינה חלה. כברירת מחדל אף אחת אינה מוגדרת.

roles:
  - id: analyst
    rate_limit:
      max_query_complexity: 200
הציון נמדד אחרי ניתוח השאילתה ולפני הממשל או הביצוע שלה, כך שמגבלה אחת חלה על כל ממשק שדרכו יכולה להגיע הצהרה: GraphQL, SQL על HTTP, pgwire, Arrow Flight, gRPC, Cypher, JSON:API ו-MCP. הצהרה מעבר למגבלה נדחית עם הציון שלה, המגבלה ומה שהיא ביקשה. ממשקי HTTP עונים 413. הדחייה נרשמת כדחיית מדיניות.

אימות

ספקי אימות ניתנים לחיבור: (REQ-120)

ספק אישור תרחיש
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) ספריות ארגוניות (REQ-1265)
saml assertion חתום של SAML 2.0, ואז JWT של סשן כניסה יחידה (SSO) ארגונית (REQ-1265)
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 דרך כללים הניתנים להגדרה. (REQ-120) השדה assignments_source שולט במקור הקצאות התפקידים: claims קורא אותן מ-claims של טוקן ה-JWT (ברירת מחדל), provisa מהמאגר הפנימי של הקצאות Provisa. (REQ-551) פריסה מרובת דיירים (multitenancy: true) מקצה תפקידים בעצמה, לפי ארגון, ולכן חייבת להשתמש ב-assignments_source: provisa: עם claims היא אינה עולה, ושינוי הגדרות שהיה מביא לצירוף הזה נדחה. פריסה של דייר יחיד יכולה להשתמש בכל אחת מהאפשרויות.

משתמש-על שמוגדר ב-provisa.yaml (שם משתמש וסיסמה מסוד סביבה) תמיד מקבל את התפקיד admin ואת כל היכולות בלי קשר לספק המוגדר — נתיב האתחול להגדרה הראשונית. (REQ-125)

משטחים ואישורים

כל משטח מאמת דרך אותו חוזה ספק, כך שאישורים שעובדים באחד עובדים בכולם, היכן שהפרוטוקול יכול לשאת אותם. (REQ-124, REQ-1263) טבלה זו היא ההפניה היחידה; התיעוד של המשטחים הבודדים אינו חוזר עליה.

משטח סיסמה טוקן ספק אסימון גישה אישי תעודת לקוח (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 מסיים

אפשרות האימות היחידה של הרחבת DuckDB airport היא auth_token מסוג bearer של הסוד שלה, ולכן לקוח DuckDB מציג שם טוקן ספק או אסימון גישה אישי ופועל בתפקיד שהזהות שלו מתפענחת אליו. לקוח שיכול לקבוע כותרות קריאה יכול לבקש תפקיד מוחזק אחד או קבוצה מופרדת בפסיקים ב-x-provisa-role. (REQ-1120, REQ-1263)

דחייה מדווחת על סוגה בכל אחד מהמשטחים האלה. אישורים חסרים, נדחים או לא שמישים הם כשל אימות: HTTP 401, gRPC UNAUTHENTICATED, Arrow Flight ו-Airport Unauthenticated. אישורים תקפים שמבקשים לפעול בתפקיד שאין לזהות הם כשל הרשאה, עם ציון התפקיד: HTTP 403, gRPC PERMISSION_DENIED, Arrow Flight ו-Airport Unauthorized. (REQ-273, REQ-1263) חריגה אחת נמצאת בספריית הלקוח של Arrow, לא בשרת: דחייה של קריאת Flight get_schema מגיעה ללקוח Python כשגיאת Arrow כללית עם הסיבה של השרת אבל בלי הסוג שלה; list_flights, get_flight_info ו-do_get מעבירים את הסוג.

היכן שבתא יש —, הפרוטוקול אינו נושא שדה שם משתמש שאפשר לצרף אליו סיסמה; צורות הטוקן מכסות זאת. pgwire הוא המקרה ההפוך: בחבילת ההתחלה יש שדה סוד אחד ואין סכמה, ולכן השיטה בוחרת לפי מה שהסוד הוא: PAT מזוהה לפי הקידומת, הסוד נקרא כטוקן bearer כשהספק המוגדר הוא ספק טוקנים, וכל השאר הוא סיסמה. הבחירה נעשית פעם אחת — אישורים שהמאמת שנבחר דחה אינם נבדקים שוב על ידי אחר.

המטריצה נאכפת על ידי tests/unit/test_auth_surface_conformance.py, שמפעיל את נקודת הכניסה האמיתית של האימות בכל משטח ונכשל אם נוסף משטח בלי שורה.

אסימוני גישה אישיים

PAT הוא סוד bearer ארוך-חיים שמשתמש יוצר עבור לקוח שאינו יכול לבצע כניסה אינטראקטיבית — סקריפט, כלי BI, דרייבר. (REQ-1263) הוא נושא ארגון ותפקיד משלו, וכל משטח פותר אותו דרך אותו מאמת, כך שאף משטח אינו צריך לדעת מהו PAT.

צורת התעבורה היא provisa_pat_ ואחריה 43 תווי base64 בטוחים ל-URL. הקידומת היא מה שמנתב סוד מוצג למאגר הטוקנים ולא לספק הזהות, והיא הופכת טוקן שדלף לניתן למציאה ביומנים ובמאגרי קוד.

  • אחסון — נשמר רק ה-SHA-256 של הסוד. הסוד עצמו מוצג פעם אחת בדיוק, ביצירה, ואינו ניתן לשחזור. הרשימה נושאת קידומת לתצוגה וחותמות זמן של מחזור החיים, אך לעולם לא את האישור הפעיל.
  • הנפקה וביטול — POST /auth/tokens, GET /auth/tokens, DELETE /auth/tokens/{token_hash}, וגם אזור שירות עצמי בפרופיל של המשתמש עצמו בממשק הניהול. יצירה וביטול של אישור הם פעולה של בעל הטוקן.
  • ייחוס — PAT מאומת מתפענח לחשבון של בעליו: מזהה משתמש, אימייל ושם תצוגה. לכן שורת ביקורת או דוח שימוש שנכתבו תחת PAT נוקבים באדם, לא באישור. איזה מהטוקנים של אותו אדם פעל עובר בנפרד, ב-raw_claims["token_name"].
  • תפוגה — לטוקן יכולה להיות תפוגה; טוקן שפג תוקפו נדחה באימות. הסרת החברות של משתמש מבטלת יחד איתה את הטוקנים שלו.

SCRAM-SHA-256 על pgwire

עם הספק basic, ההגדרה auth.scram: true גורמת ל-pgwire להציע SASL (קוד אימות 10) עם המנגנון SCRAM-SHA-256, כך שהסיסמה מוכחת ולא נשלחת. (REQ-1394) קשירת ערוץ (SCRAM-SHA-256-PLUS) אינה מוצעת.

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

TLS הדדי

אימות תעודת לקוח מעביר את הבדיקה הראשונה ללחיצת היד של TLS: קורא בלי תעודה שנחתמה על ידי ה-CA של הפריסה לעולם אינו מגיע לשכבת האישורים. (REQ-1228) הוא זמין ב-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. לכן המונה נמצא בשכבת אימות האישורים, לא במשטח בודד, כך שנעילה שהושגה בכל מקום חלה בכל מקום. (REQ-1393)

היא פעילה כברירת מחדל — חמישה כשלים בחמש דקות נועלים את הנושא לחמש-עשרה דקות — וניתנת להגדרה ב-auth.login_throttle. נושא נעול נדחה עוד לפני בדיקת האישורים, ואימות מוצלח מנקה את ההיסטוריה של אותו נושא.

המפתח הוא ה-principal שהפרוטוקול נושא. משטח של bearer בלבד אינו נושא principal, ולכן המפתח הוא תקציר של האישורים עצמם; זה מונע חזרה בלתי מוגבלת על טוקן רע. המאגר הוא בתוך התהליך, ולכן פריסה עם כמה workers של API מאפשרת עד max_attempts לכל worker — ההגבלה מאטה ניחוש, אינה מכסה מבוזרת.

גישוש תפקידים

ציון תפקיד שהמבקש אינו מחזיק מטופל כאירוע אבטחה שזוהה, בחומרה גבוהה, בכל ממשק: ה-API של HTTP,‏ MCP,‏ pgwire,‏ Bolt,‏ Arrow Flight, שירות ה-Flight של airport ו-gRPC. לקוח לגיטימי מציין את התפקידים שניתנו לו; לקוח שמציין תפקיד אחר הוא תקול או מגשש. (REQ-2076)

הבקשה נדחית. המבקש מקבל רק ‎"not permitted"‎, בלי שום מידע על קיום התפקיד, והחיבור או ההפעלה מסתיימים היכן שיש כאלה: חיבור pgwire, הפעלת Bolt, קריאת gRPC, הפעלת MCP. החריג הוא ה-API של HTTP עם כל אישור גישה מלבד אסימון גישה אישי: שם הדפדפן מציג את ההפעלה שלו או את האסימון של ספק הזהות שלו, ולכן הדחייה ממשיכה לציין את התפקיד.

מה עוד קורה תלוי באישור הגישה ובערוץ:

אישור גישה ערוץ מה מושעה
אסימון גישה אישי כל ערוץ האסימון
אסימון שחתם עליו ספק זהות חיצוני MCP,‏ pgwire,‏ Bolt,‏ Flight,‏ gRPC גישת האסימון של הישות לארגון
אסימון שחתם עליו ספק זהות חיצוני ה-API של HTTP דבר
הפעלה או סיסמה שהפריסה הזו בודקת בעצמה כל ערוץ דבר
הפעלת MCP מקומית (בלי bearer) MCP דבר; האירוע נרשם ביומן המפעיל

ה-API של HTTP הוא החריג משום שהדפדפן מציג את אותו אסימון bearer שלקוח API מציג, כך שהשרת אינו יכול להבחין ביניהם, ולשונית שעדיין מציינת תפקיד שהמשתמש איבד (לאחר שמנהל שינה את תפקידיו, או מעבר ארגון) היא דבר שגרתי. MCP ושאר הערוצים לעולם אינם הפעלת דפדפן. שם האירוע נדחה, נרשם ומדווח כמו כל אירוע אחר; רק ההשעיה אינה מתבצעת.

אסימון מושעה נדחה כמו אסימון שבוטל. ישות שגישת האסימון שלה מושעית נדחית ב-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 מוגדר. מה שיוצא מהארגון מציין את הישות, מזהה אישור הגישה, הערוץ, נקודת הקצה, התפקיד שצוין, הזמן והחומרה, עם קישור לתצוגת הניהול; הוא אינו כולל משפט ואינו כולל תפקיד מוחזק. הודעה שלא ניתן למסור נרשמת ביומן ולעולם אינה מבטלת את הדחייה או את ההשעיה.

ציון הארגון

במצב מרובה דיירים כל בקשה נוקבת בארגון שהיא מיועדת לו. חברות בארגון אחד בדיוק אינה נוקבת בו: בקשה שאינה נוקבת באף ארגון נדחית, והדחייה אומרת מה לשלוח. (REQ-1235) כל משטח נוקב בארגון כך:

משטח איך הלקוח נוקב בארגון
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 קוראת את הארגון משם (REQ-1234). כששם המארח ושם מסד הנתונים נוקבים שניהם בארגון והם שונים, החיבור נדחה; אף אחד אינו קודם. שם מסד הנתונים provisa, שמשמש כברירת מחדל, אינו נוקב בארגון, וכלי BI שמחובר לארגון מציג את אותו ארגון כמסד הנתונים שלו. ב-Bolt, SHOW DATABASES מפרט <org>.provisa_<role> אחד לכל ארגון שהמשתמש שייך אליו; provisa_<role> לבדו נדחה במצב מרובה דיירים, והסשן נשאר בארגון שבו נקב ראשון.

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

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

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

סודות בין ארגונים

את הסודות של ארגון קורא רק אותו ארגון. במאגר של Provisa עצמה כל סוד נשמר תחת הארגון שלו. בשירות מרכזי (Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) של פריסה עם ארגונים רבים, כל קריאה מתבצעת בתוך הקידומת של הארגון הקורא, והפניה שהייתה יוצאת ממנה נדחית. (REQ-1557) הקידומות והשלבים להעברת סודות קיימים תחתן מתוארים בסודות.

טלמטריה בין ארגונים

בפריסה שמשותפת לארגונים רבים, אותות התפעול ("ops"."traces", ובמנוע Trino גם המדדים והלוגים שלצידו) הם מאגר אחד שאליו מגיעה העבודה של כל ארגון. כל span רושם את הארגון שעבורו הוא התחיל, שנשמר כ-tenant_id, וכל קריאה של האותות מחזירה רק את השורות של הארגון הקורא, בכל תפקיד. span שהתחיל מחוץ לעבודה של ארגון כלשהו (הפעלת השרת, למשל) אינו שייך לאף ארגון ואף ארגון אינו קורא אותו. למנהל הפלטפורמה אין גישה לנתונים באף ארגון והוא אינו קורא אף אחד מהם דרך Provisa. (REQ-1349, REQ-1327)

אותות התפעול נשמרים ללא הגבלת זמן בשני המנועים, כמו יומן הביקורת: הדחיסה כותבת מחדש את הקבצים שבהם הם מאוחסנים ואינה מסירה אף אחת מהשורות שלהם. ב-Trino הם טבלאות ה-Iceberg של הקטלוג otel. במנוע מקומי (DuckDB) השרת מניח את רשומות ה-trace שלו כקובצי Parquet בתיקיית הטלמטריה שלו, והמנוע קורא אותן תחת אותו שם, "ops"."traces", עם תצוגת השושלת של הרענונים "ops"."mv_refresh_lineage" מעליהן; שתיהן נקראות לפי אותו כלל לכל ארגון. (REQ-862)

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

עבודת רקע בין ארגונים

תהליך שמשרת ארגונים רבים מריץ את עבודת הרקע של כל אחד כארגון עצמו. עיבוד השינויים של כל ארגון (לולאת האירועים שמנחיתה שינויים ממקורות ומרעננת את התצוגות שמעליהם), בניית הרפליקות שלו וטריגרי הטבלה שהם מפעילים שייכים ל-runtime שלו: שינוי מעובד על ידי משימת הארגון שלו, קורא את המקור שלו בכתובת ובאישורי הגישה של אותו ארגון וכותב למאגר של אותו ארגון. כל סביבה של ארגון מחזיקה slot משלה לשכפול לוגי של PostgreSQL ו-publication משלה, הנקראים על שם הארגון, הסביבה והמקור, כך ששני ארגונים שקוראים למקור באותו שם על אותו מסד נתונים קוראים כל אחד את השינויים שלו.

מגורי נתונים

בפריסה עם אזורים, טבלה יכולה לציין אזור אחד כבית שלה, והנתונים האישיים שלה מאוחסנים ומעובדים שם. (REQ-1921, REQ-1922)

  • רק אזור הבית בונה ומרענן את הרפליקה של הטבלה. כל אזור אחר קורא את הרפליקה הזו במקומה, כשכללי השורה והעמודה של הקורא מוחלים באזור הבית, כך שרק מה שהקורא רשאי לראות יוצא ממנו.
  • אזור בית שהרפליקה שלו לא נבנתה, או שהמאגרים שלו אינם זמינים, דוחה את הקריאה (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, ומסרבים לה. (REQ-2072)
  • נתונים שנשפכו (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) מוגבלים בנוסף לכתובת שאליה נבדקו. הארגון של הפריסה עצמה, שהמפעיל מגדיר את מקורותיו, אינו מוגבל.

הצפנה במנוחה והמפתחות של הארגון עצמו

הצפנה תחת מפתח של הארגון זמינה, והיא כבויה עד שהארגון מפעיל אותה. דבר אינו מוצפן תחת מפתח של הארגון אלא אם מנהל ארגון יוצר אחד.

מפתח ההצפנה של הארגון (REQ-1574) עוטף את סודות הארגון, את העמודות המוצפנות שלו ואת טקסט שאילתות הביקורת שלו. מנהל ארגון יוצר אותו תחת Admin → Org Secrets → Org Key, או באמצעות PUT /admin/org-encryption: ללא גוף Provisa מייצרת את המפתח, שאינו מוצג לעולם, וגוף key_b64 מספק 32 בתים משלכם. GET /admin/org-encryption מדווח רק על טביעת האצבע שלו. ללא מפתח ארגון, נתוני הארגון עטופים במפתח הפריסה (encryption.provider ב-provisa.yaml). פריסה שאינה מגדירה ספק הצפנה שומרת עמודות מוצפנות וטקסט שאילתות ביקורת בלי הצפנה; סודות אינם נשמרים לעולם בגלוי (ראו סודות).

תיקיית הארגון (REQ-1992) מחזיקה את הקבצים שהועלו ונטענו שלו, את קבצי ה-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 ונעדכן. אם אתם זקוקים ל-SOC 2, אנו ממליצים על אירוח עצמי.

Python בדפדפן

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

מצב אבטחה גבוהה

security.mode: high ב-provisa.yaml מבטיח ערובה אחת: ה-backend של Provisa לעולם אינו מעבד נתונים בטקסט גלוי. (REQ-693) כל עמודה משמעותית מוצפנת במקור, ורק לקוח שמחזיק את מפתח הפענוח יכול לקרוא אותה. לערובה הזו יש השלכות שפריסה חייבת לתכנן עבורן.

מה המצב עושה:

  • נקודות הקצה של הנתונים דורשות הוכחה לפענוח בצד הלקוח. כל מה שתחת /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 עם הודעה שנוקבת ב-REQ-693, ופורטי pgwire, Bolt ו-MCP אינם מאזינים.

Hook אישור ABAC

hook מדיניות חיצוני אופציונלי שמופעל לפני ביצוע השאילתה. (REQ-203) כשהוא מוגדר, Provisa פונה למנוע המדיניות שלכם עם זהות המשתמש, התפקידים, הטבלאות, העמודות והפעולה. התשובה קובעת אם השאילתה ממשיכה. (REQ-203) פקודה שנרשמה עם דורשת אישור עוברת לאותו hook לפני כל קריאה; ראו קריאות לפקודה. (REQ-1924)

היקף

ה-hook מופעל רק כשהשאילתה נוגעת בטבלה או במקור שהוא מופעל עליהם — אין תקורה לכל השאר. (REQ-204)

קונפיגורציה השפעה
auth.approval_hook.scope: all כל שאילתה מפעילה את ה-hook
sources[].approval_hook: true כל הטבלאות של המקור הזה מפעילות את ה-hook
tables[].approval_hook: true הטבלה הזו מפעילה את ה-hook

פרוטוקולים

שלוש תעבורות נתמכות: (REQ-246)

סוג תרחיש שדה קונפיגורציה
webhook כל שירות מדיניות עם HTTP (OPA, מותאם) url
unix_socket OPA או sidecar מדיניות באותו מארח socket_path + url
grpc שירות מדיניות קרוב בעל ביצועים גבוהים url (host:port)

תעבורת gRPC משתמשת בחוזה provisa.auth.ApprovalService המוגדר ב-provisa/auth/approval.proto. ממשו את השירות הזה במנוע המדיניות שלכם: (REQ-246)

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. (REQ-555)

בקשה / תגובה

כל שלוש התעבורות נושאות את אותו מטען: (REQ-246)

שדה סוג תיאור
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). (REQ-246)

קריאות לפקודה

פקודה שמוגדר בה requires_approval (המתג דורשת אישור בטופס הפקודה) עוברת ל-hook לפני כל קריאה, בכל משטח. היא רצה רק אם ה-hook מאשר. הבקשה נושאת operation: "command", את שם הפקודה ואת הארגומנטים שלה; tables ו-columns ריקים. (REQ-1924) [tool-verified: provisa/api/data/action_exec.py _require_approval (operation="command", tables=[], columns=[], command=fn["name"], arguments=args)]

{
  "user": "analyst",
  "roles": ["analyst"],
  "tables": [],
  "columns": [],
  "operation": "command",
  "command": "create_issue",
  "arguments": {"input": {"title": "Crash on save"}}
}
[inferred: the payload also carries session_vars; values shown are placeholders]

שתי דחיות, שתיהן 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. (REQ-247) מפסק זרם (ברירת מחדל: נפתח אחרי 5 שגיאות רצופות, חצי-פתוח אחרי 30 ש') מונע כשלים מתגלגלים מנקודת קצה איטית של ה-hook. (REQ-556)

דוגמת קונפיגורציה

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}, שמתפענח בזמן ריצה. (REQ-557) סיסמאות לעולם אינן נשמרות במסד הנתונים של הקונפיגורציה. (REQ-557)

לתיאור המלא של שירות הסודות — מאגרים, תחביר הפניות וספקים — ראו סודות.