מודל אבטחה¶
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 — גישה ציבורית¶
טבלאות בתחומים ללא הגבלת domain_access גלויות לכל הזהויות המאומתות ללא הגדרה נוספת. אפס חיכוך לנתונים ציבוריים במובן המלא.
שכבה 2 — גישת תחום¶
כל תפקיד נושא רשימת domain_access של מזהי תחומים. שאילתה שנוגעת בטבלה מחוץ לתחומים אלה נדחית לפני הביצוע. (REQ-038, REQ-039) זהו גבול הבעלות הגס — תפקיד משאבי אנוש אינו יכול להגיע לטבלאות כספים ללא קשר לאופן שבו נכתב ה-SQL. (REQ-002)
ראו מודל הרשאות.
שכבה 3 — אבטחה ברמת השורה¶
לאחר אישור גישת התחום, פרדיקטי WHERE לפי טבלה ולפי תפקיד מוזרקים לכל SELECT בזמן הביצוע. (REQ-041, REQ-263) הפרדיקטים מוערכים כנגד הנתונים הגולמיים. מנהל אזורי המבצע שאילתה על טבלת הזמנות משותפת רואה רק את שורות האזור שלו גם ב-SELECT *. (REQ-264)
שכבה 4 — נראות עמודה ומיסוך¶
עמודות עם רשימת visible_to שאינה כוללת את התפקיד המבקש מוסרות מפלט השאילתה. (REQ-040, REQ-263) לעמודות עם כלל מיסוך הערכים מוחלפים — עריכת regex, החלפה בקבוע, או קיצוץ — לפני שהתוצאות עוזבות את השרת. (REQ-263) המיסוך חל בכל שפות השאילתה ובכל פורמטי הפלט. (REQ-263)
ראו מודל הרשאות עמודה ו-מיסוך ברמת עמודה.
שכבה 5 — Predicate Guard¶
עמודות ממוסכות נדחות מסעיפי WHERE ו-HAVING. (REQ-263) ללא זה, קורא היה יכול להסיק את הערך הלא-ממוסך על ידי חיפוש בינארי שלו בפילטר גם אם הפלט ממוסך. הדחייה נאכפת בזמן ניתוח השאילתה (parse), לפני הביצוע. (REQ-531)
ממשל קשרים (V002)¶
תנאי JOIN ב-SQL חייבים להתאים לקשר רשום ומאושר בין טבלאות. (REQ-001) חיבורים (joins) לא מאושרים נדחים. כל קשר נושא סיבה תיאור קריאים לבני אדם — הנחיה הן למשתמשים והן לסוכנים אוטונומיים לגבי הסיבה שנתיב מעבר מסוים קיים. זוהי מדיניות ממשל, לא גבול אבטחה קשיח: שכבות 2–5 עומדות בעינן ללא קשר למבנה החיבור, כך שעקיפה מכוונת אינה חושפת נתונים שהתפקיד לא היה יכול להגיע אליהם דרך שתי שאילתות נפרדות. ניסיונות עקיפה נרשמים ביומן וניתנים לביקורת.
מנגנוני עקיפה — ניתן לעקוף את V002 בשתי דרכים. הראשונה היא יכולת: תפקיד המחזיק ב-ignore_relationships מבצע חיבורים (joins) על פני יחסים שהקטלוג אינו מכסה. מבין תפקידי המערכת המוזרעים, רק modeler מחזיק בה — תפקיד הגילוי שתפקידו לקבוע את המודל ולא לאכוף אותו. (REQ-1297) analyst לא מחזיק בה. [tool-verified: provisa/core/db.py:84]
השנייה היא ביטול משתתף בשני תנאים, כאשר שניהם חייבים להתקיים:
- דגל תפקיד —
relationship_guard: falseבהגדרת התפקיד (ברירת מחדל:true). [tool-verified:provisa/core/models.py:349] - ביטול לפי שאילתה — ה-SQL מכיל את ההערה
--relationship-guard=false. [tool-verified:provisa/compiler/params.py:80]
דגל התפקיד לבדו אינו עוקף את V002; ההערה לבדה אינה עוקפת את V002.
מצב אבטחה גבוהה מקבע את השומר (guard). תחת security.mode: high אף עקיפה אינה חלה: ignore_relationships מתעלם ממנו, relationship_guard: false מתעלם ממנו, וכל חיבור (join) חייב להתקיים בקטלוג הקשרים המאושר. (REQ-693) זו יתירות מכוונת — תפקיד ייצור שקיבל את היכולת בטעות עדיין אינו יכול לפרוץ מהמודל. [tool-verified: provisa/pgwire/_pipeline.py:377]
נתיב GraphQL — V002 מדולג ללא תנאי עבור שאילתות GraphQL. קשרים המוגדרים ב-SDL מאושרים מראש מעצם התכנון; הבדיקה מיותרת ואינה מיושמת. [tool-verified: provisa/api/data/endpoint.py:468]
נתיבי SQL ו-Cypher — V002 פעיל כברירת מחדל. גם endpoint_dev.py וגם cypher_router.py מיישמים את הבדיקה הדו-תנאית לפני קריאה ל-validate_sql. [tool-verified: provisa/api/data/endpoint_dev.py:127, provisa/api/rest/cypher_router.py:260]
נתיב pgwire — אותה בדיקה דו-תנאית כמו ב-SQL. ההערה --relationship-guard=false מוסרת מהשאילתה לפני הביצוע; היא אינה מגיעה למסד הנתונים. [tool-verified: provisa/pgwire/_pipeline.py:60]
השכבות הללו מצטרפות זו לזו. לתפקיד עם גישת תחום, RLS, ועמודות ממוסכות יש את כל חמשת האילוצים פעילים בו-זמנית. הוספת מקור נתונים, עמודה, או קשר חדשים אינה מחייבת עדכון של כל כלל — כל שכבה מוגדרת באופן עצמאי וחלה אוטומטית על כל שאילתה הנוגעת באובייקטים מנוהלים.
מודל הרשאות¶
יכולות המוקצות באופן עצמאי עם היררכיית תפקידים אופציונלית באמצעות parent_role_id. admin מעניק הכול. (REQ-042)
| יכולת | תיאור |
|---|---|
source_registration |
רישום מקורות נתונים |
table_registration |
רישום טבלאות, עמודות |
create_relationship |
הגדרת קשרי מפתח זר |
access_config |
הגדרת RLS, מיסוך |
query_development |
ביצוע שאילתות |
write |
הפעלת מוטציות רשומות (שער גס; ראו הרשאת מוטציה) |
full_results |
עקיפת מגבלות דגימה |
ignore_relationships |
עקיפת ממשל קשרים (V002). מוחזק על ידי modeler בלבד מבין תפקידי המערכת, ומתעלמים ממנו לחלוטין במצב אבטחה גבוהה |
admin |
Superuser — מעניק הכול |
הורשת תפקידים¶
תפקידים יכולים לרשת מתפקיד אב אחד באמצעות parent_role_id, המוגדר בתצורה או בעמוד האבטחה כ-"יורש מ" ("Inherits from"). (REQ-215) השרשרת מקופלת פנימה בעת ההפעלה: תפקיד-בן מחזיק באיחוד של יכולות ושל גישת התחום של אבותיו, בכל הענקת עמודה, מדד, פונקציה ו-webhook הנוקבת באחד מאבותיו, ובכללי ה-RLS של אבותיו לפי טבלה כאשר לתפקיד-הבן עדיפות — התפקיד הקרוב ביותר בשרשרת שיש לו כלל לטבלה מסוימת הוא זה שמספק את הפרדיקט של אותה טבלה, וכלל שנשמר עבור התפקיד-הבן מחליף את זה של האב עבור אותה טבלה. (REQ-1677)
roles:
- id: basic_user
capabilities: [query_development]
domain_access: [public]
- id: analyst
capabilities: [full_results]
domain_access: [sales, analytics]
parent_role_id: basic_user # inherits query_development + public domain
מודל הרשאות עמודה¶
לכל עמודה מודל הרשאות בעל ארבעה שדות השולט בגישת קריאה, כתיבה, ומיסוך לפי תפקיד. (REQ-042, REQ-249)
נראות בשלוש רמות¶
| רמה | תנאי | תוצאה |
|---|---|---|
| מוסתר (Hidden) | התפקיד אינו ב-visible_to |
העמודה נעדרת מה-SDL של GraphQL |
| ממוסך (Masked) | התפקיד ב-visible_to, יש כלל מיסוך, התפקיד אינו ב-unmasked_to |
העמודה גלויה אך הנתונים ממוסכים ב-SQL |
| לא ממוסך (Unmasked) | התפקיד ב-visible_to וגם ב-unmasked_to (או שאין כלל מיסוך) |
גישת קריאה מלאה |
הרשאות כתיבה¶
| שדה | ריקנות משמעה | מטרה |
|---|---|---|
visible_to |
כל התפקידים יכולים לקרוא | שולט מי רואה את העמודה (ממוסכת או לא) |
unmasked_to |
אף תפקיד אינו רואה ללא מיסוך | שולט מי עוקף את המיסוך |
writable_by |
אף תפקיד אינו יכול לכתוב | שולט מי יכול לבצע מוטציה (INSERT/UPDATE) |
הרשאת כתיבה נאכפת ב-Pipeline של המוטציות. תפקיד שאינו ב-writable_by מקבל שגיאת 403 בעת ניסיון לכתוב לעמודה מוגבלת. (REQ-033, REQ-034)
דוגמה¶
columns:
- name: email
visible_to: [admin, analyst, viewer]
writable_by: [admin]
unmasked_to: [admin]
mask_type: regex
mask_pattern: "(.).*@"
mask_replace: "$1***@"
- name: salary
visible_to: [admin, hr]
writable_by: [hr]
unmasked_to: [admin, hr]
mask_type: constant
mask_value: "0"
- name: created_at
visible_to: [] # all can read
writable_by: [] # nobody can write (auto-set)
בדוגמה זו:
email: admin רואהalice@example.comויכול לערוך; analyst/viewer רואיםa***@example.comsalary: admin ו-hr רואים את הערך האמיתי; hr יכול לערוך; כל שאר התפקידים אינם רואים את העמודה כללcreated_at: כולם יכולים לקרוא, אף אחד אינו יכול לכתוב
הרשאת מוטציה¶
מוטציות רשומות (GraphQL מרוחק, OpenAPI, gRPC, Hasura) נשערות על ידי שתי בדיקות עצמאיות. (REQ-867, REQ-868) תפקיד יכול להפעיל מוטציה רק אם הוא מחזיק ביכולת הגלובלית write וגם מופיע ברשימת ה-writable_by של אותה מוטציה. (REQ-868) writable_by ריק פירושו דחייה כברירת מחדל — אף תפקיד אינו יכול להפעיל אותה. (REQ-867)
מוטציות מסווגות ככתיבה לפי חוזה, לא לפי הצהרת הקורא. (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')"
הפילטר מחובר באמצעות AND לסעיף ה-WHERE של השאילתה. עובד גם עבור שאילתות וגם עבור מוטציות (UPDATE/DELETE). (REQ-035, REQ-041)
ביטוי current_setting('provisa.<name>') נפתר כנגד משתני הסביבה (session variables) שהבקשה מחברת: user_id מהזהות המאומתת, role מהתפקיד הפועל, וכל תביעה (claim) סקלרית של האסימון תחת שמה באותיות קטנות עם הסרת הקידומת x-hasura-, כך שפילטר Hasura מיובא על X-Hasura-User-Id נקרא כ-provisa.user_id. session_vars המוגדרים של תפקיד הם הקבועים שמתחת; חיבורי הבקשה מוסיפים עליהם שכבה. במצב לא מאובטח, כותרת x-provisa-session-<name> מחברת את <name>. משתנה שאינו מחובר בשום מקום נפתר ל-NULL ואינו תואם אף שורה. (REQ-1682)
מיסוך ברמת עמודה¶
המיסוך מוגדר פעם אחת לעמודה — זו תכונה של העמודה, לא של התפקיד. השדה unmasked_to שולט אילו תפקידים עוקפים אותו. (REQ-249)
| סוג מיסוך | סוגים נתמכים | ביטוי SQL |
|---|---|---|
regex |
מחרוזת (varchar, char, text) | REGEXP_REPLACE(col, pattern, replace) |
constant |
כל סוג | ערך ליטרלי (NULL, 0, מותאם אישית) |
truncate |
תאריך/חותמת זמן | DATE_TRUNC(precision, col) |
המיסוך מוזרק לתוך פרויקציית ה-SELECT ב-SQL — מסד הנתונים מחזיר נתונים ממוסכים. (REQ-263) נתונים לא-ממוסכים לעולם אינם עוברים בקו התקשורת עבור תפקידים ממוסכים. (REQ-263) עמודות ממוסכות חסומות גם מסעיפי WHERE ו-HAVING (שכבה 5, Predicate Guard) כדי למנוע הסקה של הערך הלא-ממוסך באמצעות סינון. (REQ-263, REQ-531)
תגובות פעולה מנוהלות (Governing Action Responses)¶
התגובה של פונקציה או webhook במעקב מנוהלת כמו שורות של טבלה. (REQ-1679) השורות שפעולה מחזירה נקשרות כרלציה שהעמודות שלה הן חוזה הפלט המוצהר של הפעולה — output_columns עבור פונקציה, inline_return_type עבור webhook, או מאפייני האובייקט של return_schema ממערך של פונקציה — ואותה רלציה עוברת דרך אותו שלב ממשל שכל קריאת טבלה עוברת דרכו, מעל VALUES CTE המחזיק את השורות. (REQ-1679) שום דבר ב-RLS, מיסוך או נראות אינו ממומש מחדש עבור פעולות.
שלושה דברים חלים, באותו סדר כמו עבור טבלה. (REQ-1679)
- פילטר שורות: כלל ה-RLS של התפקיד השמור כנגד הפעולה לפי שם (
upsertRlsRuleעםactionName), או, כשלפעולה אין כלל משלה, כלל התחום של תחום הפעולה. הפרדיקט מאומת כנגד החוזה בעת השמירה באותו אופן שבו כלל טבלה מאומת. (REQ-1676) - נראות עמודה: עמודת חוזה המצהירה
visible_toמושמטת עבור תפקיד שהרשימה אינה נוקבת בו; עמודה שאינה מצהירה דבר היא חלק מהצורה הציבורית של הפעולה ונשארת. - מיסוכים: עמודת חוזה עשויה לשאת
mask_type,mask_pattern,mask_replace,mask_value,mask_precisionו-unmasked_to, אותם שדות שעמודת טבלה נושאת.
הורשת תפקידים פותרת את השרשרת עבור כל שלושת אלה. (REQ-1677) תגובה המחזירה עמודה מחוץ לחוזה המוצהר נדחית עם 502 במקום לעבור ללא ממשל. פעולה שאינה מצהירה חוזה עמודות מחזירה סקלר ואין לה מה לקשור; היא מוחזרת כמות שהיא. בדיקת ההרצה של האדמין (test-invoke) מדווחת על הפילטרים, ההשמטות והמיסוכים שהוחלו, וההצהרה המנוהלת נכתבת ליומן הביקורת של השאילתות.
דגימה¶
כל התפקידים רואים תוצאות מדוגמות (ברירת מחדל: 100 שורות) אלא אם יש להם יכולת full_results. (REQ-554) נשלט באמצעות משתנה הסביבה PROVISA_SAMPLE_SIZE. (REQ-554)
רישום ביקורת¶
כל שאילתה הנוגעת בנכס תחום נרשמת ב-query_audit_log שניתן להוסיף אליו בלבד (append-only). (REQ-596, REQ-613) כל שורה לוכדת tenant_id, user_id, role_id, גיבוב SHA-256 של טקסט השאילתה, table_ids, source, status_code, duration_ms, ו-logged_at. (REQ-596) טקסט השאילתה לעולם אינו נשמר כלשונו — רק הגיבוב שלו. (REQ-596)
היומן הוא append-only ברמת מסד הנתונים: כללי PostgreSQL חוסמים DELETE ו-UPDATE. (REQ-596, REQ-613) שני אינדקסים — (tenant_id, logged_at) ו-(user_id, logged_at) — תומכים בשאילתות ציות (compliance) בתחום דייר ובטווח זמן לפי משתמש. (REQ-596, REQ-613)
כאשר ההצפנה מופעלת, עמודת גיבוב טקסט השאילתה נשמרת מוצפנת ומפוענחת רק בקריאות אדמין מורשות. (REQ-689)
הגבלת קצב (Rate Limiting)¶
הגבלות קצב לפי תפקיד מוגדרות ב-provisa.yaml: מספר בקשות מרבי לשנייה, מספר מרבי של מנויי SSE במקביל, ומספר מרבי של זרמי Arrow Flight במקביל. (REQ-369) ההגבלות נאכפות בשכבת ה-API לפני הקומפילציה או הביצוע; בקשות שחורגות מהמגבלה נדחות עם HTTP 429 וכותרת Retry-After. (REQ-369)
לשירות שאילתות השפה הטבעית (POST /query/nl) יש מגבלה עצמאית באמצעות nl.rate_limit (בקשות לדקה לפי תפקיד). בקשות שחורגות מהמגבלה נדחות לפני שכל קריאת LLM מתבצעת. (REQ-370)
מצב הגבלת הקצב שוכן ב-Redis (cache.redis_url) כמונה חלון-נגלל (sliding-window) — ללא מצב פר-מופע — כך שההגבלות תקפות על פני כל מופעי Provisa האופקיים. (REQ-371)
אימות (Authentication)¶
ספקי אימות שניתנים לחיבור (Pluggable): (REQ-120)
| ספק | סוג אסימון | מקרה שימוש |
|---|---|---|
none |
כותרת X-Provisa-Role | פיתוח |
basic |
חשבונות מקומיים עם bcrypt + JWT | פריסות עצמאיות |
firebase |
אסימון זיהוי Firebase | ייצור |
keycloak |
JWT של Keycloak | ארגוני |
oauth |
OIDC JWT | PingFed, Okta, Azure AD, Auth0 |
simple |
bcrypt + JWT | בדיקות |
מיפוי תפקידים: תביעות זהות (claims) → תפקיד Provisa באמצעות כללים הניתנים להגדרה. (REQ-120) השדה assignments_source שולט מהיכן מגיעות הקצאות התפקידים: claims קורא אותן מתביעות אסימון JWT (ברירת מחדל), provisa קורא אותן ממאגר ההקצאות הפנימי של Provisa. (REQ-551)
superuser המוגדר ב-provisa.yaml (שם משתמש בתוספת סיסמה מסוד סביבה) תמיד מקבל את תפקיד ה-admin ואת כל היכולות ללא קשר לספק המוגדר — נתיב bootstrap להגדרה ראשונית. (REQ-125)
משטחים (Surfaces) ואישורים¶
כל משטח מאמת דרך אותו חוזה ספק, כך שאישור שעובד באחד עובד בכולם בכל מקום שהפרוטוקול מסוגל לשאת אותו. (REQ-124, REQ-1263) טבלה זו היא הרפרנס היחיד; התיעוד לפי-משטח אינו חוזר עליה.
| משטח | סיסמה | אסימון ספק | אסימון גישה אישי | אישור לקוח (mTLS) |
|---|---|---|---|---|
| HTTP (REST, JSON:API, GraphQL) | Authorization: Basic |
Authorization: Bearer |
Authorization: Bearer |
דרך פרוקסי מסיים |
| pgwire | שדה סיסמה (טקסט גלוי או SCRAM) | שדה סיסמה, פריסות OIDC | שדה סיסמה | כן |
| Bolt | סכמת basic |
סכמת bearer |
סכמת bearer |
כן |
| Arrow Flight | — | token בלחיצת יד (handshake) או ב-payload של הכרטיס |
זהה | כן |
| gRPC | — | מטא-דאטה authorization |
מטא-דאטה authorization |
כן |
| MCP | — | Authorization: Bearer |
Authorization: Bearer |
דרך פרוקסי מסיים |
היכן שתא קורא — הפרוטוקול אינו נושא שדה שם משתמש כדי לצרף אליו סיסמה; צורות האסימון מכסות זאת. pgwire הוא המקרה המראה — לחבילת ההתחלה (startup packet) יש שדה סוד אחד ואין סכמה, כך שמה שהסוד הוא קובע את השיטה — PAT מזוהה לפי הקידומת שלו, הסוד נקרא כאסימון bearer כאשר הספק המוגדר הוא ספק אסימונים, וכל דבר אחר הוא סיסמה. הבחירה נעשית פעם אחת — אישור שהמאמת שנבחר דוחה אינו מנוסה שוב מול מאמת אחר.
המטריצה נאכפת על ידי tests/unit/test_auth_surface_conformance.py, המפעיל את נקודת האימות האמיתית של כל משטח ונכשל כאשר משטח חדש מתווסף ללא שורה.
אסימוני גישה אישיים (PAT)¶
PAT הוא סוד bearer ארוך-חיים שמשתמש טוען עבור לקוח שאינו יכול להשלים כניסה אינטראקטיבית — סקריפט, כלי BI, מנהל התקן (driver). (REQ-1263) הוא נושא את הארגון והתפקיד שלו, וכל משטח פותר אותו דרך אותו מאמת, כך שאף משטח אינו צריך לדעת מהו PAT.
צורת חוט התקשורת (wire form) היא provisa_pat_ ואחריה 43 תווי base64 בטוחים ל-URL. הקידומת היא מה שמנתב סוד מוצג למאגר האסימונים במקום לספק הזהות, והיא הופכת אסימון שדלף לגלוי (greppable) ביומנים ובמאגרי קוד.
- אחסון — רק SHA-256 של הסוד נשמר. הסוד עצמו מוצג פעם אחת בלבד, בעת היצירה, ולא ניתן לשחזרו. הרשימה נושאת את קידומת התצוגה וחותמות הזמן של מחזור החיים, לעולם לא אישור עובד.
- הנפקה וביטול —
POST /auth/tokens,GET /auth/tokens,DELETE /auth/tokens/{token_hash}, בתוספת אזור השירות העצמי בפרופיל המשתמש עצמו בממשק הניהול. הטבעה וביטול של אישור הם פעולת מחזיק האסימון. - ייחוס (Attribution) — 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 הראשון של כל משתמש עוקב אחר הזנת הסיסמה הבאה שלו. משתמש שעדיין אין לו מאמת נענה בחילופין מדומה שאינו ניתן להבחנה מחילופין אמיתי, כך שקו התקשורת אינו חושף מי כבר עבר הגירה.
mTLS (אישור הדדי)¶
אימות אישור לקוח (client-certificate) מעביר את הבדיקה הראשונה ללחיצת היד (handshake) של 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, השם הנפוץ (common name) של האישור חייב להיות שווה לשם המשתמש שהחיבור מתאמת כמותו לאחר מכן |
עקיפות לפי פרוטוקול פועלות לפי אותה שיטת מתן שמות כמו הגדרות ה-TLS. שום דבר אינו מוסק: מצב שהוגדר ללא CA מסרב להתחיל, ומצב לא מזוהה מסרב להתחיל במקום להיקרא כשכן הבטוח ביותר — פריסה המאמינה שהיא מחייבת אישורי לקוח ואינה כך גרועה יותר מפריסה שנכשלת בהתחלה.
הגבלת ניסיונות כניסה (Login throttling)¶
ניחוש סיסמאות אינו תלוי-פרוטוקול: אותו חשבון יכול להיות מותקף על HTTP, pgwire ו-Bolt כאחד. המונה שוכן אפוא בשכבת אימות האישורים, לא על משטח יחיד כלשהו, כך שנעילה שהושגה בכל מקום נאכפת בכל מקום. (REQ-1393)
זה פועל כברירת מחדל — חמישה כשלונות בחמש דקות נועלים את הנושא (subject) למשך חמש עשרה דקות — והמכוונן תחת auth.login_throttle. נושא נעול נדחה לפני שהאישור נבדק בכלל, ואימות מוצלח מנקה את ההיסטוריה של אותו נושא.
המפתח הוא ה-principal שהפרוטוקול נושא. משטח bearer-בלבד אינו נושא principal, כך שהמפתח הוא digest של האישור עצמו; מה שזה עוצר הוא הפעלה חוזרת של אסימון רע ללא הגבלה. המאגר הוא לפי תהליך, כך שפריסה המריצה מספר עובדי API מאפשרת עד max_attempts לכל עובד — ההגבלה היא בלם לניחוש, לא מכסה מבוזרת.
כתובת ארגון בפרוטוקול חוט תקשורת (wire protocol)¶
תחת ריבוי-דיירים ארגון מכוון לפי שם מארח: acme.provisa.dev הוא ארגון acme. מעל HTTP שם זה מגיע בכותרת ה-Host. לקוח pgwire או Bolt אינו שולח כותרת כזו, אך הוא כן שולח את שם המארח שאליו חייג ב-ClientHello של ה-TLS, ו-Provisa קורא את הארגון משם. (REQ-1234) שום דבר בלקוח אינו משתנה — חיבור אל acme.provisa.dev הוא כל מה שנדרש.
שם המארח הוא בקשה, לא הענקה. הוא מגיע לאותו פותר (resolver) שאליו מגיעה כותרת ה-Host, אשר מסרב לכל ארגון שה-principal המאומת אינו חבר בו ואינו מחזיק בזכות חוצת-ארגונים עבורו. חיוג לשם מארח שאין לך חברות בו אינו מגיע לשום נתון. לקוח שהתחבר לפי כתובת IP אינו שולח שם מארח ופותר את הארגון שלו מה-principal בלבד, מה שקורה בכל חיבור בפריסה של ארגון יחיד.
gRPC, Arrow Flight ו-MCP מוסרים את האישורים שלהם לספריות שאינן חושפות callback של שם מארח; תעבורות אלה מציינות ארגון עם כותרת המטא-דאטה x-provisa-org במקום זאת.
מצב אבטחה גבוהה (High-Security Mode)¶
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 מוסרת את תוצאותיה למודל כטקסט. פורט מוגדר עבור כל אחד מהם נדחה בהפעלה במקום להישרת.
- לא ניתן לעקוף את שומר הקשרים (relationship guard). גם
ignore_relationshipsוגםrelationship_guard: falseמתעלמים; ראו ממשל קשרים.
אימות שפריסה נמצאת במצב: יומן ההפעלה נוקב בו, בקשת /data/sql ללא מפתח KMS עונה 403 עם הודעה הנוקבת ב-REQ-693, ופורטי ה-pgwire, Bolt ו-MCP אינם מאזינים.
Hook אישור ABAC¶
Hook מדיניות חיצוני אופציונלי הנפעל לפני ביצוע השאילתה. (REQ-203) כאשר מוגדר, Provisa קוראת למנוע המדיניות שלך עם זהות המשתמש, התפקידים, הטבלאות, העמודות, והפעולה. התגובה קובעת האם השאילתה ממשיכה. (REQ-203)
היקף (Scoping)¶
ה-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;
}
message ApprovalResponse {
bool approved = 1;
string reason = 2;
}
ערוץ ה-gRPC הוא קבוע (persistent) — ערוץ אחד למופע Provisa, המשמש שוב עבור כל הקריאות לאותה נקודת קצה של Hook. (REQ-555)
בקשה / תגובה¶
כל שלוש התעבורות נושאות את אותו payload: (REQ-246)
| שדה | סוג | תיאור |
|---|---|---|
user |
string | זהות משתמש מאומתת |
roles |
string[] | תפקידי Provisa של המשתמש |
tables |
string[] | מזהי טבלה המוזכרים בשאילתה |
columns |
string[] | עמודות שנבחרו בשאילתה |
operation |
string | "query" או "mutation" |
תעבורות ה-webhook ו-Unix socket מחליפות JSON. התגובה חייבת לכלול approved (bool) ואופציונלית reason (string). (REQ-246)
Timeout ונפילה חוזרת (Fallback)¶
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) מפסק מעגלים (circuit breaker) (ברירת מחדל: נפתח לאחר 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)
לשירות הסודות המלא — כספות, תחביר הפניה, וספקים — ראו סודות.