add local auth to registry

This commit is contained in:
Aiden McClelland
2025-12-17 18:57:56 -07:00
parent cd70fa4c32
commit 7b3c74179b
18 changed files with 417 additions and 233 deletions

15
core/Cargo.lock generated
View File

@@ -5290,7 +5290,7 @@ dependencies = [
"nix 0.30.1",
"patch-db-macro",
"serde",
"serde_cbor",
"serde_cbor 0.11.1",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -6477,7 +6477,7 @@ dependencies = [
[[package]]
name = "rpc-toolkit"
version = "0.3.2"
source = "git+https://github.com/Start9Labs/rpc-toolkit.git?rev=068db90#068db905ee38a7da97cc4a43b806409204e73723"
source = "git+https://github.com/Start9Labs/rpc-toolkit.git#81d18147fd0ca9725b820c010c006e8a2cada322"
dependencies = [
"async-stream",
"async-trait",
@@ -6494,6 +6494,7 @@ dependencies = [
"pin-project",
"reqwest",
"serde",
"serde_cbor 0.11.2",
"serde_json",
"thiserror 2.0.17",
"tokio",
@@ -6933,6 +6934,16 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
dependencies = [
"half 1.8.3",
"serde",
]
[[package]]
name = "serde_core"
version = "1.0.228"

View File

@@ -212,8 +212,8 @@ reqwest = { version = "0.12.25", features = [
] }
reqwest_cookie_store = "0.9.0"
rpassword = "7.2.0"
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", rev = "068db90" }
rust-argon2 = "3.0.0"
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" }
safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
semver = { version = "1.0.20", features = ["serde"] }
serde = { version = "1.0", features = ["derive", "rc"] }

View File

@@ -14,8 +14,8 @@ use tracing::instrument;
use ts_rs::TS;
use crate::context::{CliContext, RpcContext};
use crate::middleware::auth::{
AsLogoutSessionId, AuthContext, HasLoggedOutSessions, HashSessionToken, LoginRes,
use crate::middleware::auth::session::{
AsLogoutSessionId, HasLoggedOutSessions, HashSessionToken, LoginRes, SessionAuthContext,
};
use crate::prelude::*;
use crate::util::crypto::EncryptedWire;
@@ -110,7 +110,7 @@ impl std::str::FromStr for PasswordType {
})
}
}
pub fn auth<C: Context, AC: AuthContext>() -> ParentHandler<C>
pub fn auth<C: Context, AC: SessionAuthContext>() -> ParentHandler<C>
where
CliContext: CallRemote<AC>,
{
@@ -173,7 +173,7 @@ fn gen_pwd() {
}
#[instrument(skip_all)]
async fn cli_login<C: AuthContext>(
async fn cli_login<C: SessionAuthContext>(
HandlerArgs {
context: ctx,
parent_method,
@@ -227,7 +227,7 @@ pub struct LoginParams {
}
#[instrument(skip_all)]
pub async fn login_impl<C: AuthContext>(
pub async fn login_impl<C: SessionAuthContext>(
ctx: C,
LoginParams {
password,
@@ -283,7 +283,7 @@ pub struct LogoutParams {
session: InternedString,
}
pub async fn logout<C: AuthContext>(
pub async fn logout<C: SessionAuthContext>(
ctx: C,
LogoutParams { session }: LogoutParams,
) -> Result<Option<HasLoggedOutSessions>, Error> {
@@ -312,7 +312,7 @@ pub struct SessionList {
sessions: Sessions,
}
pub fn session<C: Context, AC: AuthContext>() -> ParentHandler<C>
pub fn session<C: Context, AC: SessionAuthContext>() -> ParentHandler<C>
where
CliContext: CallRemote<AC>,
{
@@ -379,7 +379,7 @@ pub struct ListParams {
// #[command(display(display_sessions))]
#[instrument(skip_all)]
pub async fn list<C: AuthContext>(
pub async fn list<C: SessionAuthContext>(
ctx: C,
ListParams { session, .. }: ListParams,
) -> Result<SessionList, Error> {
@@ -418,7 +418,10 @@ pub struct KillParams {
}
#[instrument(skip_all)]
pub async fn kill<C: AuthContext>(ctx: C, KillParams { ids }: KillParams) -> Result<(), Error> {
pub async fn kill<C: SessionAuthContext>(
ctx: C,
KillParams { ids }: KillParams,
) -> Result<(), Error> {
HasLoggedOutSessions::new(ids.into_iter().map(KillSessionId::new), &ctx).await?;
Ok(())
}

View File

@@ -22,7 +22,7 @@ use crate::db::model::{Database, DatabaseModel};
use crate::disk::mount::backup::BackupMountGuard;
use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
use crate::middleware::auth::AuthContext;
use crate::middleware::auth::session::SessionAuthContext;
use crate::notifications::{NotificationLevel, notify};
use crate::prelude::*;
use crate::util::io::{AtomicFile, dir_copy};

View File

@@ -24,7 +24,7 @@ use super::setup::CURRENT_SECRET;
use crate::context::config::{ClientConfig, local_config_path};
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::developer::{OS_DEVELOPER_KEY_PATH, default_developer_key_path};
use crate::middleware::auth::AuthContext;
use crate::middleware::auth::local::LocalAuthContext;
use crate::prelude::*;
use crate::rpc_continuations::Guid;
use crate::util::io::read_file_to_string;
@@ -307,7 +307,7 @@ impl CallRemote<RpcContext> for CliContext {
)
.with_kind(crate::ErrorKind::Network)?;
}
crate::middleware::signature::call_remote(
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),
@@ -320,7 +320,7 @@ impl CallRemote<RpcContext> for CliContext {
}
impl CallRemote<DiagnosticContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
crate::middleware::signature::call_remote(
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),
@@ -333,7 +333,7 @@ impl CallRemote<DiagnosticContext> for CliContext {
}
impl CallRemote<InitContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
crate::middleware::signature::call_remote(
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),
@@ -346,7 +346,7 @@ impl CallRemote<InitContext> for CliContext {
}
impl CallRemote<SetupContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
crate::middleware::signature::call_remote(
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),
@@ -359,7 +359,7 @@ impl CallRemote<SetupContext> for CliContext {
}
impl CallRemote<InstallContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
crate::middleware::signature::call_remote(
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),

View File

@@ -20,7 +20,7 @@ use crate::db::model::Database;
use crate::db::model::public::ServerStatus;
use crate::developer::OS_DEVELOPER_KEY_PATH;
use crate::hostname::Hostname;
use crate::middleware::auth::AuthContext;
use crate::middleware::auth::local::LocalAuthContext;
use crate::net::gateway::UpgradableListener;
use crate::net::net_controller::{NetController, NetService};
use crate::net::socks::DEFAULT_SOCKS_LISTEN;

View File

@@ -0,0 +1,101 @@
use base64::Engine;
use basic_cookies::Cookie;
use http::HeaderValue;
use http::header::COOKIE;
use rand::random;
use rpc_toolkit::yajrc::{RpcError, RpcResponse};
use rpc_toolkit::{Context, Empty, Middleware};
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use crate::context::RpcContext;
use crate::prelude::*;
use crate::util::Invoke;
use crate::util::io::{create_file_mod, read_file_to_string};
use crate::util::serde::BASE64;
pub trait LocalAuthContext: Context {
const LOCAL_AUTH_COOKIE_PATH: &str;
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str;
fn init_auth_cookie() -> impl Future<Output = Result<(), Error>> + Send {
async {
let mut file = create_file_mod(Self::LOCAL_AUTH_COOKIE_PATH, 0o640).await?;
file.write_all(BASE64.encode(random::<[u8; 32]>()).as_bytes())
.await?;
file.sync_all().await?;
drop(file);
Command::new("chown")
.arg(Self::LOCAL_AUTH_COOKIE_OWNERSHIP)
.arg(Self::LOCAL_AUTH_COOKIE_PATH)
.invoke(crate::ErrorKind::Filesystem)
.await?;
Ok(())
}
}
}
impl LocalAuthContext for RpcContext {
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/rpc.authcookie";
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:startos";
}
fn unauthorized() -> Error {
Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization)
}
async fn check_from_header<C: LocalAuthContext>(header: Option<&HeaderValue>) -> Result<(), Error> {
if let Some(cookie_header) = header {
let cookies = Cookie::parse(
cookie_header
.to_str()
.with_kind(crate::ErrorKind::Authorization)?,
)
.with_kind(crate::ErrorKind::Authorization)?;
if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "local") {
return check_cookie::<C>(cookie).await;
}
}
Err(unauthorized())
}
async fn check_cookie<C: LocalAuthContext>(local: &Cookie<'_>) -> Result<(), Error> {
if let Ok(token) = read_file_to_string(C::LOCAL_AUTH_COOKIE_PATH).await {
if local.get_value() == &*token {
return Ok(());
}
}
Err(unauthorized())
}
#[derive(Clone)]
pub struct LocalAuth {
cookie: Option<HeaderValue>,
}
impl LocalAuth {
pub fn new() -> Self {
Self { cookie: None }
}
}
impl<C: LocalAuthContext> Middleware<C> for LocalAuth {
type Metadata = Empty;
async fn process_http_request(
&mut self,
_: &C,
request: &mut axum::extract::Request,
) -> Result<(), axum::response::Response> {
self.cookie = request.headers().get(COOKIE).cloned();
Ok(())
}
async fn process_rpc_request(
&mut self,
_: &C,
_: Self::Metadata,
_: &mut rpc_toolkit::RpcRequest,
) -> Result<(), rpc_toolkit::RpcResponse> {
check_from_header::<C>(self.cookie.as_ref())
.await
.map_err(|e| RpcResponse::from(RpcError::from(e)))
}
}

View File

@@ -0,0 +1,112 @@
use axum::extract::Request;
use axum::response::Response;
use rpc_toolkit::{Context, DynMiddleware, Middleware, RpcRequest, RpcResponse};
use serde::Deserialize;
use crate::context::RpcContext;
use crate::db::model::Database;
use crate::middleware::auth::local::{LocalAuth, LocalAuthContext};
use crate::middleware::auth::session::{SessionAuth, SessionAuthContext};
use crate::middleware::auth::signature::{SignatureAuth, SignatureAuthContext};
use crate::prelude::*;
use crate::util::serde::const_true;
pub mod local;
pub mod session;
pub mod signature;
pub trait DbContext: Context {
type Database: HasModel<Model = Model<Self::Database>> + Send + Sync;
fn db(&self) -> &TypedPatchDb<Self::Database>;
}
impl DbContext for RpcContext {
type Database = Database;
fn db(&self) -> &TypedPatchDb<Self::Database> {
&self.db
}
}
#[derive(Deserialize)]
pub struct Metadata {
#[serde(default = "const_true")]
authenticated: bool,
}
pub struct Auth<C: Context>(Vec<DynMiddleware<C>>);
impl<C: Context> Clone for Auth<C> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<C: Context> Auth<C> {
pub fn new() -> Self {
Self(Vec::new())
}
}
impl<C: LocalAuthContext> Auth<C> {
pub fn with_local_auth(mut self) -> Self {
self.0.push(DynMiddleware::new(LocalAuth::new()));
self
}
}
impl<C: SignatureAuthContext> Auth<C> {
pub fn with_signature_auth(mut self) -> Self {
self.0.push(DynMiddleware::new(SignatureAuth::new()));
self
}
}
impl<C: SessionAuthContext> Auth<C> {
pub fn with_session_auth(mut self) -> Self {
self.0.push(DynMiddleware::new(SessionAuth::new()));
self
}
}
impl<C: Context> Middleware<C> for Auth<C> {
type Metadata = Value;
async fn process_http_request(
&mut self,
context: &C,
request: &mut Request,
) -> Result<(), Response> {
for middleware in self.0.iter_mut() {
middleware.process_http_request(context, request).await?;
}
Ok(())
}
async fn process_rpc_request(
&mut self,
context: &C,
metadata: Self::Metadata,
request: &mut RpcRequest,
) -> Result<(), RpcResponse> {
let m: Metadata =
from_value(metadata.clone()).map_err(|e| RpcResponse::from_result(Err(e)))?;
if m.authenticated {
let mut err = None;
for middleware in self.0.iter_mut() {
if let Err(e) = middleware
.process_rpc_request(context, metadata.clone(), request)
.await
{
err = Some(e);
} else {
return Ok(());
}
}
if let Some(e) = err {
return Err(e);
}
}
Ok(())
}
async fn process_rpc_response(&mut self, context: &C, response: &mut RpcResponse) {
for middleware in self.0.iter_mut() {
middleware.process_rpc_response(context, response).await;
}
}
async fn process_http_response(&mut self, context: &C, response: &mut Response) {
for middleware in self.0.iter_mut() {
middleware.process_http_response(context, response).await;
}
}
}

View File

@@ -1,32 +1,23 @@
use std::borrow::Borrow;
use std::collections::BTreeSet;
use std::future::Future;
use std::ops::Deref;
use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::extract::Request;
use axum::response::Response;
use base64::Engine;
use basic_cookies::Cookie;
use chrono::Utc;
use color_eyre::eyre::eyre;
use digest::Digest;
use http::HeaderValue;
use http::header::{COOKIE, USER_AGENT};
use imbl_value::{InternedString, json};
use rand::random;
use rpc_toolkit::yajrc::INTERNAL_ERROR;
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tokio::sync::Mutex;
use sha2::{Digest, Sha256};
use crate::auth::{Sessions, check_password, write_shadow};
use crate::context::RpcContext;
use crate::middleware::signature::{SignatureAuth, SignatureAuthContext};
use crate::middleware::auth::DbContext;
use crate::prelude::*;
use crate::rpc_continuations::OpenAuthedContinuations;
use crate::util::Invoke;
@@ -34,24 +25,7 @@ use crate::util::io::{create_file_mod, read_file_to_string};
use crate::util::serde::{BASE64, const_true};
use crate::util::sync::SyncMutex;
pub trait AuthContext: SignatureAuthContext {
const LOCAL_AUTH_COOKIE_PATH: &str;
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str;
fn init_auth_cookie() -> impl Future<Output = Result<(), Error>> + Send {
async {
let mut file = create_file_mod(Self::LOCAL_AUTH_COOKIE_PATH, 0o640).await?;
file.write_all(BASE64.encode(random::<[u8; 32]>()).as_bytes())
.await?;
file.sync_all().await?;
drop(file);
Command::new("chown")
.arg(Self::LOCAL_AUTH_COOKIE_OWNERSHIP)
.arg(Self::LOCAL_AUTH_COOKIE_PATH)
.invoke(crate::ErrorKind::Filesystem)
.await?;
Ok(())
}
}
pub trait SessionAuthContext: DbContext {
fn ephemeral_sessions(&self) -> &SyncMutex<Sessions>;
fn open_authed_continuations(&self) -> &OpenAuthedContinuations<Option<InternedString>>;
fn access_sessions(db: &mut Model<Self::Database>) -> &mut Model<Sessions>;
@@ -62,9 +36,7 @@ pub trait AuthContext: SignatureAuthContext {
}
}
impl AuthContext for RpcContext {
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/rpc.authcookie";
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:startos";
impl SessionAuthContext for RpcContext {
fn ephemeral_sessions(&self) -> &SyncMutex<Sessions> {
&self.ephemeral_sessions
}
@@ -103,7 +75,7 @@ pub trait AsLogoutSessionId {
pub struct HasLoggedOutSessions(());
impl HasLoggedOutSessions {
pub async fn new<C: AuthContext>(
pub async fn new<C: SessionAuthContext>(
sessions: impl IntoIterator<Item = impl AsLogoutSessionId>,
ctx: &C,
) -> Result<Self, Error> {
@@ -134,90 +106,6 @@ impl HasLoggedOutSessions {
}
}
/// Used when we need to know that we have logged in with a valid user
#[derive(Clone)]
pub struct HasValidSession(SessionType);
#[derive(Clone)]
enum SessionType {
Local,
Session(HashSessionToken),
}
impl HasValidSession {
pub async fn from_header<C: AuthContext>(
header: Option<&HeaderValue>,
ctx: &C,
) -> Result<Self, Error> {
if let Some(cookie_header) = header {
let cookies = Cookie::parse(
cookie_header
.to_str()
.with_kind(crate::ErrorKind::Authorization)?,
)
.with_kind(crate::ErrorKind::Authorization)?;
if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "local") {
if let Ok(s) = Self::from_local::<C>(cookie).await {
return Ok(s);
}
}
if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "session") {
if let Ok(s) = Self::from_session(HashSessionToken::from_cookie(cookie), ctx).await
{
return Ok(s);
}
}
}
Err(Error::new(
eyre!("UNAUTHORIZED"),
crate::ErrorKind::Authorization,
))
}
pub async fn from_session<C: AuthContext>(
session_token: HashSessionToken,
ctx: &C,
) -> Result<Self, Error> {
let session_hash = session_token.hashed();
if !ctx.ephemeral_sessions().mutate(|s| {
if let Some(session) = s.0.get_mut(session_hash) {
session.last_active = Utc::now();
true
} else {
false
}
}) {
ctx.db()
.mutate(|db| {
C::access_sessions(db)
.as_idx_mut(session_hash)
.ok_or_else(|| {
Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization)
})?
.mutate(|s| {
s.last_active = Utc::now();
Ok(())
})
})
.await
.result?;
}
Ok(Self(SessionType::Session(session_token)))
}
pub async fn from_local<C: AuthContext>(local: &Cookie<'_>) -> Result<Self, Error> {
let token = read_file_to_string(C::LOCAL_AUTH_COOKIE_PATH).await?;
if local.get_value() == &*token {
Ok(Self(SessionType::Local))
} else {
Err(Error::new(
eyre!("UNAUTHORIZED"),
crate::ErrorKind::Authorization,
))
}
}
}
/// When we have a need to create a new session,
/// Or when we are using internal valid authenticated service.
#[derive(Debug, Clone)]
@@ -312,51 +200,97 @@ impl Borrow<str> for HashSessionToken {
}
}
pub struct ValidSessionToken(pub HashSessionToken);
impl ValidSessionToken {
pub async fn from_header<C: SessionAuthContext>(
header: Option<&HeaderValue>,
ctx: &C,
) -> Result<Self, Error> {
if let Some(cookie_header) = header {
let cookies = Cookie::parse(
cookie_header
.to_str()
.with_kind(crate::ErrorKind::Authorization)?,
)
.with_kind(crate::ErrorKind::Authorization)?;
if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "session") {
if let Ok(s) = Self::from_session(HashSessionToken::from_cookie(cookie), ctx).await
{
return Ok(s);
}
}
}
Err(Error::new(
eyre!("UNAUTHORIZED"),
crate::ErrorKind::Authorization,
))
}
pub async fn from_session<C: SessionAuthContext>(
session_token: HashSessionToken,
ctx: &C,
) -> Result<Self, Error> {
let session_hash = session_token.hashed();
if !ctx.ephemeral_sessions().mutate(|s| {
if let Some(session) = s.0.get_mut(session_hash) {
session.last_active = Utc::now();
true
} else {
false
}
}) {
ctx.db()
.mutate(|db| {
C::access_sessions(db)
.as_idx_mut(session_hash)
.ok_or_else(|| {
Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization)
})?
.mutate(|s| {
s.last_active = Utc::now();
Ok(())
})
})
.await
.result?;
}
Ok(Self(session_token))
}
}
#[derive(Deserialize)]
pub struct Metadata {
#[serde(default = "const_true")]
authenticated: bool,
#[serde(default)]
login: bool,
#[serde(default)]
get_session: bool,
#[serde(default)]
get_signer: bool,
}
#[derive(Clone)]
pub struct Auth {
rate_limiter: Arc<Mutex<(usize, Instant)>>,
cookie: Option<HeaderValue>,
pub struct SessionAuth {
rate_limiter: Arc<SyncMutex<(usize, Instant)>>,
is_login: bool,
cookie: Option<HeaderValue>,
set_cookie: Option<HeaderValue>,
user_agent: Option<HeaderValue>,
signature_auth: SignatureAuth,
}
impl Auth {
impl SessionAuth {
pub fn new() -> Self {
Self {
rate_limiter: Arc::new(Mutex::new((0, Instant::now()))),
cookie: None,
rate_limiter: Arc::new(SyncMutex::new((0, Instant::now()))),
is_login: false,
cookie: None,
set_cookie: None,
user_agent: None,
signature_auth: SignatureAuth::new(),
}
}
}
impl<C: AuthContext> Middleware<C> for Auth {
impl<C: SessionAuthContext> Middleware<C> for SessionAuth {
type Metadata = Metadata;
async fn process_http_request(
&mut self,
context: &C,
request: &mut Request,
) -> Result<(), Response> {
self.cookie = request.headers_mut().remove(COOKIE);
self.user_agent = request.headers_mut().remove(USER_AGENT);
self.signature_auth
.process_http_request(context, request)
.await?;
async fn process_http_request(&mut self, _: &C, request: &mut Request) -> Result<(), Response> {
self.cookie = request.headers().get(COOKIE).cloned();
self.user_agent = request.headers().get(USER_AGENT).cloned();
Ok(())
}
async fn process_rpc_request(
@@ -368,56 +302,37 @@ impl<C: AuthContext> Middleware<C> for Auth {
async {
if metadata.login {
self.is_login = true;
let guard = self.rate_limiter.lock().await;
if guard.1.elapsed() < Duration::from_secs(20) && guard.0 >= 3 {
return Err(Error::new(
eyre!("Please limit login attempts to 3 per 20 seconds."),
crate::ErrorKind::RateLimited,
));
}
self.rate_limiter.mutate(|(count, time)| {
if time.elapsed() < Duration::from_secs(20) && *count >= 3 {
Err(Error::new(
eyre!("Please limit login attempts to 3 per 20 seconds."),
crate::ErrorKind::RateLimited,
))
} else {
*count += 1;
*time = Instant::now();
Ok(())
}
})?;
if let Some(user_agent) = self.user_agent.as_ref().and_then(|h| h.to_str().ok()) {
request.params["__Auth_userAgent"] =
Value::String(Arc::new(user_agent.to_owned()))
// TODO: will this panic?
}
} else if metadata.authenticated {
if self
.signature_auth
.process_rpc_request(
context,
from_value(json!({
"get_signer": metadata.get_signer
}))?,
request,
)
.await
.is_err()
{
match HasValidSession::from_header(self.cookie.as_ref(), context).await? {
HasValidSession(SessionType::Session(s)) if metadata.get_session => {
request.params["__Auth_session"] =
Value::String(Arc::new(s.hashed().deref().to_owned()));
}
_ => (),
}
} else {
let ValidSessionToken(s) =
ValidSessionToken::from_header(self.cookie.as_ref(), context).await?;
if metadata.get_session {
request.params["__Auth_session"] =
Value::String(Arc::new(s.hashed().deref().to_owned()));
}
}
Ok(())
Ok::<_, Error>(())
}
.await
.map_err(|e| RpcResponse::from_result(Err(e)))
}
async fn process_rpc_response(&mut self, _: &C, response: &mut RpcResponse) {
if self.is_login {
let mut guard = self.rate_limiter.lock().await;
if guard.1.elapsed() < Duration::from_secs(20) {
if response.result.is_err() {
guard.0 += 1;
}
} else {
guard.0 = 0;
}
guard.1 = Instant::now();
if response.result.is_ok() {
let res = std::mem::replace(&mut response.result, Err(INTERNAL_ERROR));
response.result = async {

View File

@@ -8,14 +8,14 @@ use axum::extract::Request;
use http::{HeaderMap, HeaderValue};
use reqwest::Client;
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{Context, Middleware, RpcRequest, RpcResponse};
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
use serde::Deserialize;
use serde::de::DeserializeOwned;
use tokio::sync::Mutex;
use url::Url;
use crate::context::{CliContext, RpcContext};
use crate::db::model::Database;
use crate::middleware::auth::DbContext;
use crate::prelude::*;
use crate::sign::commitment::Commitment;
use crate::sign::commitment::request::RequestCommitment;
@@ -25,11 +25,9 @@ use crate::util::serde::Base64;
pub const AUTH_SIG_HEADER: &str = "X-StartOS-Auth-Sig";
pub trait SignatureAuthContext: Context {
type Database: HasModel<Model = Model<Self::Database>> + Send + Sync;
pub trait SignatureAuthContext: DbContext {
type AdditionalMetadata: DeserializeOwned + Send;
type CheckPubkeyRes: Send;
fn db(&self) -> &TypedPatchDb<Self::Database>;
fn sig_context(
&self,
) -> impl Future<Output = impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send>
@@ -47,12 +45,8 @@ pub trait SignatureAuthContext: Context {
}
impl SignatureAuthContext for RpcContext {
type Database = Database;
type AdditionalMetadata = ();
type CheckPubkeyRes = ();
fn db(&self) -> &TypedPatchDb<Self::Database> {
&self.db
}
async fn sig_context(
&self,
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
@@ -96,7 +90,7 @@ impl SignatureAuthContext for RpcContext {
}
Err(Error::new(
eyre!("Developer Key is not authorized"),
eyre!("Key is not authorized"),
ErrorKind::IncorrectPassword,
))
}

View File

@@ -2,4 +2,3 @@ pub mod auth;
pub mod connect_info;
pub mod cors;
pub mod db;
pub mod signature;

View File

@@ -32,7 +32,8 @@ use url::Url;
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::hostname::Hostname;
use crate::middleware::auth::{Auth, HasValidSession};
use crate::middleware::auth::Auth;
use crate::middleware::auth::session::ValidSessionToken;
use crate::middleware::cors::Cors;
use crate::middleware::db::SyncDb;
use crate::net::gateway::GatewayInfo;
@@ -79,7 +80,12 @@ impl UiContext for RpcContext {
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server
.middleware(Cors::new())
.middleware(Auth::new())
.middleware(
Auth::new()
.with_local_auth()
.with_signature_auth()
.with_session_auth(),
)
.middleware(SyncDb::new())
}
fn extend_router(self, router: Router) -> Router {
@@ -404,8 +410,9 @@ async fn if_authorized<
f: F,
) -> Result<Response, Error> {
if let Err(e) =
HasValidSession::from_header(request.headers().get(http::header::COOKIE), ctx).await
ValidSessionToken::from_header(request.headers().get(http::header::COOKIE), ctx).await
{
// TODO: other auth methods
Ok(unauthorized(e, request.uri().path()))
} else {
f(request).await

View File

@@ -1,4 +1,5 @@
pub use color_eyre::eyre::eyre;
pub use imbl_value::InternedString;
pub use lazy_format::lazy_format;
pub use tracing::instrument;

View File

@@ -5,6 +5,7 @@ use std::sync::Arc;
use chrono::Utc;
use clap::Parser;
use cookie::{Cookie, Expiration, SameSite};
use http::HeaderMap;
use imbl_value::InternedString;
use patch_db::PatchDb;
@@ -21,7 +22,9 @@ use url::Url;
use crate::context::config::{CONFIG_PATH, ContextConfig};
use crate::context::{CliContext, RpcContext};
use crate::middleware::signature::SignatureAuthContext;
use crate::middleware::auth::DbContext;
use crate::middleware::auth::local::LocalAuthContext;
use crate::middleware::auth::signature::SignatureAuthContext;
use crate::prelude::*;
use crate::registry::RegistryDatabase;
use crate::registry::device_info::{DEVICE_INFO_HEADER, DeviceInfo};
@@ -29,7 +32,7 @@ use crate::registry::migrations::run_migrations;
use crate::registry::signer::SignerInfo;
use crate::rpc_continuations::RpcContinuations;
use crate::sign::AnyVerifyingKey;
use crate::util::io::append_file;
use crate::util::io::{append_file, read_file_to_string};
const DEFAULT_REGISTRY_LISTEN: SocketAddr =
SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 5959);
@@ -104,6 +107,8 @@ impl RegistryContext {
}
db.mutate(|db| run_migrations(db)).await.result?;
Self::init_auth_cookie().await?;
let tor_proxy_url = config
.tor_proxy
.clone()
@@ -169,9 +174,26 @@ impl CallRemote<RegistryContext> for CliContext {
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
let mut has_cookie = false;
if let Ok(local) = read_file_to_string(RegistryContext::LOCAL_AUTH_COOKIE_PATH).await {
self.cookie_store
.lock()
.unwrap()
.insert_raw(
&Cookie::build(("local", local))
.domain("localhost")
.expires(Expiration::Session)
.same_site(SameSite::Strict)
.build(),
&"http://localhost".parse()?,
)
.with_kind(crate::ErrorKind::Network)?;
has_cookie = true;
}
let url = if let Some(url) = self.registry_url.clone() {
url
} else if !self.registry_hostname.is_empty() {
} else if has_cookie || !self.registry_hostname.is_empty() {
let mut url: Url = format!(
"http://{}",
self.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN)
@@ -196,7 +218,7 @@ impl CallRemote<RegistryContext> for CliContext {
.cloned()
.or_else(|| url.host().as_ref().map(InternedString::from_display));
crate::middleware::signature::call_remote(
crate::middleware::auth::signature::call_remote(
self,
url,
HeaderMap::new(),
@@ -230,7 +252,7 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
method = method.strip_prefix("registry.").unwrap_or(method);
let sig_context = registry.host_str().map(InternedString::from);
crate::middleware::signature::call_remote(
crate::middleware::auth::signature::call_remote(
self,
registry,
headers,
@@ -257,13 +279,19 @@ pub struct AdminLogRecord {
pub key: AnyVerifyingKey,
}
impl SignatureAuthContext for RegistryContext {
impl DbContext for RegistryContext {
type Database = RegistryDatabase;
type AdditionalMetadata = RegistryAuthMetadata;
type CheckPubkeyRes = Option<(AnyVerifyingKey, SignerInfo)>;
fn db(&self) -> &TypedPatchDb<Self::Database> {
&self.db
}
}
impl LocalAuthContext for RegistryContext {
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/registry.authcookie";
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:root";
}
impl SignatureAuthContext for RegistryContext {
type AdditionalMetadata = RegistryAuthMetadata;
type CheckPubkeyRes = Option<(AnyVerifyingKey, SignerInfo)>;
async fn sig_context(
&self,
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {

View File

@@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::context::CliContext;
use crate::middleware::auth::Auth;
use crate::middleware::cors::Cors;
use crate::middleware::signature::SignatureAuth;
use crate::net::static_server::{bad_request, not_found, server_error};
use crate::prelude::*;
use crate::registry::context::RegistryContext;
@@ -108,7 +108,7 @@ pub fn registry_router(ctx: RegistryContext) -> Router {
any(
Server::new(move || ready(Ok(ctx.clone())), registry_api())
.middleware(Cors::new())
.middleware(SignatureAuth::new())
.middleware(Auth::new().with_local_auth().with_signature_auth())
.middleware(DeviceInfoMiddleware::new()),
)
})

View File

@@ -46,6 +46,7 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.subcommand(
"get",
from_fn_async(get::get_package)
.with_metadata("authenticated", Value::Bool(false))
.with_metadata("get_device_info", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| {

View File

@@ -9,8 +9,10 @@ use ts_rs::TS;
use crate::auth::{Sessions, check_password};
use crate::context::CliContext;
use crate::middleware::auth::AuthContext;
use crate::middleware::signature::SignatureAuthContext;
use crate::middleware::auth::DbContext;
use crate::middleware::auth::local::LocalAuthContext;
use crate::middleware::auth::session::SessionAuthContext;
use crate::middleware::auth::signature::SignatureAuthContext;
use crate::prelude::*;
use crate::rpc_continuations::OpenAuthedContinuations;
use crate::sign::AnyVerifyingKey;
@@ -19,13 +21,15 @@ use crate::tunnel::db::TunnelDatabase;
use crate::util::serde::{HandlerExtSerde, display_serializable};
use crate::util::sync::SyncMutex;
impl SignatureAuthContext for TunnelContext {
impl DbContext for TunnelContext {
type Database = TunnelDatabase;
type AdditionalMetadata = ();
type CheckPubkeyRes = ();
fn db(&self) -> &TypedPatchDb<Self::Database> {
&self.db
}
}
impl SignatureAuthContext for TunnelContext {
type AdditionalMetadata = ();
type CheckPubkeyRes = ();
async fn sig_context(
&self,
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
@@ -93,9 +97,11 @@ impl SignatureAuthContext for TunnelContext {
Ok(())
}
}
impl AuthContext for TunnelContext {
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/start-tunnel/rpc.authcookie";
impl LocalAuthContext for TunnelContext {
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/tunnel.authcookie";
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:root";
}
impl SessionAuthContext for TunnelContext {
fn access_sessions(db: &mut Model<Self::Database>) -> &mut Model<crate::auth::Sessions> {
db.as_sessions_mut()
}

View File

@@ -24,7 +24,8 @@ use crate::auth::Sessions;
use crate::context::config::ContextConfig;
use crate::context::{CliContext, RpcContext};
use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType};
use crate::middleware::auth::{Auth, AuthContext};
use crate::middleware::auth::Auth;
use crate::middleware::auth::local::LocalAuthContext;
use crate::middleware::cors::Cors;
use crate::net::forward::{PortForwardController, add_iptables_rule};
use crate::net::static_server::{EMPTY_DIR, UiContext};
@@ -279,7 +280,7 @@ impl CallRemote<TunnelContext> for CliContext {
method = method.strip_prefix("tunnel.").unwrap_or(method);
crate::middleware::signature::call_remote(
crate::middleware::auth::signature::call_remote(
self,
url,
HeaderMap::new(),
@@ -308,7 +309,7 @@ impl CallRemote<TunnelContext, TunnelUrlParams> for RpcContext {
let sig_ctx = url.host_str().map(InternedString::from_display);
crate::middleware::signature::call_remote(
crate::middleware::auth::signature::call_remote(
self,
url,
HeaderMap::new(),
@@ -331,6 +332,11 @@ impl UiContext for TunnelContext {
tunnel_api()
}
fn middleware(server: rpc_toolkit::Server<Self>) -> rpc_toolkit::HttpServer<Self> {
server.middleware(Cors::new()).middleware(Auth::new())
server.middleware(Cors::new()).middleware(
Auth::new()
.with_local_auth()
.with_signature_auth()
.with_session_auth(),
)
}
}