Skip to main content

mcp_postgres/actions/
security.rs

1use crate::errors::Result as MCPResult;
2use serde_json::{Value, json};
3use tokio_postgres::Client;
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()))
40        .ok_or_else(|| {
41            crate::errors::MCPError::InvalidParams("Missing 'username' parameter".into())
42        })?;
43
44    if username.is_empty() || username.len() > MAX_IDENTIFIER_LEN {
45        return Err(crate::errors::MCPError::InvalidParams(format!(
46            "'username' must be 1-{MAX_IDENTIFIER_LEN} characters"
47        )));
48    }
49
50    let rows = client
51        .query(
52            "SELECT grantee, table_schema, table_name, privilege_type
53             FROM information_schema.role_table_grants
54             WHERE grantee = $1
55             ORDER BY table_schema, table_name, privilege_type",
56            &[&username],
57        )
58        .await?;
59
60    let privileges: Vec<Value> = rows
61        .iter()
62        .map(|row| {
63            json!({
64                "grantee": row.get::<_, String>(0),
65                "schema": row.get::<_, String>(1),
66                "table": row.get::<_, String>(2),
67                "privilege": row.get::<_, String>(3),
68            })
69        })
70        .collect();
71
72    Ok(json!({ "privileges": privileges }))
73}
74
75/// 28. List role memberships
76pub async fn list_role_memberships(client: &Client, _params: &Option<&Value>) -> MCPResult<Value> {
77    let rows = client
78        .query(
79            "SELECT member.usename as member, role.usename as role, admin_option
80             FROM pg_auth_members
81             JOIN pg_user member ON member.usesysid = pg_auth_members.member
82             JOIN pg_user role ON role.usesysid = pg_auth_members.roleid
83             ORDER BY member.usename, role.usename",
84            &[],
85        )
86        .await?;
87
88    let memberships: Vec<Value> = rows
89        .iter()
90        .map(|row| {
91            json!({
92                "member": row.get::<_, String>(0),
93                "role": row.get::<_, String>(1),
94                "admin": row.get::<_, bool>(2),
95            })
96        })
97        .collect();
98
99    Ok(json!({ "memberships": memberships }))
100}
101
102/// 29. List database privileges
103pub async fn list_database_privileges(
104    client: &Client,
105    _params: &Option<&Value>,
106) -> MCPResult<Value> {
107    let rows = client
108        .query(
109            "SELECT datname, datacl::text
110             FROM pg_database
111             WHERE datistemplate = false
112             ORDER BY datname",
113            &[],
114        )
115        .await?;
116
117    let databases: Vec<Value> = rows
118        .iter()
119        .map(|row| {
120            json!({
121                "database": row.get::<_, String>(0),
122                "acl": row.get::<_, Option<String>>(1),
123            })
124        })
125        .collect();
126
127    Ok(json!({ "databases": databases }))
128}
129
130/// 30. Show session info
131pub async fn show_session_info(client: &Client, _params: &Option<&Value>) -> MCPResult<Value> {
132    let rows = client
133        .query(
134            "SELECT current_user, current_database(), inet_client_addr()::text,
135                    inet_client_port(), inet_server_addr()::text, inet_server_port()",
136            &[],
137        )
138        .await?;
139
140    let row = &rows[0];
141
142    Ok(json!({
143        "current_user": row.get::<_, String>(0),
144        "current_database": row.get::<_, String>(1),
145        "client_address": row.get::<_, Option<String>>(2),
146        "client_port": row.get::<_, Option<i32>>(3),
147        "server_address": row.get::<_, Option<String>>(4),
148        "server_port": row.get::<_, Option<i32>>(5),
149    }))
150}