aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBryan Newbold <bnewbold@robocracy.org>2018-12-28 14:52:38 -0800
committerBryan Newbold <bnewbold@robocracy.org>2018-12-28 15:45:10 -0800
commitf9408344464285870409c2209a8edc8d25fd9bfa (patch)
tree386e2472c22e9c79f8a58e2f419c9fbaef00dd13
parentfa1892834a4650bf2e85215a3270e309d3e9f1c9 (diff)
downloadfatcat-f9408344464285870409c2209a8edc8d25fd9bfa.tar.gz
fatcat-f9408344464285870409c2209a8edc8d25fd9bfa.zip
start refactor of auth code
Pulls auth code (which requires the persistent state of a signing keyring) into a struct. Doesn't try verify macaroon in middleware, do it in individual wrappers.
-rw-r--r--rust/src/api_server.rs2
-rw-r--r--rust/src/api_wrappers.rs5
-rw-r--r--rust/src/auth.rs293
-rw-r--r--rust/src/bin/fatcat-auth.rs3
-rw-r--r--rust/src/bin/fatcatd.rs1
-rw-r--r--rust/src/database_models.rs2
-rw-r--r--rust/src/lib.rs3
7 files changed, 121 insertions, 188 deletions
diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs
index af2a7e24..cbf5be21 100644
--- a/rust/src/api_server.rs
+++ b/rust/src/api_server.rs
@@ -2,6 +2,7 @@
use api_entity_crud::EntityCrud;
use api_helpers::*;
+use auth::*;
use chrono;
use database_models::*;
use database_schema::*;
@@ -41,6 +42,7 @@ macro_rules! entity_batch_handler {
#[derive(Clone)]
pub struct Server {
pub db_pool: ConnectionPool,
+ pub auth_confectionary: AuthConfectionary,
}
pub fn get_release_files(
diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs
index cf696d15..25b4fab1 100644
--- a/rust/src/api_wrappers.rs
+++ b/rust/src/api_wrappers.rs
@@ -2,6 +2,7 @@
use api_entity_crud::EntityCrud;
use api_helpers::*;
+use auth::*;
use api_server::Server;
use database_models::EntityEditRow;
use diesel::Connection;
@@ -80,10 +81,12 @@ macro_rules! wrap_entity_handlers {
&self,
entity: models::$model,
editgroup_id: Option<String>,
- _context: &Context,
+ context: &Context,
) -> Box<Future<Item = $post_resp, Error = ApiError> + Send> {
let conn = self.db_pool.get().expect("db_pool error");
let ret = match conn.transaction(|| {
+ let auth_context = self.auth_confectionary.parse_swagger(&conn, &context.auth_data)?;
+ // XXX: auth_context.expect("not authorized");
let editgroup_id = if let Some(s) = editgroup_id {
Some(FatCatId::from_str(&s)?)
} else { None };
diff --git a/rust/src/auth.rs b/rust/src/auth.rs
index 580cde28..c188233a 100644
--- a/rust/src/auth.rs
+++ b/rust/src/auth.rs
@@ -1,208 +1,151 @@
//! Editor bearer token authentication
-use swagger::auth::{AuthData, Authorization, Scopes};
+use swagger::auth::AuthData;
use macaroon::{Format, Macaroon, Verifier};
use data_encoding::BASE64;
-use std::collections::BTreeSet;
-use std::fmt;
+use std::collections::{BTreeSet,HashMap};
use database_models::*;
use database_schema::*;
use api_helpers::*;
use chrono::prelude::*;
use diesel;
-use iron;
use diesel::prelude::*;
use errors::*;
-//use serde_json;
use std::str::FromStr;
-//use uuid::Uuid;
// 32 bytes max (!)
static DUMMY_KEY: &[u8] = b"dummy-key-a-one-two-three-a-la";
-#[derive(Debug)]
-pub struct OpenAuthMiddleware;
-
-#[derive(Debug)]
-pub struct AuthError {
- msg: String,
+#[derive(Clone)]
+pub struct AuthContext {
+ pub editor_id: FatCatId,
+ editor_row: EditorRow,
+ roles: Vec<String>, // TODO: BTreeSet
}
-impl fmt::Display for AuthError {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "AuthError: {}", &self.msg)
- }
-}
+impl AuthContext {
-impl iron::Error for AuthError {
- fn description(&self) -> &str {
- &self.msg
- }
- fn cause(&self) -> Option<&iron::Error> {
- None
+ pub fn has_role(&self, role: &str) -> bool {
+ self.roles.contains(&role.to_string()) || self.roles.contains(&"admin".to_string())
}
}
-/*
-#[derive(Debug)]
-pub struct FatcatAuthBakery {
- root_key_store: bool, // hashmap
- signing_key: bool, // string name
+#[derive(Clone)]
+pub struct AuthConfectionary {
+ pub root_keys: HashMap<String, [u8; 32]>,
}
-// impl:
-// - new()
-// - verify(&str) -> Result<AuthContext>
-*/
-
-fn new_auth_ironerror(m: &str) -> iron::error::IronError {
- iron::error::IronError::new(
- AuthError { msg: m.to_string() },
- (iron::status::BadRequest, m.to_string())
- )
-}
-
-impl OpenAuthMiddleware {
- /// Create a middleware that authorizes with the configured subject.
- pub fn new() -> OpenAuthMiddleware {
- OpenAuthMiddleware
- }
-}
-impl iron::middleware::BeforeMiddleware for OpenAuthMiddleware {
- fn before(&self, req: &mut iron::Request) -> iron::IronResult<()> {
- req.extensions.insert::<Authorization>(Authorization {
- subject: "undefined".to_string(),
- scopes: Scopes::All,
- issuer: None,
- });
- Ok(())
+impl AuthConfectionary {
+ pub fn new() -> AuthConfectionary {
+ AuthConfectionary {
+ root_keys: HashMap::new(),
+ }
}
-}
-#[derive(Debug)]
-pub struct MacaroonAuthMiddleware;
-
-impl MacaroonAuthMiddleware {
+ pub fn create_token(&self, conn: &DbConn, editor_id: FatCatId, expires: Option<DateTime<Utc>>) -> Result<String> {
+ let _ed: EditorRow = editor::table
+ .find(&editor_id.to_uuid())
+ .get_result(conn)?;
+ let mut mac = Macaroon::create("fatcat.wiki", DUMMY_KEY, "dummy-key").expect("Macaroon creation");
+ mac.add_first_party_caveat(&format!("editor_id = {}", editor_id.to_string()));
+ // TODO: put created one second in the past to prevent timing synchronization glitches?
+ let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
+ mac.add_first_party_caveat(&format!("created = {}", now));
+ if let Some(expires) = expires {
+ mac.add_first_party_caveat(&format!("expires = {:?}",
+ &expires.to_rfc3339_opts(SecondsFormat::Secs, true)));
+ };
+ let raw = mac.serialize(Format::V2).expect("macaroon serialization");
+ Ok(BASE64.encode(&raw))
+ }
- pub fn new() -> MacaroonAuthMiddleware {
- macaroon::initialize().unwrap();
- MacaroonAuthMiddleware
+ /// On success, returns Some((editor_id, scopes)), where `scopes` is a vector of strings.
+ pub fn parse_macaroon_token(&self, conn: &DbConn, s: &str) -> Result<EditorRow> {
+ let raw = BASE64.decode(s.as_bytes())?;
+ let mac = match Macaroon::deserialize(&raw) {
+ Ok(m) => m,
+ Err(e) => bail!("macaroon deserialize error: {:?}", e),
+ };
+ let mac = match mac.validate() {
+ Ok(m) => m,
+ Err(e) => bail!("macaroon validate error: {:?}", e),
+ };
+ let mut verifier = Verifier::new();
+ let mut editor_id: Option<FatCatId> = None;
+ for caveat in mac.first_party_caveats() {
+ if caveat.predicate().starts_with("editor_id = ") {
+ editor_id = Some(FatCatId::from_str(caveat.predicate().get(12..).unwrap())?);
+ break
+ }
+ }
+ let editor_id = editor_id.expect("expected an editor_id caveat");
+ verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string()));
+ let mut created: Option<DateTime<Utc>> = None;
+ for caveat in mac.first_party_caveats() {
+ if caveat.predicate().starts_with("created = ") {
+ created = Some(DateTime::parse_from_rfc3339(caveat.predicate().get(10..).unwrap())
+ .unwrap()
+ .with_timezone(&Utc));
+ break
+ }
+ }
+ let created = created.expect("expected a 'created' caveat");
+ verifier.satisfy_exact(&format!("created = {}", created.to_rfc3339_opts(SecondsFormat::Secs, true)));
+ let editor: EditorRow = editor::table
+ .find(&editor_id.to_uuid())
+ .get_result(conn)?;
+ let auth_epoch = DateTime::<Utc>::from_utc(editor.auth_epoch, Utc);
+ if created < auth_epoch {
+ bail!("token created before current auth_epoch (was probably revoked by editor)")
+ }
+ verifier.satisfy_general(|p: &str| -> bool {
+ // not expired (based on expires)
+ if p.starts_with("expires = ") {
+ let expires: DateTime<Utc> = DateTime::parse_from_rfc3339(p.get(12..).unwrap())
+ .unwrap()
+ .with_timezone(&Utc);
+ expires < Utc::now()
+ } else {
+ false
+ }
+ });
+ if !mac.verify_signature(DUMMY_KEY) {
+ bail!("token signature verification failed");
+ };
+ match mac.verify(DUMMY_KEY, &mut verifier) {
+ Ok(true) => (),
+ Ok(false) => bail!("token overall verification failed"),
+ Err(e) => bail!("token parsing failed: {:?}", e),
+ }
+ Ok(editor)
}
-}
-impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware {
- fn before(&self, req: &mut iron::Request) -> iron::IronResult<()> {
+ pub fn parse_swagger(&self, conn: &DbConn, auth_data: &Option<AuthData>) -> Result<Option<AuthContext>> {
- let res: Option<(String, Vec<String>)> = match req.extensions.get::<AuthData>() {
+ let token: Option<String> = match auth_data {
Some(AuthData::ApiKey(header)) => {
let header: Vec<String> = header.split_whitespace().map(|s| s.to_string()).collect();
if !(header.len() == 2 && header[0] == "Bearer") {
- return Err(new_auth_ironerror("invalid bearer auth HTTP Header"));
+ bail!("invalid Bearer Auth HTTP header");
}
- sniff_macaroon_token(&header[1]).expect("valid macaroon")
+ Some(header[1].clone())
},
None => None,
- _ => {
- return Err(new_auth_ironerror("auth HTTP Header should be empty or API key"));
- }
+ _ => bail!("Authentication HTTP Header should either be empty or a Beaerer API key"),
};
- if let Some((editor_id, scopes)) = res {
- let mut scope_set = BTreeSet::new();
- for s in scopes {
- scope_set.insert(s);
- }
- req.extensions.insert::<Authorization>(Authorization {
- subject: editor_id,
- scopes: Scopes::Some(scope_set),
- issuer: None,
- });
+ let token = match token {
+ Some(t) => t,
+ None => return Ok(None),
};
- Ok(())
- }
-}
-
-/// Just checks signature and expired time; can't hit database, so nothing else
-pub fn sniff_macaroon_token(s: &str) -> Result<Option<(String,Vec<String>)>> {
- let raw = BASE64.decode(s.as_bytes())?;
- let mac = match Macaroon::deserialize(&raw) {
- Ok(m) => m,
- Err(e) => bail!("macaroon deserialize error: {:?}", e),
- };
- let mac = match mac.validate() {
- Ok(m) => m,
- Err(e) => bail!("macaroon validate error: {:?}", e),
- };
- let mut editor_id: Option<FatCatId> = None;
- for caveat in mac.first_party_caveats() {
- if caveat.predicate().starts_with("editor_id = ") {
- editor_id = Some(FatCatId::from_str(caveat.predicate().get(12..).unwrap())?);
- break
- }
- }
- let editor_id = editor_id.expect("expected an editor_id caveat");
- Ok(Some((editor_id.to_string(), vec![])))
-}
-
-/// On success, returns Some((editor_id, scopes)), where `scopes` is a vector of strings.
-pub fn parse_macaroon_token(conn: &DbConn, s: &str) -> Result<Option<(String,Vec<String>)>> {
- let raw = BASE64.decode(s.as_bytes())?;
- let mac = match Macaroon::deserialize(&raw) {
- Ok(m) => m,
- Err(e) => bail!("macaroon deserialize error: {:?}", e),
- };
- let mac = match mac.validate() {
- Ok(m) => m,
- Err(e) => bail!("macaroon validate error: {:?}", e),
- };
- let mut verifier = Verifier::new();
- let mut editor_id: Option<FatCatId> = None;
- for caveat in mac.first_party_caveats() {
- if caveat.predicate().starts_with("editor_id = ") {
- editor_id = Some(FatCatId::from_str(caveat.predicate().get(12..).unwrap())?);
- break
- }
- }
- let editor_id = editor_id.expect("expected an editor_id caveat");
- verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string()));
- let mut created: Option<DateTime<Utc>> = None;
- for caveat in mac.first_party_caveats() {
- if caveat.predicate().starts_with("created = ") {
- created = Some(DateTime::parse_from_rfc3339(caveat.predicate().get(10..).unwrap())
- .unwrap()
- .with_timezone(&Utc));
- break
- }
- }
- let created = created.expect("expected a 'created' caveat");
- verifier.satisfy_exact(&format!("created = {}", created.to_rfc3339_opts(SecondsFormat::Secs, true)));
- let editor: EditorRow = editor::table
- .find(&editor_id.to_uuid())
- .get_result(conn)?;
- let auth_epoch = DateTime::<Utc>::from_utc(editor.auth_epoch, Utc);
- if created < auth_epoch {
- bail!("token created before current auth_epoch (was probably revoked by editor)")
+ let editor_row = self.parse_macaroon_token(conn, &token)?;
+ let roles = if editor_row.is_admin { vec!["admin".to_string()] } else { vec![] };
+ Ok(Some(AuthContext {
+ editor_id: FatCatId::from_uuid(&editor_row.id),
+ editor_row: editor_row,
+ roles: roles,
+ }))
}
- verifier.satisfy_general(|p: &str| -> bool {
- // not expired (based on expires)
- if p.starts_with("expires = ") {
- let expires: DateTime<Utc> = DateTime::parse_from_rfc3339(p.get(12..).unwrap())
- .unwrap()
- .with_timezone(&Utc);
- expires < Utc::now()
- } else {
- false
- }
- });
- if !mac.verify_signature(DUMMY_KEY) {
- bail!("token signature verification failed");
- };
- match mac.verify(DUMMY_KEY, &mut verifier) {
- Ok(true) => (),
- Ok(false) => bail!("token overall verification failed"),
- Err(e) => bail!("token parsing failed: {:?}", e),
- }
- Ok(Some((editor_id.to_string(), vec![])))
}
pub fn print_editors(conn: &DbConn) -> Result<()>{
@@ -236,23 +179,6 @@ pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bo
Ok(ed)
}
-pub fn create_token(conn: &DbConn, editor_id: FatCatId, expires: Option<DateTime<Utc>>) -> Result<String> {
- let _ed: EditorRow = editor::table
- .find(&editor_id.to_uuid())
- .get_result(conn)?;
- let mut mac = Macaroon::create("fatcat.wiki", DUMMY_KEY, "dummy-key").expect("Macaroon creation");
- mac.add_first_party_caveat(&format!("editor_id = {}", editor_id.to_string()));
- // TODO: put created one second in the past to prevent timing synchronization glitches?
- let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
- mac.add_first_party_caveat(&format!("created = {}", now));
- if let Some(expires) = expires {
- mac.add_first_party_caveat(&format!("expires = {:?}",
- &expires.to_rfc3339_opts(SecondsFormat::Secs, true)));
- };
- let raw = mac.serialize(Format::V2).expect("macaroon serialization");
- Ok(BASE64.encode(&raw))
-}
-
pub fn inspect_token(conn: &DbConn, token: &str) -> Result<()> {
let raw = BASE64.decode(token.as_bytes())?;
let mac = match Macaroon::deserialize(&raw) {
@@ -266,8 +192,9 @@ pub fn inspect_token(conn: &DbConn, token: &str) -> Result<()> {
for caveat in mac.first_party_caveats() {
println!("caveat: {}", caveat.predicate());
}
+ let ac = AuthConfectionary::new();
// TODO: don't display full stacktrace on failure
- println!("verify: {:?}", parse_macaroon_token(conn, token));
+ println!("verify: {:?}", ac.parse_macaroon_token(conn, token));
Ok(())
}
diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs
index 9b4550d8..f11f7a67 100644
--- a/rust/src/bin/fatcat-auth.rs
+++ b/rust/src/bin/fatcat-auth.rs
@@ -90,6 +90,7 @@ fn run() -> Result<()> {
.get_matches();
let db_conn = database_worker_pool()?.get().expect("database pool");
+ let confectionary = fatcat::auth::AuthConfectionary::new();
match m.subcommand() {
("list-editors", Some(_subm)) => {
fatcat::auth::print_editors(&db_conn)?;
@@ -105,7 +106,7 @@ fn run() -> Result<()> {
},
("create-token", Some(subm)) => {
let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?;
- println!("{}", fatcat::auth::create_token(&db_conn, editor_id, None)?);
+ println!("{}", confectionary.create_token(&db_conn, editor_id, None)?);
},
("inspect-token", Some(subm)) => {
fatcat::auth::inspect_token(&db_conn, subm.value_of("token").unwrap())?;
diff --git a/rust/src/bin/fatcatd.rs b/rust/src/bin/fatcatd.rs
index e14296da..7def7f66 100644
--- a/rust/src/bin/fatcatd.rs
+++ b/rust/src/bin/fatcatd.rs
@@ -78,7 +78,6 @@ fn main() {
// authentication
chain.link_before(fatcat_api_spec::server::ExtractAuthData);
- chain.link_before(fatcat::auth::OpenAuthMiddleware::new());
chain.link_after(fatcat::XClacksOverheadMiddleware);
diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs
index 55ba7fb9..f6cca3e1 100644
--- a/rust/src/database_models.rs
+++ b/rust/src/database_models.rs
@@ -572,7 +572,7 @@ impl EditgroupRow {
}
}
-#[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)]
+#[derive(Debug, Clone, Queryable, Identifiable, Associations, AsChangeset)]
#[table_name = "editor"]
pub struct EditorRow {
pub id: Uuid,
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index f081be25..43911868 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -117,7 +117,8 @@ pub fn server() -> Result<api_server::Server> {
let pool = diesel::r2d2::Pool::builder()
.build(manager)
.expect("Failed to create database pool.");
- Ok(api_server::Server { db_pool: pool })
+ let confectionary = auth::AuthConfectionary::new();
+ Ok(api_server::Server { db_pool: pool, auth_confectionary: confectionary })
}
pub fn test_server() -> Result<api_server::Server> {