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| {
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<DB>) -> Mount {
api_handler.mount("/", auth_api_chain);
}
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> {
#[derive(Serialize)]
struct Version {
@ -230,9 +281,7 @@ fn initial_setup(_: &mut Request, db: &DB) -> IronResult<Response> {
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))),

View file

@ -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<T>(db: &T) -> Result<Config>
.get_results(connection)?;
config.mount_dirs = Some(mount_dirs);
let usernames: Vec<String> = 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<T>(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();

View file

@ -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)
);

Binary file not shown.

View file

@ -37,6 +37,7 @@ error_chain! {
errors {
DaemonError {}
AuthenticationRequired {}
AdminPrivilegeRequired {}
MissingUsername {}
MissingPassword {}
MissingConfig {}
@ -53,6 +54,7 @@ impl From<Error> 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, _) => {

View file

@ -15,6 +15,7 @@ pub struct User {
pub name: String,
pub password_salt: Vec<u8>,
pub password_hash: Vec<u8>,
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<T>(db: &T, username: &str, password: &str) -> Result<bool>
let connection = connection.lock().unwrap();
let connection = connection.deref();
let user: QueryResult<User> = 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<T>(db: &T) -> Result<i64>
let connection = connection.deref();
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)
}