mcp_postgres/actions/
user_mgmt.rs1use crate::errors::Result as MCPResult;
2use serde_json::{Value, json};
3use tokio_postgres::Client;
4
5const MAX_IDENTIFIER_LEN: usize = 255;
6const MAX_PASSWORD_LEN: usize = 1024;
7
8fn validate_password(pw: &str) -> MCPResult<()> {
13 if pw.len() > MAX_PASSWORD_LEN {
14 return Err(crate::errors::MCPError::InvalidParams(format!(
15 "'password' exceeds maximum length of {MAX_PASSWORD_LEN} characters"
16 )));
17 }
18 if pw.chars().any(|c| c.is_control()) {
19 return Err(crate::errors::MCPError::InvalidParams(
20 "'password' must not contain control characters".into(),
21 ));
22 }
23 Ok(())
24}
25
26pub async fn create_user(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
27 let username = params
28 .as_ref()
29 .and_then(|p| p.get("username").and_then(|v| v.as_str()))
30 .ok_or_else(|| {
31 crate::errors::MCPError::InvalidParams("Missing 'username' parameter".into())
32 })?;
33
34 if username.is_empty() || username.len() > MAX_IDENTIFIER_LEN {
35 return Err(crate::errors::MCPError::InvalidParams(format!(
36 "'username' must be 1-{MAX_IDENTIFIER_LEN} characters"
37 )));
38 }
39
40 let password = params
41 .as_ref()
42 .and_then(|p| p.get("password").and_then(|v| v.as_str()));
43 let valid_until = params
44 .as_ref()
45 .and_then(|p| p.get("valid_until").and_then(|v| v.as_str()));
46 let connection_limit = params
47 .as_ref()
48 .and_then(|p| p.get("connection_limit").and_then(|v| v.as_i64()));
49 let can_login = params
50 .as_ref()
51 .and_then(|p| p.get("can_login").and_then(|v| v.as_bool()));
52
53 let mut sql = format!("CREATE USER {}", quote_ident(username));
54 if let Some(pw) = password {
55 validate_password(pw)?;
56 sql.push_str(&format!(" PASSWORD '{}'", pw.replace('\'', "''")));
57 }
58 if let Some(limit) = connection_limit {
59 sql.push_str(&format!(" CONNECTION LIMIT {}", limit));
60 }
61 if let Some(login) = can_login {
62 if login {
63 sql.push_str(" LOGIN");
64 } else {
65 sql.push_str(" NOLOGIN");
66 }
67 }
68 if let Some(until) = valid_until {
69 sql.push_str(&format!(" VALID UNTIL '{}'", until.replace('\'', "''")));
70 }
71
72 client.execute(&sql, &[]).await?;
73 Ok(json!({ "success": true, "user": username }))
74}
75
76pub async fn alter_user(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
77 let username = params
78 .as_ref()
79 .and_then(|p| p.get("username").and_then(|v| v.as_str()))
80 .ok_or_else(|| {
81 crate::errors::MCPError::InvalidParams("Missing 'username' parameter".into())
82 })?;
83
84 if username.is_empty() || username.len() > MAX_IDENTIFIER_LEN {
85 return Err(crate::errors::MCPError::InvalidParams(format!(
86 "'username' must be 1-{MAX_IDENTIFIER_LEN} characters"
87 )));
88 }
89
90 let password = params
91 .as_ref()
92 .and_then(|p| p.get("password").and_then(|v| v.as_str()));
93 let valid_until = params
94 .as_ref()
95 .and_then(|p| p.get("valid_until").and_then(|v| v.as_str()));
96 let connection_limit = params
97 .as_ref()
98 .and_then(|p| p.get("connection_limit").and_then(|v| v.as_i64()));
99 let can_login = params
100 .as_ref()
101 .and_then(|p| p.get("can_login").and_then(|v| v.as_bool()));
102 let new_name = params
103 .as_ref()
104 .and_then(|p| p.get("new_name").and_then(|v| v.as_str()));
105
106 if password.is_none()
107 && valid_until.is_none()
108 && connection_limit.is_none()
109 && can_login.is_none()
110 && new_name.is_none()
111 {
112 return Err(crate::errors::MCPError::InvalidParams(
113 "No attributes specified to alter".into(),
114 ));
115 }
116
117 let mut sql = format!("ALTER USER {}", quote_ident(username));
118 if let Some(pw) = password {
119 validate_password(pw)?;
120 sql.push_str(&format!(" PASSWORD '{}'", pw.replace('\'', "''")));
121 }
122 if let Some(limit) = connection_limit {
123 sql.push_str(&format!(" CONNECTION LIMIT {}", limit));
124 }
125 if let Some(login) = can_login {
126 if login {
127 sql.push_str(" LOGIN");
128 } else {
129 sql.push_str(" NOLOGIN");
130 }
131 }
132 if let Some(until) = valid_until {
133 sql.push_str(&format!(" VALID UNTIL '{}'", until.replace('\'', "''")));
134 }
135 if let Some(name) = new_name {
136 sql.push_str(&format!(" RENAME TO {}", quote_ident(name)));
137 }
138
139 client.execute(&sql, &[]).await?;
140 Ok(json!({ "success": true, "user": username }))
141}
142
143pub async fn drop_user(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
144 let username = params
145 .as_ref()
146 .and_then(|p| p.get("username").and_then(|v| v.as_str()))
147 .ok_or_else(|| {
148 crate::errors::MCPError::InvalidParams("Missing 'username' parameter".into())
149 })?;
150
151 if username.is_empty() || username.len() > MAX_IDENTIFIER_LEN {
152 return Err(crate::errors::MCPError::InvalidParams(format!(
153 "'username' must be 1-{MAX_IDENTIFIER_LEN} characters"
154 )));
155 }
156
157 let if_exists = params
158 .as_ref()
159 .and_then(|p| p.get("if_exists").and_then(|v| v.as_bool()))
160 .unwrap_or(false);
161
162 let mut sql = "DROP USER".to_string();
163 if if_exists {
164 sql.push_str(" IF EXISTS");
165 }
166 sql.push_str(&format!(" {}", quote_ident(username)));
167
168 client.execute(&sql, &[]).await?;
169 Ok(json!({ "success": true, "user": username }))
170}
171
172pub async fn create_role(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
173 let role_name = params
174 .as_ref()
175 .and_then(|p| p.get("role_name").and_then(|v| v.as_str()))
176 .ok_or_else(|| {
177 crate::errors::MCPError::InvalidParams("Missing 'role_name' parameter".into())
178 })?;
179
180 if role_name.is_empty() || role_name.len() > MAX_IDENTIFIER_LEN {
181 return Err(crate::errors::MCPError::InvalidParams(format!(
182 "'role_name' must be 1-{MAX_IDENTIFIER_LEN} characters"
183 )));
184 }
185
186 let with_login = params
187 .as_ref()
188 .and_then(|p| p.get("with_login").and_then(|v| v.as_bool()))
189 .unwrap_or(false);
190 let mut sql = format!("CREATE ROLE {}", quote_ident(role_name));
191 sql.push_str(if with_login { " LOGIN" } else { " NOLOGIN" });
192
193 client.execute(&sql, &[]).await?;
194 Ok(json!({ "success": true, "role": role_name }))
195}
196
197pub async fn alter_role(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
198 let role_name = params
199 .as_ref()
200 .and_then(|p| p.get("role_name").and_then(|v| v.as_str()))
201 .ok_or_else(|| {
202 crate::errors::MCPError::InvalidParams("Missing 'role_name' parameter".into())
203 })?;
204
205 if role_name.is_empty() || role_name.len() > MAX_IDENTIFIER_LEN {
206 return Err(crate::errors::MCPError::InvalidParams(format!(
207 "'role_name' must be 1-{MAX_IDENTIFIER_LEN} characters"
208 )));
209 }
210
211 let password = params
212 .as_ref()
213 .and_then(|p| p.get("password").and_then(|v| v.as_str()));
214 let can_login = params
215 .as_ref()
216 .and_then(|p| p.get("can_login").and_then(|v| v.as_bool()));
217 let superuser = params
218 .as_ref()
219 .and_then(|p| p.get("superuser").and_then(|v| v.as_bool()));
220 let createdb = params
221 .as_ref()
222 .and_then(|p| p.get("createdb").and_then(|v| v.as_bool()));
223 let new_name = params
224 .as_ref()
225 .and_then(|p| p.get("new_name").and_then(|v| v.as_str()));
226
227 let mut sql = format!("ALTER ROLE {}", quote_ident(role_name));
228 if let Some(pw) = password {
229 validate_password(pw)?;
230 sql.push_str(&format!(" PASSWORD '{}'", pw.replace('\'', "''")));
231 }
232 if let Some(login) = can_login {
233 sql.push_str(if login { " LOGIN" } else { " NOLOGIN" });
234 }
235 if let Some(su) = superuser {
236 sql.push_str(if su { " SUPERUSER" } else { " NOSUPERUSER" });
237 }
238 if let Some(db) = createdb {
239 sql.push_str(if db { " CREATEDB" } else { " NOCREATEDB" });
240 }
241 if let Some(name) = new_name {
242 sql.push_str(&format!(" RENAME TO {}", quote_ident(name)));
243 }
244
245 client.execute(&sql, &[]).await?;
246 Ok(json!({ "success": true, "role": role_name }))
247}
248
249pub async fn drop_role(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
250 let role_name = params
251 .as_ref()
252 .and_then(|p| p.get("role_name").and_then(|v| v.as_str()))
253 .ok_or_else(|| {
254 crate::errors::MCPError::InvalidParams("Missing 'role_name' parameter".into())
255 })?;
256
257 if role_name.is_empty() || role_name.len() > MAX_IDENTIFIER_LEN {
258 return Err(crate::errors::MCPError::InvalidParams(format!(
259 "'role_name' must be 1-{MAX_IDENTIFIER_LEN} characters"
260 )));
261 }
262
263 let if_exists = params
264 .as_ref()
265 .and_then(|p| p.get("if_exists").and_then(|v| v.as_bool()))
266 .unwrap_or(false);
267
268 let mut sql = "DROP ROLE".to_string();
269 if if_exists {
270 sql.push_str(" IF EXISTS");
271 }
272 sql.push_str(&format!(" {}", quote_ident(role_name)));
273
274 client.execute(&sql, &[]).await?;
275 Ok(json!({ "success": true, "role": role_name }))
276}
277
278pub async fn grant_privileges(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
279 let privilege = params
280 .as_ref()
281 .and_then(|p| p.get("privilege").and_then(|v| v.as_str()))
282 .ok_or_else(|| {
283 crate::errors::MCPError::InvalidParams("Missing 'privilege' parameter".into())
284 })?;
285 let object_type = params
286 .as_ref()
287 .and_then(|p| p.get("object_type").and_then(|v| v.as_str()))
288 .ok_or_else(|| {
289 crate::errors::MCPError::InvalidParams("Missing 'object_type' parameter".into())
290 })?;
291 let object_name = params
292 .as_ref()
293 .and_then(|p| p.get("object_name").and_then(|v| v.as_str()))
294 .ok_or_else(|| {
295 crate::errors::MCPError::InvalidParams("Missing 'object_name' parameter".into())
296 })?;
297 let grantee = params
298 .as_ref()
299 .and_then(|p| p.get("grantee").and_then(|v| v.as_str()))
300 .ok_or_else(|| {
301 crate::errors::MCPError::InvalidParams("Missing 'grantee' parameter".into())
302 })?;
303
304 let schema = params
305 .as_ref()
306 .and_then(|p| p.get("schema").and_then(|v| v.as_str()))
307 .unwrap_or("public");
308
309 crate::validation::validate_privilege_list(privilege)?;
310
311 let valid_types = [
312 "table",
313 "sequence",
314 "schema",
315 "database",
316 "all_tables_in_schema",
317 ];
318 if !valid_types.contains(&object_type) {
319 return Err(crate::errors::MCPError::InvalidParams(format!(
320 "Unsupported object_type '{}'. Use: {:?}",
321 object_type, valid_types
322 )));
323 }
324
325 let sql = match object_type {
326 "all_tables_in_schema" => format!(
327 "GRANT {} ON ALL TABLES IN SCHEMA {} TO {}",
328 privilege,
329 quote_ident(schema),
330 quote_ident(grantee)
331 ),
332 _ => format!(
333 "GRANT {} ON {} {} TO {}",
334 privilege,
335 object_type.to_uppercase(),
336 quote_ident(object_name),
337 quote_ident(grantee)
338 ),
339 };
340
341 client.execute(&sql, &[]).await?;
342 Ok(json!({ "success": true, "sql": sql }))
343}
344
345pub async fn revoke_privileges(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
346 let privilege = params
347 .as_ref()
348 .and_then(|p| p.get("privilege").and_then(|v| v.as_str()))
349 .ok_or_else(|| {
350 crate::errors::MCPError::InvalidParams("Missing 'privilege' parameter".into())
351 })?;
352 let object_type = params
353 .as_ref()
354 .and_then(|p| p.get("object_type").and_then(|v| v.as_str()))
355 .ok_or_else(|| {
356 crate::errors::MCPError::InvalidParams("Missing 'object_type' parameter".into())
357 })?;
358 let object_name = params
359 .as_ref()
360 .and_then(|p| p.get("object_name").and_then(|v| v.as_str()))
361 .ok_or_else(|| {
362 crate::errors::MCPError::InvalidParams("Missing 'object_name' parameter".into())
363 })?;
364 let revokee = params
365 .as_ref()
366 .and_then(|p| p.get("revokee").and_then(|v| v.as_str()))
367 .ok_or_else(|| {
368 crate::errors::MCPError::InvalidParams("Missing 'revokee' parameter".into())
369 })?;
370
371 let schema = params
372 .as_ref()
373 .and_then(|p| p.get("schema").and_then(|v| v.as_str()))
374 .unwrap_or("public");
375
376 crate::validation::validate_privilege_list(privilege)?;
377
378 let valid_types = [
379 "table",
380 "sequence",
381 "schema",
382 "database",
383 "all_tables_in_schema",
384 ];
385 if !valid_types.contains(&object_type) {
386 return Err(crate::errors::MCPError::InvalidParams(format!(
387 "Unsupported object_type '{}'. Use: {:?}",
388 object_type, valid_types
389 )));
390 }
391
392 let sql = match object_type {
393 "all_tables_in_schema" => format!(
394 "REVOKE {} ON ALL TABLES IN SCHEMA {} FROM {}",
395 privilege,
396 quote_ident(schema),
397 quote_ident(revokee)
398 ),
399 _ => format!(
400 "REVOKE {} ON {} {} FROM {}",
401 privilege,
402 object_type.to_uppercase(),
403 quote_ident(object_name),
404 quote_ident(revokee)
405 ),
406 };
407
408 client.execute(&sql, &[]).await?;
409 Ok(json!({ "success": true, "sql": sql }))
410}
411
412fn quote_ident(ident: &str) -> String {
413 crate::validation::quote_ident(ident)
414}