-
Notifications
You must be signed in to change notification settings - Fork 77
Expand file tree
/
Copy pathModifySqlAst.java
More file actions
157 lines (132 loc) · 6.26 KB
/
Copy pathModifySqlAst.java
File metadata and controls
157 lines (132 loc) · 6.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package gudusoft.gsqlparser.demos.modifySqlAst;
import gudusoft.gsqlparser.EDbVendor;
import gudusoft.gsqlparser.EExpressionType;
import gudusoft.gsqlparser.ESqlStatementType;
import gudusoft.gsqlparser.TGSqlParser;
import gudusoft.gsqlparser.nodes.TExpression;
import gudusoft.gsqlparser.nodes.TResultColumn;
import gudusoft.gsqlparser.nodes.TResultColumnList;
import gudusoft.gsqlparser.nodes.TWhereClause;
import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
/**
* Demonstrates a small, fail-closed SQL policy gate built on the GSP AST.
*
* <p>The policy accepts one SELECT statement, removes the sensitive
* {@code internal_note} projection when present, adds a server-controlled
* tenant predicate, regenerates SQL, and parses the result again before it can
* be handed to a database driver.</p>
*/
public final class ModifySqlAst {
public static final String SAMPLE_SQL =
"SELECT o.order_id,\n"
+ " o.customer_id,\n"
+ " o.total_amount,\n"
+ " o.internal_note\n"
+ "FROM sales.orders o\n"
+ "WHERE o.status = 'OPEN' OR o.status = 'PENDING'\n"
+ "ORDER BY o.created_at DESC";
private static final String RESTRICTED_COLUMN = "internal_note";
private static final String TENANT_PREDICATE = "o.tenant_id = ?";
private ModifySqlAst() {
}
public static void main(String[] args) {
RewriteResult result = rewrite(SAMPLE_SQL, EDbVendor.dbvoracle);
System.out.println("Original SQL:");
System.out.println(result.getOriginalSql());
System.out.println();
System.out.println("Policy decisions:");
System.out.println("- Accepted exactly one SELECT statement");
System.out.println("- Removed restricted projection: " + result.getRemovedProjection());
System.out.println("- Added server-controlled tenant predicate: " + TENANT_PREDICATE);
System.out.println();
System.out.println("Rewritten SQL:");
System.out.println(result.getRewrittenSql());
System.out.println();
System.out.println("Validation: regenerated SQL parsed successfully as one SELECT statement.");
}
/**
* Applies the demo policy and returns SQL regenerated from the modified AST.
* The tenant value remains a bind placeholder and must be supplied by trusted
* application code when the SQL is executed.
*/
public static RewriteResult rewrite(String sql, EDbVendor vendor) {
TSelectSqlStatement select = parseOneSelect(sql, vendor, "Input SQL");
String removedProjection = removeRestrictedProjection(select);
addTenantPredicate(select, vendor);
String rewrittenSql = select.toScript();
parseOneSelect(rewrittenSql, vendor, "Regenerated SQL");
return new RewriteResult(sql, rewrittenSql, removedProjection);
}
private static TSelectSqlStatement parseOneSelect(String sql,
EDbVendor vendor,
String description) {
TGSqlParser parser = new TGSqlParser(vendor);
parser.sqltext = sql;
if (parser.parse() != 0) {
throw new IllegalArgumentException(description + " did not parse: "
+ parser.getErrormessage());
}
if (parser.sqlstatements.size() != 1) {
throw new IllegalArgumentException(description
+ " must contain exactly one statement.");
}
if (parser.sqlstatements.get(0).sqlstatementtype != ESqlStatementType.sstselect) {
throw new IllegalArgumentException(description + " must be a SELECT statement.");
}
return (TSelectSqlStatement) parser.sqlstatements.get(0);
}
private static String removeRestrictedProjection(TSelectSqlStatement select) {
TResultColumnList columns = select.getResultColumnList();
String removedProjection = null;
for (int index = columns.size() - 1; index >= 0; index--) {
TResultColumn column = columns.getResultColumn(index);
if (RESTRICTED_COLUMN.equalsIgnoreCase(column.getColumnNameOnly())) {
if (removedProjection == null) {
removedProjection = column.toScript();
}
columns.removeResultColumn(index);
}
}
return removedProjection == null ? "not present" : removedProjection;
}
private static void addTenantPredicate(TSelectSqlStatement select, EDbVendor vendor) {
TExpression tenantCondition = TGSqlParser.parseExpression(vendor, TENANT_PREDICATE);
if (tenantCondition == null) {
throw new IllegalStateException("The configured tenant predicate did not parse.");
}
TWhereClause whereClause = select.getWhereClause();
if (whereClause == null || whereClause.getCondition() == null) {
select.addWhereClause(TENANT_PREDICATE);
return;
}
// Parenthesize the existing condition before adding AND. Without this
// node, "A OR B" plus a tenant filter could become "A OR (B AND tenant)".
TExpression originalCondition = whereClause.getCondition();
TExpression parenthesized =
new TExpression(EExpressionType.parenthesis_t, originalCondition, null);
TExpression combined = new TExpression(
EExpressionType.logical_and_t, parenthesized, tenantCondition);
whereClause.setCondition(combined);
}
public static final class RewriteResult {
private final String originalSql;
private final String rewrittenSql;
private final String removedProjection;
private RewriteResult(String originalSql,
String rewrittenSql,
String removedProjection) {
this.originalSql = originalSql;
this.rewrittenSql = rewrittenSql;
this.removedProjection = removedProjection;
}
public String getOriginalSql() {
return originalSql;
}
public String getRewrittenSql() {
return rewrittenSql;
}
public String getRemovedProjection() {
return removedProjection;
}
}
}