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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// SPDX-License-Identifier: BUSL-1.1
//! String-recognized admin DDL arms: user/role, service accounts, auth-admin
//! (API keys, auth keys, auth users, blacklist), tenants, emergency DDL, and
//! system settings.
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::state::SharedState;
use crate::types::DatabaseId;
use super::super::super::result::{DdlError, DdlResult};
use super::super::apikey;
use super::super::auth_key;
use super::super::auth_user;
use super::super::blacklist;
use super::super::emergency_ddl;
use super::super::role;
use super::super::service_account;
use super::super::system_ddl;
use super::super::tenant;
use super::super::user;
pub(super) async fn try_string(
state: &SharedState,
identity: &AuthenticatedIdentity,
sql: &str,
upper: &str,
database_id: DatabaseId,
) -> Option<Result<Vec<DdlResult>, DdlError>> {
// String-recognized user/role families. `DROP USER` parses into a typed
// `AuthStmt::DropUser` that carries no `if_exists` flag (so it mishandles
// `DROP USER IF EXISTS`), and `CREATE ROLE` / `DROP ROLE` do not parse into
// any typed variant at all — the pgwire router dispatched all three from the
// raw token slice. Replicate that exactly here, before the parse gate, so
// the token-based `strip_if_exists` / `strip_if_not_exists` handling and the
// syntax messages stay byte-identical.
if upper.starts_with("DROP USER ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(user::drop_user(state, identity, &parts));
}
if upper.starts_with("CREATE ROLE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(role::create_role(state, identity, &parts));
}
if upper.starts_with("DROP ROLE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(role::drop_role(state, identity, &parts));
}
// Service accounts. These statements do not parse into any typed AST
// variant — the pgwire router dispatched all three from the raw token
// slice by string prefix. Replicate that exactly here, before the parse
// gate, so the token-based `IF [NOT] EXISTS` stripping and syntax messages
// stay byte-identical.
if upper.starts_with("CREATE SERVICE ACCOUNT ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(service_account::create_service_account(
state, identity, &parts,
));
}
if upper.starts_with("DROP SERVICE ACCOUNT ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(service_account::drop_service_account(
state, identity, &parts,
));
}
if upper.starts_with("ALTER SERVICE ACCOUNT ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(service_account::alter_service_account_set_databases(
state, identity, &parts,
));
}
// Auth-admin DDL families (API keys, auth-scoped API keys, auth user
// management, blacklist). None of these parse into any typed AST variant —
// the pgwire admin router dispatched all of them by string prefix from the
// raw token slice. Replicate that exactly here, before the parse gate, so
// the prefix recognition and syntax messages stay byte-identical. The
// `BLACKLIST ` prefix intentionally precedes the (non-migrated) emergency
// `BLACKLIST AUTH USERS WHERE` handler exactly as it did in the pgwire admin
// router, so the shadowing behavior is unchanged.
if upper.starts_with("CREATE API KEY ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(apikey::create_api_key(state, identity, &parts));
}
if upper.starts_with("REVOKE API KEY ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(apikey::revoke_api_key(state, identity, &parts));
}
if upper.starts_with("LIST API KEYS") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(apikey::list_api_keys(state, identity, &parts));
}
if upper.starts_with("SHOW API KEYS") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(apikey::list_api_keys(state, identity, &parts));
}
if upper.starts_with("CREATE AUTH KEY ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(auth_key::create_auth_key(state, identity, &parts));
}
if upper.starts_with("ROTATE AUTH KEY ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(auth_key::rotate_auth_key(state, identity, &parts));
}
if upper.starts_with("LIST AUTH KEYS") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(auth_key::list_auth_keys(state, identity, &parts));
}
if upper.starts_with("DEACTIVATE AUTH USER ") || upper.starts_with("ALTER AUTH USER ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(auth_user::handle_auth_user(state, identity, &parts));
}
if upper.starts_with("PURGE AUTH USERS ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(auth_user::purge_auth_users(state, identity, &parts));
}
if upper.starts_with("SHOW AUTH USERS") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(auth_user::show_auth_users(state, identity, &parts));
}
if upper.starts_with("BLACKLIST ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(blacklist::handle_blacklist(state, identity, &parts));
}
if upper.starts_with("SHOW BLACKLIST") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(blacklist::show_blacklist(state, identity, &parts));
}
// Tenant management. `CREATE TENANT`, `DROP TENANT`, and `PURGE TENANT`
// parse into no typed AST variant — the pgwire auth router dispatched all
// three by string prefix from the raw token slice. Replicate that exactly
// here, before the parse gate, so the `IF [NOT] EXISTS` stripping and
// syntax messages stay byte-identical. `PURGE TENANT` dispatches an async
// Data Plane meta op.
//
// `ALTER TENANT ` is ambiguous: `ALTER TENANT <id|name> SET QUOTA ...`
// (this string form) and `ALTER TENANT <name> IN DATABASE <db> SET QUOTA
// (...)` (a typed `DatabaseStmt::AlterTenant`, handled in the typed match
// below) share the same prefix. The typed `ddl_ast` tenant parser only
// recognizes the `IN DATABASE` form when `parts.len() >= 8` and tokens 3/4
// are `IN`/`DATABASE`; replicate that exact partition here so the
// `IN DATABASE` form always falls through to the typed arm instead of
// being shadowed by this string handler.
//
// `SHOW TENANT USAGE` / `SHOW TENANT QUOTA` (bare, no `IN DATABASE`) are
// NOT recognized here: the typed `ddl_ast` tenant parser never returns
// `None` for `SHOW TENANT USAGE|QUOTA...` — every such input resolves to
// either the typed `IN DATABASE` variant or a `42601` parse error. Their
// pgwire string handlers were therefore confirmed dead code and deleted,
// not migrated; adding a neutral string prefix for them would make that
// dead code reachable and break parity.
if upper.starts_with("CREATE TENANT ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(tenant::create_tenant(state, identity, &parts));
}
if upper.starts_with("ALTER TENANT ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
let is_in_database_form = parts.len() >= 8
&& parts[3].eq_ignore_ascii_case("IN")
&& parts[4].eq_ignore_ascii_case("DATABASE");
if !is_in_database_form {
return Some(tenant::alter_tenant(state, identity, &parts));
}
}
if upper.starts_with("DROP TENANT ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(tenant::drop_tenant(state, identity, &parts));
}
if upper.starts_with("PURGE TENANT ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(tenant::purge_tenant(state, identity, database_id, &parts).await);
}
// Emergency & incident response DDL. `EMERGENCY LOCKDOWN` / `EMERGENCY
// UNLOCK` parse into no typed AST variant — the pgwire admin router
// dispatched both by string prefix from the raw token slice. Replicate that
// exactly here, before the parse gate, so the prefix recognition and syntax
// messages stay byte-identical. `BLACKLIST AUTH USERS WHERE …` is likewise
// string-recognized, but the `BLACKLIST ` prefix above already claims it
// (exactly as it shadowed the pgwire emergency handler, which ran only after
// this neutral router). This guard is therefore intentionally kept after the
// `BLACKLIST ` guard so `bulk_blacklist` remains unreachable — preserving the
// dead-but-present state verbatim.
if upper.starts_with("EMERGENCY LOCKDOWN") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(emergency_ddl::emergency_lockdown(state, identity, &parts));
}
if upper.starts_with("EMERGENCY UNLOCK") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(emergency_ddl::emergency_unlock(state, identity, &parts));
}
if upper.starts_with("BLACKLIST AUTH USERS WHERE") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(emergency_ddl::bulk_blacklist(state, identity, &parts));
}
// System-level settings: `ALTER SYSTEM SET <field> = <value>`. Parses into
// no typed AST variant — the pgwire auth router dispatched it by string
// prefix from the raw token slice. Replicate that exactly here, before the
// parse gate, so the prefix recognition and the `parts`-based field / value
// extraction stay byte-identical.
if upper.starts_with("ALTER SYSTEM ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(system_ddl::alter_system(state, identity, &parts));
}
None
}