|
@@ -3,50 +3,37 @@ use microrm::prelude::*;
|
|
|
use serde::Deserialize;
|
|
|
use tide::http::Cookie;
|
|
|
|
|
|
-#[derive(Clone)]
|
|
|
-pub struct ServerState {
|
|
|
- core: &'static super::ServerCoreState,
|
|
|
- realm_cache:
|
|
|
- std::sync::Arc<std::sync::RwLock<std::collections::HashMap<String, schema::RealmID>>>,
|
|
|
+pub(super) struct SessionHelper<'l> {
|
|
|
+ qi: &'l microrm::QueryInterface<'static>,
|
|
|
+ tmpl: &'l handlebars::Handlebars<'l>,
|
|
|
+ realm_str: &'l str,
|
|
|
}
|
|
|
|
|
|
-type Request = tide::Request<ServerState>;
|
|
|
+type Request = tide::Request<super::ServerStateWrapper>;
|
|
|
|
|
|
-impl ServerState {
|
|
|
- pub fn get_realm(&self, req: &Request) -> Option<schema::RealmID> {
|
|
|
- let realm_str = req
|
|
|
- .param("realm")
|
|
|
- .expect("get_realm called with no :realm param");
|
|
|
- let cache = self.realm_cache.read().unwrap();
|
|
|
- let cache_lookup = cache.get(realm_str);
|
|
|
+const SESSION_COOKIE_NAME: &'static str = "uauth_session";
|
|
|
|
|
|
- // expected case
|
|
|
- if cache_lookup.is_some() {
|
|
|
- return cache_lookup.map(|x| *x);
|
|
|
+impl<'l> SessionHelper<'l> {
|
|
|
+ pub fn new(req: &'l Request) -> Self {
|
|
|
+ Self {
|
|
|
+ qi: req.state().core.pool.query_interface(),
|
|
|
+ tmpl: &req.state().core.templates,
|
|
|
+ realm_str: req.param("realm").expect("no realm param?"),
|
|
|
}
|
|
|
- drop(cache);
|
|
|
-
|
|
|
- // unexpected case, but maybe we haven't filled that cache entry yet
|
|
|
+ }
|
|
|
|
|
|
- let qi = self.core.pool.query_interface();
|
|
|
- let realm = qi
|
|
|
+ pub fn get_realm(&self) -> tide::Result<schema::RealmID> {
|
|
|
+ self.qi
|
|
|
.get()
|
|
|
- .by(schema::Realm::Shortname, realm_str)
|
|
|
+ .by(schema::Realm::Shortname, self.realm_str)
|
|
|
.one()
|
|
|
- .expect("couldn't query db");
|
|
|
-
|
|
|
- if let Some(with_id) = realm {
|
|
|
- let mut cache = self.realm_cache.write().unwrap();
|
|
|
- cache.insert(realm_str.to_owned(), with_id.id());
|
|
|
- return Some(with_id.id());
|
|
|
- }
|
|
|
-
|
|
|
- // other expected case, is bogus realm
|
|
|
- return None;
|
|
|
+ .expect("couldn't query db")
|
|
|
+ .map(|r| r.id())
|
|
|
+ .ok_or(tide::Error::from_str(404, "No such realm"))
|
|
|
}
|
|
|
|
|
|
fn build_session(
|
|
|
- qi: µrm::QueryInterface,
|
|
|
+ &self,
|
|
|
) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
|
|
|
let rng = ring::rand::SystemRandom::new();
|
|
|
let session_id: [u8; 32] = ring::rand::generate(&rng)
|
|
@@ -54,22 +41,25 @@ impl ServerState {
|
|
|
.expose();
|
|
|
let session_id = base64::encode_config(session_id, base64::URL_SAFE_NO_PAD);
|
|
|
|
|
|
- let maybe_id = qi.add(&schema::Session {
|
|
|
+ let maybe_id = self.qi.add(&schema::Session {
|
|
|
key: session_id.clone(),
|
|
|
});
|
|
|
+ let session_cookie = Cookie::build(SESSION_COOKIE_NAME, session_id)
|
|
|
+ .path("/")
|
|
|
+ .finish();
|
|
|
Ok((
|
|
|
maybe_id.ok().ok_or(tide::Error::from_str(
|
|
|
500,
|
|
|
"Failed to store session in database",
|
|
|
))?,
|
|
|
- Some(Cookie::new("vogt_session", session_id)),
|
|
|
+ Some(session_cookie),
|
|
|
))
|
|
|
}
|
|
|
|
|
|
pub fn verify_session(&self, req: &Request) -> Option<(schema::RealmID, schema::UserID)> {
|
|
|
self.get_or_build_session(req)
|
|
|
.ok()
|
|
|
- .zip(self.get_realm(req))
|
|
|
+ .zip(self.get_realm().ok())
|
|
|
.and_then(|((sid, _cookie), realm)| {
|
|
|
self.get_auth_for_session(realm, sid).and_then(|auth| {
|
|
|
if auth.challenges_left.len() == 0 {
|
|
@@ -81,23 +71,26 @@ impl ServerState {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+ pub fn get_session(&self, req: &Request) -> Option<schema::SessionID> {
|
|
|
+ req.cookie(SESSION_COOKIE_NAME)
|
|
|
+ .and_then(|sid| {
|
|
|
+ self.qi
|
|
|
+ .get()
|
|
|
+ .by(schema::Session::Key, sid.value())
|
|
|
+ .one()
|
|
|
+ .expect("couldn't query db")
|
|
|
+ })
|
|
|
+ .and_then(|session| Some(session.id()))
|
|
|
+ }
|
|
|
+
|
|
|
pub fn get_or_build_session(
|
|
|
&self,
|
|
|
req: &Request,
|
|
|
) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
|
|
|
- let qi = self.core.pool.query_interface();
|
|
|
- if let Some(sid) = req.cookie("vogt_session") {
|
|
|
- let existing = qi
|
|
|
- .get()
|
|
|
- .by(schema::Session::Key, sid.value())
|
|
|
- .one()
|
|
|
- .expect("couldn't query db");
|
|
|
-
|
|
|
- if existing.is_some() {
|
|
|
- return Ok((existing.unwrap().id(), None));
|
|
|
- }
|
|
|
+ match self.get_session(&req) {
|
|
|
+ Some(sid) => Ok((sid, None)),
|
|
|
+ None => self.build_session(),
|
|
|
}
|
|
|
- Self::build_session(qi)
|
|
|
}
|
|
|
|
|
|
pub fn get_auth_for_session(
|
|
@@ -105,10 +98,9 @@ impl ServerState {
|
|
|
realm: schema::RealmID,
|
|
|
session: schema::SessionID,
|
|
|
) -> Option<microrm::WithID<schema::SessionAuthentication>> {
|
|
|
- let qi = self.core.pool.query_interface();
|
|
|
-
|
|
|
use schema::SessionAuthentication as SAC;
|
|
|
- qi.get()
|
|
|
+ self.qi
|
|
|
+ .get()
|
|
|
.by(SAC::Realm, &realm)
|
|
|
.by(SAC::Session, &session)
|
|
|
.one()
|
|
@@ -116,10 +108,9 @@ impl ServerState {
|
|
|
}
|
|
|
|
|
|
pub fn destroy_auth(&self, realm: schema::RealmID, session: schema::SessionID) {
|
|
|
- let qi = self.core.pool.query_interface();
|
|
|
-
|
|
|
use schema::SessionAuthentication as SAC;
|
|
|
- qi.delete()
|
|
|
+ self.qi
|
|
|
+ .delete()
|
|
|
.by(SAC::Realm, &realm)
|
|
|
.by(SAC::Session, &session)
|
|
|
.exec()
|
|
@@ -127,10 +118,11 @@ impl ServerState {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-impl ServerState {
|
|
|
+impl<'l> SessionHelper<'l> {
|
|
|
fn render_login_from_auth(
|
|
|
&self,
|
|
|
mut response: tide::Response,
|
|
|
+ redirect: String,
|
|
|
auth: Option<schema::SessionAuthentication>,
|
|
|
error_msg: Option<String>,
|
|
|
) -> tide::Response {
|
|
@@ -143,37 +135,38 @@ impl ServerState {
|
|
|
|
|
|
if to_present.is_none() {
|
|
|
response.set_status(302);
|
|
|
- tide::Redirect::new("/").into()
|
|
|
+ tide::Redirect::new("../..").into()
|
|
|
} else {
|
|
|
- self.render_login_page(response, to_present.unwrap(), error_msg)
|
|
|
+ self.render_login_page(response, redirect, to_present.unwrap(), error_msg)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
fn render_login_page(
|
|
|
&self,
|
|
|
mut response: tide::Response,
|
|
|
+ redirect: String,
|
|
|
to_present: schema::AuthChallengeType,
|
|
|
error_msg: Option<String>,
|
|
|
) -> tide::Response {
|
|
|
- let tmpl = &self.core.templates;
|
|
|
-
|
|
|
let do_challenge = |ty, ch| {
|
|
|
- tmpl.render(
|
|
|
- "id_v1_login",
|
|
|
- &serde_json::json!(
|
|
|
- {
|
|
|
- "challenge":
|
|
|
- format!(r#"
|
|
|
+ self.tmpl
|
|
|
+ .render(
|
|
|
+ "id_v1_login",
|
|
|
+ &serde_json::json!(
|
|
|
+ {
|
|
|
+ "challenge":
|
|
|
+ format!(r#"
|
|
|
<input type="hidden" name="challenge_type" value="{:?}" />
|
|
|
<div class="challenge-type">{}</div>
|
|
|
<div class="challenge-content">{}</div>
|
|
|
"#,
|
|
|
- to_present, ty, ch),
|
|
|
- "error_msg": error_msg.iter().collect::<Vec<_>>()
|
|
|
- }
|
|
|
- ),
|
|
|
- )
|
|
|
- .unwrap()
|
|
|
+ to_present, ty, ch),
|
|
|
+ "redirect": redirect,
|
|
|
+ "error_msg": error_msg.iter().collect::<Vec<_>>()
|
|
|
+ }
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .unwrap()
|
|
|
};
|
|
|
|
|
|
response.set_content_type("text/html");
|
|
@@ -198,48 +191,57 @@ impl ServerState {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async fn v1_login(req: tide::Request<ServerState>) -> tide::Result<tide::Response> {
|
|
|
+async fn v1_login(req: Request) -> tide::Result<tide::Response> {
|
|
|
let mut response = tide::Response::builder(200).build();
|
|
|
|
|
|
- let realm = req
|
|
|
- .state()
|
|
|
- .get_realm(&req)
|
|
|
- .ok_or(tide::Error::from_str(404, "No such realm"))?;
|
|
|
- let (session_id, cookie) = req.state().get_or_build_session(&req)?;
|
|
|
+ let shelper = SessionHelper::new(&req);
|
|
|
+
|
|
|
+ let realm = shelper.get_realm()?;
|
|
|
+ let (session_id, cookie) = shelper.get_or_build_session(&req)?;
|
|
|
cookie.map(|c| response.insert_cookie(c));
|
|
|
|
|
|
- let auth = req.state().get_auth_for_session(realm, session_id);
|
|
|
+ let auth = shelper.get_auth_for_session(realm, session_id);
|
|
|
|
|
|
- Ok(req
|
|
|
- .state()
|
|
|
- .render_login_from_auth(response, auth.map(|a| a.wrapped()), None))
|
|
|
-}
|
|
|
+ #[derive(serde::Deserialize)]
|
|
|
+ struct LoginQuery {
|
|
|
+ redirect: Option<String>,
|
|
|
+ }
|
|
|
|
|
|
-async fn v1_login_post(mut req: tide::Request<ServerState>) -> tide::Result<tide::Response> {
|
|
|
- let mut response = tide::Response::builder(200).build();
|
|
|
+ let query: LoginQuery = req.query().unwrap();
|
|
|
|
|
|
- let realm = req
|
|
|
- .state()
|
|
|
- .get_realm(&req)
|
|
|
- .ok_or(tide::Error::from_str(404, "No such realm"))?;
|
|
|
- let (session_id, cookie) = req.state().get_or_build_session(&req)?;
|
|
|
- cookie.map(|c| response.insert_cookie(c));
|
|
|
+ Ok(shelper.render_login_from_auth(
|
|
|
+ response,
|
|
|
+ query.redirect.unwrap_or_else(|| "../..".to_string()),
|
|
|
+ auth.map(|a| a.wrapped()),
|
|
|
+ None,
|
|
|
+ ))
|
|
|
+}
|
|
|
|
|
|
- let mut auth = req.state().get_auth_for_session(realm, session_id);
|
|
|
+async fn v1_login_post(mut req: Request) -> tide::Result<tide::Response> {
|
|
|
+ let mut response = tide::Response::builder(200).build();
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
struct ResponseBody {
|
|
|
challenge_type: String,
|
|
|
challenge: String,
|
|
|
reset: Option<String>,
|
|
|
+ redirect: String,
|
|
|
}
|
|
|
|
|
|
let body: ResponseBody = req.body_form().await?;
|
|
|
|
|
|
+ let shelper = SessionHelper::new(&req);
|
|
|
+
|
|
|
+ let realm = shelper.get_realm()?;
|
|
|
+ let (session_id, cookie) = shelper.get_or_build_session(&req)?;
|
|
|
+ cookie.map(|c| response.insert_cookie(c));
|
|
|
+
|
|
|
+ let mut auth = shelper.get_auth_for_session(realm, session_id);
|
|
|
+
|
|
|
// check if a login reset was requested; if so, we start again from the top
|
|
|
if body.reset.is_some() {
|
|
|
if let Some(_) = auth {
|
|
|
- req.state().destroy_auth(realm, session_id);
|
|
|
+ shelper.destroy_auth(realm, session_id);
|
|
|
response.set_status(302);
|
|
|
return Ok(tide::Redirect::new("login").into());
|
|
|
}
|
|
@@ -268,7 +270,7 @@ async fn v1_login_post(mut req: tide::Request<ServerState>) -> tide::Result<tide
|
|
|
match challenge {
|
|
|
ChallengeType::Username => {
|
|
|
let qi = req.state().core.pool.query_interface();
|
|
|
- req.state().destroy_auth(realm, session_id);
|
|
|
+ shelper.destroy_auth(realm, session_id);
|
|
|
|
|
|
let user = qi
|
|
|
.get()
|
|
@@ -330,32 +332,27 @@ async fn v1_login_post(mut req: tide::Request<ServerState>) -> tide::Result<tide
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- Ok(req
|
|
|
- .state()
|
|
|
- .render_login_from_auth(response, auth.map(|a| a.wrapped()), error))
|
|
|
+ Ok(shelper.render_login_from_auth(response, body.redirect, auth.map(|a| a.wrapped()), error))
|
|
|
}
|
|
|
|
|
|
-async fn v1_logout(req: tide::Request<ServerState>) -> tide::Result<tide::Response> {
|
|
|
- let realm = req
|
|
|
- .state()
|
|
|
- .get_realm(&req)
|
|
|
- .ok_or(tide::Error::from_str(404, "No such realm"))?;
|
|
|
- let (session_id, _) = req.state().get_or_build_session(&req)?;
|
|
|
+async fn v1_logout(req: Request) -> tide::Result<tide::Response> {
|
|
|
+ let shelper = SessionHelper::new(&req);
|
|
|
|
|
|
- req.state().destroy_auth(realm, session_id);
|
|
|
- Ok(tide::Redirect::new("/").into())
|
|
|
-}
|
|
|
-
|
|
|
-pub fn session_v1_server(core: &'static super::ServerCoreState) -> tide::Server<ServerState> {
|
|
|
- let mut srv = tide::with_state(ServerState {
|
|
|
- core,
|
|
|
- realm_cache: std::sync::Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
|
|
|
- });
|
|
|
+ #[derive(serde::Deserialize)]
|
|
|
+ struct LogoutQuery {
|
|
|
+ redirect: Option<String>,
|
|
|
+ }
|
|
|
|
|
|
- srv.with(tide::log::LogMiddleware::new());
|
|
|
+ let query: LogoutQuery = req.query().unwrap();
|
|
|
|
|
|
- srv.at("login").get(v1_login).post(v1_login_post);
|
|
|
- srv.at("logout").get(v1_logout);
|
|
|
+ let realm = shelper.get_realm()?;
|
|
|
+ shelper
|
|
|
+ .get_session(&req)
|
|
|
+ .map(|sid| shelper.destroy_auth(realm, sid));
|
|
|
+ Ok(tide::Redirect::new(query.redirect.unwrap_or_else(|| "../..".into())).into())
|
|
|
+}
|
|
|
|
|
|
- srv
|
|
|
+pub(super) fn session_v1_server(mut route: tide::Route<super::ServerStateWrapper>) {
|
|
|
+ route.at("login").get(v1_login).post(v1_login_post);
|
|
|
+ route.at("logout").get(v1_logout);
|
|
|
}
|