...
 
Commits (74)
edition = "2018"
This diff is collapsed.
......@@ -4,16 +4,13 @@ members = [
# qaul.net service library
"libqaul",
# Platform specific utilities
"libqaul/platform",
# libqaul additions
"libqaul/http-api",
"libqaul/ipc",
# qaul.net main services
"libqaul/service/messaging",
"libqaul/service/files",
"libqaul/service/core",
# qaul.net http-api
# "libqaul/http-api",
# decentralised routing protocol
"ratman",
......@@ -25,8 +22,9 @@ members = [
"visn",
"permute",
# An entirely in-memory netmod endpoint
# Available netmod drivers
"netmod-mem",
"netmod-udp",
# Client specific targets
"clients/linux",
......
......@@ -6,3 +6,5 @@ edition = "2018"
license = "GPL-3.0"
[dependencies]
libqaul = { path = "../../libqaul" }
ratman = { path = "../../ratman" }
\ No newline at end of file
fn main() {
println!("Hello, world!");
use libqaul::{messages::Recipient, Qaul};
use ratman::Router;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let r = Router::new();
// TDOD: Add network drivers
let q = Qaul::new(r);
let user = q.users().create("password")?;
// Register a service
q.services().register("de.spacekookie.myapp")?;
q.messages().send(
user.clone(),
Recipient::Flood,
"de.spacekokie.myapp",
vec![1, 2, 3, 4],
)?;
q.messages().listen(user, "de.spacekookie.myapp", |msg| {
println!("Received message: {:?}", msg);
Ok(()) // Return error if parsing fails
}).unwrap();
Ok(())
}
......@@ -8,7 +8,7 @@ edition = "2018"
[dependencies]
identity = { path = "../ratman/identity", package = "ratman-identity", features = ["digest"] }
platform = { path = "platform", package = "qaul-platform" }
libqaul-ipc = { path = "ipc" }
ratman = { path = "../ratman" }
async-std = "1.0"
......@@ -21,3 +21,7 @@ blake2 = "0.8.0"
mime = "0.3"
rand = "0.7"
[features]
default = ["generate-message"]
generate-message = []
[package]
name = "qaul-http"
name = "libqaul-http"
description = "An IPC layer to expose libqaul and service APIs via http/json:api"
version = "0.1.0"
authors = ["Jess 3Jane <me@jess.coffee>"]
authors = ["Jess 3Jane <me@jess.coffee>", "Katharina Fey <kookie@spacekookie.de>"]
edition = "2018"
license = "AGPL-3.0"
......@@ -9,16 +10,27 @@ license = "AGPL-3.0"
base64 = "0.10"
iron = "0.6"
persistent = "0.4"
japi = "0.1"
japi = "0.3"
serde_derive = "1.0"
serde = "1.0"
serde_json = "1.0"
lazy_static = "1.3"
chrono = "0.4"
cookie = "0.12"
hex = "0.4"
router = "0.6"
libqaul = { path = ".." }
identity = { path = "../../ratman/identity", package = "ratman-identity" }
text_messaging = { path = "../service/messaging", package = "qaul-messaging", optional = true }
[dev-dependencies]
anneal = { features = ["jsonapi", "cookies"], version = "0.3" }
anneal = { features = ["jsonapi"], version = "0.3" }
netmod_mem = { path = "../../netmod-mem", package = "netmod-mem" }
ratman = { path = "../../ratman", package = "ratman" }
[features]
default = ["messaging"]
# add support for the qaul_messaging service
messaging = ["text_messaging"]
use crate::{
models::{ConversionError, GrantType},
JSONAPI_MIME,
};
use identity::ID_LEN;
use iron::{status::Status, IronError};
use japi::{Document, Error, ErrorSource, ObjectConversionError};
use libqaul::QaulError;
use std::{
error::Error as StdError,
fmt::{Display, Formatter, Result},
};
#[derive(Debug)]
pub(crate) enum AuthError {
MultipleData,
NoData,
ConversionError(ObjectConversionError),
NoAttributes,
InvalidIdentity(ConversionError),
QaulError(QaulError),
NotLoggedIn,
InvalidToken(GrantType),
DifferingLogins,
}
impl AuthError {
fn detail(&self) -> String {
match self {
AuthError::MultipleData => {
"Multiple data were provided when the endpoint expects exactly one".into()
}
AuthError::NoData => "Document contains no data".into(),
AuthError::ConversionError(e) => format!("Error converting generic object ({})", e),
AuthError::NoAttributes => "Object has no attributes".into(),
AuthError::InvalidIdentity(e) => format!("Conversion Error ({})", e),
AuthError::QaulError(e) => format!("Qaul Error ({:?})", e),
AuthError::NotLoggedIn => "Not logged in".into(),
AuthError::InvalidToken(g) => match g {
GrantType::Cookie => "Invalid cookie",
GrantType::Token => "Invalid token",
}
.into(),
AuthError::DifferingLogins => "Both an authorization header and cookie are present, \
both are valid, but they point to different users"
.into(),
}
}
fn into_error(&self) -> (Error, Status) {
let status = match self {
AuthError::QaulError(QaulError::NotAuthorised) => Status::Unauthorized,
AuthError::QaulError(QaulError::NoUser) => Status::NotFound,
AuthError::QaulError(QaulError::CallbackTimeout) => Status::InternalServerError,
AuthError::NotLoggedIn => Status::Unauthorized,
_ => Status::BadRequest,
};
let title = match self {
AuthError::MultipleData => Some("Multiple Data".into()),
AuthError::NoData => Some("No Data".into()),
AuthError::ConversionError(_) => Some("Object Error".into()),
AuthError::NoAttributes => Some("No Attributes".into()),
AuthError::InvalidIdentity(_) => Some("Invalid identity".into()),
AuthError::QaulError(QaulError::NotAuthorised) => Some("Not Authorized".into()),
AuthError::QaulError(QaulError::NoUser) => Some("Unknown User".into()),
AuthError::QaulError(QaulError::InvalidQuery) => Some("Invalid Query".into()),
AuthError::QaulError(QaulError::InvalidPayload) => Some("Invalid Payload".into()),
AuthError::QaulError(QaulError::CallbackTimeout) => None,
AuthError::NotLoggedIn => Some("Not Logged In".into()),
AuthError::InvalidToken(GrantType::Cookie) => Some("Invalid Login Cookie".into()),
AuthError::InvalidToken(GrantType::Token) => Some("Invalid Login Token".into()),
AuthError::DifferingLogins => Some("Differing Logins".into()),
};
let detail = match self {
AuthError::ConversionError(ObjectConversionError::ImproperType{ expected, got }) =>
Some(format!("Primary data should be of type {} but is of type {} instead",
expected, got)),
AuthError::ConversionError(ObjectConversionError::FailedDeserialization(e)) =>
Some(format!("Failed to deserialize attributes of primary data: {}", e)),
AuthError::InvalidIdentity(ConversionError::Base64Decode(e)) =>
Some(format!("Failed to decode identity, base 64 invalid: {}", e)),
AuthError::InvalidIdentity(ConversionError::BadIdLength(l)) =>
Some(format!("Failed to decode identity, decoded identity is {} bytes long when it should be {}", l, ID_LEN)),
AuthError::QaulError(QaulError::NotAuthorised) =>
Some("Current user is not authorised to perform this action".into()),
AuthError::QaulError(QaulError::NoUser) =>
Some("Target user is not known to Qaul".into()),
AuthError::QaulError(QaulError::InvalidQuery) => None,
AuthError::QaulError(QaulError::InvalidPayload) =>
Some("Most likely the payload is too large".into()),
AuthError::QaulError(QaulError::CallbackTimeout) => None,
AuthError::InvalidToken(GrantType::Cookie) =>
Some("The 'bearer' cookie contains a token that is either no long or never was valid".into()),
AuthError::InvalidToken(GrantType::Token) =>
Some("The Authorization header contains a token that is either no longer or never was valid".into()),
_ => Some(self.detail()),
};
let pointer = match self {
AuthError::MultipleData => Some("/data".into()),
AuthError::NoData => Some("/".into()),
AuthError::ConversionError(ObjectConversionError::ImproperType {
expected: _,
got: _,
}) => Some("/data/type".into()),
AuthError::ConversionError(ObjectConversionError::FailedDeserialization(_)) => {
Some("/data/attributes".into())
}
AuthError::NoAttributes => Some("/data".into()),
AuthError::InvalidIdentity(_) => Some("/data/id".into()),
_ => None,
};
(
Error {
status: Some(format!("{}", status.to_u16())),
title,
detail,
source: pointer.map(|p| ErrorSource {
pointer: Some(p),
..Default::default()
}),
..Default::default()
},
status,
)
}
}
impl StdError for AuthError {}
impl Display for AuthError {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "Auth Error: {}", self.detail())
}
}
impl From<AuthError> for IronError {
fn from(e: AuthError) -> IronError {
let (err, status) = e.into_error();
let document = Document {
errors: Some(vec![err]),
..Default::default()
};
Self::new(
e,
(
status,
serde_json::to_string(&document).unwrap(),
JSONAPI_MIME.clone(),
),
)
}
}
use super::{AuthError, Authenticator};
use crate::{
models::{GrantType, Success, UserAuth, UserGrant},
Cookies, JsonApi, QaulCore, JSONAPI_MIME,
};
use cookie::Cookie;
use iron::{prelude::*, status::Status};
use japi::{Document, OptionalVec, ResourceObject};
use libqaul::UserAuth as QaulUserAuth;
use std::convert::TryInto;
pub fn login(req: &mut Request) -> IronResult<Response> {
// data should contain exactly one object
let data = match &req.extensions.get::<JsonApi>().unwrap().data {
OptionalVec::One(Some(d)) => d,
OptionalVec::Many(_) => {
return Err(AuthError::MultipleData.into());
}
_ => {
return Err(AuthError::NoData.into());
}
};
// try to decode the payload
let ua: ResourceObject<UserAuth> = match data.try_into() {
Ok(ua) => ua,
Err(e) => {
return Err(AuthError::ConversionError(e).into());
}
};
// is the identity valid
let identity = match UserAuth::identity(&ua) {
Ok(id) => id,
Err(e) => {
return Err(AuthError::InvalidIdentity(e).into());
}
};
// is there a secret (there has to be a secret!)
let attr = match ua.attributes {
Some(s) => s,
None => {
return Err(AuthError::NoAttributes.into());
}
};
let secret = attr.secret;
let grant_type = attr.grant_type;
let qaul = req.extensions.get::<QaulCore>().unwrap();
// perform the login
let (ident, token) = match qaul.user_login(identity.clone(), &secret) {
Ok(QaulUserAuth::Trusted(ident, token)) => (ident, token),
Ok(QaulUserAuth::Untrusted(_)) => {
unreachable!();
}
Err(e) => {
return Err(AuthError::QaulError(e).into());
}
};
// register the token with the authenticator
{
req.extensions
.get::<Authenticator>()
.unwrap()
.tokens
.lock()
.unwrap()
.insert(token.clone(), ident);
}
// return the grant
let obj = match grant_type {
GrantType::Token => ResourceObject::<UserGrant>::new(token, None).into(),
GrantType::Cookie => {
// TODO: what should we do when a user is already logged in?
req.extensions
.get_mut::<Cookies>()
.unwrap()
.add(Cookie::new("bearer", token));
Success::from_message("Successfully logged in".into()).into()
}
};
let doc = Document {
data: OptionalVec::One(Some(obj)),
..Default::default()
};
Ok(Response::with((
Status::Ok,
JSONAPI_MIME.clone(),
serde_json::to_string(&doc).unwrap(),
)))
}
#[cfg(test)]
mod test {
use super::*;
use crate::cookie::CookieManager;
use anneal::RequestBuilder;
use libqaul::{Identity, Qaul};
fn setup() -> (RequestBuilder, Identity, QaulUserAuth, Authenticator) {
let qaul = Qaul::start();
let user_auth = qaul.user_create("a").unwrap();
let _qaul_core = QaulCore::new(&qaul);
let (before_manager, _) = CookieManager::new();
let mut rb = RequestBuilder::default_post();
let auth = Authenticator::new();
rb.add_middleware(QaulCore::new(&qaul))
.add_middleware(before_manager)
.add_middleware(JsonApi)
.add_middleware(auth.clone());
(rb, user_auth.clone().identity(), user_auth, auth)
}
#[test]
fn valid_login_token() {
let (mut rb, id, _, auth) = setup();
let go = rb
.set_primary_data(
UserAuth::from_identity(id.clone(), "a".into(), GrantType::Token).into(),
)
.request_response(|mut req| {
let response = login(&mut req).unwrap();
assert_eq!(req.extensions.get::<Cookies>().unwrap().get("bearer"), None);
Ok(response)
})
.unwrap()
.get_primary_data()
.unwrap();
let ro: ResourceObject<UserGrant> = go.try_into().unwrap();
let token = ro.id;
assert_eq!(auth.tokens.lock().unwrap().get(&token), Some(&id));
}
#[test]
fn valid_login_cookie() {
let (mut rb, id, _, auth) = setup();
let go = rb
.set_primary_data(
UserAuth::from_identity(id.clone(), "a".into(), GrantType::Cookie).into(),
)
.request_response(|mut req| {
let response = login(&mut req).unwrap();
let token = req
.extensions
.get::<Cookies>()
.unwrap()
.get("bearer")
.unwrap();
assert_eq!(auth.tokens.lock().unwrap().get(token.value()), Some(&id));
Ok(response)
})
.unwrap()
.get_primary_data()
.unwrap();
let _ro: ResourceObject<Success> = go.try_into().unwrap();
}
#[test]
fn multiple_data() {
let (mut rb, _, _, _) = setup();
rb.set_document(&Document {
data: OptionalVec::Many(vec![]),
..Default::default()
})
.request(|mut req| assert!(login(&mut req).is_err()))
}
#[test]
fn no_data() {
let (mut rb, _, _, _) = setup();
rb.set_document(&Document {
data: OptionalVec::NotPresent,
..Default::default()
})
.request(|mut req| assert!(login(&mut req).is_err()))
}
#[test]
fn wrong_object() {
let (mut rb, _, _, _) = setup();
rb.set_primary_data(Success::from_message("test".into()).into())
.request(|mut req| assert!(login(&mut req).is_err()))
}
#[test]
fn invalid_identity() {
let (mut rb, _, _, _) = setup();
rb.set_primary_data(ResourceObject::<UserAuth>::new("".into(), None).into())
.request(|mut req| assert!(login(&mut req).is_err()))
}
#[test]
fn no_secret() {
let (mut rb, id, _, _) = setup();
let mut ro = UserAuth::from_identity(id, "".into(), GrantType::Token);
ro.attributes = None;
rb.set_primary_data(ro.into())
.request(|mut req| assert!(login(&mut req).is_err()))
}
#[test]
fn bad_password() {
let (mut rb, id, _, _) = setup();
rb.set_primary_data(UserAuth::from_identity(id, "".into(), GrantType::Token).into())
.request(|mut req| assert!(login(&mut req).is_err()))
}
}
use super::{AuthError, Authenticator, CurrentUser};
use crate::{models::Success, Cookies, QaulCore, JSONAPI_MIME};
use iron::{prelude::*, status::Status};
use japi::{Document, OptionalVec};
use libqaul::UserAuth;
pub fn logout(req: &mut Request) -> IronResult<Response> {
// we can't log out until we know who we are
let (identity, token) = match req.extensions.get::<CurrentUser>() {
Some(UserAuth::Trusted(identity, token)) => (identity, token),
_ => {
return Err(AuthError::NotLoggedIn.into());
}
};
// log us out
let qaul = req.extensions.get::<QaulCore>().unwrap();
if let Err(e) = qaul.user_logout(UserAuth::Trusted(identity.clone(), token.clone())) {
return Err(AuthError::QaulError(e).into());
}
// tell the authenticator we've logged out
{
req.extensions
.get::<Authenticator>()
.unwrap()
.tokens
.lock()
.unwrap()
.remove(token);
}
// if an auth cookie had been set mark it for removal
//
// FIXME: This pattern has become disallowed, but I couldn't
// quickly figure out how to make it not need this pattern. Maybe
// a patch to `cookies` might be required?
let cookies = req.extensions.get_mut::<Cookies>().unwrap();
if let Some(cookie) = cookies.get("bearer") {
#[allow(mutable_borrow_reservation_conflict)]
cookies.remove(cookie.clone());
}
// return a little success message
// we're a JSON:API endpoint (well, probably) so we gotta return something
let obj = Success::from_message("Successfully logged out".into());
let doc = Document {
data: OptionalVec::One(Some(obj.into())),
..Default::default()
};
Ok(Response::with((
Status::Ok,
serde_json::to_string(&doc).unwrap(),
JSONAPI_MIME.clone(),
)))
}
#[cfg(test)]
mod test {
use super::*;
use crate::{cookie::CookieManager, JsonApi};
use anneal::RequestBuilder;
use cookie::{Cookie, CookieJar};
use iron::headers::{Authorization, Bearer};
use japi::ResourceObject;
use libqaul::Qaul;
use std::convert::TryInto;
fn setup() -> (RequestBuilder, Authenticator, String) {
let qaul = Qaul::start();
let user_auth = qaul.user_create("a".into()).unwrap();
let (ident, key) = qaul.user_authenticate(user_auth).unwrap();
let (before_manager, _) = CookieManager::new();
let auth = Authenticator::new();
{
auth.tokens.lock().unwrap().insert(key.clone(), ident);
}
let mut rb = RequestBuilder::default();
rb.add_middleware(QaulCore::new(&qaul));
rb.add_middleware(before_manager);
rb.add_middleware(JsonApi);
rb.add_middleware(auth.clone());
(rb, auth, key)
}
#[test]
fn logout_cookie() {
let (mut rb, auth, key) = setup();
let mut jar = CookieJar::new();
jar.add(Cookie::new("bearer", key.clone()));
let go = rb
.set_cookies(&jar)
.request_response(|mut req| {
let response = logout(&mut req).unwrap();
assert_eq!(auth.tokens.lock().unwrap().get(&key), None);
assert_eq!(req.extensions.get::<Cookies>().unwrap().get("bearer"), None);
Ok(response)
})
.unwrap()
.get_primary_data()
.unwrap();
let _ro: ResourceObject<Success> = go.try_into().unwrap();
}
#[test]
fn logout_token() {
let (mut rb, auth, key) = setup();
let go = rb
.set_header(Authorization(Bearer { token: key.clone() }))
.request_response(|mut req| {
let response = logout(&mut req).unwrap();
assert_eq!(auth.tokens.lock().unwrap().get(&key), None);
Ok(response)
})
.unwrap()
.get_primary_data()
.unwrap();
let _ro: ResourceObject<Success> = go.try_into().unwrap();
}
#[test]
fn no_login() {
let (rb, _, _) = setup();
rb.request(|mut req| assert!(logout(&mut req).is_err()));
}
}
mod authenticator;
pub(crate) use authenticator::Authenticator;
pub use authenticator::CurrentUser;
mod login;
pub(crate) use login::login;
mod logout;
pub(crate) use logout::logout;
mod error;
pub(crate) use error::AuthError;
use super::AuthError;
use crate::{models::GrantType, Cookies};
use crate::{error::GenericError, QaulCore};
use iron::{
headers::{Authorization, Bearer},
prelude::*,
typemap, BeforeMiddleware,
};
use libqaul::{Identity, UserAuth};
use libqaul::{users::UserAuth, Identity};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
......@@ -15,7 +14,7 @@ use std::{
///
/// ```
/// # use iron::prelude::*;
/// # use qaul_http::CurrentUser;
/// # use libqaul_http::CurrentUser;
/// fn handler(req: &mut Request) -> IronResult<Response> {
/// // Some(UserAuth) if an authenticated user is is requesting this endpoint
/// // None otherwise
......@@ -57,30 +56,12 @@ impl BeforeMiddleware for Authenticator {
match self.tokens.lock().unwrap().get(&bearer.token) {
Some(identity) => {
req.extensions
.insert::<CurrentUser>(UserAuth::Trusted(*identity, bearer.token.clone()));
.insert::<CurrentUser>(UserAuth(*identity, bearer.token.clone()));
}
None => {
return Err(AuthError::InvalidToken(GrantType::Token).into());
}
}
}
// attempt to authenticate with the `bearer` cookie
if let Some(cookie) = req.extensions.get::<Cookies>().unwrap().get("bearer") {
match self.tokens.lock().unwrap().get(cookie.value()) {
Some(identity) => {
let ua = UserAuth::Trusted(*identity, cookie.value().into());
if req
.extensions
.get::<CurrentUser>()
.map_or(false, |other_id| *other_id != ua)
{
return Err(AuthError::DifferingLogins.into());
}
req.extensions.insert::<CurrentUser>(ua);
}
None => {
return Err(AuthError::InvalidToken(GrantType::Cookie).into());
return Err(GenericError::new("Invalid Login Token".into())
.detail("The authorization header contains a token that is either no longer valid or never was valid".into())
.into());
}
}
}
......@@ -92,15 +73,14 @@ impl BeforeMiddleware for Authenticator {
#[cfg(test)]
mod test {
use super::*;
use crate::cookie::CookieManager;
use anneal::RequestBuilder;
use cookie::{Cookie, CookieJar};
use iron::method::Method;
use libqaul::Qaul;
fn setup() -> (RequestBuilder, Authenticator, UserAuth, String) {
let qaul = Qaul::start();
let user_auth = qaul.user_create("a".into()).unwrap();
let (ident, key) = qaul.user_authenticate(user_auth.clone()).unwrap();
let qaul = Qaul::dummy();
let user_auth = qaul.users().create("a".into()).unwrap();
let UserAuth(ident, key) = user_auth.clone();
let authenticator = Authenticator::new();
{
......@@ -112,7 +92,6 @@ mod test {
}
let mut rb = RequestBuilder::default();
rb.add_middleware(CookieManager::new().0);
rb.add_middleware(authenticator.clone());
(rb, authenticator, user_auth, key)
......@@ -137,88 +116,13 @@ mod test {
#[test]
fn invalid_token_login() {
let (mut rb, authenticator, _, _) = setup();
let (mut rb, authenticator, user_auth, _) = setup();
rb.set_header(Authorization(Bearer {
token: "i am not valid".into(),
}))
.set_chain(vec![Box::new(CookieManager::new().0)])
.set_chain(vec![])
.request(|mut req| {
assert!(authenticator.before(&mut req).is_err());
});
}
#[test]
fn valid_login_cookie() {
let (mut rb, _, user_auth, key) = setup();
let mut jar = CookieJar::new();
jar.add(Cookie::new("bearer", key));
rb.set_cookies(&jar).request(|req| {
assert_eq!(req.extensions.get::<CurrentUser>(), Some(&user_auth));
});
}
#[test]
fn invalid_login_cookie() {
let (mut rb, authenticator, _, _) = setup();
let mut jar = CookieJar::new();
jar.add(Cookie::new("bearer", "i'm not the right key"));
rb.set_cookies(&jar)
.set_chain(vec![Box::new(CookieManager::new().0)])
.request(|mut req| {
assert!(authenticator.before(&mut req).is_err());
});
}
// this test ensures that if you log in as two seperate users you'll fail to authenticate
#[test]
fn two_rights_dont_make_a_left() {
let qaul = Qaul::start();
let user_auth = qaul.user_create("a".into()).unwrap();
let (ident, key) = qaul.user_authenticate(user_auth.clone()).unwrap();
let user_auth2 = qaul.user_create("b".into()).unwrap();
let (ident2, key2) = qaul.user_authenticate(user_auth2.clone()).unwrap();
let authenticator = Authenticator::new();
{
let mut tokens = authenticator.tokens.lock().unwrap();
tokens.insert(key.clone(), ident);
tokens.insert(key2.clone(), ident2);
}
let mut jar = CookieJar::new();
jar.add(Cookie::new("bearer", key2));
RequestBuilder::default()
.set_header(Authorization(Bearer { token: key }))
.set_cookies(&jar)
.add_middleware(CookieManager::new().0)
.request(|mut req| {
assert!(authenticator.before(&mut req).is_err());
});
}
// this test ensures that if you user both a cookie and a bearer token to log in as the same
// user everything works
#[test]
fn unless_theyre_180_degrees() {
let qaul = Qaul::start();
let user_auth = qaul.user_create("a".into()).unwrap();
let (ident, key) = qaul.user_authenticate(user_auth.clone()).unwrap();
let authenticator = Authenticator::new();
{
let mut tokens = authenticator.tokens.lock().unwrap();
tokens.insert(key.clone(), ident);
}
let mut jar = CookieJar::new();
jar.add(Cookie::new("bearer", key.clone()));
RequestBuilder::default()
.set_header(Authorization(Bearer { token: key }))
.set_cookies(&jar)
.add_middleware(CookieManager::new().0)
.add_middleware(authenticator)
.request(|req| {
assert_eq!(req.extensions.get::<CurrentUser>(), Some(&user_auth));
});
}
}
use crate::JSONAPI_MIME;
use cookie::{Cookie, CookieJar, ParseError};
use iron::{
headers::{Cookie as CookieHeader, SetCookie},
middleware::{AfterMiddleware, BeforeMiddleware},
prelude::*,
status::Status,
typemap::Key,
};
use japi::{Document, Error};
use std::{
error::Error as StdError,
fmt::{Display, Formatter, Result as FmtResult},
};
#[derive(Debug)]
struct CookieJarError(ParseError);
impl CookieJarError {
fn detail(&self) -> String {
format!("Error parsing cookies ({})", self.0)
}
fn into_error(&self) -> (Error, Status) {
let status = Status::BadRequest;
let title = Some("Cookie Parse Error".into());
let detail = Some(self.detail());
(
Error {
status: Some(format!("{}", status.to_u16())),
title,
detail,
..Default::default()
},
status,
)
}
}
impl Display for CookieJarError {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "Cookie jar error: {}", self.detail())
}
}
impl StdError for CookieJarError {}
impl From<CookieJarError> for IronError {
fn from(e: CookieJarError) -> Self {
let (err, status) = e.into_error();
let document = Document {
errors: Some(vec![err]),
..Default::default()
};
Self::new(
e,
(
status,
serde_json::to_string(&document).unwrap(),
JSONAPI_MIME.clone(),
),
)
}
}
impl From<ParseError> for CookieJarError {
fn from(e: ParseError) -> Self {
CookieJarError(e)
}
}
/// User this key to get the CookieJar for the request
///
/// Any changes performed on the cookie jar will be sent back to the
/// client in the response
///
/// ```
/// # use iron::prelude::*;
/// # use qaul_http::Cookies;
/// fn handler(req: &mut Request) -> IronResult<Response> {
/// let cookie_jar = req.extensions.get::<Cookies>().unwrap();
///
/// // ...
/// # Ok(Response::with(""))
/// # }
/// ```
pub struct Cookies;
impl Key for Cookies {
type Value = CookieJar;
}
pub(crate) struct CookieManager;
impl CookieManager {
fn build_jar(req: &mut Request) -> Result<CookieJar, CookieJarError> {
let mut jar = CookieJar::new();
if let Some(cookies) = req.headers.get::<CookieHeader>() {
for cookie in cookies.iter() {
jar.add_original(Cookie::parse(cookie)?.into_owned());
}
}
Ok(jar)
}
pub fn new() -> (Self, Self) {
(Self, Self)
}
}
impl BeforeMiddleware for CookieManager {
fn before(&self, req: &mut Request) -> IronResult<()> {
let jar = Self::build_jar(req)?;
req.extensions.insert::<Cookies>(jar);
Ok(())
}
}
impl AfterMiddleware for CookieManager {
fn after(&self, req: &mut Request, mut res: Response) -> IronResult<Response> {
let cookies: Vec<String> = req
.extensions
.get::<Cookies>()
.unwrap()
.delta()
.map(|c| c.to_string())
.collect();
if cookies.len() != 0 {
res.headers.set::<SetCookie>(SetCookie(cookies));
}
Ok(res)
}
}
#[cfg(test)]
mod test {
use super::*;
use anneal::RequestBuilder;
#[test]
fn no_cookies() {
RequestBuilder::default().request(|mut req| {
CookieManager.before(&mut req).unwrap();
assert_eq!(req.extensions.get::<Cookies>().unwrap().iter().count(), 0);
let res = CookieManager.after(&mut req, Response::with("")).unwrap();
assert!(!res.headers.has::<SetCookie>());
});
}
#[test]
fn valid_cookies() {
RequestBuilder::default()
.set_header(CookieHeader(vec!["a=b".into(), "c=d".into()]))
.request(|mut req| {
CookieManager.before(&mut req).unwrap();
assert_eq!(req.extensions.get::<Cookies>().unwrap().iter().count(), 2);
let cookies = req.extensions.get_mut::<Cookies>().unwrap();
assert_eq!(cookies.get("a").unwrap().value(), "b");
assert_eq!(cookies.get("c").unwrap().value(), "d");
cookies.add(Cookie::new("e", "f"));
let res = CookieManager.after(&mut req, Response::with("")).unwrap();
assert_eq!(
*res.headers.get::<SetCookie>().unwrap(),
SetCookie(vec!["e=f".into()])
);
})
}
#[test]
fn invalid_cookies() {
RequestBuilder::default()
.set_header(CookieHeader(vec!["a".into()]))
.request(|mut req| {
let err = match CookieManager.before(&mut req) {
Ok(_) => panic!("Request completed successfully"),
Err(e) => e.error,
};
assert_eq!(err.to_string(),
"Cookie jar error: Error parsing cookies (the cookie is missing a name/value pair)");
});
}
}
/// Core service routes
use crate::QaulCore;
use iron::{prelude::*, status};
pub fn get_all_users(req: &mut Request) -> IronResult<Response> {
let qaul = req.extensions.get::<QaulCore>().unwrap();
let all_users = format!("{:?}", qaul.user_get_all());
Ok(Response::with((status::Ok, all_users)))
}
use crate::{
error::{ApiError, DocumentError, QaulError},
models::{into_identity, Grant, User},
Authenticator, JsonApi, QaulCore, JSONAPI_MIME,
};
use iron::{prelude::*, status::Status};
use japi::{Document, OptionalVec, ResourceObject};
use libqaul::users::UserAuth;
use serde_json;
use std::convert::TryFrom;
pub fn grant_create(req: &mut Request) -> IronResult<Response> {
let ro = req
.extensions
.get::<JsonApi>()
.ok_or(DocumentError::NoDocument)
.and_then(|d| match &d.data {
OptionalVec::One(Some(go)) => Ok(go),
OptionalVec::Many(_) => Err(DocumentError::MultipleData),
_ => Err(DocumentError::NoData),
})
.and_then(|go| ResourceObject::<Grant>::try_from(go).map_err(|e| DocumentError::from(e)))?;
let id = ro
.relationships
.as_ref()
.ok_or(DocumentError::no_relationships(
"/data/relationships".into(),
))
.and_then(|rels| {
rels.get("user").ok_or(DocumentError::no_relationship(
"user".into(),
"/data/relationships/user".into(),
))
})
.and_then(|rel| match &rel.data {
OptionalVec::One(Some(go)) => Ok(go),
OptionalVec::Many(_) => Err(DocumentError::MultipleData),
_ => Err(DocumentError::NoData),
})
.map_err(|e| ApiError::from(e))
.and_then(|go| {
ResourceObject::<User>::try_from(go).map_err(|e| {
DocumentError::ConversionError {
err: e,
pointer: Some("/data/relationships/user".into()),
}
.into()
})
})
.and_then(|ro| {
ro.id.ok_or(
DocumentError::NoId {
pointer: Some("/data/relationships/user/id".into()),
}
.into(),
)
})
.and_then(|id| into_identity(&id).map_err(|e| ApiError::from(e)))?;
let attr = ro
.attributes
.ok_or(DocumentError::no_attributes("/data/attributes".into()))?;
let ua = req
.extensions
.get::<QaulCore>()
.unwrap()
.users()
.login(id, &attr.secret)
.map_err(|e| QaulError::from(e))?;
{
let UserAuth(id, grant) = ua.clone();
req.extensions
.get::<Authenticator>()
.unwrap()
.tokens
.lock()
.unwrap()
.insert(grant, id);
}
let grant = Grant::from_user_auth(ua)?;
let doc = Document {
data: OptionalVec::One(Some(grant.into())),
..Default::default()
};
Ok(Response::with((
Status::Created,
JSONAPI_MIME.clone(),
serde_json::to_string(&doc).unwrap(),
)))
}
#[cfg(test)]
mod test {
use super::*;
use crate::models::from_identity;
use anneal::RequestBuilder;
use japi::{Identifier, Relationship, Relationships};
use libqaul::{Qaul};
use std::sync::Arc;
#[test]
fn works() {
let qaul = Arc::new(Qaul::dummy());
let id = qaul.users().create("test").unwrap().0;
let mut relationships = Relationships::new();
relationships.insert(
"user".into(),
Relationship {
data: OptionalVec::One(Some(Identifier::new(from_identity(&id), "user".into()))),
..Default::default()
},
);
let ro = ResourceObject {
id: None,
attributes: Some(Grant {
secret: "test".into(),
}),
relationships: Some(relationships.clone()),
links: None,
meta: None,
};
let auth = Authenticator::new();
let go = RequestBuilder::default_post()
.add_middleware(QaulCore::new(qaul.clone()))
.add_middleware(JsonApi)
.add_middleware(auth.clone())
.set_primary_data(ro.into())
.request_response(|mut req| grant_create(&mut req))
.unwrap()
.get_primary_data()
.unwrap();
let ro = ResourceObject::<Grant>::try_from(go).unwrap();
assert_eq!(ro.relationships.unwrap(), relationships);
let grant = ro.id.unwrap();
assert_eq!(auth.tokens.lock().unwrap().get(&grant), Some(&id));
qaul.users().change_pw(UserAuth(id, grant), "test2").unwrap();
}
}
use crate::{
error::{AuthError, GenericError, QaulError},