Compare commits

..

3 Commits

4 changed files with 213 additions and 43 deletions

View File

@ -33,11 +33,11 @@
},
"nixpkgs_22_05": {
"locked": {
"lastModified": 1668459637,
"narHash": "sha256-HqnWCKujmtu8v0CjzOT0sr7m2AR7+vpbZJOp1R0rodY=",
"lastModified": 1668766498,
"narHash": "sha256-UjZlIrbHGlL3H3HZNPTxPSwJfr49jIfbPWCYxk0EQm4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16f4e04658c2ab10114545af2f39db17d51bd1bd",
"rev": "f42a45c015f28ac3beeb0df360e50cdbf495d44b",
"type": "github"
},
"original": {
@ -48,11 +48,11 @@
},
"nixpkgs_unstable": {
"locked": {
"lastModified": 1668505710,
"narHash": "sha256-DulcfsGjpSXL9Ma0iQIsb3HRbARCDcA+CNH67pPyMQ0=",
"lastModified": 1668765800,
"narHash": "sha256-rC40+/W6Hio7b/RsY8SvQPKNx4WqNcTgfYv8cUMAvJk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "85d6b3990def7eef45f4502a82496de02a02b6e8",
"rev": "52b2ac8ae18bbad4374ff0dd5aeee0fdf1aea739",
"type": "github"
},
"original": {
@ -67,7 +67,7 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"narHash": "sha256-WjyKSpFY44ysIHSN3C0L5PKUJuwXDnSg6p5OcYwbZZ4=",
"narHash": "sha256-F2ro05D6tGMwSaOYeIediJq6X0ATD7JgWEG2TgOs9Wo=",
"type": "tarball",
"url": "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"
},

View File

@ -1,5 +1,6 @@
use crate::errors::{error, fatal, ok, AppResult};
use crate::errors::{error, fatal, ok, AppResult, FatalError};
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::{convert::Infallible, str::FromStr};
use thiserror::Error;
@ -8,7 +9,7 @@ use uuid::{adapter::Hyphenated, Uuid};
#[cfg(test)]
use crate::errors::maybe_fail;
#[derive(Debug, Error)]
#[derive(Debug, Error, PartialEq)]
pub enum AuthenticationError {
#[error("username already exists")]
DuplicateUsername,
@ -20,7 +21,7 @@ pub enum AuthenticationError {
UserNotFound,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SessionToken(String);
impl From<&str> for SessionToken {
@ -43,7 +44,7 @@ impl From<SessionToken> for String {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Invitation(String);
impl From<&str> for Invitation {
@ -66,7 +67,7 @@ impl From<Invitation> for String {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct UserId(String);
impl From<&str> for UserId {
@ -100,7 +101,7 @@ impl FromSql for UserId {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Username(String);
impl From<&str> for Username {
@ -156,11 +157,16 @@ pub trait AuthenticationDB: Send + Sync {
fn delete_user(&mut self, user: UserId) -> AppResult<(), AuthenticationError>;
fn validate_session(&self, session: SessionToken) -> AppResult<(), AuthenticationError>;
fn validate_session(
&self,
session: SessionToken,
) -> AppResult<(Username, UserId), AuthenticationError>;
fn get_user_id(&self, username: Username) -> AppResult<UserId, AuthenticationError>;
fn get_userid(&self, username: &Username) -> AppResult<UserId, AuthenticationError>;
fn list_users(&self) -> AppResult<Vec<Username>, AuthenticationError>;
fn get_username(&self, userid: &UserId) -> AppResult<Username, AuthenticationError>;
fn list_users(&self) -> AppResult<Vec<UserId>, AuthenticationError>;
}
#[derive(Debug, Default)]
@ -261,15 +267,22 @@ impl AuthenticationDB for MemoryAuth {
}
}
fn validate_session(&self, session: SessionToken) -> AppResult<(), AuthenticationError> {
if self.sessions.contains_key(&session) {
ok(())
fn validate_session(
&self,
session: SessionToken,
) -> AppResult<(Username, UserId), AuthenticationError> {
if let Some(userid) = self.sessions.get(&session) {
if let Some(username) = self.inverse_users.get(&userid) {
ok((username.clone(), userid.clone()))
} else {
fatal(FatalError::DatabaseInconsistency)
}
} else {
error::<(), AuthenticationError>(AuthenticationError::InvalidSession)
error(AuthenticationError::InvalidSession)
}
}
fn get_user_id(&self, username: Username) -> AppResult<UserId, AuthenticationError> {
fn get_userid(&self, username: &Username) -> AppResult<UserId, AuthenticationError> {
Ok(self
.users
.get(&username)
@ -277,10 +290,99 @@ impl AuthenticationDB for MemoryAuth {
.ok_or(AuthenticationError::UserNotFound))
}
fn list_users(&self) -> AppResult<Vec<Username>, AuthenticationError> {
ok(self.users.keys().cloned().collect::<Vec<Username>>())
fn get_username(&self, userid: &UserId) -> AppResult<Username, AuthenticationError> {
Ok(self
.inverse_users
.get(&userid)
.map(|u| u.clone())
.ok_or(AuthenticationError::UserNotFound))
}
fn list_users(&self) -> AppResult<Vec<UserId>, AuthenticationError> {
ok(self.inverse_users.keys().cloned().collect::<Vec<UserId>>())
}
}
#[cfg(test)]
mod test {}
mod test {
use super::*;
fn with_memory_db<F>(test: F)
where
F: Fn(Box<dyn AuthenticationDB>),
{
let authdb: Box<MemoryAuth> = Box::new(Default::default());
test(authdb);
}
#[test]
fn it_can_create_a_user() {
fn test_case(mut authdb: Box<dyn AuthenticationDB>) {
let username = Username::from("shephard");
let userid = authdb
.create_user(username.clone())
.expect("no fatal errors")
.expect("creation should succeed");
assert_eq!(authdb.get_userid(&username).unwrap(), Ok(userid.clone()));
assert_eq!(authdb.get_username(&userid).unwrap(), Ok(username));
assert!(authdb.list_users().unwrap().unwrap().contains(&userid));
}
with_memory_db(test_case);
}
#[test]
fn it_does_not_allow_duplicate_users() {
fn test_case(mut authdb: Box<dyn AuthenticationDB>) {
let username = Username::from("shephard");
let _ = authdb
.create_user(username.clone())
.expect("no fatal errors")
.expect("creation should succeed");
assert_eq!(
authdb.create_user(username.clone()).unwrap(),
Err(AuthenticationError::DuplicateUsername)
);
}
with_memory_db(test_case);
}
#[test]
fn it_allows_multiple_invitations_per_user() {
unimplemented!()
}
#[test]
fn it_exchanges_an_invitation_for_a_session() {
unimplemented!()
}
#[test]
fn it_allows_multiple_sessions_per_user() {
unimplemented!()
}
#[test]
fn it_disallows_invitation_reuse() {
unimplemented!()
}
#[test]
fn it_identifies_a_user_by_session() {
unimplemented!()
}
#[test]
fn it_deletes_a_user_and_invalidates_tokens() {
unimplemented!()
}
#[test]
fn it_deletes_a_session() {
unimplemented!()
}
#[test]
fn it_deletes_an_invitation() {
unimplemented!()
}
}

View File

@ -5,6 +5,8 @@ use thiserror::Error;
/// down and that the administrator fix a problem.
#[derive(Debug, Error)]
pub enum FatalError {
#[error("database is inconsistent")]
DatabaseInconsistency,
#[error("disk is full")]
DiskFull,
#[error("io error: {0}")]

View File

@ -1,3 +1,4 @@
use errors::{ok, AppResult};
use serde::{Deserialize, Serialize};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
@ -6,7 +7,9 @@ use std::{
use warp::Filter;
mod authentication;
use authentication::{AuthenticationDB, AuthenticationError, MemoryAuth, UserId, Username};
use authentication::{
AuthenticationDB, AuthenticationError, Invitation, MemoryAuth, SessionToken, UserId, Username,
};
mod database;
mod errors;
@ -15,7 +18,7 @@ mod errors;
struct AuthenticationRefused;
impl warp::reject::Reject for AuthenticationRefused {}
fn with_authentication(
fn with_session(
auth_ctx: Arc<RwLock<impl AuthenticationDB>>,
) -> impl Filter<Extract = ((Username, UserId),), Error = warp::Rejection> + Clone {
let auth_ctx = auth_ctx.clone();
@ -25,13 +28,11 @@ fn with_authentication(
let auth_ctx = auth_ctx.clone();
async move {
if auth_header.starts_with("Basic ") {
let username = auth_header.split(" ").skip(1).collect::<String>();
match auth_ctx
.read()
.unwrap()
.get_user_id(Username::from(username.as_str()))
{
Ok(Ok(userid)) => Ok((Username::from(username.as_str()), userid)),
let session_token = SessionToken::from(
auth_header.split(" ").skip(1).collect::<String>().as_str(),
);
match auth_ctx.read().unwrap().validate_session(session_token) {
Ok(Ok((username, userid))) => Ok((username, userid)),
Ok(Err(_)) => Err(warp::reject::custom(AuthenticationRefused)),
Err(err) => panic!("{}", err),
}
@ -49,13 +50,13 @@ struct ErrorResponse {
}
#[derive(Deserialize)]
struct MakeUserParameters {
username: String,
struct MakeUserParams {
username: Username,
}
#[derive(Serialize)]
struct MakeUserResponse {
userid: String,
userid: UserId,
}
fn make_user(
@ -64,13 +65,11 @@ fn make_user(
warp::path!("api" / "v1" / "users")
.and(warp::put())
.and(warp::body::json())
.map(move |params: MakeUserParameters| {
.map(move |params: MakeUserParams| {
let mut auth_ctx = auth_ctx.write().unwrap();
match (*auth_ctx).create_user(Username::from(params.username.as_str())) {
Ok(Ok(userid)) => warp::reply::json(&MakeUserResponse {
userid: String::from(userid),
}),
Ok(auth_error) => warp::reply::json(&ErrorResponse {
match (*auth_ctx).create_user(Username::from(params.username)) {
Ok(Ok(userid)) => warp::reply::json(&MakeUserResponse { userid }),
Ok(Err(auth_error)) => warp::reply::json(&ErrorResponse {
error: format!("{:?}", auth_error),
}),
Err(err) => panic!("{}", err),
@ -78,6 +77,63 @@ fn make_user(
})
}
#[derive(Deserialize)]
struct MakeInvitationParams {
userid: UserId,
}
#[derive(Serialize)]
struct MakeInvitationResponse {
invitation: Invitation,
}
fn make_invitation(
auth_ctx: Arc<RwLock<impl AuthenticationDB>>,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "v1" / "invitations")
.and(warp::put())
.and(warp::body::json())
.map(move |params: MakeInvitationParams| {
let mut auth_ctx = auth_ctx.write().unwrap();
match (*auth_ctx).create_invitation(params.userid) {
Ok(Ok(invitation)) => warp::reply::json(&MakeInvitationResponse { invitation }),
Ok(Err(auth_error)) => warp::reply::json(&ErrorResponse {
error: format!("{:?}", auth_error),
}),
Err(err) => panic!("{}", err),
}
})
}
#[derive(Deserialize)]
struct AuthenticateParams {
invitation: Invitation,
}
#[derive(Serialize)]
struct AuthenticateResponse {
session_token: SessionToken,
}
fn authenticate(
auth_ctx: Arc<RwLock<impl AuthenticationDB>>,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "v1" / "authenticate")
.and(warp::put())
.and(warp::body::json())
.map(move |params: AuthenticateParams| {
let mut auth_ctx = auth_ctx.write().unwrap();
match (*auth_ctx).authenticate(params.invitation) {
Ok(Ok(session_token)) => warp::reply::json(&AuthenticateResponse { session_token }),
Ok(Err(auth_error)) => warp::reply::json(&ErrorResponse {
error: format!("{:?}", auth_error),
}),
Err(err) => panic!("{}", err),
}
})
}
/*
fn list_users(
auth_ctx: Arc<RwLock<impl AuthenticationDB>>,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
@ -99,6 +155,7 @@ fn list_users(
}
})
}
*/
#[tokio::main]
pub async fn main() {
@ -117,7 +174,7 @@ pub async fn main() {
*/
let echo_authenticated = warp::path!("api" / "v1" / "echo" / String)
.and(with_authentication(auth_ctx.clone()))
.and(with_session(auth_ctx.clone()))
.map(|param: String, (username, userid)| {
println!("param: {:?}", username);
println!("param: {:?}", userid);
@ -125,8 +182,17 @@ pub async fn main() {
warp::reply::json(&vec!["authed", param.as_str()])
});
/*
let filter = list_users(auth_ctx.clone())
.or(make_user(auth_ctx.clone()))
.or(make_invitation(auth_ctx.clone()))
.or(authenticate(auth_ctx.clone()))
.or(echo_authenticated)
.or(echo_unauthenticated);
*/
let filter = make_user(auth_ctx.clone())
.or(make_invitation(auth_ctx.clone()))
.or(authenticate(auth_ctx.clone()))
.or(echo_authenticated)
.or(echo_unauthenticated);