Added notion of admin accouts, required to read/write settings

This commit is contained in:
Antoine Gersant 2017-07-03 17:38:28 -07:00
parent 3ed2c75b30
commit 27cfa19b77
6 changed files with 145 additions and 13 deletions

View file

@ -124,13 +124,18 @@ fn get_endpoints(db: Arc<DB>) -> Mount {
move |request: &mut Request| { move |request: &mut Request| {
self::get_config(request, get_db.deref()) self::get_config(request, get_db.deref())
}, },
"get_settings"); "get_config");
settings_router.put("/", settings_router.put("/",
move |request: &mut Request| { move |request: &mut Request| {
self::put_config(request, put_db.deref()) self::put_config(request, put_db.deref())
}, },
"put_config"); "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); let mut auth_api_chain = Chain::new(auth_api_mount);
@ -139,6 +144,7 @@ fn get_endpoints(db: Arc<DB>) -> Mount {
api_handler.mount("/", auth_api_chain); api_handler.mount("/", auth_api_chain);
} }
api_handler api_handler
} }
@ -206,6 +212,51 @@ impl Handler for AuthHandler {
} }
} }
struct AdminRequirement {
db: Arc<DB>,
}
impl AroundMiddleware for AdminRequirement {
fn around(self, handler: Box<Handler>) -> Box<Handler> {
Box::new(AdminHandler {
db: self.db,
handler: handler,
}) as Box<Handler>
}
}
struct AdminHandler {
handler: Box<Handler>,
db: Arc<DB>,
}
impl Handler for AdminHandler {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
{
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::<SessionKey>() {
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<Response> { fn version(_: &mut Request) -> IronResult<Response> {
#[derive(Serialize)] #[derive(Serialize)]
struct Version { struct Version {
@ -230,9 +281,7 @@ fn initial_setup(_: &mut Request, db: &DB) -> IronResult<Response> {
has_any_users: bool, has_any_users: bool,
}; };
let initial_setup = InitialSetup { let initial_setup = InitialSetup { has_any_users: user::count(db)? > 0 };
has_any_users: user::count(db)? > 0,
};
match serde_json::to_string(&initial_setup) { match serde_json::to_string(&initial_setup) {
Ok(result_json) => Ok(Response::with((status::Ok, result_json))), Ok(result_json) => Ok(Response::with((status::Ok, result_json))),

View file

@ -28,6 +28,7 @@ pub struct MiscSettings {
pub struct ConfigUser { pub struct ConfigUser {
pub name: String, pub name: String,
pub password: String, pub password: String,
pub admin: bool,
} }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -99,15 +100,16 @@ pub fn read<T>(db: &T) -> Result<Config>
.get_results(connection)?; .get_results(connection)?;
config.mount_dirs = Some(mount_dirs); config.mount_dirs = Some(mount_dirs);
let usernames: Vec<String> = users::table let found_users: Vec<(String, i32)> = users::table
.select(users::columns::name) .select((users::columns::name, users::columns::admin))
.get_results(connection)?; .get_results(connection)?;
config.users = Some(usernames config.users = Some(found_users
.into_iter() .into_iter()
.map(|s| { .map(|(n, a)| {
ConfigUser { ConfigUser {
name: s, name: n,
password: "".to_owned(), password: "".to_owned(),
admin: a != 0,
} }
}) })
.collect::<_>()); .collect::<_>());
@ -181,11 +183,18 @@ pub fn amend<T>(db: &T, new_config: &Config) -> Result<()>
.filter(|u| !u.password.is_empty()) .filter(|u| !u.password.is_empty())
.collect::<_>(); .collect::<_>();
for ref config_user in insert_users { 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) diesel::insert(&new_user)
.into(users::table) .into(users::table)
.execute(connection)?; .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 { if let Some(sleep_duration) = new_config.reindex_every_n_seconds {
@ -246,6 +255,7 @@ fn test_amend() {
users: Some(vec![ConfigUser { users: Some(vec![ConfigUser {
name: "Teddy🐻".into(), name: "Teddy🐻".into(),
password: "Tasty🍖".into(), password: "Tasty🍖".into(),
admin: false,
}]), }]),
ydns: None, ydns: None,
}; };
@ -260,6 +270,7 @@ fn test_amend() {
users: Some(vec![ConfigUser { users: Some(vec![ConfigUser {
name: "Kermit🐸".into(), name: "Kermit🐸".into(),
password: "🐞🐞".into(), password: "🐞🐞".into(),
admin: false,
}]), }]),
ydns: Some(DDNSConfig { ydns: Some(DDNSConfig {
host: "🐸🐸🐸.ydns.eu".into(), host: "🐸🐸🐸.ydns.eu".into(),
@ -295,6 +306,7 @@ fn test_amend_preserve_password_hashes() {
users: Some(vec![ConfigUser { users: Some(vec![ConfigUser {
name: "Teddy🐻".into(), name: "Teddy🐻".into(),
password: "Tasty🍖".into(), password: "Tasty🍖".into(),
admin: false,
}]), }]),
ydns: None, ydns: None,
}; };
@ -318,10 +330,12 @@ fn test_amend_preserve_password_hashes() {
users: Some(vec![ConfigUser { users: Some(vec![ConfigUser {
name: "Kermit🐸".into(), name: "Kermit🐸".into(),
password: "tasty🐞".into(), password: "tasty🐞".into(),
admin: false,
}, },
ConfigUser { ConfigUser {
name: "Teddy🐻".into(), name: "Teddy🐻".into(),
password: "".into(), password: "".into(),
admin: false,
}]), }]),
ydns: None, ydns: None,
}; };
@ -341,6 +355,56 @@ fn test_amend_preserve_password_hashes() {
assert_eq!(new_hash, initial_hash); 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] #[test]
fn test_clean_path_string() { fn test_clean_path_string() {
let mut correct_path = path::PathBuf::new(); let mut correct_path = path::PathBuf::new();

View file

@ -3,5 +3,6 @@ CREATE TABLE users (
name TEXT NOT NULL, name TEXT NOT NULL,
password_salt BLOB NOT NULL, password_salt BLOB NOT NULL,
password_hash BLOB NOT NULL, password_hash BLOB NOT NULL,
admin INTEGER NOT NULL,
UNIQUE(name) UNIQUE(name)
); );

Binary file not shown.

View file

@ -37,6 +37,7 @@ error_chain! {
errors { errors {
DaemonError {} DaemonError {}
AuthenticationRequired {} AuthenticationRequired {}
AdminPrivilegeRequired {}
MissingUsername {} MissingUsername {}
MissingPassword {} MissingPassword {}
MissingConfig {} MissingConfig {}
@ -53,6 +54,7 @@ impl From<Error> for IronError {
e @ Error(ErrorKind::AuthenticationRequired, _) => { e @ Error(ErrorKind::AuthenticationRequired, _) => {
IronError::new(e, Status::Unauthorized) 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::MissingUsername, _) => IronError::new(e, Status::BadRequest),
e @ Error(ErrorKind::MissingPassword, _) => IronError::new(e, Status::BadRequest), e @ Error(ErrorKind::MissingPassword, _) => IronError::new(e, Status::BadRequest),
e @ Error(ErrorKind::IncorrectCredentials, _) => { e @ Error(ErrorKind::IncorrectCredentials, _) => {

View file

@ -15,6 +15,7 @@ pub struct User {
pub name: String, pub name: String,
pub password_salt: Vec<u8>, pub password_salt: Vec<u8>,
pub password_hash: Vec<u8>, pub password_hash: Vec<u8>,
pub admin: i32,
} }
static DIGEST_ALG: &'static pbkdf2::PRF = &pbkdf2::HMAC_SHA256; static DIGEST_ALG: &'static pbkdf2::PRF = &pbkdf2::HMAC_SHA256;
@ -23,13 +24,14 @@ const HASH_ITERATIONS: u32 = 10000;
type PasswordHash = [u8; CREDENTIAL_LEN]; type PasswordHash = [u8; CREDENTIAL_LEN];
impl User { 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 salt = rand::random::<[u8; 16]>().to_vec();
let hash = User::hash_password(&salt, password); let hash = User::hash_password(&salt, password);
User { User {
name: name.to_owned(), name: name.to_owned(),
password_salt: salt, password_salt: salt,
password_hash: hash, password_hash: hash,
admin: admin as i32,
} }
} }
@ -61,7 +63,7 @@ pub fn auth<T>(db: &T, username: &str, password: &str) -> Result<bool>
let connection = connection.lock().unwrap(); let connection = connection.lock().unwrap();
let connection = connection.deref(); let connection = connection.deref();
let user: QueryResult<User> = users let user: QueryResult<User> = users
.select((name, password_salt, password_hash)) .select((name, password_salt, password_hash, admin))
.filter(name.eq(username)) .filter(name.eq(username))
.get_result(connection); .get_result(connection);
match user { match user {
@ -80,3 +82,17 @@ pub fn count<T>(db: &T) -> Result<i64>
let connection = connection.deref(); let connection = connection.deref();
Ok(users.select(expression::count(name)).first(connection)?) Ok(users.select(expression::count(name)).first(connection)?)
} }
pub fn is_admin<T>(db: &T, username: &str) -> Result<bool>
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)
}