mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-02-04 10:26:01 -05:00
better error handling for authentication
This commit is contained in:
parent
cb4f30b5a2
commit
2d45799b7d
@ -36,6 +36,15 @@ impl Error {
|
|||||||
pub fn compat(self) -> failure::Compat<Context<ErrorKind>> {
|
pub fn compat(self) -> failure::Compat<Context<ErrorKind>> {
|
||||||
self.inner.compat()
|
self.inner.compat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map<F>(self, op: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnOnce(ErrorKind) -> ErrorKind,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
inner: self.inner.map(op),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fail for Error {
|
impl Fail for Error {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
||||||
|
|
||||||
use crate::schema::Permissions;
|
use crate::schema::Permissions;
|
||||||
use base::strutil;
|
use base::{bail_t, format_err_t, strutil, ErrorKind, ResultExt};
|
||||||
use failure::{bail, format_err, Error};
|
use failure::{bail, format_err, Error};
|
||||||
use fnv::FnvHashMap;
|
use fnv::FnvHashMap;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
@ -678,23 +678,31 @@ impl State {
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
req: Request,
|
req: Request,
|
||||||
hash: &SessionHash,
|
hash: &SessionHash,
|
||||||
) -> Result<(&Session, &User), Error> {
|
) -> Result<(&Session, &User), base::Error> {
|
||||||
let s = match self.sessions.entry(*hash) {
|
let s = match self.sessions.entry(*hash) {
|
||||||
::std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
|
::std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
|
||||||
::std::collections::hash_map::Entry::Vacant(e) => e.insert(lookup_session(conn, hash)?),
|
::std::collections::hash_map::Entry::Vacant(e) => {
|
||||||
|
let s = lookup_session(conn, hash).map_err(|e| {
|
||||||
|
e.map(|k| match k {
|
||||||
|
ErrorKind::NotFound => ErrorKind::Unauthenticated,
|
||||||
|
e => e,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
e.insert(s)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let u = match self.users_by_id.get(&s.user_id) {
|
let u = match self.users_by_id.get(&s.user_id) {
|
||||||
None => bail!("session references nonexistent user!"),
|
None => bail_t!(Internal, "session references nonexistent user!"),
|
||||||
Some(u) => u,
|
Some(u) => u,
|
||||||
};
|
};
|
||||||
if let Some(r) = s.revocation_reason {
|
if let Some(r) = s.revocation_reason {
|
||||||
bail!("session is no longer valid (reason={})", r);
|
bail_t!(Unauthenticated, "session is no longer valid (reason={})", r);
|
||||||
}
|
}
|
||||||
s.last_use = req;
|
s.last_use = req;
|
||||||
s.use_count += 1;
|
s.use_count += 1;
|
||||||
s.dirty = true;
|
s.dirty = true;
|
||||||
if u.disabled() {
|
if u.disabled() {
|
||||||
bail!("user {:?} is disabled", &u.username);
|
bail_t!(Unauthenticated, "user {:?} is disabled", &u.username);
|
||||||
}
|
}
|
||||||
Ok((s, u))
|
Ok((s, u))
|
||||||
}
|
}
|
||||||
@ -811,9 +819,10 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Error> {
|
fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, base::Error> {
|
||||||
let mut stmt = conn.prepare_cached(
|
let mut stmt = conn
|
||||||
r#"
|
.prepare_cached(
|
||||||
|
r#"
|
||||||
select
|
select
|
||||||
user_id,
|
user_id,
|
||||||
seed,
|
seed,
|
||||||
@ -839,39 +848,52 @@ fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Erro
|
|||||||
where
|
where
|
||||||
session_id_hash = ?
|
session_id_hash = ?
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)
|
||||||
let mut rows = stmt.query(params![&hash.0[..]])?;
|
.err_kind(ErrorKind::Internal)?;
|
||||||
let row = rows.next()?.ok_or_else(|| format_err!("no such session"))?;
|
let mut rows = stmt
|
||||||
let creation_addr: FromSqlIpAddr = row.get(8)?;
|
.query(params![&hash.0[..]])
|
||||||
let revocation_addr: FromSqlIpAddr = row.get(11)?;
|
.err_kind(ErrorKind::Internal)?;
|
||||||
let last_use_addr: FromSqlIpAddr = row.get(16)?;
|
let row = rows
|
||||||
|
.next()
|
||||||
|
.err_kind(ErrorKind::Internal)?
|
||||||
|
.ok_or_else(|| format_err_t!(NotFound, "no such session"))?;
|
||||||
|
let creation_addr: FromSqlIpAddr = row.get(8).err_kind(ErrorKind::Internal)?;
|
||||||
|
let revocation_addr: FromSqlIpAddr = row.get(11).err_kind(ErrorKind::Internal)?;
|
||||||
|
let last_use_addr: FromSqlIpAddr = row.get(16).err_kind(ErrorKind::Internal)?;
|
||||||
let mut permissions = Permissions::new();
|
let mut permissions = Permissions::new();
|
||||||
permissions.merge_from_bytes(row.get_raw_checked(18)?.as_blob()?)?;
|
permissions
|
||||||
|
.merge_from_bytes(
|
||||||
|
row.get_raw_checked(18)
|
||||||
|
.err_kind(ErrorKind::Internal)?
|
||||||
|
.as_blob()
|
||||||
|
.err_kind(ErrorKind::Internal)?,
|
||||||
|
)
|
||||||
|
.err_kind(ErrorKind::Internal)?;
|
||||||
Ok(Session {
|
Ok(Session {
|
||||||
user_id: row.get(0)?,
|
user_id: row.get(0).err_kind(ErrorKind::Internal)?,
|
||||||
seed: row.get(1)?,
|
seed: row.get(1).err_kind(ErrorKind::Internal)?,
|
||||||
flags: row.get(2)?,
|
flags: row.get(2).err_kind(ErrorKind::Internal)?,
|
||||||
domain: row.get(3)?,
|
domain: row.get(3).err_kind(ErrorKind::Internal)?,
|
||||||
description: row.get(4)?,
|
description: row.get(4).err_kind(ErrorKind::Internal)?,
|
||||||
creation_password_id: row.get(5)?,
|
creation_password_id: row.get(5).err_kind(ErrorKind::Internal)?,
|
||||||
creation: Request {
|
creation: Request {
|
||||||
when_sec: row.get(6)?,
|
when_sec: row.get(6).err_kind(ErrorKind::Internal)?,
|
||||||
user_agent: row.get(7)?,
|
user_agent: row.get(7).err_kind(ErrorKind::Internal)?,
|
||||||
addr: creation_addr.0,
|
addr: creation_addr.0,
|
||||||
},
|
},
|
||||||
revocation: Request {
|
revocation: Request {
|
||||||
when_sec: row.get(9)?,
|
when_sec: row.get(9).err_kind(ErrorKind::Internal)?,
|
||||||
user_agent: row.get(10)?,
|
user_agent: row.get(10).err_kind(ErrorKind::Internal)?,
|
||||||
addr: revocation_addr.0,
|
addr: revocation_addr.0,
|
||||||
},
|
},
|
||||||
revocation_reason: row.get(12)?,
|
revocation_reason: row.get(12).err_kind(ErrorKind::Internal)?,
|
||||||
revocation_reason_detail: row.get(13)?,
|
revocation_reason_detail: row.get(13).err_kind(ErrorKind::Internal)?,
|
||||||
last_use: Request {
|
last_use: Request {
|
||||||
when_sec: row.get(14)?,
|
when_sec: row.get(14).err_kind(ErrorKind::Internal)?,
|
||||||
user_agent: row.get(15)?,
|
user_agent: row.get(15).err_kind(ErrorKind::Internal)?,
|
||||||
addr: last_use_addr.0,
|
addr: last_use_addr.0,
|
||||||
},
|
},
|
||||||
use_count: row.get(17)?,
|
use_count: row.get(17).err_kind(ErrorKind::Internal)?,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
permissions,
|
permissions,
|
||||||
})
|
})
|
||||||
@ -969,7 +991,10 @@ mod tests {
|
|||||||
let e = state
|
let e = state
|
||||||
.authenticate_session(&conn, req.clone(), &sid.hash())
|
.authenticate_session(&conn, req.clone(), &sid.hash())
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(format!("{}", e), "session is no longer valid (reason=1)");
|
assert_eq!(
|
||||||
|
format!("{}", e),
|
||||||
|
"Unauthenticated: session is no longer valid (reason=1)"
|
||||||
|
);
|
||||||
|
|
||||||
// Everything should persist across reload.
|
// Everything should persist across reload.
|
||||||
drop(state);
|
drop(state);
|
||||||
@ -977,7 +1002,10 @@ mod tests {
|
|||||||
let e = state
|
let e = state
|
||||||
.authenticate_session(&conn, req, &sid.hash())
|
.authenticate_session(&conn, req, &sid.hash())
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(format!("{}", e), "session is no longer valid (reason=1)");
|
assert_eq!(
|
||||||
|
format!("{}", e),
|
||||||
|
"Unauthenticated: session is no longer valid (reason=1)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1028,7 +1056,10 @@ mod tests {
|
|||||||
let e = state
|
let e = state
|
||||||
.authenticate_session(&conn, req, &sid.hash())
|
.authenticate_session(&conn, req, &sid.hash())
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(format!("{}", e), "session is no longer valid (reason=1)");
|
assert_eq!(
|
||||||
|
format!("{}", e),
|
||||||
|
"Unauthenticated: session is no longer valid (reason=1)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1159,7 +1190,10 @@ mod tests {
|
|||||||
let e = state
|
let e = state
|
||||||
.authenticate_session(&conn, req.clone(), &sid.hash())
|
.authenticate_session(&conn, req.clone(), &sid.hash())
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(format!("{}", e), "user \"slamb\" is disabled");
|
assert_eq!(
|
||||||
|
format!("{}", e),
|
||||||
|
"Unauthenticated: user \"slamb\" is disabled"
|
||||||
|
);
|
||||||
|
|
||||||
// The user should still be disabled after reload.
|
// The user should still be disabled after reload.
|
||||||
drop(state);
|
drop(state);
|
||||||
@ -1167,7 +1201,10 @@ mod tests {
|
|||||||
let e = state
|
let e = state
|
||||||
.authenticate_session(&conn, req, &sid.hash())
|
.authenticate_session(&conn, req, &sid.hash())
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(format!("{}", e), "user \"slamb\" is disabled");
|
assert_eq!(
|
||||||
|
format!("{}", e),
|
||||||
|
"Unauthenticated: user \"slamb\" is disabled"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1206,7 +1243,7 @@ mod tests {
|
|||||||
let e = state
|
let e = state
|
||||||
.authenticate_session(&conn, req.clone(), &sid.hash())
|
.authenticate_session(&conn, req.clone(), &sid.hash())
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(format!("{}", e), "no such session");
|
assert_eq!(format!("{}", e), "Unauthenticated: no such session");
|
||||||
|
|
||||||
// The user should still be deleted after reload.
|
// The user should still be deleted after reload.
|
||||||
drop(state);
|
drop(state);
|
||||||
@ -1215,7 +1252,7 @@ mod tests {
|
|||||||
let e = state
|
let e = state
|
||||||
.authenticate_session(&conn, req.clone(), &sid.hash())
|
.authenticate_session(&conn, req.clone(), &sid.hash())
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(format!("{}", e), "no such session");
|
assert_eq!(format!("{}", e), "Unauthenticated: no such session");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -2173,7 +2173,7 @@ impl LockedDatabase {
|
|||||||
&mut self,
|
&mut self,
|
||||||
req: auth::Request,
|
req: auth::Request,
|
||||||
sid: &auth::SessionHash,
|
sid: &auth::SessionHash,
|
||||||
) -> Result<(&auth::Session, &User), Error> {
|
) -> Result<(&auth::Session, &User), base::Error> {
|
||||||
self.auth.authenticate_session(&self.conn, req, sid)
|
self.auth.authenticate_session(&self.conn, req, sid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1180,6 +1180,18 @@ impl Service {
|
|||||||
serve_json(req, &signals)
|
serve_json(req, &signals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Authenticates the session (if any) and returns a Caller.
|
||||||
|
///
|
||||||
|
/// If there's no session,
|
||||||
|
/// 1. if `allow_unauthenticated_permissions` is configured, returns okay
|
||||||
|
/// with those permissions.
|
||||||
|
/// 2. if the caller specifies `unauth_path`, returns okay with no
|
||||||
|
/// permissions.
|
||||||
|
/// 3. returns `Unauthenticated` error otherwise.
|
||||||
|
///
|
||||||
|
/// Does no authorization. That is, this doesn't check that the returned
|
||||||
|
/// permissions are sufficient for whatever operation the caller is
|
||||||
|
/// performing.
|
||||||
fn authenticate(
|
fn authenticate(
|
||||||
&self,
|
&self,
|
||||||
req: &Request<hyper::Body>,
|
req: &Request<hyper::Body>,
|
||||||
@ -1188,22 +1200,28 @@ impl Service {
|
|||||||
if let Some(sid) = extract_sid(req) {
|
if let Some(sid) = extract_sid(req) {
|
||||||
let authreq = self.authreq(req);
|
let authreq = self.authreq(req);
|
||||||
|
|
||||||
// TODO: real error handling! this assumes all errors are due to lack of
|
match self
|
||||||
// authentication, when they could be logic errors in SQL or such.
|
|
||||||
if let Ok((s, u)) = self
|
|
||||||
.db
|
.db
|
||||||
.lock()
|
.lock()
|
||||||
.authenticate_session(authreq.clone(), &sid.hash())
|
.authenticate_session(authreq.clone(), &sid.hash())
|
||||||
{
|
{
|
||||||
return Ok(Caller {
|
Ok((s, u)) => {
|
||||||
permissions: s.permissions.clone(),
|
return Ok(Caller {
|
||||||
session: Some(json::Session {
|
permissions: s.permissions.clone(),
|
||||||
username: u.username.clone(),
|
session: Some(json::Session {
|
||||||
csrf: s.csrf(),
|
username: u.username.clone(),
|
||||||
}),
|
csrf: s.csrf(),
|
||||||
});
|
}),
|
||||||
}
|
})
|
||||||
info!("authenticate_session failed");
|
}
|
||||||
|
Err(e) if e.kind() == base::ErrorKind::Unauthenticated => {
|
||||||
|
// Log the specific reason this session is unauthenticated.
|
||||||
|
// Don't let the API client see it, as it may have a
|
||||||
|
// revocation reason that isn't for their eyes.
|
||||||
|
warn!("Session authentication failed: {:?}", &e);
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(s) = self.allow_unauthenticated_permissions.as_ref() {
|
if let Some(s) = self.allow_unauthenticated_permissions.as_ref() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user