summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBryan Newbold <bnewbold@robocracy.org>2018-12-27 21:06:09 -0800
committerBryan Newbold <bnewbold@robocracy.org>2018-12-27 21:06:09 -0800
commitfa1892834a4650bf2e85215a3270e309d3e9f1c9 (patch)
treedd676f82952df845527fe0c1e1aa6965cdb9f0b2
parent26438d2392eb68345ee7eff204b85c6471ce59c2 (diff)
downloadfatcat-fa1892834a4650bf2e85215a3270e309d3e9f1c9.tar.gz
fatcat-fa1892834a4650bf2e85215a3270e309d3e9f1c9.zip
more basic work on auth
-rw-r--r--rust/src/api_server.rs11
-rw-r--r--rust/src/auth.rs197
-rw-r--r--rust/src/bin/fatcat-auth.rs36
-rw-r--r--rust/src/lib.rs3
4 files changed, 200 insertions, 47 deletions
diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs
index d264afbc..af2a7e24 100644
--- a/rust/src/api_server.rs
+++ b/rust/src/api_server.rs
@@ -38,17 +38,6 @@ macro_rules! entity_batch_handler {
}
}
-macro_rules! count_entity {
- ($table:ident, $conn:expr) => {{
- let count: i64 = $table::table
- .filter($table::is_live.eq(true))
- .filter($table::redirect_id.is_null())
- .count()
- .first($conn)?;
- count
- }};
-}
-
#[derive(Clone)]
pub struct Server {
pub db_pool: ConnectionPool,
diff --git a/rust/src/auth.rs b/rust/src/auth.rs
index 6ded1188..580cde28 100644
--- a/rust/src/auth.rs
+++ b/rust/src/auth.rs
@@ -1,24 +1,68 @@
//! Editor bearer token authentication
use swagger::auth::{AuthData, Authorization, Scopes};
-//use macaroon::{Macaroon, Verifier};
+use macaroon::{Format, Macaroon, Verifier};
+use data_encoding::BASE64;
use std::collections::BTreeSet;
+use std::fmt;
use database_models::*;
use database_schema::*;
use api_helpers::*;
-use chrono;
+use chrono::prelude::*;
use diesel;
use iron;
use diesel::prelude::*;
use errors::*;
-use serde_json;
+//use serde_json;
use std::str::FromStr;
-use uuid::Uuid;
+//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,
+}
+
+impl fmt::Display for AuthError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "AuthError: {}", &self.msg)
+ }
+}
+
+impl iron::Error for AuthError {
+ fn description(&self) -> &str {
+ &self.msg
+ }
+ fn cause(&self) -> Option<&iron::Error> {
+ None
+ }
+}
+
+/*
+#[derive(Debug)]
+pub struct FatcatAuthBakery {
+ root_key_store: bool, // hashmap
+ signing_key: bool, // string name
+}
+
+// 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 {
@@ -42,6 +86,7 @@ pub struct MacaroonAuthMiddleware;
impl MacaroonAuthMiddleware {
pub fn new() -> MacaroonAuthMiddleware {
+ macaroon::initialize().unwrap();
MacaroonAuthMiddleware
}
}
@@ -52,13 +97,15 @@ impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware {
let res: Option<(String, Vec<String>)> = match req.extensions.get::<AuthData>() {
Some(AuthData::ApiKey(header)) => {
let header: Vec<String> = header.split_whitespace().map(|s| s.to_string()).collect();
- // TODO: error types
- assert!(header.len() == 2);
- assert!(header[0] == "Bearer");
- parse_macaroon_token(&header[1]).expect("valid macaroon")
+ if !(header.len() == 2 && header[0] == "Bearer") {
+ return Err(new_auth_ironerror("invalid bearer auth HTTP Header"));
+ }
+ sniff_macaroon_token(&header[1]).expect("valid macaroon")
},
None => None,
- _ => panic!("valid auth header, or none")
+ _ => {
+ return Err(new_auth_ironerror("auth HTTP Header should be empty or API key"));
+ }
};
if let Some((editor_id, scopes)) = res {
let mut scope_set = BTreeSet::new();
@@ -75,10 +122,87 @@ impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware {
}
}
-// DUMMY: parse macaroon
+/// 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(s: &str) -> Result<Option<(String,Vec<String>)>> {
- Ok(Some(("some_editor_id".to_string(), vec![])))
+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)")
+ }
+ 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<()>{
@@ -99,6 +223,8 @@ pub fn print_editors(conn: &DbConn) -> Result<()>{
Ok(())
}
+// TODO: move to api_helpers or some such
+// TODO: verify username
pub fn create_editor(conn: &DbConn, username: String, is_admin: bool, is_bot: bool) -> Result<EditorRow> {
let ed: EditorRow = diesel::insert_into(editor::table)
.values((
@@ -110,18 +236,51 @@ 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<chrono::NaiveDateTime>) -> Result<String> {
- unimplemented!();
+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(token: &str) -> Result<()> {
- unimplemented!();
+pub fn inspect_token(conn: &DbConn, token: &str) -> Result<()> {
+ let raw = BASE64.decode(token.as_bytes())?;
+ let mac = match Macaroon::deserialize(&raw) {
+ Ok(m) => m,
+ Err(e) => bail!("macaroon deserialize error: {:?}", e),
+ };
+ let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
+ println!("current time: {}", now);
+ println!("domain (location): {:?}", mac.location());
+ println!("signing key name (identifier): {}", mac.identifier());
+ for caveat in mac.first_party_caveats() {
+ println!("caveat: {}", caveat.predicate());
+ }
+ // TODO: don't display full stacktrace on failure
+ println!("verify: {:?}", parse_macaroon_token(conn, token));
+ Ok(())
}
pub fn revoke_tokens(conn: &DbConn, editor_id: FatCatId) -> Result<()>{
- unimplemented!();
+ diesel::update(editor::table.filter(editor::id.eq(&editor_id.to_uuid())))
+ .set(editor::auth_epoch.eq(Utc::now()))
+ .execute(conn)?;
+ Ok(())
}
-pub fn revoke_tokens_everyone(conn: &DbConn) -> Result<u64> {
- unimplemented!();
+pub fn revoke_tokens_everyone(conn: &DbConn) -> Result<()> {
+ diesel::update(editor::table)
+ .set(editor::auth_epoch.eq(Utc::now()))
+ .execute(conn)?;
+ Ok(())
}
diff --git a/rust/src/bin/fatcat-auth.rs b/rust/src/bin/fatcat-auth.rs
index a5fedc1f..9b4550d8 100644
--- a/rust/src/bin/fatcat-auth.rs
+++ b/rust/src/bin/fatcat-auth.rs
@@ -1,13 +1,13 @@
//! JSON Export Helper
-#[macro_use]
+//#[macro_use]
extern crate clap;
extern crate diesel;
extern crate dotenv;
#[macro_use]
extern crate error_chain;
extern crate fatcat;
-#[macro_use]
+//#[macro_use]
extern crate log;
extern crate env_logger;
extern crate serde_json;
@@ -23,12 +23,12 @@ use fatcat::ConnectionPool;
use fatcat::errors::*;
use fatcat::api_helpers::FatCatId;
use std::str::FromStr;
-use uuid::Uuid;
+//use uuid::Uuid;
-use error_chain::ChainedError;
+//use error_chain::ChainedError;
//use std::io::{Stdout,StdoutLock};
-use std::io::prelude::*;
-use std::io::{BufReader, BufWriter};
+//use std::io::prelude::*;
+//use std::io::{BufReader, BufWriter};
/// Instantiate a new API server with a pooled database connection
@@ -72,24 +72,29 @@ fn run() -> Result<()> {
.subcommand(
SubCommand::with_name("inspect-token")
.about("Dumps token metadata (and whether it is valid)")
+ .args_from_usage(
+ "<token> 'base64-encoded token (macaroon)'"
+ )
)
.subcommand(
SubCommand::with_name("revoke-tokens")
.about("Resets auth_epoch for a single editor (invalidating all existing tokens)")
+ .args_from_usage(
+ "<editor-id> 'identifier (fcid) of editor'"
+ )
)
.subcommand(
- SubCommand::with_name("revoke-tokens-all")
+ SubCommand::with_name("revoke-tokens-everyone")
.about("Resets auth_epoch for all editors (invalidating tokens for all users!)")
)
.get_matches();
+ let db_conn = database_worker_pool()?.get().expect("database pool");
match m.subcommand() {
("list-editors", Some(_subm)) => {
- let db_conn = database_worker_pool()?.get().expect("database pool");
fatcat::auth::print_editors(&db_conn)?;
},
("create-editor", Some(subm)) => {
- let db_conn = database_worker_pool()?.get().expect("database pool");
let editor = fatcat::auth::create_editor(
&db_conn,
subm.value_of("username").unwrap().to_string(),
@@ -99,21 +104,20 @@ fn run() -> Result<()> {
println!("{}", FatCatId::from_uuid(&editor.id).to_string());
},
("create-token", Some(subm)) => {
- let db_conn = database_worker_pool()?.get().expect("database pool");
- let editor_id = FatCatId::from_str(subm.value_of("editor").unwrap())?;
- fatcat::auth::create_token(&db_conn, editor_id, None)?;
+ let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?;
+ println!("{}", fatcat::auth::create_token(&db_conn, editor_id, None)?);
},
("inspect-token", Some(subm)) => {
- fatcat::auth::inspect_token(subm.value_of("token").unwrap())?;
+ fatcat::auth::inspect_token(&db_conn, subm.value_of("token").unwrap())?;
},
("revoke-tokens", Some(subm)) => {
- let db_conn = database_worker_pool()?.get().expect("database pool");
- let editor_id = FatCatId::from_str(subm.value_of("editor").unwrap())?;
+ let editor_id = FatCatId::from_str(subm.value_of("editor-id").unwrap())?;
fatcat::auth::revoke_tokens(&db_conn, editor_id)?;
+ println!("success!");
},
("revoke-tokens-everyone", Some(_subm)) => {
- let db_conn = database_worker_pool()?.get().expect("database pool");
fatcat::auth::revoke_tokens_everyone(&db_conn)?;
+ println!("success!");
},
_ => {
println!("Missing or unimplemented command!");
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 2a6c7203..f081be25 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -15,7 +15,6 @@ extern crate swagger;
#[macro_use]
extern crate error_chain;
extern crate iron;
-#[macro_use]
extern crate serde_json;
#[macro_use]
extern crate log;
@@ -43,6 +42,8 @@ pub mod errors {
Uuid(::uuid::ParseError);
Io(::std::io::Error) #[cfg(unix)];
Serde(::serde_json::Error);
+ Utf8Decode(::std::string::FromUtf8Error);
+ StringDecode(::data_encoding::DecodeError);
}
errors {
InvalidFatcatId(id: String) {