use super::{
constants::{SAFE_AUTHD_ENDPOINT_HOST, SAFE_AUTHD_ENDPOINT_PORT},
helpers::send_authd_request,
quic_endpoint::quic_listen,
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,
sync::mpsc,
thread,
};
#[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 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_INSTALL: &str = "install";
const SAFE_AUTHD_CMD_UNINSTALL: &str = "uninstall";
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, thread::JoinHandle<()>, thread::JoinHandle<()>)>,
}
impl Drop for SafeAuthdClient {
fn drop(&mut self) {
trace!("SafeAuthdClient instance being dropped...");
match &self.subscribed_endpoint {
None => {}
Some((url, _, _)) => {
match send_unsubscribe(url, &self.authd_endpoint) {
Ok(msg) => {
debug!("{}", msg);
}
Err(err) => {
debug!("Failed to unsubscribe endpoint from authd: {}", err);
}
}
}
}
}
}
#[allow(dead_code)]
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 install(&self, authd_path: Option<&str>) -> Result<()> {
authd_run_cmd(authd_path, &[SAFE_AUTHD_CMD_INSTALL])
}
pub fn uninstall(&self, authd_path: Option<&str>) -> Result<()> {
authd_run_cmd(authd_path, &[SAFE_AUTHD_CMD_UNINSTALL])
}
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 fn status(&mut self) -> Result<AuthdStatus> {
debug!("Attempting to retrieve status report from remote authd...");
info!("Sending status report request to SAFE Authenticator...");
let status_report = send_authd_request::<AuthdStatus>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_STATUS,
serde_json::Value::Null,
)?;
info!(
"SAFE status report retrieved successfully: {:?}",
status_report
);
Ok(status_report)
}
pub fn log_in(&mut self, passphrase: &str, password: &str) -> Result<()> {
debug!("Attempting to log in on remote authd...");
info!(
"Sending login action to SAFE Authenticator ({})...",
self.authd_endpoint
);
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_LOGIN,
json!(vec![passphrase, password]),
)?;
info!("SAFE login action was successful: {}", authd_response);
Ok(())
}
pub fn log_out(&mut self) -> Result<()> {
debug!("Dropping logged in session and logging out in remote authd...");
info!("Sending logout action to SAFE Authenticator...");
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_LOGOUT,
serde_json::Value::Null,
)?;
info!("SAFE logout action was successful: {}", authd_response);
Ok(())
}
pub fn create_acc(&self, sk: &str, passphrase: &str, password: &str) -> Result<()> {
debug!("Attempting to create a SAFE account on remote authd...");
debug!("Sending account creation request to SAFE Authenticator...");
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_CREATE,
json!(vec![passphrase, password, sk]),
)?;
debug!(
"SAFE account creation action was successful: {}",
authd_response
);
Ok(())
}
pub fn authed_apps(&self) -> Result<AuthedAppsList> {
debug!("Attempting to fetch list of authorised apps from remote authd...");
debug!("Sending request request to SAFE Authenticator...");
let authed_apps_list = send_authd_request::<AuthedAppsList>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_AUTHED_APPS,
serde_json::Value::Null,
)?;
debug!(
"List of applications authorised successfully received: {:?}",
authed_apps_list
);
Ok(authed_apps_list)
}
pub fn revoke_app(&self, app_id: &str) -> Result<()> {
debug!(
"Requesting to revoke permissions from application: {}",
app_id
);
debug!("Sending revoke action request to SAFE Authenticator...");
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_REVOKE,
json!(app_id),
)?;
debug!(
"Application revocation action successful: {}",
authd_response
);
Ok(())
}
pub fn auth_reqs(&self) -> Result<PendingAuthReqs> {
debug!("Attempting to fetch list of pending authorisation requests from remote authd...");
debug!("Sending request request to SAFE Authenticator...");
let auth_reqs_list = send_authd_request::<PendingAuthReqs>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_AUTH_REQS,
serde_json::Value::Null,
)?;
debug!(
"List of pending authorisation requests successfully received: {:?}",
auth_reqs_list
);
Ok(auth_reqs_list)
}
pub fn allow(&self, req_id: SafeAuthReqId) -> Result<()> {
debug!("Requesting to allow authorisation request: {}", req_id);
debug!("Sending allow action request to SAFE Authenticator...");
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_ALLOW,
json!(req_id.to_string()),
)?;
debug!(
"Action to allow authorisation request was successful: {}",
authd_response
);
Ok(())
}
pub fn deny(&self, req_id: SafeAuthReqId) -> Result<()> {
debug!("Requesting to deny authorisation request: {}", req_id);
debug!("Sending deny action request to SAFE Authenticator...");
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_DENY,
json!(req_id.to_string()),
)?;
debug!(
"Action to deny authorisation request was successful: {}",
authd_response
);
Ok(())
}
pub 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());
debug!("Sending subscribe action request to SAFE Authenticator...");
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_SUBSCRIBE,
json!(vec![endpoint_url, &cert_base_path.display().to_string()]),
)?;
debug!(
"Successfully subscribed to receive authorisation requests notifications: {}",
authd_response
);
let (tx, rx): (mpsc::Sender<AuthReq>, mpsc::Receiver<AuthReq>) = mpsc::channel();
let listen = endpoint_url.to_string();
let endpoint_thread_join_handle =
thread::spawn(move || match quic_listen(&listen, tx, cert_base_path) {
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 = thread::spawn(move || loop {
match rx.recv() {
Ok(auth_req) => {
debug!(
"Notification for authorisation request ({}) from app ID '{}' received",
auth_req.req_id, auth_req.app_id
);
let _user_decision = cb(auth_req);
}
Err(err) => {
debug!("Failed to receive message: {}", err);
}
}
});
self.subscribed_endpoint = Some((
endpoint_url.to_string(),
endpoint_thread_join_handle,
cb_thread_join_handle,
));
Ok(())
}
pub fn subscribe_url(&self, endpoint_url: &str) -> Result<()> {
debug!(
"Subscribing '{}' as endpoint for authorisation requests notifications...",
endpoint_url
);
debug!("Sending subscribe action request to SAFE Authenticator...");
let authd_response = send_authd_request::<String>(
&self.authd_endpoint,
SAFE_AUTHD_METHOD_UNSUBSCRIBE,
json!(endpoint_url),
)?;
debug!(
"Successfully subscribed a URL for authorisation requests notifications: {}",
authd_response
);
Ok(())
}
pub fn unsubscribe(&mut self, endpoint_url: &str) -> Result<()> {
debug!("Unsubscribing from authorisation requests notifications...",);
let authd_response = send_unsubscribe(endpoint_url, &self.authd_endpoint)?;
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(())
}
}
fn send_unsubscribe(endpoint_url: &str, authd_endpoint: &str) -> Result<String> {
debug!(
"Sending unsubscribe action request to SAFE Authenticator on {}...",
authd_endpoint
);
send_authd_request::<String>(
authd_endpoint,
SAFE_AUTHD_METHOD_UNSUBSCRIBE,
json!(endpoint_url),
)
}
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 child = Command::new(&path_str).args(args).spawn().map_err(|err| {
Error::AuthdClientError(format!(
"Failed to execute authd from '{}': {}",
path_str, err
))
})?;
let output = child.wait_with_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 '{}':\n{}",
path_str,
String::from_utf8_lossy(&output.stderr)
))),
}
}
}
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)
}
}
}
}