@macaw-ai/client 0.0.0-stage → 0.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +339 -0
- package/bin/install.js +422 -0
- package/package.json +37 -4
- package/payload/README.md +56 -0
- package/payload/secureAI/LICENSE +190 -0
- package/payload/secureAI/NOTICE +5 -0
- package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
- package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
- package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
- package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
- package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
- package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
- package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
- package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
- package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
- package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
- package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
- package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
- package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
- package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
- package/payload/secureAI/examples/README.md +244 -0
- package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
- package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
- package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
- package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
- package/payload/secureAI/examples/attestations/README.md +128 -0
- package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
- package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
- package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
- package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
- package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
- package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
- package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
- package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
- package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
- package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
- package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
- package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
- package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
- package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
- package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
- package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
- package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
- package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
- package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
- package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
- package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
- package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
- package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
- package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
- package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
- package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
- package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
- package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
- package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
- package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
- package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
- package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
- package/payload/secureAI/macaw_adapters/__init__.py +55 -0
- package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
- package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
- package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
- package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
- package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
- package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
- package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
- package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
- package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
- package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
- package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
- package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
- package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
- package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
- package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
- package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
- package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
- package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
- package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
- package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
- package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
- package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
- package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
- package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
- package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
- package/payload/secureAI/test_harness_quick.py +296 -0
- package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
- package/README.md +0 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Tutorial 2: Intent-Based Access Control
|
|
2
|
+
|
|
3
|
+
This tutorial demonstrates intent-based access controls, policy enforcement, model restrictions and
|
|
4
|
+
action-tracking for a fictional XYZ Corp. Controls are specified through MAPL policies enforced by
|
|
5
|
+
the MACAW runtime invisbly. We use Alation as the contextual data layer, Databricks as the backend
|
|
6
|
+
lakehouse, and OpenAI as the LLM of choice. All these components are replaceable modularly.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The demo shows how different users get different access control based on their org role,
|
|
11
|
+
demonstrating policy based control.
|
|
12
|
+
|
|
13
|
+
It also shows control by intent. The same tool call is judged by what the SQL is trying to do depending upon the policy.
|
|
14
|
+
|
|
15
|
+
| User | Role | Tool available | Model available | Max Token | SQL allowed | Needs approval when |
|
|
16
|
+
|------|------|----------------|-----------------|-----------|-------------|---------------------|
|
|
17
|
+
| alice | Financial Analyst | `run_query_sql_custom_adi` | `gpt-4o-mini` | 100 | select, update | any statement touching `eng_comp` |
|
|
18
|
+
| bob | manager | `run_query_sql_custom_adi`, `run_data_product_query_macaw_ii`, `run_analytics_agent_macaw`, `run_generate_pdf_from_a_chat_macaw` | `gpt-4o-mini`, `gpt-4o` | 2000 | select, update | an UPDATE touching `eng_comp` |
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Directory Structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
demo-alation/
|
|
26
|
+
├── script.py # Main demo
|
|
27
|
+
├── Policies/
|
|
28
|
+
│ ├── company.json # company:alation-MACAW
|
|
29
|
+
│ ├── bu.json # bu:analytics
|
|
30
|
+
│ ├── user_alice.json # user:alice
|
|
31
|
+
│ ├── user_bob.json # user:bob
|
|
32
|
+
│ └── alation-remote-proxy.json # app:alation-remote-proxy
|
|
33
|
+
├── setup/
|
|
34
|
+
│ ├── alation_verifier.py # AlationSQLGuardVerifier
|
|
35
|
+
│ └── get_alation_user_token.py # Alation OAuth + PKCE token
|
|
36
|
+
└── README.md
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### 1. Prerequisites
|
|
42
|
+
|
|
43
|
+
- Python 3.12+
|
|
44
|
+
- MACAW console account
|
|
45
|
+
- macaw client installed and configured
|
|
46
|
+
- env's configured
|
|
47
|
+
- A alation tenant
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
export ALATION_BASE_URL="https://<tenant>.alationcloud.com"
|
|
51
|
+
export ALATION_CLIENT_ID="<oauth client id>"
|
|
52
|
+
export ALATION_CLIENT_SECRET="<oauth client secret>"
|
|
53
|
+
export ALATION_MCP_URL="https://<tenant>.alationcloud.com/ai/mcp/<uuid>"
|
|
54
|
+
export OPENAI_API_KEY="<key>"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Install Dependencies
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install "$MACAW_HOME"/macaw_client-*.whl "$MACAW_HOME/secureAI[all]"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Set Up Identity Provider
|
|
64
|
+
|
|
65
|
+
Console → Tutorials → Make it Real → Connect Identity Provider → Option B.
|
|
66
|
+
|
|
67
|
+
When done with the setup follow this claims mapping:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
name:
|
|
71
|
+
domain:
|
|
72
|
+
client_id:
|
|
73
|
+
client_secret:
|
|
74
|
+
api_audience:
|
|
75
|
+
|
|
76
|
+
mappings:
|
|
77
|
+
subject_path: sub
|
|
78
|
+
email_path: email
|
|
79
|
+
name_path: https://macaw.local/username
|
|
80
|
+
organization_path: https://macaw.local/organization
|
|
81
|
+
roles_path: https://macaw.local/roles
|
|
82
|
+
team_path: https://macaw.local/team
|
|
83
|
+
business_unit_path: https://macaw.local/business_unit
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Claims Mapping
|
|
87
|
+
|
|
88
|
+
Each mapped claim becomes the name of a policy to look up:
|
|
89
|
+
|
|
90
|
+
| Claim | Value | Policy |
|
|
91
|
+
|-------|-------|--------|
|
|
92
|
+
| `name_path` → username | `alice` | `user:alice` |
|
|
93
|
+
| `business_unit_path` | `analytics` | `bu:analytics` |
|
|
94
|
+
| `organization_path` | `alation-MACAW` | `company:alation-MACAW` |
|
|
95
|
+
| `roles_path` | `["manager"]` | matches `approval_criteria: role:manager` |
|
|
96
|
+
|
|
97
|
+
This allows the same policies to work with any OIDC-compliant IdP.
|
|
98
|
+
|
|
99
|
+
### 4. Get the Alation Token
|
|
100
|
+
|
|
101
|
+
To get alation token run, `setup/get_alation_user_token.py`
|
|
102
|
+
|
|
103
|
+
This open a browser where you login to get a access token and a refresh token.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
export ALATION_TOKEN="<the access token>"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Later you can use the same refresh token to get a new access token without the going through the
|
|
110
|
+
browser again.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python setup/get_alation_user_token.py --refresh <REFRESH_TOKEN>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 5. Load Policies
|
|
117
|
+
|
|
118
|
+
Import the policies from the `Policies/` directory into your MACAW workspace via the Console.
|
|
119
|
+
Load each one: Policies → Add Policy → Code Editor → paste JSON → Validate → Save.
|
|
120
|
+
|
|
121
|
+
| # | File | Policy id |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| 1 | `Policies/company.json` | `company:alation-MACAW` |
|
|
124
|
+
| 2 | `Policies/bu.json` | `bu:analytics` |
|
|
125
|
+
| 3 | `Policies/user_alice.json` | `user:alice` |
|
|
126
|
+
| 4 | `Policies/user_bob.json` | `user:bob` |
|
|
127
|
+
| 5 | `Policies/alation-remote-proxy.json` | `app:alation-remote-proxy` |
|
|
128
|
+
|
|
129
|
+
#### Policy Hierarchy
|
|
130
|
+
|
|
131
|
+
The policies demonstrate MACAW's hierarchical policy model:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
company:alation-MACAW (base restrictions)
|
|
135
|
+
└── bu:analytics (business unit)
|
|
136
|
+
└── user:bob (individual user)
|
|
137
|
+
└── user:alice (individual user)
|
|
138
|
+
|
|
139
|
+
app:alation-remote-proxy
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Lower levels can only restrict, never expand permissions from parent policies.
|
|
143
|
+
|
|
144
|
+
### 6. Run the Demo
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
export MACAW_HOME="<path to macaw-client-0.9.9.6-Linux-x86_64-py3.12>"
|
|
148
|
+
export ALATION_MCP_URL="https://<tenant>.alationcloud.com/ai/mcp/<uuid>"
|
|
149
|
+
export ALATION_TOKEN="<fresh 72h bearer>" # from setup/get_alation_user_token.py
|
|
150
|
+
export OPENAI_API_KEY="<key>"
|
|
151
|
+
|
|
152
|
+
python script.py
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
## Custom Verifier
|
|
158
|
+
|
|
159
|
+
Custom verifiers run inside the verification pipeline before the policy decision, compute facts
|
|
160
|
+
about the request, and stamp them onto the parameters so MAPL can gate on them.
|
|
161
|
+
|
|
162
|
+
`setup/alation_verifier.py` — AlationSQLGuardVerifier
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
proxy.macaw_client.agent.verification_pipeline.add_verifier(AlationSQLGuardVerifier(), priority=20)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
It parses the SQL with sqlglot and stamps three parameters:
|
|
169
|
+
|
|
170
|
+
| Parameter | Values | Comes from |
|
|
171
|
+
|-----------|--------|------------|
|
|
172
|
+
| `stmt_type` | select, update, insert, delete, merge, create, drop, truncate, other, nl_only, denied | the AST root node |
|
|
173
|
+
| `touches_salary` | true / false | any identifier containing salary (SENSITIVE_TOKENS) |
|
|
174
|
+
| `touches_eng_comp` | true / false | any table in SENSITIVE_TABLES = ("eng_comp",) |
|
|
175
|
+
|
|
176
|
+
| What it does | Why |
|
|
177
|
+
|--------------|-----|
|
|
178
|
+
| Looks at what the statement really is, not the words in it | INSERT … SELECT has the word SELECT in it, but it writes |
|
|
179
|
+
| Only the types it recognises get through | anything else is marked other, and no policy allows other |
|
|
180
|
+
| If it cannot read the SQL, it says no | empty, two statements at once, or unparseable → denied |
|
|
181
|
+
| A SQL tool called with no SQL is refused | otherwise Alation writes the SQL itself, where we cannot see it |
|
|
182
|
+
| Matches the table name however it is written | workspace.macaw_demo.eng_comp matches, eng_comp_archive does not |
|
|
183
|
+
| Checks every SQL field and keeps the worst answer | a safe sql cannot hide a dangerous query |
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
## What the Tutorial Shows
|
|
187
|
+
|
|
188
|
+
1. **Authentication**: Each user authenticates with the IdP (Keycloak/Auth0)
|
|
189
|
+
2. **Policy Resolution**: MACAW resolves the user's effective policy from the hierarchy
|
|
190
|
+
3. **Enforcement**: Requests are allowed or blocked based on policy rules
|
|
191
|
+
|
|
192
|
+
### Expected Output
|
|
193
|
+
|
|
194
|
+
**Bob**
|
|
195
|
+
|
|
196
|
+
| # | Call | Request | Result | What MACAW said |
|
|
197
|
+
|---|---|---|---|---|
|
|
198
|
+
| 1 | LLM | `gpt-4-turbo`, 80 tokens | 🔴 BLOCKED | `Parameter 'model'=gpt-4-turbo not in allowed values: ['gpt-4o-mini', 'gpt-4o']` |
|
|
199
|
+
| 2 | LLM | `gpt-4o`, 3000 tokens | 🔴 BLOCKED | `Parameter 'max_tokens'=3000 exceeds maximum: 2000` |
|
|
200
|
+
| 3 | LLM | `gpt-4o`, 500 tokens | 🟢 ALLOWED | *"Compound interest is the process where interest is added to the principal…"* |
|
|
201
|
+
| 4 | SQL | `SELECT name, base_salary FROM workspace.macaw_demo.eng_comp LIMIT 3` | 🟢 ALLOWED | rows returned |
|
|
202
|
+
| 5 | SQL | `SELECT COUNT(*) FROM workspace.macaw_demo.eng_comp` | 🟢 ALLOWED | `COUNT(*) = 4` |
|
|
203
|
+
|
|
204
|
+
**Alice**
|
|
205
|
+
|
|
206
|
+
| # | Call | Request | Result | What MACAW said |
|
|
207
|
+
|---|---|---|---|---|
|
|
208
|
+
| 1 | LLM | `gpt-4o`, 80 tokens | 🔴 BLOCKED | `Parameter 'model'=gpt-4o not in allowed values: ['gpt-4o-mini']` |
|
|
209
|
+
| 2 | LLM | `gpt-4o-mini`, 300 tokens | 🔴 BLOCKED | `Parameter 'max_tokens'=300 exceeds maximum: 100` |
|
|
210
|
+
| 3 | LLM | `gpt-4o-mini`, 80 tokens | 🟢 ALLOWED | *"Compound interest is the interest calculated on the initial principal…"* |
|
|
211
|
+
| 4 | SQL | `DELETE FROM workspace.macaw_demo.eng_comp WHERE name = 'nobody'` | 🔴 BLOCKED | `Parameter 'stmt_type'=delete not in allowed values: ['select', 'update']` |
|
|
212
|
+
| 5 | SQL | `SELECT name, base_salary FROM workspace.macaw_demo.eng_comp LIMIT 3` | 🟡 ATTESTATION | `Missing or invalid attestation: eng_comp_attestation` |
|
|
213
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
script.py - Per-user LLM and Alation SQL access, gated by MACAW
|
|
4
|
+
|
|
5
|
+
alice (manager) and bob (analyst) each make 3 LLM calls and 2 Alation SQL calls
|
|
6
|
+
through identical code. MAPL policy is evaluated per request against the calling
|
|
7
|
+
user's identity, so the same call succeeds for one user and is blocked or held
|
|
8
|
+
for approval for the other.
|
|
9
|
+
|
|
10
|
+
Prerequisites:
|
|
11
|
+
- MACAW SDK installed (pip install macaw-client macaw-adapters)
|
|
12
|
+
- Identity Provider configured (Console -> Settings -> Identity Bridge)
|
|
13
|
+
- Policies loaded in the Console (see Policies/)
|
|
14
|
+
- Test users: alice.alation@gmail.com, bob.alation@gmail.com
|
|
15
|
+
|
|
16
|
+
Run:
|
|
17
|
+
export MACAW_HOME=.../macaw-client-0.9.9.6-Linux-x86_64-py3.12
|
|
18
|
+
export ALATION_MCP_URL=https://<tenant>.alationcloud.com/ai/mcp/<uuid>
|
|
19
|
+
export ALATION_TOKEN=<fresh bearer>
|
|
20
|
+
export OPENAI_API_KEY=sk-...
|
|
21
|
+
python script.py
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
|
|
30
|
+
from macaw_adapters.openai import SecureOpenAI
|
|
31
|
+
from macaw_adapters.mcp import SecureMCPProxy
|
|
32
|
+
from macaw_client import MACAWClient, RemoteIdentityProvider
|
|
33
|
+
|
|
34
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "setup"))
|
|
35
|
+
from alation_verifier import AlationSQLGuardVerifier
|
|
36
|
+
|
|
37
|
+
logging.getLogger("macaw_client").setLevel(logging.ERROR)
|
|
38
|
+
|
|
39
|
+
PASSWORD = "test@123"
|
|
40
|
+
PROMPT = "What is compound interest? Answer in one short sentence."
|
|
41
|
+
DATA_PRODUCT = "databricks-macaw-product"
|
|
42
|
+
ENG_COMP = "workspace.macaw_demo.eng_comp" # Databricks will not resolve a bare 'eng_comp'
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Test configurations based on user policies
|
|
46
|
+
# Alice: gpt-4o-mini/gpt-4o, max 2000 tokens; reads eng_comp freely
|
|
47
|
+
# Bob: gpt-4o-mini only, max 100 tokens; eng_comp needs a manager's attestation
|
|
48
|
+
USER_TESTS = {
|
|
49
|
+
"bob": {
|
|
50
|
+
"email": "bob.alation@gmail.com",
|
|
51
|
+
"policy_desc": "gpt-4o-mini/gpt-4o, max 2000 tokens",
|
|
52
|
+
"llm": [
|
|
53
|
+
# (model, max_tokens)
|
|
54
|
+
("gpt-4-turbo", 80), # BLOCKED - model not allowed
|
|
55
|
+
("gpt-4o", 3000), # BLOCKED - exceeds token ceiling
|
|
56
|
+
("gpt-4o", 500), # ALLOWED
|
|
57
|
+
],
|
|
58
|
+
"sql": [
|
|
59
|
+
f"SELECT name, base_salary FROM {ENG_COMP} LIMIT 3", # ALLOWED
|
|
60
|
+
f"SELECT COUNT(*) FROM {ENG_COMP}", # ALLOWED
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
"alice": {
|
|
64
|
+
"email": "alice.alation@gmail.com",
|
|
65
|
+
"policy_desc": "gpt-4o-mini only, max 100 tokens",
|
|
66
|
+
"llm": [
|
|
67
|
+
("gpt-4o", 80), # BLOCKED - model not allowed
|
|
68
|
+
("gpt-4o-mini", 300), # BLOCKED - exceeds token ceiling
|
|
69
|
+
("gpt-4o-mini", 80), # ALLOWED
|
|
70
|
+
],
|
|
71
|
+
"sql": [
|
|
72
|
+
f"DELETE FROM {ENG_COMP} WHERE name = 'nobody'", # BLOCKED - stmt_type
|
|
73
|
+
f"SELECT name, base_salary FROM {ENG_COMP} LIMIT 3", # ATTESTATION
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_env(name: str) -> str:
|
|
80
|
+
"""Read a required environment variable.
|
|
81
|
+
|
|
82
|
+
Strips surrounding quotes, including the curly ones a copy-paste from a document
|
|
83
|
+
leaves behind - those reach the Authorization header and fail as a bad credential.
|
|
84
|
+
"""
|
|
85
|
+
value = os.environ.get(name, "").strip().strip("\"'“”‘’")
|
|
86
|
+
if not value:
|
|
87
|
+
sys.exit(f"Missing {name}. Set it with: export {name}=...")
|
|
88
|
+
if not value.isascii():
|
|
89
|
+
sys.exit(f"{name} contains a non-ASCII character - re-copy it as plain text.")
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def verdict_for(error: Exception) -> str:
|
|
94
|
+
"""Separate a MACAW policy decision from any other failure.
|
|
95
|
+
|
|
96
|
+
A demo about policy enforcement must never report a broken credential or a network
|
|
97
|
+
fault as though the policy had blocked it.
|
|
98
|
+
"""
|
|
99
|
+
message = str(error).lower()
|
|
100
|
+
if "attest" in message:
|
|
101
|
+
return "ATTESTATION"
|
|
102
|
+
if any(s in message for s in ("not in allowed", "exceeds maximum", "policy",
|
|
103
|
+
"denied", "not permitted")):
|
|
104
|
+
return "BLOCKED"
|
|
105
|
+
return f"ERROR - {str(error)[:70]}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def patch_proxy_timeout():
|
|
109
|
+
"""Tune the upstream HTTP client for Alation's long-running agent endpoint.
|
|
110
|
+
|
|
111
|
+
httpx ships a 5s default that suits short request/response APIs. Here we widen the
|
|
112
|
+
read timeout to 300s for this upstream while keeping connect, write and pool tight,
|
|
113
|
+
so long analytical calls complete without loosening anything else.
|
|
114
|
+
"""
|
|
115
|
+
def create_http_client(self):
|
|
116
|
+
auth = self.upstream_auth
|
|
117
|
+
headers = ({"Authorization": f"Bearer {auth.token}"}
|
|
118
|
+
if getattr(auth, "type", None) == "bearer" and getattr(auth, "token", None)
|
|
119
|
+
else {})
|
|
120
|
+
return httpx.AsyncClient(
|
|
121
|
+
headers=headers or None,
|
|
122
|
+
timeout=httpx.Timeout(connect=30, read=300, write=30, pool=30),
|
|
123
|
+
)
|
|
124
|
+
SecureMCPProxy._create_http_client = create_http_client
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_user(username: str, openai_service: SecureOpenAI, proxy: SecureMCPProxy):
|
|
128
|
+
"""Run one user's LLM and SQL calls under their own identity."""
|
|
129
|
+
config = USER_TESTS[username]
|
|
130
|
+
print(f"\n{'=' * 60}")
|
|
131
|
+
print(f"{username.upper()} - {config['policy_desc']}")
|
|
132
|
+
print("=" * 60)
|
|
133
|
+
|
|
134
|
+
# 1. Authenticate and register the user agent
|
|
135
|
+
jwt_token, _ = RemoteIdentityProvider().login(config["email"], PASSWORD)
|
|
136
|
+
user = MACAWClient(user_name=username, iam_token=jwt_token,
|
|
137
|
+
agent_type="user", app_name="alation")
|
|
138
|
+
user.register()
|
|
139
|
+
|
|
140
|
+
# 2. LLM calls - A2A to the shared OpenAI service, gated on model and max_tokens
|
|
141
|
+
for model, max_tokens in config["llm"]:
|
|
142
|
+
try:
|
|
143
|
+
result = user.invoke_tool(
|
|
144
|
+
tool_name="tool:openai-service/generate",
|
|
145
|
+
parameters={
|
|
146
|
+
"model": model,
|
|
147
|
+
"max_tokens": max_tokens,
|
|
148
|
+
"messages": [{"role": "user", "content": PROMPT}],
|
|
149
|
+
},
|
|
150
|
+
target_agent=openai_service.server_id,
|
|
151
|
+
)
|
|
152
|
+
answer = result["choices"][0]["message"]["content"].strip()
|
|
153
|
+
print(f" LLM {model} /{max_tokens} -> {answer}")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
print(f" LLM {model} /{max_tokens} -> {verdict_for(e)}")
|
|
156
|
+
|
|
157
|
+
# 3. SQL calls - through the user-bound proxy, gated by the verifier and policy
|
|
158
|
+
bound = proxy.bind_to_user(user)
|
|
159
|
+
for sql in config["sql"]:
|
|
160
|
+
try:
|
|
161
|
+
result = bound.call_tool(
|
|
162
|
+
"run_query_sql_custom_adi",
|
|
163
|
+
{"sql": sql, "message": "demo", "data_product_id": DATA_PRODUCT},
|
|
164
|
+
)
|
|
165
|
+
print(f" SQL {sql}\n -> {str(result)[:220]}")
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(f" SQL {sql} -> {verdict_for(e)}")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main():
|
|
171
|
+
openai_service = SecureOpenAI(api_key=get_env("OPENAI_API_KEY"), app_name="openai-service")
|
|
172
|
+
|
|
173
|
+
patch_proxy_timeout()
|
|
174
|
+
proxy = SecureMCPProxy(
|
|
175
|
+
app_name="alation-remote-proxy",
|
|
176
|
+
upstream_url=get_env("ALATION_MCP_URL"),
|
|
177
|
+
upstream_auth={"type": "bearer", "token": get_env("ALATION_TOKEN")},
|
|
178
|
+
)
|
|
179
|
+
proxy.macaw_client.agent.verification_pipeline.add_verifier(
|
|
180
|
+
AlationSQLGuardVerifier(), priority=20
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
for username in USER_TESTS:
|
|
184
|
+
test_user(username, openai_service, proxy)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
main()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
alation_verifier.py -- SQL statement-type guardrail for the Alation MCP SQL tools.
|
|
3
|
+
|
|
4
|
+
Parses the caller's SQL and stamps THREE flat params the MAPL policy gates on:
|
|
5
|
+
|
|
6
|
+
stmt_type = "select" | "update" | "insert" | "delete" | "merge" | "create"
|
|
7
|
+
| "drop" | "truncate" | "other" | "nl_only" | "denied"
|
|
8
|
+
("denied" = fail-closed; "nl_only" = a SQL tool called with NO SQL)
|
|
9
|
+
touches_salary = "true" | "false" (does any identifier in the SQL contain "salary"?)
|
|
10
|
+
touches_eng_comp = "true" | "false" (does the SQL reference a SENSITIVE_TABLES table?)
|
|
11
|
+
|
|
12
|
+
The verifier CLASSIFIES; the policy DECIDES:
|
|
13
|
+
|
|
14
|
+
A) ANALYST (bob) -> ANY statement touching eng_comp attests to a manager;
|
|
15
|
+
other tables are ungated (within the {select, update} allow-list).
|
|
16
|
+
B) MANAGER (alice) -> only an UPDATE touching eng_comp attests (to an admin);
|
|
17
|
+
everything else within the allow-list is ungated.
|
|
18
|
+
C) DELETE -> denied for everyone. So is anything outside the {select, update}
|
|
19
|
+
allow-list (insert / merge / CTAS / drop / truncate / other).
|
|
20
|
+
|
|
21
|
+
touches_eng_comp closes the `SELECT *` hole in touches_salary: `SELECT * FROM eng_comp` names
|
|
22
|
+
no column identifier, so salary detection misses it, but the TABLE is still named.
|
|
23
|
+
|
|
24
|
+
SALARY DETECTION scans EVERY identifier in the whole AST -- projection AND WHERE / ORDER BY /
|
|
25
|
+
GROUP BY / JOIN / CTE / INSERT column-list -- because `WHERE base_salary > 200000` leaks salary
|
|
26
|
+
by row selection without projecting it. Identifiers are normalized first (Databricks is
|
|
27
|
+
CASE_INSENSITIVE, backticks stripped) so `Base_Salary` / `BASE_SALARY` / `` `base_salary` ``
|
|
28
|
+
all match. KNOWN GAP: `SELECT *` names no identifier, so it does NOT trip salary detection
|
|
29
|
+
(the statement-type allow-list still applies).
|
|
30
|
+
|
|
31
|
+
DESIGN (all verified against sqlglot 30.12.0, dialect="databricks"):
|
|
32
|
+
- Classify by the ROOT node ONLY, never "contains a SELECT" -- INSERT / CREATE-AS / MERGE all
|
|
33
|
+
embed a Select, so a contains-check is trivially fooled.
|
|
34
|
+
- ALLOW-LIST, not blocklist: anything unrecognized becomes "other" and the policy denies it.
|
|
35
|
+
DROP / TRUNCATE / MERGE / CTAS / INSERT OVERWRITE all destroy data without the word DELETE.
|
|
36
|
+
- FAIL CLOSED. Empty sql, multi-statement (`SELECT 1; DELETE ...`), unparseable SQL, or an
|
|
37
|
+
`exp.Command` stamp stmt_type="denied". `exp.Command` is sqlglot's fallback for unsupported
|
|
38
|
+
syntax and covers BOTH dynamic SQL (`EXECUTE IMMEDIATE`) and `ALTER ... DROP COLUMN`.
|
|
39
|
+
- MULTI-PARAM: the live endpoint's `run_analytics_agent_macaw` exposes BOTH `sql` and `query`.
|
|
40
|
+
Every name in SQL_PARAM_NAMES is classified and the WORST result wins, so a benign `sql`
|
|
41
|
+
cannot mask a destructive `query`. Checking only one name is a free bypass.
|
|
42
|
+
- A scoped SQL tool called with NO SQL is stamped "nl_only": the agent would generate the SQL
|
|
43
|
+
server-side where this verifier can never see it, so it is denied by the policy allow-list.
|
|
44
|
+
"""
|
|
45
|
+
from typing import Tuple # noqa: F401 (kept for callers importing type hints)
|
|
46
|
+
|
|
47
|
+
import sqlglot
|
|
48
|
+
from sqlglot import expressions as exp
|
|
49
|
+
from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
|
|
50
|
+
|
|
51
|
+
DIALECT = "databricks" # MUST match the executor's dialect
|
|
52
|
+
SENSITIVE_TOKENS = ("salary",) # substring match on identifiers; add "bonus","ssn" to extend
|
|
53
|
+
|
|
54
|
+
# Guarded tables, matched on the TABLE-NAME SEGMENT ONLY (exact, after normalization) so that
|
|
55
|
+
# `eng_comp`, `macaw_demo.eng_comp` and `workspace.macaw_demo.eng_comp` all match the same entry
|
|
56
|
+
# -- the live endpoint requires the fully-qualified form, but an unqualified name must not slip
|
|
57
|
+
# past. Exact (not substring) so a distinct table like `eng_comp_archive` is NOT silently caught.
|
|
58
|
+
SENSITIVE_TABLES = ("eng_comp",)
|
|
59
|
+
|
|
60
|
+
# Every param that can carry raw SQL on the scoped tools. VERIFIED from the live endpoint:
|
|
61
|
+
# run_analytics_agent_macaw exposes BOTH `sql` and `query`; the others expose `sql`.
|
|
62
|
+
SQL_PARAM_NAMES = ("sql", "query")
|
|
63
|
+
|
|
64
|
+
# ROOT expression class -> stmt_type. Verified on sqlglot 30.12.0 / databricks:
|
|
65
|
+
# WITH..SELECT -> Select, UNION -> Union, INSERT OVERWRITE -> Insert, TRUNCATE -> TruncateTable.
|
|
66
|
+
_ROOT_MAP = (
|
|
67
|
+
(exp.Select, "select"),
|
|
68
|
+
(exp.Union, "select"), # a set operation over SELECTs is still a read
|
|
69
|
+
(exp.Update, "update"),
|
|
70
|
+
(exp.Insert, "insert"),
|
|
71
|
+
(exp.Delete, "delete"),
|
|
72
|
+
(exp.Merge, "merge"),
|
|
73
|
+
(exp.Create, "create"),
|
|
74
|
+
(exp.Drop, "drop"),
|
|
75
|
+
(exp.TruncateTable, "truncate"),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Worst-wins ranking when a call carries more than one SQL param.
|
|
79
|
+
_SEVERITY = {
|
|
80
|
+
"select": 10, "update": 20, "insert": 50, "delete": 60, "merge": 60,
|
|
81
|
+
"create": 60, "drop": 70, "truncate": 70, "other": 80, "nl_only": 90, "denied": 99,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def classify_sql(sql: str) -> Tuple[str, bool, bool]:
|
|
86
|
+
"""Return (stmt_type, touches_salary, touches_sensitive_table) for exactly ONE statement.
|
|
87
|
+
|
|
88
|
+
Raises for every input that must FAIL CLOSED: empty, multi-statement, `exp.Command`
|
|
89
|
+
(dynamic / unsupported syntax), or unparseable (sqlglot.ParseError propagates).
|
|
90
|
+
"""
|
|
91
|
+
if not sql or not sql.strip():
|
|
92
|
+
raise ValueError("empty sql")
|
|
93
|
+
stmts = sqlglot.parse(sql, read=DIALECT)
|
|
94
|
+
if len(stmts) != 1 or stmts[0] is None:
|
|
95
|
+
raise ValueError(f"expected exactly 1 statement, got {len(stmts)}")
|
|
96
|
+
root = stmts[0]
|
|
97
|
+
if isinstance(root, exp.Command): # EXECUTE IMMEDIATE, ALTER ..., SET ...
|
|
98
|
+
raise ValueError("dynamic or unsupported statement (parsed as Command)")
|
|
99
|
+
root = normalize_identifiers(root, dialect=DIALECT)
|
|
100
|
+
|
|
101
|
+
stmt_type = "other"
|
|
102
|
+
for cls, name in _ROOT_MAP:
|
|
103
|
+
if isinstance(root, cls):
|
|
104
|
+
stmt_type = name
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
toks = tuple(t.lower() for t in SENSITIVE_TOKENS)
|
|
108
|
+
touches = any(any(k in i.name.lower() for k in toks) for i in root.find_all(exp.Identifier))
|
|
109
|
+
|
|
110
|
+
# .name is the table segment only -- catalog/schema are .catalog/.db -- so a guarded table is
|
|
111
|
+
# caught however the caller qualifies it. Covers the UPDATE/DELETE target and every FROM/JOIN.
|
|
112
|
+
guarded = tuple(t.lower() for t in SENSITIVE_TABLES)
|
|
113
|
+
touches_table = any(t.name.lower() in guarded for t in root.find_all(exp.Table))
|
|
114
|
+
return stmt_type, touches, touches_table
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# --- SDK glue (lazy import so classify_sql stays importable/testable without the SDK) --------
|
|
118
|
+
# NOTE: `macaw` is a submodule injected by the macaw_client .so, so it imports only AFTER
|
|
119
|
+
# macaw_client has been imported (the gateway imports macaw_adapters/macaw_client first).
|
|
120
|
+
try:
|
|
121
|
+
from macaw.security.verification.verifier import Verifier
|
|
122
|
+
from macaw.security.verification.result import VerificationResult
|
|
123
|
+
|
|
124
|
+
class AlationSQLGuardVerifier(Verifier):
|
|
125
|
+
"""Stamps params.stmt_type so the MAPL policy can gate A/B/C.
|
|
126
|
+
|
|
127
|
+
Always returns success=True -- the DENY decision belongs to the policy. Fail-closed
|
|
128
|
+
inputs are stamped stmt_type="denied", which is outside the policy's allowed_values
|
|
129
|
+
and therefore hard-denied.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(self):
|
|
133
|
+
super().__init__(name="alation_sql_guard_verifier")
|
|
134
|
+
# EVERY SQL-bearing tool on the endpoint. Verified live: leaving one out is a free
|
|
135
|
+
# bypass (call the unscoped agent instead). Re-check after publishing a new agent.
|
|
136
|
+
self.scope.resource_patterns = [
|
|
137
|
+
"*run_query_sql_custom_adi",
|
|
138
|
+
"*run_analytics_agent_macaw",
|
|
139
|
+
"*run_data_product_query_macaw_ii",
|
|
140
|
+
"*sql_execution_tool", # not currently published; kept for legacy/return
|
|
141
|
+
]
|
|
142
|
+
self.scope.compile_patterns()
|
|
143
|
+
|
|
144
|
+
def verify(self, invocation, context):
|
|
145
|
+
params = invocation.params # mutate the REAL params, never a copy
|
|
146
|
+
if params is None:
|
|
147
|
+
return VerificationResult(True, "no params to gate")
|
|
148
|
+
|
|
149
|
+
sqls = [str(params[p]) for p in SQL_PARAM_NAMES
|
|
150
|
+
if params.get(p) and str(params[p]).strip()]
|
|
151
|
+
if not sqls:
|
|
152
|
+
# Scoped SQL tool called with NO SQL: the agent would write the SQL server-side
|
|
153
|
+
# where this verifier can never see it -- allowing that is a free bypass
|
|
154
|
+
# ("in English: delete the rows"). Not in the policy allow-list.
|
|
155
|
+
params["stmt_type"] = "nl_only"
|
|
156
|
+
params["touches_salary"] = "true" # unknown -> conservative
|
|
157
|
+
params["touches_eng_comp"] = "true" # unknown -> conservative
|
|
158
|
+
return VerificationResult(True, "no SQL param -> nl_only (fail-closed)")
|
|
159
|
+
|
|
160
|
+
results = []
|
|
161
|
+
for s in sqls:
|
|
162
|
+
try:
|
|
163
|
+
results.append(classify_sql(s))
|
|
164
|
+
except Exception: # noqa: BLE001 -- ParseError, ValueError, anything
|
|
165
|
+
results.append(("denied", True, True)) # fail closed
|
|
166
|
+
|
|
167
|
+
# Worst classification wins, so a benign `sql` can't mask a destructive `query`.
|
|
168
|
+
stmt_type = max((r[0] for r in results), key=lambda t: _SEVERITY.get(t, 99))
|
|
169
|
+
touches = any(r[1] for r in results)
|
|
170
|
+
touches_table = any(r[2] for r in results)
|
|
171
|
+
params["stmt_type"] = stmt_type
|
|
172
|
+
params["touches_salary"] = "true" if touches else "false"
|
|
173
|
+
params["touches_eng_comp"] = "true" if touches_table else "false"
|
|
174
|
+
return VerificationResult(
|
|
175
|
+
True, f"{stmt_type}/salary={touches}/eng_comp={touches_table}")
|
|
176
|
+
except ImportError:
|
|
177
|
+
pass # classify_sql remains usable without macaw_client loaded
|