• בלוג
  • בואו נבנה מנגנון הרשאות פשוט עם Decorators בפייתון

בואו נבנה מנגנון הרשאות פשוט עם Decorators בפייתון

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

1. קצת עבודת תשתית

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

אז נתחיל עם Enum שמגדיר את כל ההרשאות האפשריות, ובשביל המשחק הוא יכלול רק שני סוגים בינתיים:

from enum import Enum, auto

class Permissions(Enum):
    NETWORK = auto()
    FILESYSTEM = auto()

ועוד פונקציה שבודקת אם יש או אין לתוכנית הרשאות לפעולה מסוימת:

def has_permission(permission: Permissions):
    match permission:
        case Permissions.NETWORK:
            return "--network" in sys.argv
        case Permissions.FILESYSTEM:
            return False

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

2. הממשק שאני רוצה

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

@requires_permission(Permissions.NETWORK)
def find_my_ip():
    with urllib.request.urlopen('https://api.ipify.org') as response:
        data = response.read()
        return data.decode('utf8')

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

3. מימוש פונקציית הגדרת ההרשאות

המימוש של ה Decorator הוא די פשוט אחרי ששמנו את הממשק במקום:

def requires_permission(permission: Permissions):
    def decorator(f):
        def inner(*args, **kwargs):
            if not has_permission(permission):
                raise Exception("Not Authorized")
            return f(*args, **kwargs)
        inner.permissions = f.__dict__.get('permissions', []) + [permission]
        return inner
    return decorator

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

4. וידוא הגדרת הרשאות לכל פונקציה

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

def list_users():
    with open('/etc/passwd') as f:
        print(f.read())

ואז כולם יוכלו להדפיס תוכן של קובץ בלי הרשאות גישה למערכת הקבצים.

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

import sys
import inspect

def init():
    safe_functions = {'has_permission', 'init', 'requires_permission'}
    functions = [(name, obj) for name, obj in inspect.getmembers(sys.modules[__name__])
                 if inspect.isfunction(obj)
                 and 'permissions' not in obj.__dict__
                 and name not in safe_functions]
    if len(functions) > 0:
        raise Exception(f"Functions {functions} don't define required permissions")


init()

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