feat: add JWT auth, configurable username, switch password auth to Basic
Add server-side JWT authentication with permission-based access control (read/write/delete claims). Password authentication now uses HTTP Basic auth only (replacing Bearer). Add configurable username for both server and client (--server-username/--client-username, defaults to "keep"). JWT secret supports file-based loading via --server-jwt-secret-file for Docker secrets. OPTIONS preflight requests bypass auth. HEAD mapped to read permission. Co-Authored-By: opencode <noreply@opencode.ai>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use crate::services::error::CoreError;
|
||||
use base64::Engine;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
@@ -17,17 +18,26 @@ pub struct ItemInfo {
|
||||
pub struct KeepClient {
|
||||
base_url: String,
|
||||
agent: ureq::Agent,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
jwt: Option<String>,
|
||||
}
|
||||
|
||||
impl KeepClient {
|
||||
pub fn new(base_url: &str, password: Option<String>) -> Result<Self, CoreError> {
|
||||
pub fn new(
|
||||
base_url: &str,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
jwt: Option<String>,
|
||||
) -> Result<Self, CoreError> {
|
||||
let base_url = base_url.trim_end_matches('/').to_string();
|
||||
let agent = ureq::Agent::new_with_defaults();
|
||||
Ok(Self {
|
||||
base_url,
|
||||
agent,
|
||||
username,
|
||||
password,
|
||||
jwt,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,14 +45,40 @@ impl KeepClient {
|
||||
&self.base_url
|
||||
}
|
||||
|
||||
pub fn username(&self) -> Option<&String> {
|
||||
self.username.as_ref()
|
||||
}
|
||||
|
||||
pub fn password(&self) -> Option<&String> {
|
||||
self.password.as_ref()
|
||||
}
|
||||
|
||||
pub fn jwt(&self) -> Option<&String> {
|
||||
self.jwt.as_ref()
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!("{}{}", self.base_url, path)
|
||||
}
|
||||
|
||||
/// Get the Authorization header value for the current credentials.
|
||||
///
|
||||
/// JWT token is sent as `Bearer <token>`.
|
||||
/// Password is sent as `Basic base64(username:password)`
|
||||
/// where username defaults to "keep".
|
||||
fn auth_header(&self) -> Option<String> {
|
||||
if let Some(ref jwt) = self.jwt {
|
||||
Some(format!("Bearer {jwt}"))
|
||||
} else if let Some(ref password) = self.password {
|
||||
let username = self.username.as_deref().unwrap_or("keep");
|
||||
let credentials = format!("{username}:{password}");
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(&credentials);
|
||||
Some(format!("Basic {encoded}"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_error<T>(&self, result: Result<T, ureq::Error>) -> Result<T, CoreError> {
|
||||
match result {
|
||||
Ok(v) => Ok(v),
|
||||
@@ -57,8 +93,8 @@ impl KeepClient {
|
||||
pub fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T, CoreError> {
|
||||
let url = self.url(path);
|
||||
let mut req = self.agent.get(&url);
|
||||
if let Some(ref password) = self.password {
|
||||
req = req.header("Authorization", &format!("Bearer {password}"));
|
||||
if let Some(ref auth) = self.auth_header() {
|
||||
req = req.header("Authorization", auth);
|
||||
}
|
||||
let response = self.handle_error(req.call())?;
|
||||
let body: T = self.handle_error(response.into_body().read_json())?;
|
||||
@@ -81,8 +117,8 @@ impl KeepClient {
|
||||
}
|
||||
}
|
||||
let mut req = self.agent.get(&url);
|
||||
if let Some(ref password) = self.password {
|
||||
req = req.header("Authorization", &format!("Bearer {password}"));
|
||||
if let Some(ref auth) = self.auth_header() {
|
||||
req = req.header("Authorization", auth);
|
||||
}
|
||||
let response = self.handle_error(req.call())?;
|
||||
let body: T = self.handle_error(response.into_body().read_json())?;
|
||||
@@ -92,8 +128,8 @@ impl KeepClient {
|
||||
pub fn get_bytes(&self, path: &str) -> Result<Vec<u8>, CoreError> {
|
||||
let url = self.url(path);
|
||||
let mut req = self.agent.get(&url);
|
||||
if let Some(ref password) = self.password {
|
||||
req = req.header("Authorization", &format!("Bearer {password}"));
|
||||
if let Some(ref auth) = self.auth_header() {
|
||||
req = req.header("Authorization", auth);
|
||||
}
|
||||
let response = self.handle_error(req.call())?;
|
||||
let mut body = response.into_body();
|
||||
@@ -135,8 +171,8 @@ impl KeepClient {
|
||||
}
|
||||
|
||||
let mut req = self.agent.post(&url);
|
||||
if let Some(ref password) = self.password {
|
||||
req = req.header("Authorization", &format!("Bearer {password}"));
|
||||
if let Some(ref auth) = self.auth_header() {
|
||||
req = req.header("Authorization", auth);
|
||||
}
|
||||
req = req.header("Content-Type", "application/octet-stream");
|
||||
|
||||
@@ -162,8 +198,8 @@ impl KeepClient {
|
||||
pub fn delete(&self, path: &str) -> Result<(), CoreError> {
|
||||
let url = self.url(path);
|
||||
let mut req = self.agent.delete(&url);
|
||||
if let Some(ref password) = self.password {
|
||||
req = req.header("Authorization", &format!("Bearer {password}"));
|
||||
if let Some(ref auth) = self.auth_header() {
|
||||
req = req.header("Authorization", auth);
|
||||
}
|
||||
self.handle_error(req.call())?;
|
||||
Ok(())
|
||||
@@ -254,8 +290,8 @@ impl KeepClient {
|
||||
) -> Result<(), CoreError> {
|
||||
let url = self.url(&format!("/api/item/{id}/meta"));
|
||||
let mut req = self.agent.post(&url);
|
||||
if let Some(ref password) = self.password {
|
||||
req = req.header("Authorization", &format!("Bearer {password}"));
|
||||
if let Some(ref auth) = self.auth_header() {
|
||||
req = req.header("Authorization", auth);
|
||||
}
|
||||
req = req.header("Content-Type", "application/json");
|
||||
|
||||
@@ -274,8 +310,8 @@ impl KeepClient {
|
||||
);
|
||||
|
||||
let mut req = self.agent.get(&url);
|
||||
if let Some(ref password) = self.password {
|
||||
req = req.header("Authorization", &format!("Bearer {password}"));
|
||||
if let Some(ref auth) = self.auth_header() {
|
||||
req = req.header("Authorization", auth);
|
||||
}
|
||||
|
||||
let response = self.handle_error(req.call())?;
|
||||
|
||||
Reference in New Issue
Block a user