Added user management API [WIP]

This commit is contained in:
Emi Tatsuo 2020-11-16 01:13:16 -05:00
parent 070ed2a222
commit 0e2f8d5f62
Signed by: Emi
GPG Key ID: 68FAB2E2E6DFC98B
7 changed files with 575 additions and 34 deletions

View File

@ -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"]

View 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()
}

View File

@ -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")?;

View File

@ -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 {

View 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)
}
}
}

View 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
View 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
}
}