6.3 KiB
6.3 KiB
Python ABAC Implementation
Attribute-Based Access Control implementation with policy evaluation.
from typing import List, Callable, Dict, Any
from dataclasses import dataclass
from enum import Enum
from datetime import datetime
import threading
class Effect(Enum):
ALLOW = "allow"
DENY = "deny"
@dataclass
class Policy:
"""Policy defining access rules with conditions."""
name: str
effect: Effect
resource: str
action: str
conditions: List[Callable[[Dict[str, Any]], bool]]
def matches(self, context: Dict[str, Any]) -> bool:
"""Check if policy matches the request context."""
if self.resource != "*" and self.resource != context.get("resource"):
return False
if self.action != "*" and self.action != context.get("action"):
return False
return True
def evaluate(self, context: Dict[str, Any]) -> bool:
"""Evaluate all conditions against context."""
return all(condition(context) for condition in self.conditions)
class ABACEngine:
"""Attribute-Based Access Control decision engine with thread-safe policy management."""
def __init__(self):
self.policies: List[Policy] = []
self._lock = threading.Lock()
def add_policy(self, policy: Policy):
"""Register a policy with the engine (thread-safe)."""
with self._lock:
self.policies.append(policy)
def remove_policy(self, policy_id: str) -> bool:
"""Remove a policy by ID. Returns True if removed, False if not found."""
with self._lock:
for i, policy in enumerate(self.policies):
if policy.policy_id == policy_id:
del self.policies[i]
return True
return False
def update_policy(self, policy_id: str, new_policy: Policy) -> bool:
"""Update a policy by ID. Returns True if updated, False if not found."""
with self._lock:
for i, policy in enumerate(self.policies):
if policy.policy_id == policy_id:
self.policies[i] = new_policy
return True
return False
def clear_policies(self):
"""Remove all policies from the engine."""
with self._lock:
self.policies.clear()
def check_access(self, context: Dict[str, Any]) -> bool:
"""
Evaluate access request against all policies.
Deny-by-default: returns False if no matching ALLOW policy.
Thread-safe: uses shallow copy to avoid holding lock during evaluation.
"""
# Take shallow copy to avoid holding lock during evaluation
with self._lock:
policies_snapshot = self.policies.copy()
for policy in policies_snapshot:
if policy.matches(context) and policy.evaluate(context):
if policy.effect == Effect.DENY:
return False
elif policy.effect == Effect.ALLOW:
return True
return False # Deny by default
# Common condition functions
def is_resource_owner(context: Dict[str, Any]) -> bool:
"""Check if user owns the resource."""
return context.get("user_id") == context.get("resource_owner_id")
def is_within_business_hours(context: Dict[str, Any]) -> bool:
"""Check if current time is within business hours (9 AM - 6 PM)."""
current_hour = datetime.now().hour
return 9 <= current_hour < 18
def has_department(department: str) -> Callable:
"""Factory for department check condition."""
def check(context: Dict[str, Any]) -> bool:
return context.get("user_department") == department
return check
def has_minimum_clearance(level: int) -> Callable:
"""Factory for clearance level check."""
def check(context: Dict[str, Any]) -> bool:
return context.get("user_clearance", 0) >= level
return check
# Usage Example
if __name__ == "__main__":
engine = ABACEngine()
# Policy: Users can read their own documents
engine.add_policy(Policy(
name="owner-read",
effect=Effect.ALLOW,
resource="document",
action="read",
conditions=[is_resource_owner]
))
# Policy: Finance department can read financial reports during business hours
engine.add_policy(Policy(
name="finance-reports",
effect=Effect.ALLOW,
resource="financial_report",
action="read",
conditions=[has_department("finance"), is_within_business_hours]
))
# Policy: High clearance users can access sensitive data
engine.add_policy(Policy(
name="sensitive-access",
effect=Effect.ALLOW,
resource="sensitive_data",
action="*",
conditions=[has_minimum_clearance(5)]
))
# Test access
context = {
"user_id": "user123",
"resource_owner_id": "user123",
"resource": "document",
"action": "read"
}
print(f"Owner read access: {engine.check_access(context)}") # True
Flask Integration
from functools import wraps
from flask import Flask, request, g, jsonify
app = Flask(__name__)
abac = ABACEngine()
def require_access(resource: str, action: str):
"""Decorator for ABAC-protected endpoints."""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Guard: check if user is authenticated
user = getattr(g, "user", None)
if user is None:
return jsonify({"error": "Authentication required"}), 401
# Guard: check required user attributes exist
if not all(hasattr(user, attr) for attr in ["id", "department", "clearance_level"]):
return jsonify({"error": "Incomplete user authentication"}), 401
context = {
"user_id": user.id,
"user_department": user.department,
"user_clearance": user.clearance_level,
"resource": resource,
"action": action,
"resource_owner_id": kwargs.get("owner_id")
}
if not abac.check_access(context):
return jsonify({"error": "Access denied"}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route("/documents/<doc_id>")
@require_access("document", "read")
def get_document(doc_id):
return jsonify({"document": doc_id})