Added notion of admin accouts, required to read/write settings
This commit is contained in:
parent
3ed2c75b30
commit
27cfa19b77
6 changed files with 145 additions and 13 deletions
59
src/api.rs
59
src/api.rs
|
@ -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))),
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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.
|
@ -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, _) => {
|
||||||
|
|
20
src/user.rs
20
src/user.rs
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue