2019-03-29 22:18:25 +00:00
|
|
|
extern crate reqwest;
|
2019-04-05 18:49:32 +00:00
|
|
|
extern crate serde;
|
2021-06-14 01:01:23 +00:00
|
|
|
extern crate thiserror;
|
2019-03-29 22:18:25 +00:00
|
|
|
|
2021-12-11 00:29:38 +00:00
|
|
|
use reqwest::blocking::Response;
|
2019-04-05 18:49:32 +00:00
|
|
|
use serde::Deserialize;
|
2019-03-29 22:18:25 +00:00
|
|
|
use std::collections::HashMap;
|
2020-07-09 06:02:10 +00:00
|
|
|
use std::fs::File;
|
|
|
|
use std::io::Read;
|
2020-07-10 03:14:12 +00:00
|
|
|
use std::time::{Duration, Instant};
|
2021-06-14 01:01:23 +00:00
|
|
|
use thiserror::Error;
|
2019-03-29 22:18:25 +00:00
|
|
|
|
|
|
|
const COOKIE_LIFESPAN: Duration = Duration::from_secs(20 * 60);
|
|
|
|
|
2021-06-14 01:01:23 +00:00
|
|
|
#[derive(Error, Debug)]
|
|
|
|
pub enum ResponseError {
|
|
|
|
#[error("vaultwarden error {0}")]
|
|
|
|
ApiError(String),
|
|
|
|
|
|
|
|
#[error("http error making request {0:?}")]
|
|
|
|
HttpError(#[from] reqwest::Error),
|
|
|
|
}
|
|
|
|
|
2019-04-15 16:58:19 +00:00
|
|
|
#[derive(Debug, Deserialize)]
|
2019-04-05 18:49:32 +00:00
|
|
|
pub struct User {
|
2019-04-11 23:20:00 +00:00
|
|
|
#[serde(rename = "Email")]
|
|
|
|
email: String,
|
2019-04-12 23:40:50 +00:00
|
|
|
#[serde(rename = "_Status")]
|
|
|
|
status: i32,
|
2019-04-05 18:49:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl User {
|
|
|
|
pub fn get_email(&self) -> String {
|
2019-04-11 23:20:00 +00:00
|
|
|
self.email.clone()
|
2019-04-05 18:49:32 +00:00
|
|
|
}
|
2019-04-12 23:40:50 +00:00
|
|
|
|
|
|
|
pub fn is_disabled(&self) -> bool {
|
|
|
|
// HACK: Magic number
|
2019-04-15 16:58:19 +00:00
|
|
|
self.status == 2
|
2019-04-05 18:49:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-29 22:18:25 +00:00
|
|
|
pub struct Client {
|
|
|
|
url: String,
|
|
|
|
admin_token: String,
|
2020-07-10 03:14:12 +00:00
|
|
|
root_cert_file: String,
|
|
|
|
cookie: Option<String>,
|
|
|
|
cookie_created: Option<Instant>,
|
2019-03-29 22:18:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Client {
|
2019-03-29 22:40:26 +00:00
|
|
|
/// Create new instance of client
|
2020-07-10 03:14:12 +00:00
|
|
|
pub fn new(url: String, admin_token: String, root_cert_file: String) -> Client {
|
2019-03-29 22:18:25 +00:00
|
|
|
Client {
|
|
|
|
url,
|
|
|
|
admin_token,
|
2020-07-10 03:14:12 +00:00
|
|
|
root_cert_file,
|
|
|
|
cookie: None,
|
|
|
|
cookie_created: None,
|
2019-03-29 22:18:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-10 03:14:12 +00:00
|
|
|
fn get_root_cert(&self) -> reqwest::Certificate {
|
|
|
|
let mut buf = Vec::new();
|
2020-07-09 06:02:10 +00:00
|
|
|
|
2020-07-10 03:14:12 +00:00
|
|
|
// read a local binary DER encoded certificate
|
|
|
|
File::open(&self.root_cert_file)
|
|
|
|
.expect("Could not open root cert file")
|
|
|
|
.read_to_end(&mut buf)
|
|
|
|
.expect("Could not read root cert file");
|
2020-07-09 06:02:10 +00:00
|
|
|
|
2021-06-14 01:01:23 +00:00
|
|
|
reqwest::Certificate::from_der(&buf).expect("Could not load DER root cert file")
|
2020-07-10 03:14:12 +00:00
|
|
|
}
|
2020-07-09 06:02:10 +00:00
|
|
|
|
2021-12-11 00:29:38 +00:00
|
|
|
fn get_http_client(&self) -> reqwest::blocking::Client {
|
|
|
|
let mut client =
|
|
|
|
reqwest::blocking::Client::builder().redirect(reqwest::redirect::Policy::none());
|
2020-07-09 06:02:10 +00:00
|
|
|
|
2020-07-10 03:14:12 +00:00
|
|
|
if !&self.root_cert_file.is_empty() {
|
|
|
|
let cert = self.get_root_cert();
|
2020-07-09 06:02:10 +00:00
|
|
|
client = client.add_root_certificate(cert);
|
|
|
|
}
|
|
|
|
|
2021-06-14 01:01:23 +00:00
|
|
|
client.build().expect("Failed to build http client")
|
2020-07-09 06:02:10 +00:00
|
|
|
}
|
|
|
|
|
2019-03-29 22:40:26 +00:00
|
|
|
/// Authenticate client
|
2021-06-14 01:01:23 +00:00
|
|
|
fn auth(&mut self) -> Result<Response, ResponseError> {
|
2019-03-29 22:18:25 +00:00
|
|
|
let cookie_created = Instant::now();
|
2020-07-10 03:14:12 +00:00
|
|
|
let client = self.get_http_client();
|
2019-03-29 22:40:26 +00:00
|
|
|
let result = client
|
2019-03-29 22:18:25 +00:00
|
|
|
.post(format!("{}{}", &self.url, "/admin/").as_str())
|
|
|
|
.form(&[("token", &self.admin_token)])
|
2021-06-14 01:01:23 +00:00
|
|
|
.send()?
|
|
|
|
.error_for_status()?;
|
|
|
|
|
|
|
|
let cookie = result
|
|
|
|
.headers()
|
|
|
|
.get(reqwest::header::SET_COOKIE)
|
|
|
|
.ok_or_else(|| {
|
|
|
|
ResponseError::ApiError(String::from("Could not read authentication cookie"))
|
|
|
|
})?;
|
|
|
|
|
|
|
|
self.cookie = cookie.to_str().map(String::from).ok();
|
|
|
|
self.cookie_created = Some(cookie_created);
|
2019-03-29 22:18:25 +00:00
|
|
|
|
2021-06-14 01:01:23 +00:00
|
|
|
Ok(result)
|
2019-03-29 22:18:25 +00:00
|
|
|
}
|
|
|
|
|
2021-06-14 01:01:23 +00:00
|
|
|
fn cookie_expired(&self) -> bool {
|
2019-03-29 22:18:25 +00:00
|
|
|
match &self.cookie {
|
2021-06-14 01:01:23 +00:00
|
|
|
Some(_) => self
|
|
|
|
.cookie_created
|
|
|
|
.map_or(true, |created| (created.elapsed() >= COOKIE_LIFESPAN)),
|
|
|
|
None => true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Ensure that the client has a current auth cookie
|
|
|
|
fn ensure_auth(&mut self) -> Result<(), ResponseError> {
|
|
|
|
if self.cookie_expired() {
|
|
|
|
match self.auth() {
|
|
|
|
Ok(_) => Ok(()),
|
|
|
|
Err(err) => Err(err),
|
|
|
|
}?
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
2019-03-29 22:18:25 +00:00
|
|
|
}
|
|
|
|
|
2019-03-29 22:40:26 +00:00
|
|
|
/// Make an authenticated GET to Bitwarden Admin
|
2021-06-14 01:01:23 +00:00
|
|
|
fn get(&mut self, path: &str) -> Result<Response, ResponseError> {
|
|
|
|
self.ensure_auth()?;
|
2019-03-29 22:18:25 +00:00
|
|
|
|
2021-06-14 01:01:23 +00:00
|
|
|
let url = format!("{}/admin{}", &self.url, path);
|
|
|
|
let client = self.get_http_client();
|
|
|
|
let request = client.get(url.as_str()).header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.cookie
|
|
|
|
.as_ref()
|
|
|
|
.expect("No cookie found to add to header")
|
|
|
|
.clone(),
|
|
|
|
);
|
|
|
|
|
|
|
|
let response = request.send()?.error_for_status()?;
|
|
|
|
|
|
|
|
Ok(response)
|
2019-03-29 22:18:25 +00:00
|
|
|
}
|
|
|
|
|
2019-03-29 22:40:26 +00:00
|
|
|
/// Make authenticated POST to Bitwarden Admin with JSON data
|
2021-06-14 01:01:23 +00:00
|
|
|
fn post(
|
|
|
|
&mut self,
|
|
|
|
path: &str,
|
|
|
|
json: &HashMap<String, String>,
|
|
|
|
) -> Result<Response, ResponseError> {
|
|
|
|
self.ensure_auth()?;
|
|
|
|
|
|
|
|
let url = format!("{}/admin{}", &self.url, path);
|
|
|
|
let client = self.get_http_client();
|
|
|
|
let request = client.post(url.as_str()).json(&json).header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.cookie
|
|
|
|
.as_ref()
|
|
|
|
.expect("No cookie found to add to header")
|
|
|
|
.clone(),
|
|
|
|
);
|
2019-03-29 22:18:25 +00:00
|
|
|
|
2021-06-14 01:01:23 +00:00
|
|
|
let response = request.send()?.error_for_status()?;
|
|
|
|
|
|
|
|
Ok(response)
|
2019-03-29 22:18:25 +00:00
|
|
|
}
|
|
|
|
|
2019-03-29 22:40:26 +00:00
|
|
|
/// Invite user with provided email
|
2021-06-14 01:01:23 +00:00
|
|
|
pub fn invite(&mut self, email: &str) -> Result<Response, ResponseError> {
|
2019-03-29 22:18:25 +00:00
|
|
|
let mut json = HashMap::new();
|
|
|
|
json.insert("email".to_string(), email.to_string());
|
|
|
|
|
|
|
|
self.post("/invite", &json)
|
|
|
|
}
|
2019-04-05 18:49:32 +00:00
|
|
|
|
|
|
|
/// Get all existing users
|
2021-06-14 01:01:23 +00:00
|
|
|
pub fn users(&mut self) -> Result<Vec<User>, ResponseError> {
|
|
|
|
let all_users: Vec<User> = self.get("/users")?.json()?;
|
2019-04-05 18:49:32 +00:00
|
|
|
Ok(all_users)
|
|
|
|
}
|
2019-03-29 22:18:25 +00:00
|
|
|
}
|