diff --git a/src/api.rs b/src/api.rs index ec66fe8..3ad0e84 100644 --- a/src/api.rs +++ b/src/api.rs @@ -124,13 +124,18 @@ fn get_endpoints(db: Arc) -> Mount { move |request: &mut Request| { self::get_config(request, get_db.deref()) }, - "get_settings"); + "get_config"); settings_router.put("/", move |request: &mut Request| { self::put_config(request, put_db.deref()) }, "put_config"); - auth_api_mount.mount("/settings/", settings_router); + + let mut settings_api_chain = Chain::new(settings_router); + let admin_req = AdminRequirement { db: db.clone() }; + settings_api_chain.link_around(admin_req); + + auth_api_mount.mount("/settings/", settings_api_chain); } let mut auth_api_chain = Chain::new(auth_api_mount); @@ -139,6 +144,7 @@ fn get_endpoints(db: Arc) -> Mount { api_handler.mount("/", auth_api_chain); } + api_handler } @@ -206,6 +212,51 @@ impl Handler for AuthHandler { } } + +struct AdminRequirement { + db: Arc, +} + +impl AroundMiddleware for AdminRequirement { + fn around(self, handler: Box) -> Box { + Box::new(AdminHandler { + db: self.db, + handler: handler, + }) as Box + } +} + +struct AdminHandler { + handler: Box, + db: Arc, +} + +impl Handler for AdminHandler { + fn handle(&self, req: &mut Request) -> IronResult { + { + let mut auth_success = false; + + // Skip auth for first time setup + if user::count(self.db.deref())? == 0 { + auth_success = true; + } + + if !auth_success { + match req.extensions.get::() { + Some(s) => auth_success = user::is_admin(self.db.deref(), &s.username)?, + _ => return Err(Error::from(ErrorKind::AuthenticationRequired).into()), + } + } + + if !auth_success { + return Err(Error::from(ErrorKind::AdminPrivilegeRequired).into()); + } + } + + self.handler.handle(req) + } +} + fn version(_: &mut Request) -> IronResult { #[derive(Serialize)] struct Version { @@ -230,9 +281,7 @@ fn initial_setup(_: &mut Request, db: &DB) -> IronResult { has_any_users: bool, }; - let initial_setup = InitialSetup { - has_any_users: user::count(db)? > 0, - }; + let initial_setup = InitialSetup { has_any_users: user::count(db)? > 0 }; match serde_json::to_string(&initial_setup) { Ok(result_json) => Ok(Response::with((status::Ok, result_json))), diff --git a/src/config.rs b/src/config.rs index 1fcf3a0..65fc5de 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,6 +28,7 @@ pub struct MiscSettings { pub struct ConfigUser { pub name: String, pub password: String, + pub admin: bool, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -99,15 +100,16 @@ pub fn read(db: &T) -> Result .get_results(connection)?; config.mount_dirs = Some(mount_dirs); - let usernames: Vec = users::table - .select(users::columns::name) + let found_users: Vec<(String, i32)> = users::table + .select((users::columns::name, users::columns::admin)) .get_results(connection)?; - config.users = Some(usernames + config.users = Some(found_users .into_iter() - .map(|s| { + .map(|(n, a)| { ConfigUser { - name: s, + name: n, password: "".to_owned(), + admin: a != 0, } }) .collect::<_>()); @@ -181,11 +183,18 @@ pub fn amend(db: &T, new_config: &Config) -> Result<()> .filter(|u| !u.password.is_empty()) .collect::<_>(); for ref config_user in insert_users { - let new_user = User::new(&config_user.name, &config_user.password); + let new_user = User::new(&config_user.name, &config_user.password, config_user.admin); diesel::insert(&new_user) .into(users::table) .execute(connection)?; } + + // Grant admin rights + for ref user in config_users { + diesel::update(users::table.filter(users::name.eq(&user.name))) + .set(users::admin.eq(user.admin as i32)) + .execute(connection)?; + } } if let Some(sleep_duration) = new_config.reindex_every_n_seconds { @@ -246,6 +255,7 @@ fn test_amend() { users: Some(vec![ConfigUser { name: "Teddy🐻".into(), password: "Tasty🍖".into(), + admin: false, }]), ydns: None, }; @@ -260,6 +270,7 @@ fn test_amend() { users: Some(vec![ConfigUser { name: "Kermit🐸".into(), password: "🐞🐞".into(), + admin: false, }]), ydns: Some(DDNSConfig { host: "🐸🐸🐸.ydns.eu".into(), @@ -295,6 +306,7 @@ fn test_amend_preserve_password_hashes() { users: Some(vec![ConfigUser { name: "Teddy🐻".into(), password: "Tasty🍖".into(), + admin: false, }]), ydns: None, }; @@ -318,10 +330,12 @@ fn test_amend_preserve_password_hashes() { users: Some(vec![ConfigUser { name: "Kermit🐸".into(), password: "tasty🐞".into(), + admin: false, }, ConfigUser { name: "Teddy🐻".into(), password: "".into(), + admin: false, }]), ydns: None, }; @@ -341,6 +355,56 @@ fn test_amend_preserve_password_hashes() { assert_eq!(new_hash, initial_hash); } + +#[test] +fn test_toggle_admin() { + use self::users::dsl::*; + + let db = _get_test_db("amend_toggle_admin.sqlite"); + + let initial_config = Config { + album_art_pattern: None, + reindex_every_n_seconds: None, + mount_dirs: None, + users: Some(vec![ConfigUser { + name: "Teddy🐻".into(), + password: "Tasty🍖".into(), + admin: true, + }]), + ydns: None, + }; + amend(&db, &initial_config).unwrap(); + + { + let connection = db.get_connection(); + let connection = connection.lock().unwrap(); + let connection = connection.deref(); + let is_admin: i32 = users.select(admin).get_result(connection).unwrap(); + assert_eq!(is_admin, 1); + } + + let new_config = Config { + album_art_pattern: None, + reindex_every_n_seconds: None, + mount_dirs: None, + users: Some(vec![ConfigUser { + name: "Teddy🐻".into(), + password: "".into(), + admin: false, + }]), + ydns: None, + }; + amend(&db, &new_config).unwrap(); + + { + let connection = db.get_connection(); + let connection = connection.lock().unwrap(); + let connection = connection.deref(); + let is_admin: i32 = users.select(admin).get_result(connection).unwrap(); + assert_eq!(is_admin, 0); + } +} + #[test] fn test_clean_path_string() { let mut correct_path = path::PathBuf::new(); diff --git a/src/db/migrations/201706272129_users_table/up.sql b/src/db/migrations/201706272129_users_table/up.sql index 8a05e6d..fd43421 100644 --- a/src/db/migrations/201706272129_users_table/up.sql +++ b/src/db/migrations/201706272129_users_table/up.sql @@ -3,5 +3,6 @@ CREATE TABLE users ( name TEXT NOT NULL, password_salt BLOB NOT NULL, password_hash BLOB NOT NULL, + admin INTEGER NOT NULL, UNIQUE(name) ); diff --git a/src/db/schema.sqlite b/src/db/schema.sqlite index 8509e0c..eabf4ac 100644 Binary files a/src/db/schema.sqlite and b/src/db/schema.sqlite differ diff --git a/src/errors.rs b/src/errors.rs index ebbaabd..0d4dc62 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -37,6 +37,7 @@ error_chain! { errors { DaemonError {} AuthenticationRequired {} + AdminPrivilegeRequired {} MissingUsername {} MissingPassword {} MissingConfig {} @@ -53,6 +54,7 @@ impl From for IronError { e @ Error(ErrorKind::AuthenticationRequired, _) => { IronError::new(e, Status::Unauthorized) } + e @ Error(ErrorKind::AdminPrivilegeRequired, _) => IronError::new(e, Status::Forbidden), e @ Error(ErrorKind::MissingUsername, _) => IronError::new(e, Status::BadRequest), e @ Error(ErrorKind::MissingPassword, _) => IronError::new(e, Status::BadRequest), e @ Error(ErrorKind::IncorrectCredentials, _) => { diff --git a/src/user.rs b/src/user.rs index 93be791..0f02861 100644 --- a/src/user.rs +++ b/src/user.rs @@ -15,6 +15,7 @@ pub struct User { pub name: String, pub password_salt: Vec, pub password_hash: Vec, + pub admin: i32, } static DIGEST_ALG: &'static pbkdf2::PRF = &pbkdf2::HMAC_SHA256; @@ -23,13 +24,14 @@ const HASH_ITERATIONS: u32 = 10000; type PasswordHash = [u8; CREDENTIAL_LEN]; impl User { - pub fn new(name: &str, password: &str) -> User { + pub fn new(name: &str, password: &str, admin: bool) -> User { let salt = rand::random::<[u8; 16]>().to_vec(); let hash = User::hash_password(&salt, password); User { name: name.to_owned(), password_salt: salt, password_hash: hash, + admin: admin as i32, } } @@ -61,7 +63,7 @@ pub fn auth(db: &T, username: &str, password: &str) -> Result let connection = connection.lock().unwrap(); let connection = connection.deref(); let user: QueryResult = users - .select((name, password_salt, password_hash)) + .select((name, password_salt, password_hash, admin)) .filter(name.eq(username)) .get_result(connection); match user { @@ -80,3 +82,17 @@ pub fn count(db: &T) -> Result let connection = connection.deref(); Ok(users.select(expression::count(name)).first(connection)?) } + +pub fn is_admin(db: &T, username: &str) -> Result + where T: ConnectionSource +{ + use db::users::dsl::*; + let connection = db.get_connection(); + let connection = connection.lock().unwrap(); + let connection = connection.deref(); + let is_admin: i32 = users + .filter(name.eq(username)) + .select(admin) + .get_result(connection)?; + Ok(is_admin != 0) +}