1use rmcp::schemars::JsonSchema;
16use serde::Deserialize;
17use serde_json::Value;
18use tailscale_rest::models::user::{USER_ROLE_FILTERS, USER_ROLES, USER_TYPE_FILTERS};
19
20use crate::context::ToolContext;
21use crate::error::ToolResult;
22use crate::tools::common::{Done, answered_or, one_of, path_segment};
23
24crate::tools! {
25 tailnet_user_list => UserListParams, user_list,
30 toolset: TailnetUsers, tier: Read, idempotent: true;
31
32 tailnet_user_get => UserParams, user_get,
34 toolset: TailnetUsers, tier: Read, idempotent: true;
35
36 tailnet_user_role_set => UserRoleParams, user_role_set,
41 toolset: TailnetUsers, tier: Write, idempotent: true;
42
43 tailnet_user_approve => UserParams, user_approve,
48 toolset: TailnetUsers, tier: Write, idempotent: true;
49
50 tailnet_user_suspend => UserParams, user_suspend,
53 toolset: TailnetUsers, tier: Write, idempotent: true;
54
55 tailnet_user_restore => UserParams, user_restore,
57 toolset: TailnetUsers, tier: Write, idempotent: true;
58
59 tailnet_user_delete => UserParams, user_delete,
64 toolset: TailnetUsers, tier: Destructive, idempotent: true;
65}
66
67fn user_path(id: &str, rest: &str) -> ToolResult<String> {
72 let id = path_segment("user_id", id)?;
73 Ok(format!("/api/v2/users/{id}{rest}"))
74}
75
76#[derive(Debug, Deserialize, JsonSchema)]
77pub struct UserListParams {
78 #[serde(default)]
81 pub user_type: Option<String>,
82 #[serde(default)]
85 pub role: Option<String>,
86}
87
88async fn user_list(ctx: &ToolContext, params: UserListParams) -> ToolResult<Value> {
89 let client = ctx.tailnet()?;
90 let user_type = params
91 .user_type
92 .as_deref()
93 .map(|value| one_of("user_type", value, USER_TYPE_FILTERS))
94 .transpose()?;
95 let role = params
96 .role
97 .as_deref()
98 .map(|value| one_of("role", value, USER_ROLE_FILTERS))
99 .transpose()?;
100 Ok(client
101 .get(client.tailnet_path(None, "/users"))
102 .maybe_query("type", user_type)
106 .maybe_query("role", role)
107 .send_as::<Value>()
108 .await?)
109}
110
111#[derive(Debug, Deserialize, JsonSchema)]
112pub struct UserParams {
113 pub user_id: String,
115}
116
117async fn user_get(ctx: &ToolContext, params: UserParams) -> ToolResult<Value> {
118 let client = ctx.tailnet()?;
119 Ok(client
120 .get(user_path(¶ms.user_id, "")?)
121 .send_as::<Value>()
122 .await?)
123}
124
125#[derive(Debug, Deserialize, JsonSchema)]
126pub struct UserRoleParams {
127 pub user_id: String,
129 pub role: String,
132}
133
134async fn user_role_set(ctx: &ToolContext, params: UserRoleParams) -> ToolResult<Value> {
135 let client = ctx.tailnet()?;
136 let path = user_path(¶ms.user_id, "/role")?;
137 let mut body = tailscale_rest::models::user::UserRole::default();
138 body.role = Some(one_of("role", ¶ms.role, USER_ROLES)?);
141 let answer = client.post(path).json(&body).send().await?;
142 answered_or(
143 answer,
144 Done::new("role changed")
145 .about("user_id", params.user_id)
146 .about("role", params.role),
147 )
148}
149
150async fn standing(
156 ctx: &ToolContext,
157 user_id: String,
158 action: &'static str,
159 done: &'static str,
160) -> ToolResult<Value> {
161 let client = ctx.tailnet()?;
162 let path = user_path(&user_id, &format!("/{action}"))?;
163 let answer = client.post(path).send().await?;
164 answered_or(answer, Done::new(done).about("user_id", user_id))
165}
166
167async fn user_approve(ctx: &ToolContext, params: UserParams) -> ToolResult<Value> {
168 standing(ctx, params.user_id, "approve", "user approved").await
169}
170
171async fn user_suspend(ctx: &ToolContext, params: UserParams) -> ToolResult<Value> {
172 standing(ctx, params.user_id, "suspend", "user suspended").await
173}
174
175async fn user_restore(ctx: &ToolContext, params: UserParams) -> ToolResult<Value> {
176 standing(ctx, params.user_id, "restore", "user restored").await
177}
178
179async fn user_delete(ctx: &ToolContext, params: UserParams) -> ToolResult<Value> {
180 standing(ctx, params.user_id, "delete", "user deleted").await
181}