summaryrefslogtreecommitdiffstats
path: root/rust/src/auth.rs
diff options
context:
space:
mode:
authorBryan Newbold <bnewbold@robocracy.org>2019-01-08 16:28:27 -0800
committerBryan Newbold <bnewbold@robocracy.org>2019-01-08 16:28:27 -0800
commit16f2e78298dbd2231f5f337ea17c89a6a131a052 (patch)
tree6e72581e625e73c97cbab72d0f9c35665c99e5d7 /rust/src/auth.rs
parenteb40a5f274f3608db34309cfd16739a7642ef5e7 (diff)
parentffb721f90c5d97ee80885209bf45feb85ca9625c (diff)
downloadfatcat-16f2e78298dbd2231f5f337ea17c89a6a131a052.tar.gz
fatcat-16f2e78298dbd2231f5f337ea17c89a6a131a052.zip
Merge branch 'bnewbold-crude-auth'
Fixed a conflict in: python/fatcat_export.py
Diffstat (limited to 'rust/src/auth.rs')
-rw-r--r--rust/src/auth.rs470
1 files changed, 470 insertions, 0 deletions
diff --git a/rust/src/auth.rs b/rust/src/auth.rs
new file mode 100644
index 00000000..d4e03ecf
--- /dev/null
+++ b/rust/src/auth.rs
@@ -0,0 +1,470 @@
+//! Editor bearer token authentication
+
+use data_encoding::BASE64;
+use macaroon::{Format, Macaroon, Verifier};
+use std::fmt;
+use swagger::auth::{AuthData, Authorization, Scopes};
+
+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, PartialEq)]
+pub enum FatcatRole {
+ Public,
+ Editor,
+ Bot,
+ Human,
+ Admin,
+ Superuser,
+}
+
+#[derive(Clone)]
+pub struct AuthContext {
+ pub editor_id: FatCatId,
+ editor_row: EditorRow,
+}
+
+impl AuthContext {
+ pub fn has_role(&self, role: FatcatRole) -> bool {
+ if !self.editor_row.is_active {
+ // if account is disabled, only allow public role
+ return role == FatcatRole::Public;
+ }
+ if self.editor_row.is_superuser {
+ 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,
+ FatcatRole::Superuser => self.editor_row.is_superuser,
+ }
+ }
+
+ pub fn require_role(&self, role: FatcatRole) -> Result<()> {
+ match self.has_role(role) {
+ true => Ok(()),
+ false => Err(ErrorKind::InsufficientPrivileges(format!(
+ "doesn't have required role: {:?}",
+ role
+ ))
+ .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()),
+ }
+ }
+}
+
+#[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
+ }
+}
+
+fn new_auth_ironerror(m: &str) -> iron::error::IronError {
+ iron::error::IronError::new(
+ AuthError { msg: m.to_string() },
+ (iron::status::BadRequest, m.to_string()),
+ )
+}
+
+#[derive(Debug)]
+pub struct OpenAuthMiddleware;
+
+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(())
+ }
+}
+
+#[derive(Debug)]
+pub struct MacaroonAuthMiddleware;
+
+impl MacaroonAuthMiddleware {
+ pub fn new() -> MacaroonAuthMiddleware {
+ MacaroonAuthMiddleware
+ }
+}
+impl iron::middleware::BeforeMiddleware for MacaroonAuthMiddleware {
+ fn before(&self, req: &mut iron::Request) -> iron::IronResult<()> {
+ // Structure here is sorta funky because we might some day actually want to parse token
+ // here in some way
+ let token: Option<String> = match req.extensions.get::<AuthData>() {
+ 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"));
+ }
+ Some(header[1].to_string())
+ }
+ None => None,
+ _ => {
+ return Err(new_auth_ironerror(
+ "auth HTTP Header should be empty or API token",
+ ));
+ }
+ };
+ if let Some(_token) = token {
+ req.extensions.insert::<Authorization>(Authorization {
+ // This is just a dummy; all actual authentication happens later
+ subject: "undefined".to_string(),
+ scopes: Scopes::All,
+ issuer: None,
+ });
+ };
+ Ok(())
+ }
+}
+
+#[derive(Clone)]
+pub struct AuthConfectionary {
+ pub location: String,
+ pub identifier: String,
+ pub key: Vec<u8>,
+ pub root_keys: HashMap<String, Vec<u8>>,
+}
+
+impl AuthConfectionary {
+ pub fn new(
+ location: String,
+ identifier: String,
+ key_base64: String,
+ ) -> Result<AuthConfectionary> {
+ macaroon::initialize().unwrap();
+ let key = BASE64.decode(key_base64.as_bytes())?;
+ let mut root_keys = HashMap::new();
+ root_keys.insert(identifier.clone(), key.clone());
+ Ok(AuthConfectionary {
+ location: location,
+ identifier: identifier,
+ key: key,
+ root_keys: root_keys,
+ })
+ }
+
+ pub fn new_dummy() -> AuthConfectionary {
+ AuthConfectionary::new(
+ "test.fatcat.wiki".to_string(),
+ "dummy".to_string(),
+ BASE64.encode(DUMMY_KEY),
+ )
+ .unwrap()
+ }
+
+ pub fn add_keypair(&mut self, identifier: String, key_base64: String) -> Result<()> {
+ let key = BASE64.decode(key_base64.as_bytes())?;
+ self.root_keys.insert(identifier, key);
+ Ok(())
+ }
+
+ pub fn create_token(
+ &self,
+ editor_id: FatCatId,
+ duration: Option<chrono::Duration>,
+ ) -> 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()));
+ let now_utc = Utc::now();
+ let now = now_utc.to_rfc3339_opts(SecondsFormat::Secs, true);
+ mac.add_first_party_caveat(&format!("time > {}", now));
+ if let Some(duration) = duration {
+ let expires = now_utc + duration;
+ mac.add_first_party_caveat(&format!(
+ "time < {:?}",
+ &expires.to_rfc3339_opts(SecondsFormat::Secs, true)
+ ));
+ };
+ let raw = mac.serialize(Format::V2).expect("macaroon serialization");
+ Ok(BASE64.encode(&raw))
+ }
+
+ pub fn parse_macaroon_token(
+ &self,
+ conn: &DbConn,
+ s: &str,
+ endpoint: Option<&str>,
+ ) -> Result<EditorRow> {
+ let raw = BASE64.decode(s.as_bytes())?;
+ let mac = match Macaroon::deserialize(&raw) {
+ Ok(m) => m,
+ Err(e) => {
+ // TODO: should be "chaining" here
+ return Err(ErrorKind::InvalidCredentials(format!(
+ "macaroon deserialize error: {:?}",
+ e
+ ))
+ .into());
+ }
+ };
+ let mac = match mac.validate() {
+ Ok(m) => m,
+ Err(e) => {
+ // TODO: should be "chaining" here
+ return Err(ErrorKind::InvalidCredentials(format!(
+ "macaroon validate error: {:?}",
+ e
+ ))
+ .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;
+ }
+ }
+ let editor_id = match editor_id {
+ Some(id) => id,
+ None => {
+ return Err(ErrorKind::InvalidCredentials(
+ "expected an editor_id caveat".to_string(),
+ )
+ .into());
+ }
+ };
+ verifier.satisfy_exact(&format!("editor_id = {}", editor_id.to_string()));
+ if let Some(endpoint) = endpoint {
+ // API endpoint
+ verifier.satisfy_exact(&format!("endpoint = {}", endpoint));
+ }
+ let mut created: Option<DateTime<Utc>> = None;
+ for caveat in mac.first_party_caveats() {
+ if caveat.predicate().starts_with("time > ") {
+ created = Some(
+ DateTime::parse_from_rfc3339(caveat.predicate().get(7..).unwrap())
+ .unwrap()
+ .with_timezone(&Utc),
+ );
+ break;
+ }
+ }
+ let created = match created {
+ Some(c) => c,
+ None => {
+ return Err(ErrorKind::InvalidCredentials(
+ "expected a 'created' (time >) caveat".to_string(),
+ )
+ .into());
+ }
+ };
+ verifier.satisfy_exact(&format!(
+ "time > {}",
+ 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);
+ // allow a second of wiggle room for precision and, eg, tests
+ if created < (auth_epoch - chrono::Duration::seconds(1)) {
+ 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 time)
+ if p.starts_with("time < ") {
+ let expires: DateTime<Utc> = DateTime::parse_from_rfc3339(p.get(7..).unwrap())
+ .unwrap()
+ .with_timezone(&Utc);
+ expires < Utc::now()
+ } else {
+ false
+ }
+ });
+ let verify_key = match self.root_keys.get(mac.identifier()) {
+ Some(key) => key,
+ None => {
+ return Err(ErrorKind::InvalidCredentials(format!(
+ "no valid auth signing key for identifier: {}",
+ mac.identifier()
+ ))
+ .into());
+ }
+ };
+ match mac.verify(verify_key, &mut verifier) {
+ Ok(true) => (),
+ Ok(false) => {
+ return Err(ErrorKind::InvalidCredentials(
+ "auth token (macaroon) not valid (signature and/or caveats failed)".to_string(),
+ )
+ .into());
+ }
+ Err(e) => {
+ // TODO: chain
+ return Err(
+ ErrorKind::InvalidCredentials(format!("token parsing failed: {:?}", e)).into(),
+ );
+ }
+ }
+ Ok(editor)
+ }
+
+ pub fn parse_swagger(
+ &self,
+ conn: &DbConn,
+ auth_data: &Option<AuthData>,
+ endpoint: Option<&str>,
+ ) -> 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();
+ if !(header.len() == 2 && header[0] == "Bearer") {
+ return Err(ErrorKind::InvalidCredentials(
+ "invalid Bearer Auth HTTP header".to_string(),
+ )
+ .into());
+ }
+ Some(header[1].clone())
+ }
+ None => None,
+ _ => {
+ 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, endpoint)?;
+ Ok(Some(AuthContext {
+ editor_id: FatCatId::from_uuid(&editor_row.id),
+ editor_row: editor_row,
+ }))
+ }
+
+ pub fn require_auth(
+ &self,
+ conn: &DbConn,
+ auth_data: &Option<AuthData>,
+ endpoint: Option<&str>,
+ ) -> Result<AuthContext> {
+ match self.parse_swagger(conn, auth_data, endpoint)? {
+ 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<()> {
+ 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());
+ }
+ println!("verify: {:?}", self.parse_macaroon_token(conn, token, None));
+ Ok(())
+ }
+}
+
+pub fn create_key() -> String {
+ let mut key: Vec<u8> = vec![0; 32];
+ for v in key.iter_mut() {
+ *v = rand::random()
+ }
+ BASE64.encode(&key)
+}
+
+pub fn revoke_tokens(conn: &DbConn, editor_id: FatCatId) -> Result<()> {
+ 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<()> {
+ diesel::update(editor::table)
+ .set(editor::auth_epoch.eq(Utc::now()))
+ .execute(conn)?;
+ Ok(())
+}
+
+// TODO: refactor out of this file?
+/// Only used from CLI tool
+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)?;
+ println!("editor_id\t\t\tsuper/admin/bot\tauth_epoch\t\t\tusername\twrangler_id");
+ for e in all_editors {
+ println!(
+ "{}\t{}/{}/{}\t{}\t{}\t{:?}",
+ FatCatId::from_uuid(&e.id).to_string(),
+ e.is_superuser,
+ e.is_admin,
+ e.is_bot,
+ e.auth_epoch,
+ e.username,
+ e.wrangler_id,
+ );
+ }
+ Ok(())
+}