paul-purecipher commited on
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
+ )