aboutsummaryrefslogtreecommitdiffstats
path: root/rust/src/auth.rs
diff options
context:
space:
mode:
authorBryan Newbold <bnewbold@robocracy.org>2018-12-31 14:57:29 -0800
committerBryan Newbold <bnewbold@robocracy.org>2018-12-31 14:57:29 -0800
commite16ba3d02564121ae5f27e0784be86137f3b9386 (patch)
treee1e19c2e1b678d041b64501c35a1ace6e8da900b /rust/src/auth.rs
parent230032ec1a13dd3830bcffed6112c2fddabc4b6e (diff)
downloadfatcat-e16ba3d02564121ae5f27e0784be86137f3b9386.tar.gz
fatcat-e16ba3d02564121ae5f27e0784be86137f3b9386.zip
rustfmt; implement role-based auth checks
Diffstat (limited to 'rust/src/auth.rs')
-rw-r--r--rust/src/auth.rs192
1 files changed, 151 insertions, 41 deletions
diff --git a/rust/src/auth.rs b/rust/src/auth.rs
index 450a19d6..ee3c6fb0 100644
--- a/rust/src/auth.rs
+++ b/rust/src/auth.rs
@@ -1,33 +1,76 @@
//! Editor bearer token authentication
-use swagger::auth::AuthData;
-use macaroon::{Format, Macaroon, Verifier};
use data_encoding::BASE64;
+use macaroon::{Format, Macaroon, Verifier};
+use swagger::auth::AuthData;
-use std::collections::HashMap;
-use database_models::*;
-use database_schema::*;
use api_helpers::*;
use chrono::prelude::*;
+use database_models::*;
+use database_schema::*;
use diesel;
use diesel::prelude::*;
use errors::*;
+use std::collections::HashMap;
use std::str::FromStr;
// 32 bytes max (!)
static DUMMY_KEY: &[u8] = b"dummy-key-a-one-two-three-a-la";
+#[derive(Clone, Copy, Debug)]
+pub enum FatcatRole {
+ Public,
+ Editor,
+ Bot,
+ Human,
+ Admin,
+}
+
#[derive(Clone)]
pub struct AuthContext {
pub editor_id: FatCatId,
editor_row: EditorRow,
- roles: Vec<String>, // TODO: BTreeSet
}
impl AuthContext {
+ pub fn has_role(&self, role: FatcatRole) -> bool {
+ if self.editor_row.is_admin {
+ return true;
+ }
+ match role {
+ FatcatRole::Public => true,
+ FatcatRole::Editor => true,
+ FatcatRole::Bot => self.editor_row.is_bot,
+ FatcatRole::Human => !self.editor_row.is_bot,
+ FatcatRole::Admin => self.editor_row.is_admin,
+ }
+ }
- pub fn has_role(&self, role: &str) -> bool {
- self.roles.contains(&role.to_string()) || self.roles.contains(&"admin".to_string())
+ pub fn require_role(&self, role: FatcatRole) -> Result<()> {
+ match self.has_role(role) {
+ true => Ok(()),
+ // TODO: better message
+ false => Err(ErrorKind::InsufficientPrivileges(
+ "doesn't have required role".to_string(),
+ )
+ .into()),
+ }
+ }
+
+ pub fn require_editgroup(&self, conn: &DbConn, editgroup_id: FatCatId) -> Result<()> {
+ if self.has_role(FatcatRole::Admin) {
+ return Ok(())
+ }
+ let editgroup: EditgroupRow = editgroup::table
+ .find(editgroup_id.to_uuid())
+ .get_result(conn)?;
+ match editgroup.editor_id == self.editor_id.to_uuid() {
+ true => Ok(()),
+ false => Err(ErrorKind::InsufficientPrivileges(
+ "editor does not own this editgroup".to_string(),
+ )
+ .into()),
+ }
}
}
@@ -40,7 +83,11 @@ pub struct AuthConfectionary {
}
impl AuthConfectionary {
- pub fn new(location: String, identifier: String, key_base64: String) -> Result<AuthConfectionary> {
+ pub fn new(
+ location: String,
+ identifier: String,
+ key_base64: String,
+ ) -> Result<AuthConfectionary> {
let key = BASE64.decode(key_base64.as_bytes())?;
let mut root_keys = HashMap::new();
root_keys.insert(identifier.clone(), key.clone());
@@ -57,18 +104,26 @@ impl AuthConfectionary {
"test.fatcat.wiki".to_string(),
"dummy".to_string(),
BASE64.encode(DUMMY_KEY),
- ).unwrap()
+ )
+ .unwrap()
}
- pub fn create_token(&self, editor_id: FatCatId, expires: Option<DateTime<Utc>>) -> Result<String> {
- let mut mac = Macaroon::create(&self.location, &self.key, &self.identifier).expect("Macaroon creation");
+ pub fn create_token(
+ &self,
+ editor_id: FatCatId,
+ expires: Option<DateTime<Utc>>,
+ ) -> Result<String> {
+ let mut mac = Macaroon::create(&self.location, &self.key, &self.identifier)
+ .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)));
+ 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))
@@ -79,18 +134,30 @@ impl AuthConfectionary {
let raw = BASE64.decode(s.as_bytes())?;
let mac = match Macaroon::deserialize(&raw) {
Ok(m) => m,
- Err(e) => bail!("macaroon deserialize error: {:?}", e),
+ Err(_e) => {
+ // TODO: should be "chaining" here
+ //bail!("macaroon deserialize error: {:?}", e),
+ return Err(
+ ErrorKind::InvalidCredentials("macaroon deserialize error".to_string()).into(),
+ );
+ }
};
let mac = match mac.validate() {
Ok(m) => m,
- Err(e) => bail!("macaroon validate error: {:?}", e),
+ Err(_e) => {
+ // TODO: should be "chaining" here
+ //bail!("macaroon validate error: {:?}", e),
+ return Err(
+ ErrorKind::InvalidCredentials("macaroon validate error".to_string()).into(),
+ );
+ }
};
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
+ break;
}
}
let editor_id = editor_id.expect("expected an editor_id caveat");
@@ -98,20 +165,27 @@ impl AuthConfectionary {
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
+ 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)?;
+ 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)")
+ return Err(ErrorKind::InvalidCredentials(
+ "token created before current auth_epoch (was probably revoked by editor)"
+ .to_string(),
+ )
+ .into());
}
verifier.satisfy_general(|p: &str| -> bool {
// not expired (based on expires)
@@ -126,42 +200,78 @@ impl AuthConfectionary {
});
let verify_key = match self.root_keys.get(mac.identifier()) {
Some(key) => key,
- None => bail!("key not found for identifier: {}", mac.identifier()),
+ None => {
+ // TODO: better message
+ //bail!("key not found for identifier: {}", mac.identifier()),
+ return Err(ErrorKind::InvalidCredentials(
+ "key not found for identifier".to_string(),
+ )
+ .into());
+ }
};
match mac.verify(verify_key, &mut verifier) {
Ok(true) => (),
- Ok(false) => bail!("token overall verification failed"),
- Err(e) => bail!("token parsing failed: {:?}", e),
+ Ok(false) => {
+ return Err(ErrorKind::InvalidCredentials(
+ "token overall verification failed".to_string(),
+ )
+ .into());
+ }
+ Err(_e) => {
+ // TODO: chain
+ //bail!("token parsing failed: {:?}", e),
+ return Err(
+ ErrorKind::InvalidCredentials("token parsing failed".to_string()).into(),
+ );
+ }
}
Ok(editor)
}
- pub fn parse_swagger(&self, conn: &DbConn, auth_data: &Option<AuthData>) -> Result<Option<AuthContext>> {
-
+ pub fn parse_swagger(
+ &self,
+ conn: &DbConn,
+ auth_data: &Option<AuthData>,
+ ) -> Result<Option<AuthContext>> {
let token: Option<String> = match auth_data {
Some(AuthData::ApiKey(header)) => {
- let header: Vec<String> = header.split_whitespace().map(|s| s.to_string()).collect();
+ let header: Vec<String> =
+ header.split_whitespace().map(|s| s.to_string()).collect();
if !(header.len() == 2 && header[0] == "Bearer") {
- bail!("invalid Bearer Auth HTTP header");
+ return Err(ErrorKind::InvalidCredentials(
+ "invalid Bearer Auth HTTP header".to_string(),
+ )
+ .into());
}
Some(header[1].clone())
- },
+ }
None => None,
- _ => bail!("Authentication HTTP Header should either be empty or a Beaerer API key"),
+ _ => {
+ return Err(ErrorKind::InvalidCredentials(
+ "Authentication HTTP Header should either be empty or a Beaerer API key"
+ .to_string(),
+ )
+ .into());
+ }
};
let token = match token {
Some(t) => t,
None => return Ok(None),
};
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,
}))
}
+ pub fn require_auth(&self, conn: &DbConn, auth_data: &Option<AuthData>) -> Result<AuthContext> {
+ match self.parse_swagger(conn, auth_data)? {
+ Some(auth) => Ok(auth),
+ None => Err(ErrorKind::InvalidCredentials("no token supplied".to_string()).into()),
+ }
+ }
+
// TODO: refactor out of this file?
/// Only used from CLI tool
pub fn inspect_token(&self, conn: &DbConn, token: &str) -> Result<()> {
@@ -206,13 +316,13 @@ pub fn revoke_tokens_everyone(conn: &DbConn) -> Result<()> {
// TODO: refactor out of this file?
/// Only used from CLI tool
-pub fn print_editors(conn: &DbConn) -> Result<()>{
+pub fn print_editors(conn: &DbConn) -> Result<()> {
// iterate over all editors. format id, print flags, auth_epoch
- let all_editors: Vec<EditorRow> = editor::table
- .load(conn)?;
+ let all_editors: Vec<EditorRow> = editor::table.load(conn)?;
println!("editor_id\t\t\tis_admin/is_bot\tauth_epoch\t\t\tusername\twrangler_id");
for e in all_editors {
- println!("{}\t{}\t{}\t{}\t{}\t{:?}",
+ println!(
+ "{}\t{}\t{}\t{}\t{}\t{:?}",
FatCatId::from_uuid(&e.id).to_string(),
e.is_admin,
e.is_bot,