sn_api/authd_client/
authd_client_api.rs

1// Copyright 2023 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use super::{
10    common::send_authd_request,
11    constants::{SN_AUTHD_ENDPOINT_HOST, SN_AUTHD_ENDPOINT_PORT},
12    notifs_endpoint::jsonrpc_listen,
13};
14
15use crate::{AuthReq, AuthedAppsList, Error, Result, SafeAuthReqId};
16
17use serde::{Deserialize, Serialize};
18use serde_json::json;
19use std::{
20    io::{self, Write},
21    path::{Path, PathBuf},
22    process::{Command, Stdio},
23};
24use tokio::{
25    sync::{mpsc, oneshot},
26    task,
27};
28use tracing::{debug, error, info, trace};
29
30#[cfg(not(target_os = "windows"))]
31const SN_AUTHD_EXECUTABLE: &str = "sn_authd";
32
33#[cfg(target_os = "windows")]
34const SN_AUTHD_EXECUTABLE: &str = "sn_authd.exe";
35
36const ENV_VAR_SN_AUTHD_PATH: &str = "SN_AUTHD_PATH";
37
38#[derive(Serialize, Deserialize, Clone, Debug)]
39pub struct AuthdStatus {
40    pub safe_unlocked: bool,
41    pub num_auth_reqs: u32,
42    pub num_notif_subs: u32,
43    pub authd_version: String,
44}
45
46// Type of the list of pending authorisation requests
47pub type PendingAuthReqs = Vec<AuthReq>;
48
49// Type of the function/callback invoked for notifying and querying if an authorisation request
50// shall be allowed. All the relevant information about the authorisation request is passed as args to the callback.
51pub type AuthAllowPrompt = dyn Fn(AuthReq) -> Option<bool> + std::marker::Send + std::marker::Sync;
52
53// Authenticator method for getting a status report of the sn_authd
54const SN_AUTHD_METHOD_STATUS: &str = "status";
55
56// Authenticator method for unlocking a Safe
57const SN_AUTHD_METHOD_UNLOCK: &str = "unlock";
58
59// Authenticator method for locking a Safe
60const SN_AUTHD_METHOD_LOCK: &str = "lock";
61
62// Authenticator method for creating a new 'Safe'
63const SN_AUTHD_METHOD_CREATE: &str = "create";
64
65// Authenticator method for fetching list of authorised apps
66const SN_AUTHD_METHOD_AUTHED_APPS: &str = "authed-apps";
67
68// Authenticator method for revoking applications and/or permissions
69const SN_AUTHD_METHOD_REVOKE: &str = "revoke";
70
71// Authenticator method for retrieving the list of pending authorisation requests
72const SN_AUTHD_METHOD_AUTH_REQS: &str = "auth-reqs";
73
74// Authenticator method for allowing an authorisation request
75const SN_AUTHD_METHOD_ALLOW: &str = "allow";
76
77// Authenticator method for denying an authorisation request
78const SN_AUTHD_METHOD_DENY: &str = "deny";
79
80// Authenticator method for subscribing to authorisation requests notifications
81const SN_AUTHD_METHOD_SUBSCRIBE: &str = "subscribe";
82
83// Authenticator method for unsubscribing from authorisation requests notifications
84const SN_AUTHD_METHOD_UNSUBSCRIBE: &str = "unsubscribe";
85
86// authd subcommand to update the binary to new available released version
87const SN_AUTHD_CMD_UPDATE: &str = "update";
88
89// authd subcommand to start the daemon
90const SN_AUTHD_CMD_START: &str = "start";
91
92// authd subcommand to stop the daemon
93const SN_AUTHD_CMD_STOP: &str = "stop";
94
95// authd subcommand to restart the daemon
96const SN_AUTHD_CMD_RESTART: &str = "restart";
97
98// Authd Client API
99pub struct SafeAuthdClient {
100    pub authd_endpoint: String,
101    pub authd_cert_path: PathBuf,
102    pub authd_notify_cert_path: PathBuf,
103    pub authd_notify_key_path: PathBuf,
104    subscribed_endpoint: Option<(String, task::JoinHandle<()>, task::JoinHandle<()>)>,
105    // TODO: add a session_token field to use for communicating with authd for restricted operations,
106    // we should restrict operations like subscribe, or allow/deny, to only be accepted with a valid token
107    // session_token: String,
108}
109
110impl Drop for SafeAuthdClient {
111    fn drop(&mut self) {
112        trace!("SafeAuthdClient instance being dropped...");
113        // Let's try to unsubscribe if we had a subscribed endpoint
114        match &self.subscribed_endpoint {
115            None => {}
116            Some((url, _, _)) => {
117                match futures::executor::block_on(send_unsubscribe(
118                    url,
119                    &self.authd_endpoint,
120                    &self.authd_cert_path,
121                )) {
122                    Ok(msg) => {
123                        debug!("{}", msg);
124                    }
125                    Err(err) => {
126                        // We are still ok, it was just us trying to be nice and unsubscribe if possible
127                        // It could be the case we were already unsubscribe automatically by authd before
128                        // we were attempting to do it now, which can happen due to our endpoint
129                        // being unresponsive, so it's all ok
130                        debug!("Failed to unsubscribe endpoint from authd: {}", err);
131                    }
132                }
133            }
134        }
135    }
136}
137
138impl SafeAuthdClient {
139    pub fn new<P: AsRef<Path>>(
140        endpoint: Option<String>,
141        authd_cert_path: P,
142        authd_notify_cert_path: P,
143        authd_notify_key_path: P,
144    ) -> Self {
145        let endpoint = match endpoint {
146            None => format!("{SN_AUTHD_ENDPOINT_HOST}:{SN_AUTHD_ENDPOINT_PORT}"),
147            Some(endpoint) => endpoint,
148        };
149        debug!("Creating new authd client for endpoint {}", endpoint);
150        Self {
151            authd_endpoint: endpoint,
152            authd_cert_path: PathBuf::from(authd_cert_path.as_ref()),
153            authd_notify_cert_path: PathBuf::from(authd_notify_cert_path.as_ref()),
154            authd_notify_key_path: PathBuf::from(authd_notify_key_path.as_ref()),
155            subscribed_endpoint: None,
156        }
157    }
158
159    // Print out the version of the Authenticator binary
160    pub fn version(&self, authd_path: Option<&str>) -> Result<()> {
161        authd_run_cmd(authd_path, &["--version"])
162    }
163
164    // Update the Authenticator binary to a new released version
165    pub fn update(&self, authd_path: Option<&str>) -> Result<()> {
166        authd_run_cmd(authd_path, &[SN_AUTHD_CMD_UPDATE])
167    }
168
169    // Start the Authenticator daemon
170    pub fn start(&self, authd_path: Option<&str>) -> Result<()> {
171        authd_run_cmd(
172            authd_path,
173            &[SN_AUTHD_CMD_START, "--listen", &self.authd_endpoint],
174        )
175    }
176
177    // Stop the Authenticator daemon
178    pub fn stop(&self, authd_path: Option<&str>) -> Result<()> {
179        authd_run_cmd(authd_path, &[SN_AUTHD_CMD_STOP])
180    }
181
182    // Restart the Authenticator daemon
183    pub fn restart(&self, authd_path: Option<&str>) -> Result<()> {
184        authd_run_cmd(
185            authd_path,
186            &[SN_AUTHD_CMD_RESTART, "--listen", &self.authd_endpoint],
187        )
188    }
189
190    // Send a request to remote authd endpoint to obtain a status report
191    pub async fn status(&mut self) -> Result<AuthdStatus> {
192        debug!("Attempting to retrieve status report from remote authd...");
193        let status_report = send_authd_request::<AuthdStatus>(
194            &self.authd_cert_path,
195            &self.authd_endpoint,
196            SN_AUTHD_METHOD_STATUS,
197            serde_json::Value::Null,
198        )
199        .await?;
200
201        info!(
202            "SAFE status report retrieved successfully: {:?}",
203            status_report
204        );
205        Ok(status_report)
206    }
207
208    // Send an action request to remote authd endpoint to unlock a Safe
209    pub async fn unlock(&mut self, passphrase: &str, password: &str) -> Result<()> {
210        debug!("Attempting to unlock a Safe on remote authd...");
211        let authd_response = send_authd_request::<String>(
212            &self.authd_cert_path,
213            &self.authd_endpoint,
214            SN_AUTHD_METHOD_UNLOCK,
215            json!(vec![passphrase, password]),
216        )
217        .await?;
218
219        info!("The Safe was unlocked successful: {}", authd_response);
220        // TODO: store the authd session token, replacing an existing one
221        // self.session_token = authd_response;
222
223        Ok(())
224    }
225
226    // Send an action request to remote authd endpoint to lock a Safe
227    pub async fn lock(&mut self) -> Result<()> {
228        debug!("Locking the Safe on a remote authd...");
229        let authd_response = send_authd_request::<String>(
230            &self.authd_cert_path,
231            &self.authd_endpoint,
232            SN_AUTHD_METHOD_LOCK,
233            serde_json::Value::Null,
234        )
235        .await?;
236
237        info!("Locking action was successful: {}", authd_response);
238
239        // TODO: clean up the stored authd session token
240        // self.session_token = "".to_string();
241
242        Ok(())
243    }
244
245    // Sends a request to create an 'Safe' to the SAFE Authenticator
246    // TODO: accept a payment proof to be used to pay the cost of creating the 'Safe'
247    pub async fn create(&self, passphrase: &str, password: &str) -> Result<()> {
248        debug!("Attempting to create a Safe using remote authd...");
249        let authd_response = send_authd_request::<String>(
250            &self.authd_cert_path,
251            &self.authd_endpoint,
252            SN_AUTHD_METHOD_CREATE,
253            json!(vec![passphrase, password]),
254        )
255        .await?;
256
257        debug!("Creation of a Safe was successful: {}", authd_response);
258        Ok(())
259    }
260
261    // Get the list of applications authorised from remote authd
262    pub async fn authed_apps(&self) -> Result<AuthedAppsList> {
263        debug!("Attempting to fetch list of authorised apps from remote authd...");
264        let authed_apps_list = send_authd_request::<AuthedAppsList>(
265            &self.authd_cert_path,
266            &self.authd_endpoint,
267            SN_AUTHD_METHOD_AUTHED_APPS,
268            serde_json::Value::Null,
269        )
270        .await?;
271
272        debug!(
273            "List of applications authorised successfully received: {:?}",
274            authed_apps_list
275        );
276        Ok(authed_apps_list)
277    }
278
279    // Revoke all permissions from an application
280    pub async fn revoke_app(&self, app_id: &str) -> Result<()> {
281        debug!(
282            "Requesting to revoke permissions from application: {}",
283            app_id
284        );
285        let authd_response = send_authd_request::<String>(
286            &self.authd_cert_path,
287            &self.authd_endpoint,
288            SN_AUTHD_METHOD_REVOKE,
289            json!(app_id),
290        )
291        .await?;
292
293        debug!(
294            "Application revocation action successful: {}",
295            authd_response
296        );
297        Ok(())
298    }
299
300    // Get the list of pending authorisation requests from remote authd
301    pub async fn auth_reqs(&self) -> Result<PendingAuthReqs> {
302        debug!("Attempting to fetch list of pending authorisation requests from remote authd...");
303        let auth_reqs_list = send_authd_request::<PendingAuthReqs>(
304            &self.authd_cert_path,
305            &self.authd_endpoint,
306            SN_AUTHD_METHOD_AUTH_REQS,
307            serde_json::Value::Null,
308        )
309        .await?;
310
311        debug!(
312            "List of pending authorisation requests successfully received: {:?}",
313            auth_reqs_list
314        );
315        Ok(auth_reqs_list)
316    }
317
318    // Allow an authorisation request
319    pub async fn allow(&self, req_id: SafeAuthReqId) -> Result<()> {
320        debug!("Requesting to allow authorisation request: {}", req_id);
321        let authd_response = send_authd_request::<String>(
322            &self.authd_cert_path,
323            &self.authd_endpoint,
324            SN_AUTHD_METHOD_ALLOW,
325            json!(req_id.to_string()),
326        )
327        .await?;
328
329        debug!(
330            "Action to allow authorisation request was successful: {}",
331            authd_response
332        );
333        Ok(())
334    }
335
336    // Deny an authorisation request
337    pub async fn deny(&self, req_id: SafeAuthReqId) -> Result<()> {
338        debug!("Requesting to deny authorisation request: {}", req_id);
339        let authd_response = send_authd_request::<String>(
340            &self.authd_cert_path,
341            &self.authd_endpoint,
342            SN_AUTHD_METHOD_DENY,
343            json!(req_id.to_string()),
344        )
345        .await?;
346
347        debug!(
348            "Action to deny authorisation request was successful: {}",
349            authd_response
350        );
351        Ok(())
352    }
353
354    // Subscribe a callback to receive notifications to allow/deny authorisation requests
355    // We support having only one subscripton at a time, a previous subscription will be dropped
356    pub async fn subscribe<
357        CB: 'static + Fn(AuthReq) -> Option<bool> + std::marker::Send + std::marker::Sync,
358    >(
359        &mut self,
360        endpoint_url: &str,
361        _app_id: &str,
362        allow_cb: CB,
363    ) -> Result<()> {
364        debug!("Subscribing to receive authorisation requests notifications...",);
365
366        let cert_path = self.authd_cert_path.to_str().ok_or_else(|| {
367            Error::AuthdClientError("Could not convert authd_cert_path to string".to_string())
368        })?;
369        let authd_response = send_authd_request::<String>(
370            &self.authd_cert_path,
371            &self.authd_endpoint,
372            SN_AUTHD_METHOD_SUBSCRIBE,
373            json!(vec![endpoint_url, cert_path]),
374        ).await.map_err(|err| Error::AuthdClientError(format!("Failed when trying to subscribe endpoint URL ({endpoint_url}) to receive authorisation request for self-auth: {err}")))?;
375
376        debug!(
377            "Successfully subscribed to receive authorisation requests notifications: {}",
378            authd_response
379        );
380
381        // Start listening first
382        // We need a channel to receive auth req notifications from the thread running the QUIC endpoint
383        let (tx, mut rx) = mpsc::unbounded_channel::<(AuthReq, oneshot::Sender<Option<bool>>)>();
384
385        let listen = endpoint_url.to_string();
386        // TODO: if there was a previous subscription,
387        // make sure we kill/stop the previously created tasks
388
389        let authd_notify_cert_path = self.authd_notify_cert_path.clone();
390        let authd_notify_key_path = self.authd_notify_key_path.clone();
391        let endpoint_thread_join_handle = tokio::spawn(async move {
392            match jsonrpc_listen(&listen, &authd_notify_cert_path, &authd_notify_key_path, tx).await
393            {
394                Ok(()) => {
395                    info!("Endpoint successfully launched for receiving auth req notifications");
396                }
397                Err(err) => {
398                    error!(
399                        "Failed to launch endpoint for receiving auth req notifications: {}",
400                        err
401                    );
402                }
403            }
404        });
405
406        let cb = Box::new(allow_cb);
407        let cb_thread_join_handle = tokio::spawn(async move {
408            while let Some((auth_req, decision_tx)) = rx.recv().await {
409                debug!(
410                    "Notification for authorisation request ({}) from app ID '{}' received",
411                    auth_req.req_id, auth_req.app_id
412                );
413
414                // Let's get the decision from the user by invoking the callback provided
415                let user_decision = cb(auth_req);
416
417                // Send the decision received back to authd-client so it
418                // can in turn send it to authd
419                match decision_tx.send(user_decision) {
420                    Ok(_) => debug!("Auth req decision sent to authd"),
421                    Err(_) => error!("Auth req decision couldn't be sent back to authd"),
422                };
423            }
424        });
425
426        self.subscribed_endpoint = Some((
427            endpoint_url.to_string(),
428            endpoint_thread_join_handle,
429            cb_thread_join_handle,
430        ));
431
432        Ok(())
433    }
434
435    // Subscribe an endpoint URL where notifications to allow/deny authorisation requests shall be sent
436    pub async fn subscribe_url(&self, endpoint_url: &str) -> Result<()> {
437        debug!(
438            "Subscribing '{}' as endpoint for authorisation requests notifications...",
439            endpoint_url
440        );
441
442        let authd_response = send_authd_request::<String>(
443            &self.authd_cert_path,
444            &self.authd_endpoint,
445            SN_AUTHD_METHOD_SUBSCRIBE,
446            json!(vec![endpoint_url]),
447        )
448        .await?;
449
450        debug!(
451            "Successfully subscribed a URL for authorisation requests notifications: {}",
452            authd_response
453        );
454        Ok(())
455    }
456
457    // Unsubscribe from notifications to allow/deny authorisation requests
458    pub async fn unsubscribe(&mut self, endpoint_url: &str) -> Result<()> {
459        debug!("Unsubscribing from authorisation requests notifications...",);
460        let authd_response =
461            send_unsubscribe(endpoint_url, &self.authd_endpoint, &self.authd_cert_path).await?;
462        debug!(
463            "Successfully unsubscribed from authorisation requests notifications: {}",
464            authd_response
465        );
466
467        // If the URL is the same as the endpoint locally launched, terminate the thread
468        if let Some((url, _, _)) = &self.subscribed_endpoint {
469            if endpoint_url == url {
470                // TODO: send signal to stop the currently running tasks
471                self.subscribed_endpoint = None;
472            }
473        }
474
475        Ok(())
476    }
477}
478
479async fn send_unsubscribe(
480    endpoint_url: &str,
481    authd_endpoint: &str,
482    authd_cert_path: &Path,
483) -> Result<String> {
484    send_authd_request::<String>(
485        authd_cert_path,
486        authd_endpoint,
487        SN_AUTHD_METHOD_UNSUBSCRIBE,
488        json!(endpoint_url),
489    )
490    .await
491}
492
493fn authd_run_cmd(authd_path: Option<&str>, args: &[&str]) -> Result<()> {
494    let mut path = get_authd_bin_path(authd_path)?;
495    path.push(SN_AUTHD_EXECUTABLE);
496    let path_str = path.display().to_string();
497    debug!("Attempting to {} authd from '{}' ...", args[0], path_str);
498
499    let output = Command::new(&path_str)
500        .args(args)
501        .stdout(Stdio::piped())
502        .stderr(Stdio::piped())
503        .output()
504        .map_err(|err| {
505            Error::AuthdClientError(format!("Failed to execute authd from '{path_str}': {err}",))
506        })?;
507
508    if output.status.success() {
509        io::stdout()
510            .write_all(&output.stdout)
511            .map_err(|err| Error::AuthdClientError(format!("Failed to output stdout: {err}")))?;
512        Ok(())
513    } else {
514        match output.status.code() {
515            Some(10) => {
516                // sn_authd exit code 10 is sn_authd::errors::Error::AuthdAlreadyStarted
517                Err(Error::AuthdAlreadyStarted(format!(
518                       "Failed to start sn_authd daemon '{path_str}' as an instance seems to be already running",
519                   )))
520            }
521            Some(_) | None => Err(Error::AuthdError(format!(
522                "Failed when invoking sn_authd executable from '{path_str}'",
523            ))),
524        }
525    }
526}
527
528fn get_authd_bin_path(authd_path: Option<&str>) -> Result<PathBuf> {
529    match authd_path {
530        Some(p) => Ok(PathBuf::from(p)),
531        None => {
532            // if SN_AUTHD_PATH is set it then overrides default
533            if let Ok(authd_path) = std::env::var(ENV_VAR_SN_AUTHD_PATH) {
534                Ok(PathBuf::from(authd_path))
535            } else {
536                let mut path = dirs_next::home_dir().ok_or_else(|| {
537                    Error::AuthdClientError("Failed to obtain user's home path".to_string())
538                })?;
539
540                path.push(".safe");
541                path.push("authd");
542                Ok(path)
543            }
544        }
545    }
546}