mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
authentication
This commit is contained in:
@@ -9,7 +9,13 @@ CREATE TABLE IF NOT EXISTS tor
|
|||||||
CREATE TABLE IF NOT EXISTS session
|
CREATE TABLE IF NOT EXISTS session
|
||||||
(
|
(
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
created_at TIMESTAMP NOT NULL,
|
logged_in TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
expires_at TIMESTAMP NOT NULL,
|
logged_out TIMESTAMP,
|
||||||
metadata JSON
|
last_active TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_agent TEXT,
|
||||||
|
metadata TEXT NOT NULL DEFAULT 'null'
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS password
|
||||||
|
(
|
||||||
|
hash TEXT NOT NULL PRIMARY KEY
|
||||||
);
|
);
|
||||||
196
appmgr/src/auth.rs
Normal file
196
appmgr/src/auth.rs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
|
use basic_cookies::Cookie;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use clap::ArgMatches;
|
||||||
|
use http::header::COOKIE;
|
||||||
|
use http::HeaderValue;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use rpc_toolkit::command;
|
||||||
|
use rpc_toolkit::command_helpers::prelude::{RequestParts, ResponseParts};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::context::EitherContext;
|
||||||
|
use crate::middleware::auth::{get_id, hash_token};
|
||||||
|
use crate::util::{display_none, display_serializable, IoFormat};
|
||||||
|
use crate::{Error, ResultExt};
|
||||||
|
|
||||||
|
#[command(subcommands(login, logout))]
|
||||||
|
pub fn auth(#[context] ctx: EitherContext) -> Result<EitherContext, Error> {
|
||||||
|
Ok(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_metadata(_: &str, _: &ArgMatches<'_>) -> Result<Value, Error> {
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"platforms": ["cli"],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command(display(display_none), metadata(authenticated = false))]
|
||||||
|
pub async fn login(
|
||||||
|
#[context] ctx: EitherContext,
|
||||||
|
#[request] req: &RequestParts,
|
||||||
|
#[response] res: &mut ResponseParts,
|
||||||
|
#[arg] password: String,
|
||||||
|
#[arg(
|
||||||
|
parse(parse_metadata),
|
||||||
|
default = "",
|
||||||
|
help = "RPC Only: This value cannot be overidden from the cli"
|
||||||
|
)]
|
||||||
|
metadata: Value,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let rpc_ctx = ctx.as_rpc().unwrap();
|
||||||
|
let mut handle = rpc_ctx.secret_store.acquire().await?;
|
||||||
|
let pw_hash = sqlx::query!("SELECT hash FROM password")
|
||||||
|
.fetch_one(&mut handle)
|
||||||
|
.await?
|
||||||
|
.hash;
|
||||||
|
argon2::verify_encoded(&pw_hash, password.as_bytes()).map_err(|_| {
|
||||||
|
Error::new(
|
||||||
|
anyhow!("Password Incorrect"),
|
||||||
|
crate::ErrorKind::Authorization,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let token = base32::encode(
|
||||||
|
base32::Alphabet::RFC4648 { padding: false },
|
||||||
|
&rand::random::<[u8; 16]>(),
|
||||||
|
)
|
||||||
|
.to_lowercase();
|
||||||
|
let id = hash_token(&token);
|
||||||
|
let user_agent = req.headers.get("user-agent").and_then(|h| h.to_str().ok());
|
||||||
|
let metadata = serde_json::to_string(&metadata).with_kind(crate::ErrorKind::Database)?;
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO session (id, user_agent, metadata) VALUES (?, ?, ?)",
|
||||||
|
id,
|
||||||
|
user_agent,
|
||||||
|
metadata,
|
||||||
|
)
|
||||||
|
.execute(&mut handle)
|
||||||
|
.await?;
|
||||||
|
res.headers.insert(
|
||||||
|
"set-cookie",
|
||||||
|
HeaderValue::from_str(&format!("session={}; HttpOnly; SameSite=Strict", token))
|
||||||
|
.with_kind(crate::ErrorKind::Unknown)?, // Should be impossible, but don't want to panic
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command(display(display_none), metadata(authenticated = false))]
|
||||||
|
pub async fn logout(
|
||||||
|
#[context] ctx: EitherContext,
|
||||||
|
#[request] req: &RequestParts,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if let Some(cookie_header) = req.headers.get(COOKIE) {
|
||||||
|
let cookies = Cookie::parse(
|
||||||
|
cookie_header
|
||||||
|
.to_str()
|
||||||
|
.with_kind(crate::ErrorKind::Authorization)?,
|
||||||
|
)
|
||||||
|
.with_kind(crate::ErrorKind::Authorization)?;
|
||||||
|
if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") {
|
||||||
|
let token = session.get_value();
|
||||||
|
let id = hash_token(token);
|
||||||
|
kill(ctx, id).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct Session {
|
||||||
|
logged_in: NaiveDateTime,
|
||||||
|
last_active: NaiveDateTime,
|
||||||
|
user_agent: Option<String>,
|
||||||
|
metadata: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct SessionList {
|
||||||
|
current: String,
|
||||||
|
sessions: IndexMap<String, Session>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command(subcommands(list, kill))]
|
||||||
|
pub async fn session(#[context] ctx: EitherContext) -> Result<EitherContext, Error> {
|
||||||
|
Ok(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_sessions(arg: SessionList, matches: &ArgMatches<'_>) {
|
||||||
|
use prettytable::*;
|
||||||
|
|
||||||
|
if matches.is_present("format") {
|
||||||
|
return display_serializable(arg, matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.add_row(row![bc =>
|
||||||
|
"ID",
|
||||||
|
"LOGGED IN",
|
||||||
|
"LAST ACTIVE",
|
||||||
|
"USER AGENT",
|
||||||
|
"METADATA",
|
||||||
|
]);
|
||||||
|
for (id, session) in arg.sessions {
|
||||||
|
let mut row = row![
|
||||||
|
&id,
|
||||||
|
&format!("{}", session.logged_in),
|
||||||
|
&format!("{}", session.last_active),
|
||||||
|
session.user_agent.as_deref().unwrap_or("N/A"),
|
||||||
|
&format!("{}", session.metadata),
|
||||||
|
];
|
||||||
|
if id == arg.current {
|
||||||
|
row.iter_mut()
|
||||||
|
.map(|c| c.style(Attr::ForegroundColor(color::GREEN)))
|
||||||
|
.collect::<()>()
|
||||||
|
}
|
||||||
|
table.add_row(row);
|
||||||
|
}
|
||||||
|
table.print_tty(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command(display(display_sessions))]
|
||||||
|
pub async fn list(
|
||||||
|
#[context] ctx: EitherContext,
|
||||||
|
#[request] req: &RequestParts,
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
#[arg(long = "format")]
|
||||||
|
format: Option<IoFormat>,
|
||||||
|
) -> Result<SessionList, Error> {
|
||||||
|
Ok(SessionList {
|
||||||
|
current: get_id(req)?,
|
||||||
|
sessions: sqlx::query!(
|
||||||
|
"SELECT * FROM session WHERE logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP"
|
||||||
|
)
|
||||||
|
.fetch_all(&mut ctx.as_rpc().unwrap().secret_store.acquire().await?)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
Ok((
|
||||||
|
row.id,
|
||||||
|
Session {
|
||||||
|
logged_in: row.logged_in,
|
||||||
|
last_active: row.last_active,
|
||||||
|
user_agent: row.user_agent,
|
||||||
|
metadata: serde_json::from_str(&row.metadata)
|
||||||
|
.with_kind(crate::ErrorKind::Database)?,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Result<_, Error>>()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command(display(display_none))]
|
||||||
|
pub async fn kill(#[context] ctx: EitherContext, #[arg] id: String) -> Result<(), Error> {
|
||||||
|
let rpc_ctx = ctx.as_rpc().unwrap();
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = ?",
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.execute(&mut rpc_ctx.secret_store.acquire().await?)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ use crate::util::{display_none, display_serializable, IoFormat};
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
#[command(subcommands(hash, manifest, license, icon, instructions, docker_images))]
|
#[command(subcommands(hash, manifest, license, icon, instructions, docker_images))]
|
||||||
pub fn inspect(#[context] ctx: EitherContext) -> Result<(), Error> {
|
pub fn inspect(#[context] _ctx: EitherContext) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ lazy_static::lazy_static! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod action;
|
pub mod action;
|
||||||
|
pub mod auth;
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use basic_cookies::Cookie;
|
use basic_cookies::Cookie;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use digest::Digest;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
|
use rpc_toolkit::command_helpers::prelude::RequestParts;
|
||||||
use rpc_toolkit::hyper::header::COOKIE;
|
use rpc_toolkit::hyper::header::COOKIE;
|
||||||
use rpc_toolkit::hyper::http::Error as HttpError;
|
use rpc_toolkit::hyper::http::Error as HttpError;
|
||||||
use rpc_toolkit::hyper::{Body, Request, Response, StatusCode};
|
use rpc_toolkit::hyper::{Body, Request, Response};
|
||||||
use rpc_toolkit::rpc_server_helpers::{
|
use rpc_toolkit::rpc_server_helpers::{
|
||||||
noop2, noop3, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3,
|
noop3, noop4, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3,
|
||||||
};
|
};
|
||||||
use rpc_toolkit::yajrc::RpcMethod;
|
use rpc_toolkit::yajrc::RpcMethod;
|
||||||
use rpc_toolkit::Metadata;
|
use rpc_toolkit::Metadata;
|
||||||
use serde::Deserialize;
|
use sha2::Sha256;
|
||||||
|
|
||||||
use crate::context::RpcContext;
|
use crate::context::RpcContext;
|
||||||
use crate::{Error, ResultExt};
|
use crate::{Error, ResultExt};
|
||||||
async fn is_authed(ctx: &RpcContext, req: &Request<Body>) -> Result<bool, Error> {
|
|
||||||
if let Some(cookie_header) = req.headers().get(COOKIE) {
|
pub fn get_id(req: &RequestParts) -> Result<String, Error> {
|
||||||
|
if let Some(cookie_header) = req.headers.get(COOKIE) {
|
||||||
let cookies = Cookie::parse(
|
let cookies = Cookie::parse(
|
||||||
cookie_header
|
cookie_header
|
||||||
.to_str()
|
.to_str()
|
||||||
@@ -24,80 +27,69 @@ async fn is_authed(ctx: &RpcContext, req: &Request<Body>) -> Result<bool, Error>
|
|||||||
)
|
)
|
||||||
.with_kind(crate::ErrorKind::Authorization)?;
|
.with_kind(crate::ErrorKind::Authorization)?;
|
||||||
if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") {
|
if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") {
|
||||||
let id = session.get_value();
|
return Ok(hash_token(session.get_value()));
|
||||||
let exp = sqlx::query!("SELECT expires_at FROM session WHERE id = ?", id)
|
|
||||||
.fetch_one(&mut ctx.secret_store.acquire().await?)
|
|
||||||
.await?;
|
|
||||||
if exp.expires_at < Utc::now().naive_utc() {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn auth<Params: for<'de> Deserialize<'de> + 'static, M: Metadata>(
|
|
||||||
ctx: RpcContext,
|
|
||||||
) -> DynMiddleware<Params, M> {
|
|
||||||
Box::new(
|
|
||||||
|req: &mut Request<Body>,
|
|
||||||
metadata: M|
|
|
||||||
-> BoxFuture<
|
|
||||||
Result<Result<DynMiddlewareStage2<Params>, Response<Body>>, HttpError>,
|
|
||||||
> {
|
|
||||||
async move {
|
|
||||||
match is_authed(&ctx, req).await {
|
|
||||||
Ok(true) => Ok(Ok(noop2())),
|
|
||||||
Ok(false) => Ok(Ok({
|
|
||||||
let mut fake_req = Request::new(Body::empty());
|
|
||||||
*fake_req.headers_mut() = req.headers().clone();
|
|
||||||
let m2: DynMiddlewareStage2<Params> =
|
|
||||||
Box::new(move |rpc_req| {
|
|
||||||
let method = rpc_req.method.as_str();
|
|
||||||
let res: Result<
|
|
||||||
Result<DynMiddlewareStage3, Response<Body>>,
|
|
||||||
HttpError,
|
|
||||||
> = if metadata.get(method, "login").unwrap_or(false) {
|
|
||||||
todo!("set cookie on success")
|
|
||||||
} else if !metadata.get(method, "authenticated").unwrap_or(true) {
|
|
||||||
Ok(Ok(noop3()))
|
|
||||||
} else {
|
|
||||||
rpc_toolkit::rpc_server_helpers::to_response(
|
|
||||||
&fake_req,
|
|
||||||
Ok((
|
|
||||||
rpc_req.id.clone(),
|
|
||||||
Err(Error::new(
|
Err(Error::new(
|
||||||
anyhow!("UNAUTHORIZED"),
|
anyhow!("UNAUTHORIZED"),
|
||||||
crate::ErrorKind::Authorization,
|
crate::ErrorKind::Authorization,
|
||||||
)
|
))
|
||||||
.into()),
|
|
||||||
)),
|
|
||||||
|_| StatusCode::OK,
|
|
||||||
)
|
|
||||||
.map(|a| Err(a))
|
|
||||||
};
|
|
||||||
async { res }.boxed()
|
|
||||||
});
|
|
||||||
m2
|
|
||||||
})),
|
|
||||||
Err(e) => Ok(Ok({
|
|
||||||
let mut fake_req = Request::new(Body::empty());
|
|
||||||
*fake_req.headers_mut() = req.headers().clone();
|
|
||||||
let m2: DynMiddlewareStage2<Params> = Box::new(move |rpc_req| {
|
|
||||||
let res: Result<
|
|
||||||
Result<DynMiddlewareStage3, Response<Body>>,
|
|
||||||
HttpError,
|
|
||||||
> = rpc_toolkit::rpc_server_helpers::to_response(
|
|
||||||
&fake_req,
|
|
||||||
Ok((rpc_req.id.clone(), Err(e.into()))),
|
|
||||||
|_| StatusCode::OK,
|
|
||||||
)
|
|
||||||
.map(|a| Err(a));
|
|
||||||
async { res }.boxed()
|
|
||||||
});
|
|
||||||
m2
|
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hash_token(token: &str) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(token.as_bytes());
|
||||||
|
base32::encode(
|
||||||
|
base32::Alphabet::RFC4648 { padding: false },
|
||||||
|
hasher.finalize().as_slice(),
|
||||||
|
)
|
||||||
|
.to_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_authed(ctx: &RpcContext, req: &RequestParts) -> Result<(), Error> {
|
||||||
|
let id = get_id(req)?;
|
||||||
|
let exp = sqlx::query!("SELECT logged_out FROM session WHERE id = ?", id)
|
||||||
|
.fetch_one(&mut ctx.secret_store.acquire().await?)
|
||||||
|
.await?;
|
||||||
|
match exp.logged_out {
|
||||||
|
Some(exp) if exp >= Utc::now().naive_utc() => Err(Error::new(
|
||||||
|
anyhow!("UNAUTHORIZED"),
|
||||||
|
crate::ErrorKind::Authorization,
|
||||||
|
)),
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
|
||||||
|
Box::new(
|
||||||
|
|req: &mut Request<Body>,
|
||||||
|
metadata: M|
|
||||||
|
-> BoxFuture<Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError>> {
|
||||||
|
async move {
|
||||||
|
let mut header_stub = Request::new(Body::empty());
|
||||||
|
*header_stub.headers_mut() = req.headers().clone();
|
||||||
|
let m2: DynMiddlewareStage2 = Box::new(move |req, rpc_req| {
|
||||||
|
async move {
|
||||||
|
if metadata
|
||||||
|
.get(rpc_req.method.as_str(), "authenticated")
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
if let Err(e) = is_authed(&ctx, req).await {
|
||||||
|
let m3: DynMiddlewareStage3 = Box::new(|_, rpc_res| {
|
||||||
|
async move {
|
||||||
|
*rpc_res = Err(e.into());
|
||||||
|
Ok(Ok(noop4()))
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
});
|
||||||
|
return Ok(Ok(m3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Ok(noop3()))
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
});
|
||||||
|
Ok(Ok(m2))
|
||||||
}
|
}
|
||||||
.boxed()
|
.boxed()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ use rpc_toolkit::hyper::{Body, Method, Request, Response};
|
|||||||
use rpc_toolkit::rpc_server_helpers::{
|
use rpc_toolkit::rpc_server_helpers::{
|
||||||
DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4,
|
DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use rpc_toolkit::Metadata;
|
||||||
|
|
||||||
pub async fn cors<Params: for<'de> Deserialize<'de> + 'static, Metadata>(
|
pub async fn cors<M: Metadata>(
|
||||||
req: &mut Request<Body>,
|
req: &mut Request<Body>,
|
||||||
) -> Result<Result<DynMiddlewareStage2<Params>, Response<Body>>, HttpError> {
|
_metadata: M,
|
||||||
|
) -> Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError> {
|
||||||
if req.method() == Method::OPTIONS {
|
if req.method() == Method::OPTIONS {
|
||||||
Ok(Err(Response::builder()
|
Ok(Err(Response::builder()
|
||||||
.header(
|
.header(
|
||||||
@@ -24,9 +25,9 @@ pub async fn cors<Params: for<'de> Deserialize<'de> + 'static, Metadata>(
|
|||||||
.header("Access-Control-Allow-Credentials", "true")
|
.header("Access-Control-Allow-Credentials", "true")
|
||||||
.body(Body::empty())?))
|
.body(Body::empty())?))
|
||||||
} else {
|
} else {
|
||||||
Ok(Ok(Box::new(|_| {
|
Ok(Ok(Box::new(|_, _| {
|
||||||
async move {
|
async move {
|
||||||
let res: DynMiddlewareStage3 = Box::new(|_| {
|
let res: DynMiddlewareStage3 = Box::new(|_, _| {
|
||||||
async move {
|
async move {
|
||||||
let res: DynMiddlewareStage4 = Box::new(|res| {
|
let res: DynMiddlewareStage4 = Box::new(|res| {
|
||||||
async move {
|
async move {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use clap::ArgMatches;
|
use clap::{Arg, ArgMatches};
|
||||||
use digest::Digest;
|
use digest::Digest;
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|||||||
Reference in New Issue
Block a user