tailscale_client/
lib.rs

1use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
2use reqwest::{Client, Response};
3use serde::{Deserialize, Serialize};
4use std::error::Error;
5
6/// A client for interacting with Tailscale's v2 API.
7pub struct TailscaleClient {
8    pub base_url: String,
9    pub token: String,
10    client: Client,
11}
12
13impl TailscaleClient {
14    /// Creates a new TailscaleClient with the given token, automatically
15    /// setting the base URL to https://api.tailscale.com/api/v2
16    pub fn new(token: String) -> Self {
17        TailscaleClient {
18            base_url: "https://api.tailscale.com/api/v2".to_string(),
19            token,
20            client: Client::new(),
21        }
22    }
23
24    /// Constructs an authorized GET request for the given path.
25    async fn get(&self, path: &str) -> Result<Response, Box<dyn Error>> {
26        let mut headers = HeaderMap::new();
27        let auth_value = format!("Bearer {}", self.token);
28        headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);
29
30        let url = format!("{}/{}", self.base_url, path);
31        let resp = self.client.get(url).headers(headers).send().await?;
32        Ok(resp)
33    }
34
35    /// Example method to call the `/whoami` endpoint which returns information
36    /// about the current user and their Tailnets.
37    pub async fn whoami(&self) -> Result<WhoAmIResponse, Box<dyn Error>> {
38        let resp = self.get("whoami").await?;
39        if resp.status().is_success() {
40            let data: WhoAmIResponse = resp.json().await?;
41            Ok(data)
42        } else {
43            let error_body = resp.text().await?;
44            Err(format!("Tailscale whoami endpoint error: {}", error_body).into())
45        }
46    }
47
48    /// Creates a new auth key in the specified tailnet, returning the newly generated key.
49    /// The `all` parameter is optional in the API, but here we surface it directly
50    /// to match the Tailscale docs example (e.g., `?all=true`).
51    pub async fn create_auth_key(
52        &self,
53        tailnet: &str,
54        all: bool,
55        req_body: &CreateAuthKeyRequest,
56    ) -> Result<CreateAuthKeyResponse, Box<dyn Error>> {
57        let mut headers = HeaderMap::new();
58        let auth_value = format!("Bearer {}", self.token);
59        headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);
60        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
61
62        let url = format!("{}/tailnet/{}/keys?all={}", self.base_url, tailnet, all);
63
64        let resp = self
65            .client
66            .post(url)
67            .headers(headers)
68            .json(req_body)
69            .send()
70            .await?;
71
72        if resp.status().is_success() {
73            let data = resp.json().await?;
74            Ok(data)
75        } else {
76            let error_body = resp.text().await?;
77            Err(format!("Tailscale create_auth_key error: {}", error_body).into())
78        }
79    }
80}
81
82/// Example response from `/whoami`
83#[derive(Debug, Deserialize)]
84pub struct WhoAmIResponse {
85    pub logged_in: bool,
86    #[serde(rename = "user")]
87    pub user_info: Option<UserInfo>,
88    #[serde(rename = "tailnet")]
89    pub tailnet_info: Option<TailnetInfo>,
90}
91
92/// Minimal user info
93#[derive(Debug, Deserialize)]
94pub struct UserInfo {
95    pub login_name: Option<String>,
96    pub display_name: Option<String>,
97    pub profile_pic_url: Option<String>,
98}
99
100/// Minimal tailnet info
101#[derive(Debug, Deserialize)]
102pub struct TailnetInfo {
103    pub name: Option<String>,
104    pub magic_dns: Option<bool>,
105}
106
107/// Request body for creating an auth key.
108/// Adjust fields as needed based on Tailscale's docs.
109#[derive(Debug, Serialize)]
110pub struct CreateAuthKeyRequest {
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub description: Option<String>,
113
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub expirySeconds: Option<u64>,
116
117    pub capabilities: Capabilities,
118}
119
120/// The `capabilities` definition for Tailscale's auth key creation.
121#[derive(Debug, Serialize)]
122pub struct Capabilities {
123    pub devices: Devices,
124}
125
126/// Minimal required field under `devices`, though you can add sub-fields as needed.
127#[derive(Debug, Serialize)]
128pub struct Devices {
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub create: Option<CreateOpts>,
131}
132
133/// Example subfields that can be used when creating a device auth key.
134#[derive(Debug, Serialize)]
135pub struct CreateOpts {
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub reusable: Option<bool>,
138
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub ephemeral: Option<bool>,
141
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub preauthorized: Option<bool>,
144
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub tags: Option<Vec<String>>,
147}
148
149/// Response body from creating an auth key.
150#[derive(Debug, Deserialize)]
151pub struct CreateAuthKeyResponse {
152    pub id: Option<String>,
153    pub key: Option<String>,
154    pub created: Option<String>,
155    pub expires: Option<String>,
156    pub revoked: Option<String>,
157
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub capabilities: Option<AuthKeyCapabilities>,
160
161    pub description: Option<String>,
162    pub invalid: Option<bool>,
163    pub userId: Option<String>,
164}
165
166/// Nested capabilities info in the create-auth-key response.
167#[derive(Debug, Deserialize)]
168pub struct AuthKeyCapabilities {
169    pub devices: Option<AuthKeyDevices>,
170}
171
172#[derive(Debug, Deserialize)]
173pub struct AuthKeyDevices {
174    pub create: Option<AuthKeyCreate>,
175}
176
177#[derive(Debug, Deserialize)]
178pub struct AuthKeyCreate {
179    pub reusable: Option<bool>,
180    pub ephemeral: Option<bool>,
181    pub preauthorized: Option<bool>,
182    pub tags: Option<Vec<String>>,
183}
184
185/// Example synchronous `main` function
186fn main() {
187    println!(
188        "Run the async example or tests to see the client in action. \
189         This file defines a base Tailscale client with a create_auth_key method."
190    );
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use std::env;
197
198    /// Integration test that performs a real create_auth_key call against Tailscale.
199    /// To run this successfully, set the following environment variables:
200    ///   - TAILSCALE_API_KEY: your Tailscale API key (e.g., tskey-api-XXXXX).
201    ///   - TAILSCALE_TAILNET: the tailnet name (e.g., "example.com") or "-" for your default.
202    ///
203    /// Example usage:
204    ///   TAILSCALE_API_KEY="tskey-api-XXXXX" \
205    ///   TAILSCALE_TAILNET="myorg.com" \
206    ///   cargo test -- --nocapture
207    #[tokio::test]
208    async fn test_create_auth_key_integration() -> Result<(), Box<dyn Error>> {
209        let token = env::var("TAILSCALE_API_KEY")
210            .expect("Please set env var TAILSCALE_API_KEY with a valid Tailscale API key");
211        let tailnet = env::var("TAILSCALE_TAILNET").unwrap_or_else(|_| "-".to_string());
212
213        // Build the Tailscale client
214        let client = TailscaleClient::new(token);
215
216        // Prepare the request body
217        let request_body = CreateAuthKeyRequest {
218            description: Some("Integration test auth key".to_string()),
219            expirySeconds: None, // e.g. Some(86400) for 1 day
220            capabilities: Capabilities {
221                devices: Devices {
222                    create: Some(CreateOpts {
223                        reusable: Some(true),
224                        ephemeral: Some(false),
225                        preauthorized: Some(false),
226                        tags: Some(vec!["tag:example".to_string()]),
227                    }),
228                },
229            },
230        };
231
232        // Make the real call
233        let response = client
234            .create_auth_key(&tailnet, true, &request_body)
235            .await?;
236
237        println!("Create Auth Key response: {:#?}", response);
238
239        // At a minimum, check that we got something back
240        assert!(
241            response.key.is_some(),
242            "Expected some auth key in the `key` field"
243        );
244
245        Ok(())
246    }
247}