use super::{
common::send_authd_request,
constants::{SAFE_AUTHD_ENDPOINT_HOST, SAFE_AUTHD_ENDPOINT_PORT},
notifs_endpoint::jsonrpc_listen,
};
use crate::{AuthedAppsList, Error, Result, SafeAuthReqId};
use directories::BaseDirs;
use log::{debug, error, info, trace};
use safe_core::ipc::req::ContainerPermissions;
use safe_nd::AppPermissions;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{
collections::HashMap,
io::{self, Write},
path::PathBuf,
process::{Command, Stdio},
};
use tokio::{
sync::{mpsc, oneshot},
task,
};
#[cfg(not(target_os = "windows"))]
const SAFE_AUTHD_EXECUTABLE: &str = "safe-authd";
#[cfg(target_os = "windows")]
const SAFE_AUTHD_EXECUTABLE: &str = "safe-authd.exe";
const ENV_VAR_SAFE_AUTHD_PATH: &str = "SAFE_AUTHD_PATH";
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AuthReq {
pub req_id: SafeAuthReqId,
pub app_id: String,
pub app_name: String,
pub app_vendor: String,
pub app_permissions: AppPermissions,
pub containers: HashMap<String, ContainerPermissions>,
pub own_container: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AuthdStatus {
pub logged_in: bool,
pub num_auth_reqs: u32,
pub num_notif_subs: u32,
pub authd_version: String,
}
pub type PendingAuthReqs = Vec<AuthReq>;
pub type AuthAllowPrompt = dyn Fn(AuthReq) -> Option<bool> + std::marker::Send + std::marker::Sync;
const SAFE_AUTHD_METHOD_STATUS: &str = "status";
const SAFE_AUTHD_METHOD_LOGIN: &str = "login";
const SAFE_AUTHD_METHOD_LOGOUT: &str = "logout";
const SAFE_AUTHD_METHOD_CREATE: &str = "create-acc";
const SAFE_AUTHD_METHOD_AUTHED_APPS: &str = "authed-apps";
const SAFE_AUTHD_METHOD_REVOKE: &str = "revoke";
const SAFE_AUTHD_METHOD_AUTH_REQS: &str = "auth-reqs";
const SAFE_AUTHD_METHOD_ALLOW: &str = "allow";
const SAFE_AUTHD_METHOD_DENY: &str = "deny";
const SAFE_AUTHD_METHOD_SUBSCRIBE: &str = "subscribe";
const SAFE_AUTHD_METHOD_UNSUBSCRIBE: &str = "unsubscribe";
const SAFE_AUTHD_CMD_UPDATE: &str = "update";
const SAFE_AUTHD_CMD_START: &str = "start";
const SAFE_AUTHD_CMD_STOP: &str = "stop";
const SAFE_AUTHD_CMD_RESTART: &str = "restart";
pub struct SafeAuthdClient {
pub authd_endpoint: String,
subscribed_endpoint: Option<(String, task::JoinHandle<()>, task::JoinHandle<()>)>,
}
impl Drop for SafeAuthdClient {
fn drop(&mut self) {
trace!("SafeAuthdClient instance being dropped...");
match &self.subscribed_endpoint {
None => {}
Some((url, _, _)) => {
match futures::executor::block_on(send_unsubscribe(url, &self.authd_endpoint)) {
Ok(msg) => {
debug!("{}", msg);
}
Err(err) => {
debug!("Failed to unsubscribe endpoint from authd: {}", err);
}
}
}
}
}
}
impl SafeAuthdClient {
pub fn new(endpoint: Option<String>) -> Self {
let endpoint = match endpoint {
None => format!("{}:{}", SAFE_AUTHD_ENDPOINT_HOST, SAFE_AUTHD_ENDPOINT_PORT),
Some(endpoint) => endpoint,
};
debug!("Creating new authd client for endpoint {}", endpoint);
Self {
authd_endpoint: endpoint,
subscribed_endpoint: None,
}
}
pub fn update(&self, authd_path: Option<&str>) -> Result<()> {
authd_run_cmd(authd_path, &[SAFE_AUTHD_CMD_UPDATE])
}
pub fn start(&self, authd_path: Option<&str>) -> Result<()> {
authd_run_cmd(
authd_path,
&[SAFE_AUTHD_CMD_START, "--listen", &self.authd_endpoint],
)
}
pub fn stop(&self, authd_path: Option<&str>) -> Result<()> {
authd_run_cmd(authd_path, &[SAFE_AUTHD_CMD_STOP])
}
pub fn restart(&self, authd_path: Option<&str>) -> Result<()> {
authd_run_cmd(
authd_path,
&[SAFE_AUTHD_CMD_RESTART, "--listen", &self.authd_endpoint],
)
}
pub async fn status(&mut self) -> Result<AuthdStatus> {
debug!("Attempting to retrieve status report from remote authd...");
let status_report = send_authd_request::<AuthdStatus>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_STATUS,
serde_json::Value::Null,
)
.await?;
info!(
"SAFE status report retrieved successfully: {:?}",
status_report
);
Ok(status_report)
}
pub async fn log_in(&mut self, passphrase: &str, password: &str) -> Result<()> {
debug!("Attempting to log in on remote authd...");
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_LOGIN,
json!(vec![passphrase, password]),
)
.await?;
info!("SAFE login action was successful: {}", authd_response);
Ok(())
}
pub async fn log_out(&mut self) -> Result<()> {
debug!("Dropping logged in session and logging out in remote authd...");
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_LOGOUT,
serde_json::Value::Null,
)
.await?;
info!("SAFE logout action was successful: {}", authd_response);
Ok(())
}
pub async fn create_acc(&self, sk: &str, passphrase: &str, password: &str) -> Result<()> {
debug!("Attempting to create a SAFE account on remote authd...");
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_CREATE,
json!(vec![passphrase, password, sk]),
)
.await?;
debug!(
"SAFE account creation action was successful: {}",
authd_response
);
Ok(())
}
pub async fn authed_apps(&self) -> Result<AuthedAppsList> {
debug!("Attempting to fetch list of authorised apps from remote authd...");
let authed_apps_list = send_authd_request::<AuthedAppsList>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_AUTHED_APPS,
serde_json::Value::Null,
)
.await?;
debug!(
"List of applications authorised successfully received: {:?}",
authed_apps_list
);
Ok(authed_apps_list)
}
pub async fn revoke_app(&self, app_id: &str) -> Result<()> {
debug!(
"Requesting to revoke permissions from application: {}",
app_id
);
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_REVOKE,
json!(app_id),
)
.await?;
debug!(
"Application revocation action successful: {}",
authd_response
);
Ok(())
}
pub async fn auth_reqs(&self) -> Result<PendingAuthReqs> {
debug!("Attempting to fetch list of pending authorisation requests from remote authd...");
let auth_reqs_list = send_authd_request::<PendingAuthReqs>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_AUTH_REQS,
serde_json::Value::Null,
)
.await?;
debug!(
"List of pending authorisation requests successfully received: {:?}",
auth_reqs_list
);
Ok(auth_reqs_list)
}
pub async fn allow(&self, req_id: SafeAuthReqId) -> Result<()> {
debug!("Requesting to allow authorisation request: {}", req_id);
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_ALLOW,
json!(req_id.to_string()),
)
.await?;
debug!(
"Action to allow authorisation request was successful: {}",
authd_response
);
Ok(())
}
pub async fn deny(&self, req_id: SafeAuthReqId) -> Result<()> {
debug!("Requesting to deny authorisation request: {}", req_id);
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_DENY,
json!(req_id.to_string()),
)
.await?;
debug!(
"Action to deny authorisation request was successful: {}",
authd_response
);
Ok(())
}
pub async fn subscribe<
CB: 'static + Fn(AuthReq) -> Option<bool> + std::marker::Send + std::marker::Sync,
>(
&mut self,
endpoint_url: &str,
app_id: &str,
allow_cb: CB,
) -> Result<()> {
debug!("Subscribing to receive authorisation requests notifications...",);
let dirs = directories::ProjectDirs::from("net", "maidsafe", "safe-authd-client")
.ok_or_else(|| {
Error::AuthdClientError(
"Failed to obtain local home directory where to store endpoint certificates to"
.to_string(),
)
})?;
let cert_base_path = dirs.config_dir().join(app_id.to_string());
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_SUBSCRIBE,
json!(vec![endpoint_url, &cert_base_path.display().to_string()]),
).await.map_err(|err| Error::AuthdClientError(format!("Failed when trying to subscribe endpoint URL ({}) to receive authorisation request for self-auth: {}", endpoint_url, err)))?;
debug!(
"Successfully subscribed to receive authorisation requests notifications: {}",
authd_response
);
let (tx, mut rx) = mpsc::unbounded_channel::<(AuthReq, oneshot::Sender<Option<bool>>)>();
let listen = endpoint_url.to_string();
let endpoint_thread_join_handle = tokio::spawn(async move {
match jsonrpc_listen(&listen, &cert_base_path.display().to_string(), tx).await {
Ok(()) => {
info!("Endpoint successfully launched for receiving auth req notifications");
}
Err(err) => {
error!(
"Failed to launch endpoint for receiving auth req notifications: {}",
err
);
}
}
});
let cb = Box::new(allow_cb);
let cb_thread_join_handle = tokio::spawn(async move {
while let Some((auth_req, decision_tx)) = rx.recv().await {
debug!(
"Notification for authorisation request ({}) from app ID '{}' received",
auth_req.req_id, auth_req.app_id
);
let user_decision = cb(auth_req);
match decision_tx.send(user_decision) {
Ok(_) => debug!("Auth req decision sent to authd"),
Err(_) => error!("Auth req decision couldn't be sent back to authd"),
};
}
});
self.subscribed_endpoint = Some((
endpoint_url.to_string(),
endpoint_thread_join_handle,
cb_thread_join_handle,
));
Ok(())
}
pub async fn subscribe_url(&self, endpoint_url: &str) -> Result<()> {
debug!(
"Subscribing '{}' as endpoint for authorisation requests notifications...",
endpoint_url
);
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_SUBSCRIBE,
json!(vec![endpoint_url]),
)
.await?;
debug!(
"Successfully subscribed a URL for authorisation requests notifications: {}",
authd_response
);
Ok(())
}
pub async fn unsubscribe(&mut self, endpoint_url: &str) -> Result<()> {
debug!("Unsubscribing from authorisation requests notifications...",);
let authd_response = send_unsubscribe(endpoint_url, &self.authd_endpoint).await?;
debug!(
"Successfully unsubscribed from authorisation requests notifications: {}",
authd_response
);
if let Some((url, _, _)) = &self.subscribed_endpoint {
if endpoint_url == url {
self.subscribed_endpoint = None;
}
}
Ok(())
}
}
async fn send_unsubscribe(endpoint_url: &str, authd_endpoint: &str) -> Result<String> {
send_authd_request::<String>(
authd_endpoint,
SAFE_AUTHD_METHOD_UNSUBSCRIBE,
json!(endpoint_url),
)
.await
}
fn authd_run_cmd(authd_path: Option<&str>, args: &[&str]) -> Result<()> {
let mut path = get_authd_bin_path(authd_path)?;
path.push(SAFE_AUTHD_EXECUTABLE);
let path_str = path.display().to_string();
debug!("Attempting to {} authd from '{}' ...", args[0], path_str);
let output = Command::new(&path_str)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|err| {
Error::AuthdClientError(format!(
"Failed to execute authd from '{}': {}",
path_str, err
))
})?;
if output.status.success() {
io::stdout()
.write_all(&output.stdout)
.map_err(|err| Error::AuthdClientError(format!("Failed to output stdout: {}", err)))?;
Ok(())
} else {
match output.status.code() {
Some(10) => {
Err(Error::AuthdAlreadyStarted(format!(
"Failed to start safe-authd daemon '{}' as an instance seems to be already running",
path_str,
)))
}
Some(_) | None => Err(Error::AuthdError(format!(
"Failed when invoking safe-authd executable from '{}'",
path_str,
))),
}
}
}
fn get_authd_bin_path(authd_path: Option<&str>) -> Result<PathBuf> {
match authd_path {
Some(p) => Ok(PathBuf::from(p)),
None => {
if let Ok(authd_path) = std::env::var(ENV_VAR_SAFE_AUTHD_PATH) {
Ok(PathBuf::from(authd_path))
} else {
let base_dirs = BaseDirs::new().ok_or_else(|| {
Error::AuthdClientError("Failed to obtain user's home path".to_string())
})?;
let mut path = PathBuf::from(base_dirs.home_dir());
path.push(".safe");
path.push("authd");
Ok(path)
}
}
}
}