Skip to main content

mcp_postgres/actions/
security.rs

1use serde_json::{json, Value};
2use tokio_postgres::Client;
3use crate::errors::Result as MCPResult;
4
5const MAX_IDENTIFIER_LEN: usize = 255;
6
7/// 26. List users
8pub async fn list_users(client: &Client, _params: &Option<&Value>) -> MCPResult<Value> {
9    // NOTE: usecanlogin was removed in PG 16+; PG now manages login
10    // privilege through LOGIN/NOLOGIN on roles (pg_authid.rolcanlogin).
11    let rows = client
12        .query(
13            "SELECT usename, usesuper, usecreatedb, valuntil::text
14             FROM pg_user
15             ORDER BY usename",
16            &[],
17        )
18        .await?;
19
20    let users: Vec<Value> = rows
21        .iter()
22        .map(|row| {
23            json!({
24                "username": row.get::<_, String>(0),
25                "superuser": row.get::<_, bool>(1),
26                "createdb": row.get::<_, bool>(2),
27                "valid_until": row.get::<_, Option<String>>(3),
28            })
29        })
30        .collect();
31
32    Ok(json!({ "users": users }))
33}
34
35/// 27. List user privileges
36pub async fn list_user_privileges(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
37    let username = params
38        .as_ref()
39        .and_then(|p| p.get("username").and_then(|v| v.as_str()).map(|s| s.to_string()))
40        .ok_or_else(|| crate::errors::MCPError::InvalidParams("Missing 'username' parameter".into()))?;
41
42    if username.is_empty() || username.len() > MAX_IDENTIFIER_LEN {
43        return Err(crate::errors::MCPError::InvalidParams(
44            format!("'username' must be 1-{MAX_IDENTIFIER_LEN} characters")
45        ));
46    }
47
48    let rows = client
49        .query(
50            "SELECT grantee, table_schema, table_name, privilege_type
51             FROM information_schema.role_table_grants
52             WHERE grantee = $1
53             ORDER BY table_schema, table_name, privilege_type",
54            &[&username],
55        )
56        .await?;
57
58    let privileges: Vec<Value> = rows
59        .iter()
60        .map(|row| {
61            json!({
62                "grantee": row.get::<_, String>(0),
63                "schema": row.get::<_, String>(1),
64                "table": row.get::<_, String>(2),
65                "privilege": row.get::<_, String>(3),
66            })
67        })
68        .collect();
69
70    Ok(json!({ "privileges": privileges }))
71}
72
73/// 28. List role memberships
74pub async fn list_role_memberships(client: &Client, _params: &Option<&Value>) -> MCPResult<Value> {
75    let rows = client
76        .query(
77            "SELECT member.usename as member, role.usename as role, admin_option
78             FROM pg_auth_members
79             JOIN pg_user member ON member.usesysid = pg_auth_members.member
80             JOIN pg_user role ON role.usesysid = pg_auth_members.roleid
81             ORDER BY member.usename, role.usename",
82            &[],
83        )
84        .await?;
85
86    let memberships: Vec<Value> = rows
87        .iter()
88        .map(|row| {
89            json!({
90                "member": row.get::<_, String>(0),
91                "role": row.get::<_, String>(1),
92                "admin": row.get::<_, bool>(2),
93            })
94        })
95        .collect();
96
97    Ok(json!({ "memberships": memberships }))
98}
99
100/// 29. List database privileges
101pub async fn list_database_privileges(client: &Client, _params: &Option<&Value>) -> MCPResult<Value> {
102    let rows = client
103        .query(
104            "SELECT datname, datacl::text
105             FROM pg_database
106             WHERE datistemplate = false
107             ORDER BY datname",
108            &[],
109        )
110        .await?;
111
112    let databases: Vec<Value> = rows
113        .iter()
114        .map(|row| {
115            json!({
116                "database": row.get::<_, String>(0),
117                "acl": row.get::<_, Option<String>>(1),
118            })
119        })
120        .collect();
121
122    Ok(json!({ "databases": databases }))
123}
124
125/// 30. Show session info
126pub async fn show_session_info(client: &Client, _params: &Option<&Value>) -> MCPResult<Value> {
127    let rows = client
128        .query(
129            "SELECT current_user, current_database(), inet_client_addr()::text,
130                    inet_client_port(), inet_server_addr()::text, inet_server_port()",
131            &[],
132        )
133        .await?;
134
135    let row = &rows[0];
136
137    Ok(json!({
138        "current_user": row.get::<_, String>(0),
139        "current_database": row.get::<_, String>(1),
140        "client_address": row.get::<_, Option<String>>(2),
141        "client_port": row.get::<_, Option<i32>>(3),
142        "server_address": row.get::<_, Option<String>>(4),
143        "server_port": row.get::<_, Option<i32>>(5),
144    }))
145}