rudimentary optional authentication for mutating operations

feat/vaults
Tomáš Mládek 2022-03-24 11:10:51 +01:00
parent 22afee0e16
commit 5bb36e9ec6
4 changed files with 233 additions and 5 deletions

114
Cargo.lock generated
View File

@ -1361,6 +1361,20 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jsonwebtoken"
version = "8.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "012bb02250fdd38faa5feee63235f7a459974440b9b57593822414c31f92839e"
dependencies = [
"base64",
"pem",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]]
name = "kamadak-exif"
version = "0.5.4"
@ -1745,6 +1759,17 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "num-bigint"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.44"
@ -1796,6 +1821,15 @@ dependencies = [
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15"
dependencies = [
"libc",
]
[[package]]
name = "once_cell"
version = "1.9.0"
@ -1862,6 +1896,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "pem"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947"
dependencies = [
"base64",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
@ -2034,6 +2077,15 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quickcheck"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
dependencies = [
"rand 0.8.4",
]
[[package]]
name = "quote"
version = "1.0.14"
@ -2231,6 +2283,21 @@ dependencies = [
"quick-error",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi 0.3.9",
]
[[package]]
name = "rustc_version"
version = "0.2.3"
@ -2390,6 +2457,18 @@ dependencies = [
"libc",
]
[[package]]
name = "simple_asn1"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a762b1c38b9b990c694b9c2f8abe3372ce6a9ceaae6bca39cfc46e054f45745"
dependencies = [
"num-bigint",
"num-traits",
"thiserror",
"time 0.3.7",
]
[[package]]
name = "siphasher"
version = "0.3.9"
@ -2429,6 +2508,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "standback"
version = "0.2.17"
@ -2633,11 +2718,24 @@ dependencies = [
"libc",
"standback",
"stdweb",
"time-macros",
"time-macros 0.1.1",
"version_check 0.9.4",
"winapi 0.3.9",
]
[[package]]
name = "time"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d"
dependencies = [
"itoa 1.0.1",
"libc",
"num_threads",
"quickcheck",
"time-macros 0.2.3",
]
[[package]]
name = "time-macros"
version = "0.1.1"
@ -2648,6 +2746,12 @@ dependencies = [
"time-macros-impl",
]
[[package]]
name = "time-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6"
[[package]]
name = "time-macros-impl"
version = "0.1.2"
@ -2899,6 +3003,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d86a8dc7f45e4c1b0d30e43038c38f274e77af056aa5f74b93c2cf9eb3c1c836"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "upend"
version = "0.0.61"
@ -2923,6 +3033,7 @@ dependencies = [
"id3",
"image",
"is_executable",
"jsonwebtoken",
"kamadak-exif",
"lazy_static",
"lexpr",
@ -2933,6 +3044,7 @@ dependencies = [
"nonempty",
"once_cell",
"opener",
"rand 0.8.4",
"rayon",
"regex",
"serde",

View File

@ -38,6 +38,7 @@ actix-files = "^0.5"
actix-rt = "^2.0"
actix-web = "^3.3"
actix_derive = "^0.5"
jsonwebtoken = "8"
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
@ -46,13 +47,16 @@ lexpr = "0.2.6"
regex = "1"
bs58 = "^0.4"
filebuffer = "0.4.0"
tiny-keccak = { version = "2.0", features = ["k12"] }
unsigned-varint = { version = "^0", features = ["std"] }
uuid = { version = "0.8", features = ["v4"] }
filebuffer = "0.4.0"
tempfile = "^3.2.0"
walkdir = "2"
rand = "0.8"
mime = "^0.3.16"
tree_magic_mini = "3.0.2"

View File

@ -12,6 +12,7 @@ use actix_web::{middleware, App, HttpServer};
use anyhow::Result;
use clap::{App as ClapApp, Arg};
use log::{debug, info, warn};
use rand::{thread_rng, Rng};
use std::sync::Arc;
use crate::{
@ -85,6 +86,20 @@ fn main() -> Result<()> {
.takes_value(true)
.long("name")
.help("Name of the vault."),
)
.arg(
Arg::with_name("SECRET")
.takes_value(true)
.long("secret")
.env("UPEND_SECRET")
.help("Secret to use for authentication."),
)
.arg(
Arg::with_name("KEY")
.takes_value(true)
.long("key")
.env("UPEND_KEY")
.help("Authentication key users must supply."),
);
let matches = app.get_matches();
@ -142,6 +157,21 @@ fn main() -> Result<()> {
.parse()
.expect("Incorrect bind format.");
let secret = matches
.value_of("SECRET")
.map(String::from)
.unwrap_or_else(|| {
warn!("No secret supplied, generating one at random.");
thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(32)
.map(char::from)
.collect()
});
let key = matches.value_of("KEY").map(String::from);
let state = routes::State {
upend: upend.clone(),
vault_name: Some(
@ -160,6 +190,8 @@ fn main() -> Result<()> {
job_container: job_container.clone(),
preview_store,
desktop_enabled,
secret,
key,
};
// Start HTTP server
@ -175,6 +207,7 @@ fn main() -> Result<()> {
.app_data(actix_web::web::PayloadConfig::new(4_294_967_296))
.data(state.clone())
.wrap(middleware::Logger::default().exclude("/api/jobs"))
.service(routes::login)
.service(routes::get_raw)
.service(routes::get_thumbnail)
.service(routes::get_query)

View File

@ -12,14 +12,19 @@ use crate::util::hash::{b58_decode, b58_encode, Hashable};
use crate::util::jobs::JobContainer;
use actix_files::NamedFile;
use actix_multipart::Multipart;
use actix_web::error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound};
use actix_web::http::header::{CacheControl, CacheDirective, ContentDisposition, DispositionType};
use actix_web::error::{
ErrorBadRequest, ErrorInternalServerError, ErrorNotFound, ErrorUnauthorized,
};
use actix_web::{delete, error, get, post, put, web, Either, Error, HttpResponse};
use actix_web::{http, Responder};
use actix_web::{
http::header::{CacheControl, CacheDirective, ContentDisposition, DispositionType},
HttpRequest,
};
use anyhow::{anyhow, Result};
use futures_util::TryStreamExt;
use log::{debug, info, trace, warn};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::convert::{TryFrom, TryInto};
use std::fs;
@ -41,6 +46,69 @@ pub struct State {
pub job_container: JobContainer,
pub preview_store: Option<Arc<PreviewStore>>,
pub desktop_enabled: bool,
pub secret: String,
pub key: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct JwtClaims {
exp: usize,
}
#[derive(Deserialize)]
pub struct LoginRequest {
key: String,
}
#[post("/api/auth/login")]
pub async fn login(
state: web::Data<State>,
payload: web::Json<LoginRequest>,
) -> Result<HttpResponse, Error> {
if state.key.is_none() || Some(&payload.key) == state.key.as_ref() {
let claims = JwtClaims {
exp: (SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(ErrorInternalServerError)?
.as_secs()
+ 7 * 24 * 60 * 60) as usize,
};
let token = jsonwebtoken::encode(
&jsonwebtoken::Header::default(),
&claims,
&jsonwebtoken::EncodingKey::from_secret(state.secret.as_ref()),
)
.map_err(ErrorInternalServerError)?;
Ok(HttpResponse::Ok().json(json!({ "token": token })))
} else {
Err(ErrorUnauthorized("Incorrect token."))
}
}
fn check_auth(req: &HttpRequest, state: &State) -> Result<(), actix_web::Error> {
if let Some(key) = &state.key {
if let Some(auth_header) = req.headers().get("Authorization") {
let auth_header = auth_header.to_str().map_err(|err| {
ErrorBadRequest(format!("Invalid value in Authorization header: {err:?}"))
})?;
let token = jsonwebtoken::decode::<JwtClaims>(
auth_header,
&jsonwebtoken::DecodingKey::from_secret(key.as_ref()),
&jsonwebtoken::Validation::default(),
);
token
.map(|_| ())
.map_err(|err| ErrorUnauthorized(format!("Invalid token: {err:?}")))
} else {
Err(ErrorUnauthorized("Authorization required."))
}
} else {
Ok(())
}
}
#[derive(Deserialize)]
@ -303,9 +371,12 @@ impl TryInto<Address> for InAddress {
#[put("/api/obj")]
pub async fn put_object(
req: HttpRequest,
state: web::Data<State>,
payload: Either<web::Json<InEntry>, Multipart>,
) -> Result<HttpResponse, Error> {
check_auth(&req, &state)?;
let (entry_address, entity_address) = match payload {
Either::A(in_entry) => {
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
@ -494,10 +565,12 @@ pub async fn put_object(
#[put("/api/obj/{address}/{attribute}")]
pub async fn put_object_attribute(
req: HttpRequest,
state: web::Data<State>,
web::Path((address, attribute)): web::Path<(Address, String)>,
value: web::Json<EntryValue>,
) -> Result<HttpResponse, Error> {
check_auth(&req, &state)?;
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
let new_address = web::block(move || {
@ -526,9 +599,12 @@ pub async fn put_object_attribute(
#[delete("/api/obj/{address_str}")]
pub async fn delete_object(
req: HttpRequest,
state: web::Data<State>,
address: web::Path<Address>,
) -> Result<HttpResponse, Error> {
check_auth(&req, &state)?;
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
let _ = web::block(move || connection.remove_object(address.into_inner()))
.await
@ -649,9 +725,12 @@ pub struct RescanRequest {
#[post("/api/refresh")]
pub async fn api_refresh(
req: HttpRequest,
state: web::Data<State>,
web::Query(query): web::Query<RescanRequest>,
) -> Result<HttpResponse, Error> {
check_auth(&req, &state)?;
block_background::<_, _, anyhow::Error>(move || {
let _ = crate::filesystem::rescan_vault(
state.upend.clone(),