Spaces:
Running
Running
Commit ·
78b489c
1
Parent(s): a17b63b
example hipaa compliance access policy
Browse files
examples/hipaa_policy_example.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Example demonstrating the actor-aware HIPAAAccessPolicy.
|
| 3 |
+
"""
|
| 4 |
+
import asyncio
|
| 5 |
+
import json
|
| 6 |
+
from fastmcp import FastMCP
|
| 7 |
+
from fastmcp.policy.policies import HIPAAAccessPolicy
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def create_hipaa_server():
|
| 11 |
+
"""Create a FastMCP server with HIPAA policy engine enabled."""
|
| 12 |
+
|
| 13 |
+
# Create server
|
| 14 |
+
server = FastMCP("HIPAA Compliance Server")
|
| 15 |
+
|
| 16 |
+
# Enable policy engine
|
| 17 |
+
policy_engine = server.enable_policy_engine()
|
| 18 |
+
policy_engine.register_policy(HIPAAAccessPolicy())
|
| 19 |
+
|
| 20 |
+
@server.tool
|
| 21 |
+
async def access_phi(
|
| 22 |
+
user_id: str,
|
| 23 |
+
user_roles: list[str],
|
| 24 |
+
action: str,
|
| 25 |
+
purpose: str,
|
| 26 |
+
patient_id: str,
|
| 27 |
+
data_elements: list[str],
|
| 28 |
+
is_clinical: bool = True,
|
| 29 |
+
recipient_id: str | None = None,
|
| 30 |
+
recipient_type: str | None = None
|
| 31 |
+
) -> dict:
|
| 32 |
+
"""Access PHI with HIPAA policy evaluation."""
|
| 33 |
+
|
| 34 |
+
# Create context for policy evaluation
|
| 35 |
+
context = {
|
| 36 |
+
"user": {"id": user_id, "roles": user_roles},
|
| 37 |
+
"action": action,
|
| 38 |
+
"purpose": purpose,
|
| 39 |
+
"resource": {
|
| 40 |
+
"is_phi": True,
|
| 41 |
+
"type": "phi",
|
| 42 |
+
"is_clinical": is_clinical,
|
| 43 |
+
"data_elements": data_elements
|
| 44 |
+
},
|
| 45 |
+
"patient": {"id": patient_id}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
# Add recipient if provided (for disclosure scenarios)
|
| 49 |
+
if recipient_id and recipient_type:
|
| 50 |
+
context["recipient"] = {"id": recipient_id, "type": recipient_type}
|
| 51 |
+
|
| 52 |
+
# Evaluate HIPAA policy
|
| 53 |
+
decision = await policy_engine.evaluate(context)
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
"access_granted": decision.allow,
|
| 57 |
+
"reason": decision.reason,
|
| 58 |
+
"obligations": decision.obligations,
|
| 59 |
+
"proof": decision.proof
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
@server.tool
|
| 63 |
+
async def access_billing_data(
|
| 64 |
+
user_id: str,
|
| 65 |
+
user_roles: list[str],
|
| 66 |
+
action: str,
|
| 67 |
+
purpose: str,
|
| 68 |
+
patient_id: str,
|
| 69 |
+
billing_elements: list[str]
|
| 70 |
+
) -> dict:
|
| 71 |
+
"""Access billing data with HIPAA policy evaluation."""
|
| 72 |
+
|
| 73 |
+
context = {
|
| 74 |
+
"user": {"id": user_id, "roles": user_roles},
|
| 75 |
+
"action": action,
|
| 76 |
+
"purpose": purpose,
|
| 77 |
+
"resource": {
|
| 78 |
+
"is_phi": True,
|
| 79 |
+
"is_clinical": False,
|
| 80 |
+
"data_elements": billing_elements
|
| 81 |
+
},
|
| 82 |
+
"patient": {"id": patient_id}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
decision = await policy_engine.evaluate(context)
|
| 86 |
+
|
| 87 |
+
return {
|
| 88 |
+
"access_granted": decision.allow,
|
| 89 |
+
"reason": decision.reason,
|
| 90 |
+
"obligations": decision.obligations,
|
| 91 |
+
"proof": decision.proof
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
return server
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
async def demonstrate_actor_aware_hipaa_policy():
|
| 98 |
+
"""Demonstrate HIPAAPolicy with scenarios for each actor type."""
|
| 99 |
+
server = create_hipaa_server()
|
| 100 |
+
policy_engine = server.get_policy_engine()
|
| 101 |
+
assert policy_engine is not None
|
| 102 |
+
|
| 103 |
+
print("🔐 Actor-Aware HIPAA Policy Evaluation Example")
|
| 104 |
+
print("=" * 60)
|
| 105 |
+
|
| 106 |
+
# Test scenarios using the tools
|
| 107 |
+
scenarios = [
|
| 108 |
+
# --- Provider Scenarios ---
|
| 109 |
+
{
|
| 110 |
+
"name": "[Provider] Access clinical data for treatment (Allowed)",
|
| 111 |
+
"tool": "access_phi",
|
| 112 |
+
"params": {
|
| 113 |
+
"user_id": "dr_smith",
|
| 114 |
+
"user_roles": ["provider"],
|
| 115 |
+
"action": "read",
|
| 116 |
+
"purpose": "Treatment",
|
| 117 |
+
"patient_id": "patient_123",
|
| 118 |
+
"data_elements": ["full_record"],
|
| 119 |
+
"is_clinical": True
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"name": "[Provider] Disclose PHI to another provider (Allowed with obligations)",
|
| 124 |
+
"tool": "access_phi",
|
| 125 |
+
"params": {
|
| 126 |
+
"user_id": "dr_jones",
|
| 127 |
+
"user_roles": ["provider"],
|
| 128 |
+
"action": "disclose",
|
| 129 |
+
"purpose": "Treatment",
|
| 130 |
+
"patient_id": "patient_123",
|
| 131 |
+
"data_elements": ["lab_results"],
|
| 132 |
+
"is_clinical": True,
|
| 133 |
+
"recipient_id": "specialist_clinic",
|
| 134 |
+
"recipient_type": "health_care_provider"
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
# --- Payee Scenarios ---
|
| 138 |
+
{
|
| 139 |
+
"name": "[Payee] Access billing information for payment (Allowed)",
|
| 140 |
+
"tool": "access_billing_data",
|
| 141 |
+
"params": {
|
| 142 |
+
"user_id": "bill_staff_01",
|
| 143 |
+
"user_roles": ["payee"],
|
| 144 |
+
"action": "read",
|
| 145 |
+
"purpose": "Payment",
|
| 146 |
+
"patient_id": "patient_456",
|
| 147 |
+
"billing_elements": ["billing_codes", "dates_of_service"]
|
| 148 |
+
}
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"name": "[Payee] Attempt to access clinical notes (Denied by Minimum Necessary)",
|
| 152 |
+
"tool": "access_phi",
|
| 153 |
+
"params": {
|
| 154 |
+
"user_id": "bill_staff_01",
|
| 155 |
+
"user_roles": ["payee"],
|
| 156 |
+
"action": "read",
|
| 157 |
+
"purpose": "Payment",
|
| 158 |
+
"patient_id": "patient_456",
|
| 159 |
+
"data_elements": ["physician_notes"],
|
| 160 |
+
"is_clinical": True
|
| 161 |
+
}
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
"name": "[Payee] Attempt to modify clinical record (Denied by Integrity Rule)",
|
| 165 |
+
"tool": "access_phi",
|
| 166 |
+
"params": {
|
| 167 |
+
"user_id": "bill_staff_01",
|
| 168 |
+
"user_roles": ["payee"],
|
| 169 |
+
"action": "write",
|
| 170 |
+
"purpose": "Payment",
|
| 171 |
+
"patient_id": "patient_456",
|
| 172 |
+
"data_elements": ["diagnosis_code"],
|
| 173 |
+
"is_clinical": True
|
| 174 |
+
}
|
| 175 |
+
},
|
| 176 |
+
# --- Patient Scenarios ---
|
| 177 |
+
{
|
| 178 |
+
"name": "[Patient] Request own full medical record (Allowed, bypasses Min. Necessary)",
|
| 179 |
+
"tool": "access_phi",
|
| 180 |
+
"params": {
|
| 181 |
+
"user_id": "patient_789",
|
| 182 |
+
"user_roles": ["patient"],
|
| 183 |
+
"action": "read",
|
| 184 |
+
"purpose": "Self_Access",
|
| 185 |
+
"patient_id": "patient_789",
|
| 186 |
+
"data_elements": ["full_record"],
|
| 187 |
+
"is_clinical": True
|
| 188 |
+
}
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"name": "[Patient] Request export of own data (Allowed with encryption obligation)",
|
| 192 |
+
"tool": "access_phi",
|
| 193 |
+
"params": {
|
| 194 |
+
"user_id": "patient_789",
|
| 195 |
+
"user_roles": ["patient"],
|
| 196 |
+
"action": "export",
|
| 197 |
+
"purpose": "Self_Access",
|
| 198 |
+
"patient_id": "patient_789",
|
| 199 |
+
"data_elements": ["full_record"],
|
| 200 |
+
"is_clinical": True
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
]
|
| 204 |
+
|
| 205 |
+
for scenario in scenarios:
|
| 206 |
+
print(f"\n📋 Scenario: {scenario['name']}")
|
| 207 |
+
print("-" * 40)
|
| 208 |
+
|
| 209 |
+
# Create context directly for policy evaluation
|
| 210 |
+
if scenario["tool"] == "access_phi":
|
| 211 |
+
context = {
|
| 212 |
+
"user": {
|
| 213 |
+
"id": scenario["params"]["user_id"],
|
| 214 |
+
"roles": scenario["params"]["user_roles"]
|
| 215 |
+
},
|
| 216 |
+
"action": scenario["params"]["action"],
|
| 217 |
+
"purpose": scenario["params"]["purpose"],
|
| 218 |
+
"resource": {
|
| 219 |
+
"is_phi": True,
|
| 220 |
+
"type": "phi",
|
| 221 |
+
"is_clinical": scenario["params"]["is_clinical"],
|
| 222 |
+
"data_elements": scenario["params"]["data_elements"]
|
| 223 |
+
},
|
| 224 |
+
"patient": {"id": scenario["params"]["patient_id"]}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
# Add recipient if provided
|
| 228 |
+
if "recipient_id" in scenario["params"]:
|
| 229 |
+
context["recipient"] = {
|
| 230 |
+
"id": scenario["params"]["recipient_id"],
|
| 231 |
+
"type": scenario["params"]["recipient_type"]
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
elif scenario["tool"] == "access_billing_data":
|
| 235 |
+
context = {
|
| 236 |
+
"user": {
|
| 237 |
+
"id": scenario["params"]["user_id"],
|
| 238 |
+
"roles": scenario["params"]["user_roles"]
|
| 239 |
+
},
|
| 240 |
+
"action": scenario["params"]["action"],
|
| 241 |
+
"purpose": scenario["params"]["purpose"],
|
| 242 |
+
"resource": {
|
| 243 |
+
"is_phi": True,
|
| 244 |
+
"is_clinical": False,
|
| 245 |
+
"data_elements": scenario["params"]["billing_elements"]
|
| 246 |
+
},
|
| 247 |
+
"patient": {"id": scenario["params"]["patient_id"]}
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
decision = await policy_engine.evaluate(context)
|
| 251 |
+
print(f"Decision: {'✅ ALLOW' if decision.allow else '❌ DENY'}")
|
| 252 |
+
print(f"Reason: {decision.reason}")
|
| 253 |
+
if decision.obligations:
|
| 254 |
+
print("Obligations:")
|
| 255 |
+
for ob in decision.obligations:
|
| 256 |
+
print(f" - {ob['type']}: {ob['description']}")
|
| 257 |
+
if decision.proof:
|
| 258 |
+
print(f"Proof: {json.dumps(decision.proof, indent=2)}")
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def main():
|
| 262 |
+
"""Run the HIPAA policy example."""
|
| 263 |
+
asyncio.run(demonstrate_actor_aware_hipaa_policy())
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
if __name__ == "__main__":
|
| 267 |
+
main()
|
| 268 |
+
|
| 269 |
+
|
src/fastmcp/policy/policies/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""Built-in policy implementations."""
|
| 2 |
|
|
|
|
| 3 |
from .minimum_necessary import MinimumNecessaryAccessPolicy
|
| 4 |
from .rbac import RBACPolicy
|
| 5 |
|
| 6 |
-
__all__ = ["MinimumNecessaryAccessPolicy", "RBACPolicy"]
|
|
|
|
| 1 |
"""Built-in policy implementations."""
|
| 2 |
|
| 3 |
+
from .hipaa import HIPAAAccessPolicy
|
| 4 |
from .minimum_necessary import MinimumNecessaryAccessPolicy
|
| 5 |
from .rbac import RBACPolicy
|
| 6 |
|
| 7 |
+
__all__ = ["HIPAAAccessPolicy", "MinimumNecessaryAccessPolicy", "RBACPolicy"]
|
src/fastmcp/policy/policies/hipaa.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Actor-Aware HIPAA Policy Implementation
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
|
| 8 |
+
from ..policy import Policy
|
| 9 |
+
from ..decision import Decision
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class HIPAAAccessPolicy(Policy):
|
| 13 |
+
"""
|
| 14 |
+
Policy that implements an actor-aware set of HIPAA security and privacy rules.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(self, name: str = "hipaa", version: str = "1.0.0"):
|
| 18 |
+
super().__init__(name, version)
|
| 19 |
+
self.permitted_purposes = [
|
| 20 |
+
"treatment",
|
| 21 |
+
"payment",
|
| 22 |
+
"operations",
|
| 23 |
+
"public_health",
|
| 24 |
+
"health_oversight",
|
| 25 |
+
"law_enforcement",
|
| 26 |
+
"research",
|
| 27 |
+
"threat_to_health_or_safety",
|
| 28 |
+
"self_access"
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
async def evaluate(self, context: Dict[str, Any]) -> Decision:
|
| 32 |
+
"""
|
| 33 |
+
Evaluate the context against HIPAA regulations.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
context: The context containing user, action, resource, and purpose information.
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
Decision indicating whether access is allowed or denied.
|
| 40 |
+
"""
|
| 41 |
+
resource = context.get("resource", {})
|
| 42 |
+
|
| 43 |
+
# 1. Pre-Evaluation: Check if the resource is PHI
|
| 44 |
+
if not resource.get("is_phi", False):
|
| 45 |
+
return Decision.allow_decision("Policy does not apply to non-PHI resources.")
|
| 46 |
+
|
| 47 |
+
# Handle emergency access
|
| 48 |
+
if context.get("is_emergency_access", False):
|
| 49 |
+
return self._evaluate_emergency_access(context)
|
| 50 |
+
|
| 51 |
+
# 2. Patient Rights Evaluation
|
| 52 |
+
patient_decision = self._evaluate_patient_rights(context)
|
| 53 |
+
if patient_decision:
|
| 54 |
+
return patient_decision
|
| 55 |
+
|
| 56 |
+
# 3. Authorization and Consent Evaluation
|
| 57 |
+
auth_decision = self._evaluate_authorizations(context)
|
| 58 |
+
if auth_decision:
|
| 59 |
+
return auth_decision
|
| 60 |
+
|
| 61 |
+
# 4. Actor-Specific Rule Evaluation (New)
|
| 62 |
+
actor_decision = self._evaluate_actor_specific_rules(context)
|
| 63 |
+
if not actor_decision.allow:
|
| 64 |
+
return actor_decision
|
| 65 |
+
|
| 66 |
+
# If all checks pass, return the decision from the actor-specific evaluation
|
| 67 |
+
return actor_decision
|
| 68 |
+
|
| 69 |
+
def _evaluate_patient_rights(self, context: Dict[str, Any]) -> Decision | None:
|
| 70 |
+
"""Check for patient-asserted rights like restrictions and deceased status."""
|
| 71 |
+
patient = context.get("patient", {})
|
| 72 |
+
if patient.get("has_restriction", False):
|
| 73 |
+
restriction = patient.get("restriction_details", {})
|
| 74 |
+
if (restriction.get("action") == context.get("action") and
|
| 75 |
+
restriction.get("recipient") == context.get("recipient", {}).get("id")):
|
| 76 |
+
return Decision.deny_decision(
|
| 77 |
+
reason="Disclosure is blocked by a patient-requested restriction.",
|
| 78 |
+
proof={"policy": self.name, "citation": "§ 164.522(a)(1)"}
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
if patient.get("is_deceased", False):
|
| 82 |
+
date_of_death_str = patient.get("date_of_death")
|
| 83 |
+
if date_of_death_str:
|
| 84 |
+
date_of_death = datetime.strptime(date_of_death_str, "%Y-%m-%d")
|
| 85 |
+
if datetime.now() > date_of_death + timedelta(days=365.25 * 50):
|
| 86 |
+
return Decision.allow_decision(
|
| 87 |
+
reason="Patient deceased for over 50 years; information is not considered PHI.",
|
| 88 |
+
proof={"policy": self.name, "citation": "§ 164.502(f)"}
|
| 89 |
+
)
|
| 90 |
+
return None
|
| 91 |
+
|
| 92 |
+
def _evaluate_authorizations(self, context: Dict[str, Any]) -> Decision | None:
|
| 93 |
+
"""Check for uses that require explicit authorization."""
|
| 94 |
+
resource = context.get("resource", {})
|
| 95 |
+
request = context.get("request", {})
|
| 96 |
+
purpose = context.get("purpose", "").lower()
|
| 97 |
+
|
| 98 |
+
if resource.get("type") == "psychotherapy_notes" and purpose != "treatment":
|
| 99 |
+
if not request.get("authorization_present", False):
|
| 100 |
+
return Decision.deny_decision(
|
| 101 |
+
reason="Disclosure of psychotherapy notes requires specific patient authorization.",
|
| 102 |
+
proof={"policy": self.name, "citation": "§ 164.508(a)(2)"}
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
if purpose in ["marketing", "sale_of_phi"]:
|
| 106 |
+
if not request.get("authorization_present", False):
|
| 107 |
+
return Decision.deny_decision(
|
| 108 |
+
reason=f"Purpose '{purpose}' requires patient authorization.",
|
| 109 |
+
proof={"policy": self.name, "citation": "§ 164.508(a)(3-4)"}
|
| 110 |
+
)
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
def _evaluate_actor_specific_rules(self, context: Dict[str, Any]) -> Decision:
|
| 114 |
+
"""Route to the correct logic based on the user's role (actor)."""
|
| 115 |
+
user_roles = context.get("user", {}).get("roles", [])
|
| 116 |
+
|
| 117 |
+
if "provider" in user_roles:
|
| 118 |
+
return self._evaluate_provider_access(context)
|
| 119 |
+
if "payee" in user_roles:
|
| 120 |
+
return self._evaluate_payee_access(context)
|
| 121 |
+
if "patient" in user_roles:
|
| 122 |
+
return self._evaluate_patient_self_access(context)
|
| 123 |
+
|
| 124 |
+
return Decision.deny_decision(
|
| 125 |
+
"User does not have a recognized HIPAA actor role (provider, payee, patient)."
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
def _evaluate_provider_access(self, context: Dict[str, Any]) -> Decision:
|
| 129 |
+
"""Evaluate access for a clinical provider."""
|
| 130 |
+
# Minimum necessary check
|
| 131 |
+
min_necessary_decision = self._check_minimum_necessary(context)
|
| 132 |
+
if not min_necessary_decision.allow:
|
| 133 |
+
return min_necessary_decision
|
| 134 |
+
|
| 135 |
+
obligations = [
|
| 136 |
+
{
|
| 137 |
+
"type": "audit_log",
|
| 138 |
+
"description": f"Provider {context['user']['id']} accessed PHI for {context['purpose']}."
|
| 139 |
+
}
|
| 140 |
+
]
|
| 141 |
+
if context['action'] == 'disclose':
|
| 142 |
+
obligations.append(
|
| 143 |
+
{
|
| 144 |
+
"type": "transmission_security",
|
| 145 |
+
"description": "PHI disclosure must be encrypted."
|
| 146 |
+
}
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
return Decision.allow_decision(
|
| 150 |
+
reason="Provider access permitted for a valid purpose.",
|
| 151 |
+
obligations=obligations,
|
| 152 |
+
proof={
|
| 153 |
+
"policy": self.name,
|
| 154 |
+
"actor": "provider",
|
| 155 |
+
"citations": ["164.502(b)", "164.308(a)(1)(ii)(D)", "164.312(e)(1)"]
|
| 156 |
+
}
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
def _evaluate_payee_access(self, context: Dict[str, Any]) -> Decision:
|
| 160 |
+
"""Evaluate access for a payee (billing staff)."""
|
| 161 |
+
# Data Integrity Check: Payees cannot modify clinical data.
|
| 162 |
+
resource = context.get("resource", {})
|
| 163 |
+
action = context.get("action")
|
| 164 |
+
if resource.get("is_clinical", False) and action in ["write", "delete"]:
|
| 165 |
+
return Decision.deny_decision(
|
| 166 |
+
reason="Payee role is prohibited from modifying clinical PHI to ensure data integrity.",
|
| 167 |
+
proof={"policy": self.name, "actor": "payee", "citation": "164.312(c)(1)"}
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
min_necessary_decision = self._check_minimum_necessary(context)
|
| 171 |
+
if not min_necessary_decision.allow:
|
| 172 |
+
return min_necessary_decision
|
| 173 |
+
|
| 174 |
+
obligations = [
|
| 175 |
+
{
|
| 176 |
+
"type": "audit_log",
|
| 177 |
+
"description": f"Payee {context['user']['id']} accessed PHI for {context['purpose']}."
|
| 178 |
+
}
|
| 179 |
+
]
|
| 180 |
+
if action == 'export':
|
| 181 |
+
obligations.append(
|
| 182 |
+
{"type": "encryption", "description": "Exported PHI must be encrypted."}
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
return Decision.allow_decision(
|
| 186 |
+
reason="Payee access to non-clinical data permitted.",
|
| 187 |
+
obligations=obligations,
|
| 188 |
+
proof={
|
| 189 |
+
"policy": self.name,
|
| 190 |
+
"actor": "payee",
|
| 191 |
+
"citations": ["164.502(b)", "164.312(a)(2)(iv)"]
|
| 192 |
+
}
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
def _evaluate_patient_self_access(self, context: Dict[str, Any]) -> Decision:
|
| 196 |
+
"""Evaluate a patient's access to their own records."""
|
| 197 |
+
user = context.get("user", {})
|
| 198 |
+
patient = context.get("patient", {})
|
| 199 |
+
|
| 200 |
+
if user.get("id") != patient.get("id"):
|
| 201 |
+
return Decision.deny_decision("Patient role can only access their own records.")
|
| 202 |
+
|
| 203 |
+
# Minimum necessary does not apply to patient's own request
|
| 204 |
+
obligations = [
|
| 205 |
+
{"type": "audit_log", "description": f"Patient {user['id']} accessed their own PHI."}
|
| 206 |
+
]
|
| 207 |
+
if context['action'] == 'export':
|
| 208 |
+
obligations.append(
|
| 209 |
+
{
|
| 210 |
+
"type": "encryption",
|
| 211 |
+
"description": "Exported PHI must be provided securely/encrypted."
|
| 212 |
+
}
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
return Decision.allow_decision(
|
| 216 |
+
reason="Patient has a right of access to their own PHI; minimum necessary does not apply.",
|
| 217 |
+
obligations=obligations,
|
| 218 |
+
proof={
|
| 219 |
+
"policy": self.name,
|
| 220 |
+
"actor": "patient",
|
| 221 |
+
"citations": ["164.524", "164.312(a)(2)(iv)"]
|
| 222 |
+
}
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
def _check_minimum_necessary(self, context: Dict[str, Any]) -> Decision:
|
| 226 |
+
"""Enforce the Minimum Necessary principle based on role and purpose."""
|
| 227 |
+
user = context.get("user", {})
|
| 228 |
+
resource = context.get("resource", {})
|
| 229 |
+
purpose = context.get("purpose", "").lower()
|
| 230 |
+
requested_elements = resource.get("data_elements", [])
|
| 231 |
+
|
| 232 |
+
if purpose == "treatment":
|
| 233 |
+
return Decision.allow_decision(
|
| 234 |
+
"Minimum Necessary does not apply to disclosures for treatment."
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
role_permissions = {
|
| 238 |
+
"provider": ["full_record"],
|
| 239 |
+
"payee": ["demographics", "billing_codes", "dates_of_service", "insurance_info"],
|
| 240 |
+
"admin": ["full_record"]
|
| 241 |
+
}
|
| 242 |
+
user_roles = user.get("roles", [])
|
| 243 |
+
permitted_elements = set()
|
| 244 |
+
for role in user_roles:
|
| 245 |
+
if role in role_permissions:
|
| 246 |
+
if "full_record" in role_permissions[role]:
|
| 247 |
+
return Decision.allow_decision("User role permits access to the full record.")
|
| 248 |
+
permitted_elements.update(role_permissions[role])
|
| 249 |
+
|
| 250 |
+
if not set(requested_elements).issubset(permitted_elements):
|
| 251 |
+
return Decision.deny_decision(
|
| 252 |
+
reason="Request exceeds the minimum necessary information for the user's role.",
|
| 253 |
+
proof={
|
| 254 |
+
"policy": self.name, "citation": "§ 164.502(b)",
|
| 255 |
+
"user_roles": user_roles,
|
| 256 |
+
"permitted_elements": list(permitted_elements),
|
| 257 |
+
"requested_elements": requested_elements
|
| 258 |
+
}
|
| 259 |
+
)
|
| 260 |
+
return Decision.allow_decision("Minimum Necessary check passed.")
|
| 261 |
+
|
| 262 |
+
def _evaluate_emergency_access(self, context: Dict[str, Any]) -> Decision:
|
| 263 |
+
"""Evaluate access during a declared emergency situation."""
|
| 264 |
+
return Decision.allow_decision(
|
| 265 |
+
reason="Access permitted under emergency provisions.",
|
| 266 |
+
obligations=[
|
| 267 |
+
{
|
| 268 |
+
"type": "audit_log",
|
| 269 |
+
"description": f"EMERGENCY access to PHI by {context['user']['id']} was permitted."
|
| 270 |
+
},
|
| 271 |
+
{
|
| 272 |
+
"type": "follow_up",
|
| 273 |
+
"description": "Document the nature of the emergency and what was disclosed."
|
| 274 |
+
}
|
| 275 |
+
],
|
| 276 |
+
proof={"policy": self.name, "citation": "§ 164.510 / § 164.512(j)"}
|
| 277 |
+
)
|