Basic HTTP auth for sd-server (#2314)

* basic http auth

* fix types

* Fix

* auth docs
This commit is contained in:
Oscar Beaumont 2024-04-12 11:31:38 +08:00 committed by GitHub
parent 785dd74297
commit 9f5396133b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 156 additions and 41 deletions

View file

@ -11,6 +11,7 @@ env:
CARGO_NET_RETRY: 10
RUST_BACKTRACE: short
RUSTUP_MAX_RETRIES: 10
SD_AUTH: disabled
# Cancel previous runs of the same workflow on the same branch.
concurrency:
@ -107,10 +108,10 @@ jobs:
with:
swap-size-mb: 3072
root-reserve-mb: 6144
remove-dotnet: "true"
remove-codeql: "true"
remove-haskell: "true"
remove-docker-images: "true"
remove-dotnet: 'true'
remove-codeql: 'true'
remove-haskell: 'true'
remove-docker-images: 'true'
- name: Symlink target to C:\
if: ${{ runner.os == 'Windows' }}
@ -143,7 +144,7 @@ jobs:
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: ./.github/actions/setup-rust
with:
restore-cache: "false"
restore-cache: 'false'
- name: Run rustfmt
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
@ -162,10 +163,10 @@ jobs:
with:
swap-size-mb: 3072
root-reserve-mb: 6144
remove-dotnet: "true"
remove-codeql: "true"
remove-haskell: "true"
remove-docker-images: "true"
remove-dotnet: 'true'
remove-codeql: 'true'
remove-haskell: 'true'
remove-docker-images: 'true'
- name: Symlink target to C:\
if: ${{ runner.os == 'Windows' }}

11
Cargo.lock generated
View file

@ -8679,11 +8679,13 @@ name = "sd-server"
version = "0.1.0"
dependencies = [
"axum",
"base64 0.21.7",
"http",
"include_dir",
"mime_guess",
"rspc",
"sd-core",
"secstr",
"tempfile",
"tokio",
"tracing",
@ -8781,6 +8783,15 @@ dependencies = [
"zbus",
]
[[package]]
name = "secstr"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04f657244f605c4cf38f6de5993e8bd050c8a303f86aeabff142d5c7c113e12"
dependencies = [
"libc",
]
[[package]]
name = "security-framework"
version = "2.9.2"

View file

@ -12,18 +12,17 @@ ai-models = ["sd-core/ai"]
[dependencies]
# Spacedrive Sub-crates
sd-core = { path = "../../core", features = [
"ffmpeg",
"heif",
] }
sd-core = { path = "../../core", features = ["ffmpeg", "heif"] }
axum = { workspace = true }
axum = { workspace = true, features = ["headers"] }
http = { workspace = true }
rspc = { workspace = true, features = ["axum"] }
tokio = { workspace = true, features = ["sync", "rt-multi-thread", "signal"] }
tracing = { workspace = true }
base64 = { workspace = true }
tempfile = "3.10.1"
include_dir = "0.7.3"
mime_guess = "2.0.4"
secstr = "0.5.1"

View file

@ -1,8 +1,17 @@
use std::{env, net::SocketAddr, path::Path};
use std::{collections::HashMap, env, net::SocketAddr, path::Path};
use axum::routing::get;
use axum::{
extract::{FromRequestParts, State},
headers::{authorization::Basic, Authorization},
http::Request,
middleware::{self, Next},
response::{IntoResponse, Response},
routing::get,
TypedHeader,
};
use sd_core::{custom_uri, Node};
use tracing::info;
use secstr::SecStr;
use tracing::{info, warn};
mod utils;
@ -10,6 +19,46 @@ mod utils;
static ASSETS_DIR: include_dir::Dir<'static> =
include_dir::include_dir!("$CARGO_MANIFEST_DIR/../web/dist");
#[derive(Clone)]
pub struct AppState {
auth: HashMap<String, SecStr>,
}
async fn basic_auth<B>(
State(state): State<AppState>,
request: Request<B>,
next: Next<B>,
) -> Response {
let (mut parts, body) = request.into_parts();
let Ok(TypedHeader(Authorization(hdr))) =
TypedHeader::<Authorization<Basic>>::from_request_parts(&mut parts, &()).await
else {
return Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"Spacedrive\"")
.body("Unauthorized".into_response().into_body())
.expect("hardcoded response will be valid");
};
let request = Request::from_parts(parts, body);
if state.auth.len() != 0 {
if state
.auth
.get(hdr.username())
.and_then(|pass| Some(*pass == SecStr::from(hdr.password())))
!= Some(true)
{
return Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"Spacedrive\"")
.body("Unauthorized".into_response().into_body())
.expect("hardcoded response will be valid");
}
}
next.run(request).await
}
#[tokio::main]
async fn main() {
let data_dir = match env::var("DATA_DIR") {
@ -43,6 +92,54 @@ async fn main() {
}
};
let (auth, disabled) = {
let input = env::var("SD_AUTH").unwrap_or_default();
if input == "disabled" {
(Default::default(), true)
} else {
(
input
.split(',')
.collect::<Vec<_>>()
.into_iter()
.enumerate()
.filter_map(|(i, s)| {
if s.len() == 0 {
return None;
}
let mut parts = s.split(':');
let result = parts.next().and_then(|user| {
parts
.next()
.map(|pass| (user.to_string(), SecStr::from(pass)))
});
if result.is_none() {
warn!("Found invalid credential {i}. Skipping...");
}
result
})
.collect::<HashMap<_, _>>(),
false,
)
}
};
// We require credentials in production builds (unless explicitly disabled)
if auth.len() == 0 && !disabled {
#[cfg(not(debug_assertions))]
{
warn!("The 'SD_AUTH' environment variable is not set!");
warn!("If you want to disable auth set 'SD_AUTH=disabled', or");
warn!("Provide your credentials in the following format 'SD_AUTH=username:password,username2:password2'");
std::process::exit(1);
}
}
let state = AppState { auth };
let (node, router) = match Node::new(
data_dir,
sd_core::Env {
@ -140,7 +237,8 @@ async fn main() {
#[cfg(not(feature = "assets"))]
let app = app
.route("/", get(|| async { "Spacedrive Server!" }))
.fallback(|| async { "404 Not Found: We're past the event horizon..." });
.fallback(|| async { "404 Not Found: We're past the event horizon..." })
.layer(middleware::from_fn_with_state(state, basic_auth));
let mut addr = "[::]:8080".parse::<SocketAddr>().unwrap(); // This listens on IPv6 and IPv4
addr.set_port(port);

View file

@ -34,15 +34,20 @@ You can run Spacedrive in a Docker container using the following command.
type="note"
text="For the best performance of the docker container, we recommend to run on Linux (linux/amd64). The container is not yet optimized for other platforms."
/>
<Notice
type="warning"
text="Currently, Spacedrive's Docker Server does not support authentication methods. Use at your own risk as your data will not be protected from other devices (or users) on your network. The feature is under active development and you can check when it will launch on the [roadmap](/roadmap)."
/>
```bash
docker run -d --name spacedrive -p 8080:8080 -v /var/spacedrive:/var/spacedrive ghcr.io/spacedriveapp/spacedrive/server
docker run -d --name spacedrive -p 8080:8080 -e SD_AUTH=admin,spacedrive -v /var/spacedrive:/var/spacedrive ghcr.io/spacedriveapp/spacedrive/server
```
#### Authentication
When using the Spacedrive server you can use the `SD_AUTH` environment variable to configure authentication.
Valid values:
- `SD_AUTH=disabled` - Disables authentication.
- `SD_AUTH=username:password` - Enables authentication for a single user.
- `SD_AUTH=username:password,username1:password1` - Enables authentication with multiple users (you can add as many users as you want).
### Mobile (Preview)
Take your Spacedrive library on the go with our mobile apps. You can join the betas by following the links below.

View file

@ -88,30 +88,31 @@ const OperationGroup = ({ group }: { group: MessageGroup }) => {
function calculateGroups(messages: CRDTOperation[]) {
return messages.reduce<MessageGroup[]>((acc, op) => {
const { data } = op;
// TODO: fix Typescript
// const { data } = op;
const id = JSON.stringify(op.record_id);
// const id = JSON.stringify(op.record_id);
const latest = (() => {
const latest = acc[acc.length - 1];
// const latest = (() => {
// const latest = acc[acc.length - 1];
if (!latest || latest.model !== op.model || latest.id !== id) {
const group: MessageGroup = {
model: op.model,
id,
messages: []
};
// if (!latest || latest.model !== op.model || latest.id !== id) {
// const group: MessageGroup = {
// model: op.model,
// id,
// messages: []
// };
acc.push(group);
// acc.push(group);
return group;
} else return latest;
})();
// return group;
// } else return latest;
// })();
latest.messages.push({
data,
timestamp: op.timestamp
});
// latest.messages.push({
// data,
// timestamp: op.timestamp
// });
return acc;
}, []);