Added user management API [WIP]
This commit is contained in:
parent
070ed2a222
commit
0e2f8d5f62
13
Cargo.toml
13
Cargo.toml
|
@ -8,6 +8,10 @@ description = "Gemini server implementation"
|
||||||
repository = "https://github.com/panicbit/northstar"
|
repository = "https://github.com/panicbit/northstar"
|
||||||
documentation = "https://docs.rs/northstar"
|
documentation = "https://docs.rs/northstar"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# default = ["user_management"]
|
||||||
|
user_management = ["sled", "bincode", "serde/derive", "bcrypt", "crc32fast"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.33"
|
anyhow = "1.0.33"
|
||||||
rustls = { version = "0.18.1", features = ["dangerous_configuration"] }
|
rustls = { version = "0.18.1", features = ["dangerous_configuration"] }
|
||||||
|
@ -21,6 +25,15 @@ itertools = "0.9.0"
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
webpki = "0.21.0"
|
webpki = "0.21.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
sled = { version = "0.34.6", optional = true }
|
||||||
|
bincode = { version = "1.3.1", optional = true }
|
||||||
|
serde = { version = "1.0", optional = true }
|
||||||
|
bcrypt = { version = "0.9", optional = true }
|
||||||
|
crc32fast = { version = "1.2.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.8.1"
|
env_logger = "0.8.1"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "user_management"
|
||||||
|
required-features = ["user_management"]
|
||||||
|
|
65
examples/user_management.rs
Normal file
65
examples/user_management.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use anyhow::*;
|
||||||
|
use futures::{future::BoxFuture, FutureExt};
|
||||||
|
use log::LevelFilter;
|
||||||
|
use northstar::{
|
||||||
|
GEMINI_MIME,
|
||||||
|
GEMINI_PORT,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
Server,
|
||||||
|
user_management::{User, UserManagerError},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
env_logger::builder()
|
||||||
|
.filter_module("northstar", LevelFilter::Debug)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
Server::bind(("0.0.0.0", GEMINI_PORT))
|
||||||
|
.serve(handle_request)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An ultra-simple demonstration of simple authentication.
|
||||||
|
///
|
||||||
|
/// If the user attempts to connect, they will be prompted to create a client certificate.
|
||||||
|
/// Once they've made one, they'll be given the opportunity to create an account by
|
||||||
|
/// selecting a username. They'll then get a message confirming their account creation.
|
||||||
|
/// Any time this user visits the site in the future, they'll get a personalized welcome
|
||||||
|
/// message.
|
||||||
|
fn handle_request(request: Request) -> BoxFuture<'static, Result<Response>> {
|
||||||
|
async move {
|
||||||
|
Ok(match request.user::<String>()? {
|
||||||
|
User::Unauthenticated => {
|
||||||
|
Response::client_certificate_required()
|
||||||
|
},
|
||||||
|
User::NotSignedIn(user) => {
|
||||||
|
if let Some(username) = request.input() {
|
||||||
|
match user.register::<String>(username.to_owned()) {
|
||||||
|
Ok(_user) => Response::success(&GEMINI_MIME)
|
||||||
|
.with_body("Your account has been created!\n=>/ Begin"),
|
||||||
|
Err(UserManagerError::UsernameNotUnique) =>
|
||||||
|
Response::input_lossy("That username is taken. Try again"),
|
||||||
|
Err(e) => panic!("Unexpected error: {}", e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Response::input_lossy("Please pick a username")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
User::SignedIn(mut user) => {
|
||||||
|
if request.path_segments()[0].eq("push") { // User connecting to /push
|
||||||
|
if let Some(push) = request.input() {
|
||||||
|
user.as_mut().push_str(push);
|
||||||
|
user.save();
|
||||||
|
} else {
|
||||||
|
return Ok(Response::input_lossy("Enter a string to push"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::success(&GEMINI_MIME)
|
||||||
|
.with_body(format!("Your current string: {}\n=> /push Push", user.as_ref()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}.boxed()
|
||||||
|
}
|
98
src/lib.rs
98
src/lib.rs
|
@ -1,6 +1,6 @@
|
||||||
#[macro_use] extern crate log;
|
#[macro_use] extern crate log;
|
||||||
|
|
||||||
use std::{panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc};
|
use std::{panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc, path::PathBuf};
|
||||||
use futures::{future::BoxFuture, FutureExt};
|
use futures::{future::BoxFuture, FutureExt};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
|
@ -17,6 +17,11 @@ use lazy_static::lazy_static;
|
||||||
|
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
#[cfg(feature = "user_management")]
|
||||||
|
pub mod user_management;
|
||||||
|
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
use user_management::UserManager;
|
||||||
|
|
||||||
pub use mime;
|
pub use mime;
|
||||||
pub use uriparse as uri;
|
pub use uriparse as uri;
|
||||||
|
@ -33,6 +38,8 @@ pub struct Server {
|
||||||
tls_acceptor: TlsAcceptor,
|
tls_acceptor: TlsAcceptor,
|
||||||
listener: Arc<TcpListener>,
|
listener: Arc<TcpListener>,
|
||||||
handler: Handler,
|
handler: Handler,
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
manager: UserManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
|
@ -59,8 +66,13 @@ impl Server {
|
||||||
.context("Failed to establish TLS session")?;
|
.context("Failed to establish TLS session")?;
|
||||||
let mut stream = BufStream::new(stream);
|
let mut stream = BufStream::new(stream);
|
||||||
|
|
||||||
let mut request = receive_request(&mut stream).await
|
#[cfg(feature="user_management")]
|
||||||
|
let mut request = self.receive_request(&mut stream).await
|
||||||
.context("Failed to receive request")?;
|
.context("Failed to receive request")?;
|
||||||
|
#[cfg(not(feature="user_management"))]
|
||||||
|
let mut request = Self::receive_request(&mut stream).await
|
||||||
|
.context("Failed to receive request")?;
|
||||||
|
|
||||||
debug!("Client requested: {}", request.uri());
|
debug!("Client requested: {}", request.uri());
|
||||||
|
|
||||||
// Identify the client certificate from the tls stream. This is the first
|
// Identify the client certificate from the tls stream. This is the first
|
||||||
|
@ -92,15 +104,65 @@ impl Server {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn receive_request(
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
&self,
|
||||||
|
stream: &mut (impl AsyncBufRead + Unpin)
|
||||||
|
) -> Result<Request> {
|
||||||
|
let limit = REQUEST_URI_MAX_LEN + "\r\n".len();
|
||||||
|
let mut stream = stream.take(limit as u64);
|
||||||
|
let mut uri = Vec::new();
|
||||||
|
|
||||||
|
stream.read_until(b'\n', &mut uri).await?;
|
||||||
|
|
||||||
|
if !uri.ends_with(b"\r\n") {
|
||||||
|
if uri.len() < REQUEST_URI_MAX_LEN {
|
||||||
|
bail!("Request header not terminated with CRLF")
|
||||||
|
} else {
|
||||||
|
bail!("Request URI too long")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip CRLF
|
||||||
|
uri.pop();
|
||||||
|
uri.pop();
|
||||||
|
|
||||||
|
let uri = URIReference::try_from(&*uri)
|
||||||
|
.context("Request URI is invalid")?
|
||||||
|
.into_owned();
|
||||||
|
let request = Request::from_uri(
|
||||||
|
uri,
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
self.manager.clone(),
|
||||||
|
) .context("Failed to create request from URI")?;
|
||||||
|
|
||||||
|
Ok(request)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Builder<A> {
|
pub struct Builder<A> {
|
||||||
addr: A,
|
addr: A,
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
data_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: ToSocketAddrs> Builder<A> {
|
impl<A: ToSocketAddrs> Builder<A> {
|
||||||
fn bind(addr: A) -> Self {
|
fn bind(addr: A) -> Self {
|
||||||
Self { addr }
|
Self {
|
||||||
|
addr,
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
data_dir: "data".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
/// Sets the directory to store user data in
|
||||||
|
///
|
||||||
|
/// Defaults to `./data` if not specified
|
||||||
|
pub fn set_database_dir(mut self, path: impl Into<PathBuf>) -> Self {
|
||||||
|
self.data_dir = path.into();
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serve<F>(self, handler: F) -> Result<()>
|
pub async fn serve<F>(self, handler: F) -> Result<()>
|
||||||
|
@ -117,40 +179,14 @@ impl<A: ToSocketAddrs> Builder<A> {
|
||||||
tls_acceptor: TlsAcceptor::from(config),
|
tls_acceptor: TlsAcceptor::from(config),
|
||||||
listener: Arc::new(listener),
|
listener: Arc::new(listener),
|
||||||
handler: Arc::new(handler),
|
handler: Arc::new(handler),
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
manager: UserManager::new(self.data_dir)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
server.serve().await
|
server.serve().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn receive_request(stream: &mut (impl AsyncBufRead + Unpin)) -> Result<Request> {
|
|
||||||
let limit = REQUEST_URI_MAX_LEN + "\r\n".len();
|
|
||||||
let mut stream = stream.take(limit as u64);
|
|
||||||
let mut uri = Vec::new();
|
|
||||||
|
|
||||||
stream.read_until(b'\n', &mut uri).await?;
|
|
||||||
|
|
||||||
if !uri.ends_with(b"\r\n") {
|
|
||||||
if uri.len() < REQUEST_URI_MAX_LEN {
|
|
||||||
bail!("Request header not terminated with CRLF")
|
|
||||||
} else {
|
|
||||||
bail!("Request URI too long")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip CRLF
|
|
||||||
uri.pop();
|
|
||||||
uri.pop();
|
|
||||||
|
|
||||||
let uri = URIReference::try_from(&*uri)
|
|
||||||
.context("Request URI is invalid")?
|
|
||||||
.into_owned();
|
|
||||||
let request = Request::from_uri(uri)
|
|
||||||
.context("Failed to create request from URI")?;
|
|
||||||
|
|
||||||
Ok(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_response(mut response: Response, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
|
async fn send_response(mut response: Response, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
|
||||||
send_response_header(response.header(), stream).await
|
send_response_header(response.header(), stream).await
|
||||||
.context("Failed to send response header")?;
|
.context("Failed to send response header")?;
|
||||||
|
|
|
@ -3,21 +3,39 @@ use anyhow::*;
|
||||||
use percent_encoding::percent_decode_str;
|
use percent_encoding::percent_decode_str;
|
||||||
use uriparse::URIReference;
|
use uriparse::URIReference;
|
||||||
use rustls::Certificate;
|
use rustls::Certificate;
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
use crate::user_management::{UserManager, User};
|
||||||
|
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
uri: URIReference<'static>,
|
uri: URIReference<'static>,
|
||||||
input: Option<String>,
|
input: Option<String>,
|
||||||
certificate: Option<Certificate>,
|
certificate: Option<Certificate>,
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
manager: UserManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Request {
|
impl Request {
|
||||||
pub fn from_uri(uri: URIReference<'static>) -> Result<Self> {
|
pub fn from_uri(
|
||||||
Self::with_certificate(uri, None)
|
uri: URIReference<'static>,
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
manager: UserManager,
|
||||||
|
) -> Result<Self> {
|
||||||
|
Self::with_certificate(
|
||||||
|
uri,
|
||||||
|
None,
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
manager
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_certificate(
|
pub fn with_certificate(
|
||||||
mut uri: URIReference<'static>,
|
mut uri: URIReference<'static>,
|
||||||
certificate: Option<Certificate>
|
certificate: Option<Certificate>,
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
manager: UserManager,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
uri.normalize();
|
uri.normalize();
|
||||||
|
|
||||||
|
@ -36,6 +54,8 @@ impl Request {
|
||||||
uri,
|
uri,
|
||||||
input,
|
input,
|
||||||
certificate,
|
certificate,
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
manager,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +83,18 @@ impl Request {
|
||||||
pub fn certificate(&self) -> Option<&Certificate> {
|
pub fn certificate(&self) -> Option<&Certificate> {
|
||||||
self.certificate.as_ref()
|
self.certificate.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="user_management")]
|
||||||
|
/// Attempt to determine the user who sent this request
|
||||||
|
///
|
||||||
|
/// May return a variant depending on if the client used a client certificate, and if
|
||||||
|
/// they've registered as a user yet.
|
||||||
|
pub fn user<UserData>(&self) -> Result<User<UserData>>
|
||||||
|
where
|
||||||
|
UserData: Serialize + DeserializeOwned
|
||||||
|
{
|
||||||
|
Ok(self.manager.get_user(self.certificate())?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::Deref for Request {
|
impl ops::Deref for Request {
|
||||||
|
|
121
src/user_management/manager.rs
Normal file
121
src/user_management/manager.rs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
use rustls::Certificate;
|
||||||
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::user_management::{User, Result};
|
||||||
|
use crate::user_management::user::{SignedInUser, NotSignedInUser, UserInner};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
/// Data stored in the certificate tree about a certain certificate
|
||||||
|
pub struct CertificateData {
|
||||||
|
#[serde(with = "CertificateDef")]
|
||||||
|
pub certificate: Certificate,
|
||||||
|
pub owner_username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(remote = "Certificate")]
|
||||||
|
struct CertificateDef(Vec<u8>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
/// A struct containing information for managing users.
|
||||||
|
///
|
||||||
|
/// Wraps a [`sled::Db`]
|
||||||
|
pub struct UserManager {
|
||||||
|
db: sled::Db,
|
||||||
|
pub (crate) users: sled::Tree, // user_id:String maps to data:UserData
|
||||||
|
pub (crate) certificates: sled::Tree, // certificate:u64 maps to data:CertificateData
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserManager {
|
||||||
|
|
||||||
|
/// Create or open a new UserManager
|
||||||
|
///
|
||||||
|
/// The `dir` argument is the path to a data directory, to be populated using sled.
|
||||||
|
/// This will be created if it does not exist.
|
||||||
|
pub fn new(dir: impl AsRef<Path>) -> Result<Self> {
|
||||||
|
let db = sled::open(dir)?;
|
||||||
|
Ok(Self {
|
||||||
|
users: db.open_tree("users")?,
|
||||||
|
certificates: db.open_tree("certificates")?,
|
||||||
|
db,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produce a u32 hash from a certificate, used for [`lookup_certficate()`]
|
||||||
|
pub fn hash_certificate(cert: &Certificate) -> u32 {
|
||||||
|
let mut hasher = crc32fast::Hasher::new();
|
||||||
|
hasher.update(cert.0.as_ref());
|
||||||
|
hasher.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lookup information about a certificate based on it's u32 hash
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// An error is thrown if there is an error reading from the database or if data
|
||||||
|
/// recieved from the database is corrupt
|
||||||
|
pub fn lookup_certificate(&self, cert: u32) -> Result<Option<CertificateData>> {
|
||||||
|
if let Some(bytes) = self.certificates.get(cert.to_le_bytes())? {
|
||||||
|
Ok(Some(bincode::deserialize(&bytes)?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lookup information about a user by username
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// An error is thrown if there is an error reading from the database or if data
|
||||||
|
/// recieved from the database is corrupt
|
||||||
|
pub fn lookup_user<'de, UserData>(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<[u8]>
|
||||||
|
) -> Result<Option<UserInner<UserData>>>
|
||||||
|
where
|
||||||
|
UserData: Serialize + DeserializeOwned
|
||||||
|
{
|
||||||
|
if let Some(bytes) = self.users.get(username)? {
|
||||||
|
Ok(Some(bincode::deserialize_from(bytes.as_ref())?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to determine the user who sent a request based on the certificate.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// An error is thrown if there is an error reading from the database or if data
|
||||||
|
/// recieved from the database is corrupt
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Pancis if the database is corrupt
|
||||||
|
pub fn get_user<'de, UserData>(
|
||||||
|
&self,
|
||||||
|
cert: Option<&Certificate>
|
||||||
|
) -> Result<User<UserData>>
|
||||||
|
where
|
||||||
|
UserData: Serialize + DeserializeOwned
|
||||||
|
{
|
||||||
|
if let Some(certificate) = cert {
|
||||||
|
let cert_hash = Self::hash_certificate(certificate);
|
||||||
|
if let Some(certificate_data) = self.lookup_certificate(cert_hash)? {
|
||||||
|
let user_inner = self.lookup_user(&certificate_data.owner_username)?
|
||||||
|
.expect("Database corruption: Certificate data refers to non-existant user");
|
||||||
|
Ok(User::SignedIn(SignedInUser {
|
||||||
|
username: certificate_data.owner_username,
|
||||||
|
active_certificate: certificate_data.certificate,
|
||||||
|
manager: self.clone(),
|
||||||
|
inner: user_inner,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(User::NotSignedIn(NotSignedInUser {
|
||||||
|
certificate: certificate.clone(),
|
||||||
|
manager: self.clone(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(User::Unauthenticated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
src/user_management/mod.rs
Normal file
71
src/user_management/mod.rs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
pub mod user;
|
||||||
|
mod manager;
|
||||||
|
pub use manager::UserManager;
|
||||||
|
pub use user::User;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum UserManagerError {
|
||||||
|
UsernameNotUnique,
|
||||||
|
PasswordNotSet,
|
||||||
|
DatabaseError(sled::Error),
|
||||||
|
DatabaseTransactionError(sled::transaction::TransactionError),
|
||||||
|
DeserializeError(bincode::Error),
|
||||||
|
BcryptError(bcrypt::BcryptError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sled::Error> for UserManagerError {
|
||||||
|
fn from(error: sled::Error) -> Self {
|
||||||
|
Self::DatabaseError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sled::transaction::TransactionError> for UserManagerError {
|
||||||
|
fn from(error: sled::transaction::TransactionError) -> Self {
|
||||||
|
Self::DatabaseTransactionError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bincode::Error> for UserManagerError {
|
||||||
|
fn from(error: bincode::Error) -> Self {
|
||||||
|
Self::DeserializeError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bcrypt::BcryptError> for UserManagerError {
|
||||||
|
fn from(error: bcrypt::BcryptError) -> Self {
|
||||||
|
Self::BcryptError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for UserManagerError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::DatabaseError(e) => Some(e),
|
||||||
|
Self::DatabaseTransactionError(e) => Some(e),
|
||||||
|
Self::DeserializeError(e) => Some(e),
|
||||||
|
Self::BcryptError(e) => Some(e),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for UserManagerError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
match self {
|
||||||
|
Self::UsernameNotUnique =>
|
||||||
|
write!(f, "Attempted to create a user using a username that's already been taken"),
|
||||||
|
Self::PasswordNotSet =>
|
||||||
|
write!(f, "Attempted to check the password of a user who has not set one yet"),
|
||||||
|
Self::DatabaseError(e) =>
|
||||||
|
write!(f, "Error accessing the user database: {}", e),
|
||||||
|
Self::DatabaseTransactionError(e) =>
|
||||||
|
write!(f, "Error accessing the user database: {}", e),
|
||||||
|
Self::DeserializeError(e) =>
|
||||||
|
write!(f, "Recieved messy data from database, possible corruption: {}", e),
|
||||||
|
Self::BcryptError(e) =>
|
||||||
|
write!(f, "Bcrypt Error, likely malformed password hash, possible database corruption: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, UserManagerError>;
|
203
src/user_management/user.rs
Normal file
203
src/user_management/user.rs
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
use rustls::Certificate;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sled::Transactional;
|
||||||
|
|
||||||
|
use crate::user_management::UserManager;
|
||||||
|
use crate::user_management::Result;
|
||||||
|
use crate::user_management::manager::CertificateData;
|
||||||
|
|
||||||
|
/// An struct corresponding to the data stored in the user tree
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct UserInner<UserData> {
|
||||||
|
data: UserData,
|
||||||
|
certificates: Vec<u32>,
|
||||||
|
pass_hash: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Any information about the connecting user
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum User<UserData> {
|
||||||
|
/// A user who is connected without using a client certificate.
|
||||||
|
///
|
||||||
|
/// This could be a user who has an account but just isn't presenting a certificate at
|
||||||
|
/// the minute, a user whose client does not support client certificates, or a user
|
||||||
|
/// who has not yet created a certificate for the site
|
||||||
|
Unauthenticated,
|
||||||
|
|
||||||
|
/// A user who is connecting with a certificate that isn't connected to an account
|
||||||
|
///
|
||||||
|
/// This is typically a new user who hasn't set up an account yet, or a user
|
||||||
|
/// connecting with a new certificate that needs to be added to an existing account.
|
||||||
|
NotSignedIn(NotSignedInUser),
|
||||||
|
|
||||||
|
/// A user connecting with an identified account
|
||||||
|
SignedIn(SignedInUser<UserData>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
/// Data about a user with a certificate not associated with an account
|
||||||
|
///
|
||||||
|
/// For more information about the user lifecycle and sign-in stages, see [`User`]
|
||||||
|
pub struct NotSignedInUser {
|
||||||
|
pub (crate) certificate: Certificate,
|
||||||
|
pub (crate) manager: UserManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotSignedInUser {
|
||||||
|
/// Register a new user with this certificate
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The provided username must be unique, or else an error will be raised.
|
||||||
|
///
|
||||||
|
/// Additional errors might occur if there is a problem writing to the database
|
||||||
|
pub fn register<UserData>(
|
||||||
|
self,
|
||||||
|
username: String,
|
||||||
|
) -> Result<SignedInUser<UserData>>
|
||||||
|
where
|
||||||
|
UserData: Serialize + Default
|
||||||
|
{
|
||||||
|
if self.manager.users.contains_key(username.as_str())? {
|
||||||
|
Err(super::UserManagerError::UsernameNotUnique)
|
||||||
|
} else {
|
||||||
|
let cert_hash = UserManager::hash_certificate(&self.certificate);
|
||||||
|
|
||||||
|
let newser = SignedInUser {
|
||||||
|
inner: UserInner {
|
||||||
|
data: UserData::default(),
|
||||||
|
certificates: vec![cert_hash],
|
||||||
|
pass_hash: None,
|
||||||
|
},
|
||||||
|
username: username.clone(),
|
||||||
|
active_certificate: self.certificate.clone(),
|
||||||
|
manager: self.manager,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cert_info = CertificateData {
|
||||||
|
certificate: self.certificate,
|
||||||
|
owner_username: username,
|
||||||
|
};
|
||||||
|
|
||||||
|
let newser_serialized = bincode::serialize(&newser.inner)?;
|
||||||
|
let cert_info_serialized = bincode::serialize(&cert_info)?;
|
||||||
|
|
||||||
|
(&newser.manager.users, &newser.manager.certificates)
|
||||||
|
.transaction(|(tx_usr, tx_crt)| {
|
||||||
|
tx_usr.insert(
|
||||||
|
newser.username.as_str(),
|
||||||
|
newser_serialized.clone(),
|
||||||
|
)?;
|
||||||
|
tx_crt.insert(
|
||||||
|
cert_hash.to_le_bytes().as_ref(),
|
||||||
|
cert_info_serialized.clone(),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?; //TODO
|
||||||
|
|
||||||
|
Ok(newser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach this certificate to an existing user
|
||||||
|
///
|
||||||
|
/// Try to add this certificate to another user using a username and password
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This will error if the username and password are incorrect, or if the user has yet
|
||||||
|
/// to set a password.
|
||||||
|
///
|
||||||
|
/// Additional errors might occur if an error occurs during database lookup and
|
||||||
|
/// deserialization
|
||||||
|
pub fn attach<'de, UserData>(
|
||||||
|
username: impl AsRef<[u8]>,
|
||||||
|
password: impl AsRef<[u8]>,
|
||||||
|
) -> Result<SignedInUser<UserData>>
|
||||||
|
where
|
||||||
|
UserData: Serialize + Deserialize<'de>
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
/// Data about a logged in user
|
||||||
|
///
|
||||||
|
/// For more information about the user lifecycle and sign-in stages, see [`User`]
|
||||||
|
pub struct SignedInUser<UserData> {
|
||||||
|
pub (crate) username: String,
|
||||||
|
pub (crate) active_certificate: Certificate,
|
||||||
|
pub (crate) manager: UserManager,
|
||||||
|
pub (crate) inner: UserInner<UserData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<UserData> SignedInUser<UserData> {
|
||||||
|
/// Get the [`Certificate`] that the user is currently using to connect.
|
||||||
|
pub const fn active_certificate(&self) -> &Certificate {
|
||||||
|
&self.active_certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produce a list of all [`Certificate`]s registered to this account
|
||||||
|
pub fn all_certificates(&self) -> Vec<Certificate> {
|
||||||
|
self.inner.certificates
|
||||||
|
.iter()
|
||||||
|
.map(
|
||||||
|
|cid| self.manager.lookup_certificate(*cid)
|
||||||
|
.expect("Database corruption: User refers to non-existant certificate")
|
||||||
|
.expect("Error accessing database")
|
||||||
|
.certificate
|
||||||
|
)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the user's current username.
|
||||||
|
///
|
||||||
|
/// NOTE: This is not guaranteed not to change.
|
||||||
|
pub const fn username(&self) -> &String {
|
||||||
|
&self.username
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check a password against the user's password hash
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// An error is raised if the user has yet to set a password, or if the user's
|
||||||
|
/// password hash is somehow malformed.
|
||||||
|
pub fn check_password(
|
||||||
|
&self,
|
||||||
|
try_password: impl AsRef<[u8]>
|
||||||
|
) -> Result<bool> {
|
||||||
|
if let Some(hash) = &self.inner.pass_hash {
|
||||||
|
Ok(bcrypt::verify(try_password, hash.as_str())?)
|
||||||
|
} else {
|
||||||
|
Err(super::UserManagerError::PasswordNotSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write any updates to the user to the database.
|
||||||
|
///
|
||||||
|
/// Updates caused by calling methods directly on the user do not need to be saved.
|
||||||
|
/// This is only for changes made to the UserData.
|
||||||
|
pub fn save(&self) -> Result<()>
|
||||||
|
where
|
||||||
|
UserData: Serialize
|
||||||
|
{
|
||||||
|
self.manager.users.insert(
|
||||||
|
&self.username,
|
||||||
|
bincode::serialize(&self.inner)?,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<UserData> AsRef<UserData> for SignedInUser<UserData> {
|
||||||
|
fn as_ref(&self) -> &UserData {
|
||||||
|
&self.inner.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<UserData> AsMut<UserData> for SignedInUser<UserData> {
|
||||||
|
/// NOTE: Changes made to the user data won't be persisted until SignedInUser::save
|
||||||
|
/// is called
|
||||||
|
fn as_mut(&mut self) -> &mut UserData {
|
||||||
|
&mut self.inner.data
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue