mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-07-13 02:51:07 -04:00
add config json to user table
This commit is contained in:
parent
721141770f
commit
24a0b2a9f1
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
//! Authentication schema: users and sessions/cookies.
|
//! Authentication schema: users and sessions/cookies.
|
||||||
|
|
||||||
|
use crate::json::UserConfig;
|
||||||
use crate::schema::Permissions;
|
use crate::schema::Permissions;
|
||||||
use base::{bail_t, format_err_t, strutil, ErrorKind, ResultExt};
|
use base::{bail_t, format_err_t, strutil, ErrorKind, ResultExt};
|
||||||
use failure::{bail, format_err, Error};
|
use failure::{bail, format_err, Error};
|
||||||
@ -13,7 +14,6 @@ use log::info;
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use protobuf::Message;
|
use protobuf::Message;
|
||||||
use ring::rand::{SecureRandom, SystemRandom};
|
use ring::rand::{SecureRandom, SystemRandom};
|
||||||
use rusqlite::types::FromSqlError;
|
|
||||||
use rusqlite::{named_params, params, Connection, Transaction};
|
use rusqlite::{named_params, params, Connection, Transaction};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -35,47 +35,15 @@ pub(crate) fn set_test_config() {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub struct UserPreferences(serde_json::Map<String, serde_json::Value>);
|
|
||||||
|
|
||||||
impl rusqlite::types::FromSql for UserPreferences {
|
|
||||||
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
|
||||||
Ok(Self(match value {
|
|
||||||
rusqlite::types::ValueRef::Null => serde_json::Map::default(),
|
|
||||||
rusqlite::types::ValueRef::Text(t) => {
|
|
||||||
serde_json::from_slice(t).map_err(|e| FromSqlError::Other(Box::new(e)))?
|
|
||||||
}
|
|
||||||
_ => return Err(FromSqlError::InvalidType),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl rusqlite::types::ToSql for UserPreferences {
|
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
|
||||||
if self.0.is_empty() {
|
|
||||||
return Ok(rusqlite::types::Null.into());
|
|
||||||
}
|
|
||||||
Ok(serde_json::to_string(&self.0)
|
|
||||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(e.into()))?
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum UserFlag {
|
|
||||||
Disabled = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub flags: i32,
|
pub config: UserConfig,
|
||||||
password_hash: Option<String>,
|
password_hash: Option<String>,
|
||||||
pub password_id: i32,
|
pub password_id: i32,
|
||||||
pub password_failure_count: i64,
|
pub password_failure_count: i64,
|
||||||
pub unix_uid: Option<i32>,
|
|
||||||
pub permissions: Permissions,
|
pub permissions: Permissions,
|
||||||
pub preferences: UserPreferences,
|
|
||||||
|
|
||||||
/// True iff this `User` has changed since the last flush.
|
/// True iff this `User` has changed since the last flush.
|
||||||
/// Only a couple things are flushed lazily: `password_failure_count` and (on upgrade to a new
|
/// Only a couple things are flushed lazily: `password_failure_count` and (on upgrade to a new
|
||||||
@ -88,10 +56,8 @@ impl User {
|
|||||||
UserChange {
|
UserChange {
|
||||||
id: Some(self.id),
|
id: Some(self.id),
|
||||||
username: self.username.clone(),
|
username: self.username.clone(),
|
||||||
flags: self.flags,
|
config: self.config.clone(),
|
||||||
set_password_hash: None,
|
set_password_hash: None,
|
||||||
preferences: self.preferences.clone(),
|
|
||||||
unix_uid: self.unix_uid,
|
|
||||||
permissions: self.permissions.clone(),
|
permissions: self.permissions.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,9 +65,6 @@ impl User {
|
|||||||
pub fn has_password(&self) -> bool {
|
pub fn has_password(&self) -> bool {
|
||||||
self.password_hash.is_some()
|
self.password_hash.is_some()
|
||||||
}
|
}
|
||||||
fn disabled(&self) -> bool {
|
|
||||||
(self.flags & UserFlag::Disabled as i32) != 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A change to a user.
|
/// A change to a user.
|
||||||
@ -114,10 +77,8 @@ impl User {
|
|||||||
pub struct UserChange {
|
pub struct UserChange {
|
||||||
id: Option<i32>,
|
id: Option<i32>,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub flags: i32,
|
pub config: UserConfig,
|
||||||
set_password_hash: Option<Option<String>>,
|
set_password_hash: Option<Option<String>>,
|
||||||
pub preferences: UserPreferences,
|
|
||||||
pub unix_uid: Option<i32>,
|
|
||||||
pub permissions: Permissions,
|
pub permissions: Permissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,10 +87,8 @@ impl UserChange {
|
|||||||
UserChange {
|
UserChange {
|
||||||
id: None,
|
id: None,
|
||||||
username,
|
username,
|
||||||
flags: 0,
|
config: UserConfig::default(),
|
||||||
set_password_hash: None,
|
set_password_hash: None,
|
||||||
preferences: UserPreferences::default(),
|
|
||||||
unix_uid: None,
|
|
||||||
permissions: Permissions::default(),
|
permissions: Permissions::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,10 +101,6 @@ impl UserChange {
|
|||||||
pub fn clear_password(&mut self) {
|
pub fn clear_password(&mut self) {
|
||||||
self.set_password_hash = Some(None);
|
self.set_password_hash = Some(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disable(&mut self) {
|
|
||||||
self.flags |= UserFlag::Disabled as i32;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
@ -385,13 +340,11 @@ impl State {
|
|||||||
select
|
select
|
||||||
id,
|
id,
|
||||||
username,
|
username,
|
||||||
flags,
|
config,
|
||||||
password_hash,
|
password_hash,
|
||||||
password_id,
|
password_id,
|
||||||
password_failure_count,
|
password_failure_count,
|
||||||
unix_uid,
|
permissions
|
||||||
permissions,
|
|
||||||
preferences
|
|
||||||
from
|
from
|
||||||
user
|
user
|
||||||
"#,
|
"#,
|
||||||
@ -401,20 +354,18 @@ impl State {
|
|||||||
let id = row.get(0)?;
|
let id = row.get(0)?;
|
||||||
let name: String = row.get(1)?;
|
let name: String = row.get(1)?;
|
||||||
let mut permissions = Permissions::new();
|
let mut permissions = Permissions::new();
|
||||||
permissions.merge_from_bytes(row.get_ref(7)?.as_blob()?)?;
|
permissions.merge_from_bytes(row.get_ref(6)?.as_blob()?)?;
|
||||||
state.users_by_id.insert(
|
state.users_by_id.insert(
|
||||||
id,
|
id,
|
||||||
User {
|
User {
|
||||||
id,
|
id,
|
||||||
username: name.clone(),
|
username: name.clone(),
|
||||||
flags: row.get(2)?,
|
config: row.get(2)?,
|
||||||
password_hash: row.get(3)?,
|
password_hash: row.get(3)?,
|
||||||
password_id: row.get(4)?,
|
password_id: row.get(4)?,
|
||||||
password_failure_count: row.get(5)?,
|
password_failure_count: row.get(5)?,
|
||||||
unix_uid: row.get(6)?,
|
|
||||||
dirty: false,
|
dirty: false,
|
||||||
permissions,
|
permissions,
|
||||||
preferences: row.get(8)?,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
state.users_by_name.insert(name, id);
|
state.users_by_name.insert(name, id);
|
||||||
@ -448,10 +399,8 @@ impl State {
|
|||||||
password_hash = :password_hash,
|
password_hash = :password_hash,
|
||||||
password_id = :password_id,
|
password_id = :password_id,
|
||||||
password_failure_count = :password_failure_count,
|
password_failure_count = :password_failure_count,
|
||||||
flags = :flags,
|
config = :config,
|
||||||
unix_uid = :unix_uid,
|
permissions = :permissions
|
||||||
permissions = :permissions,
|
|
||||||
preferences = :preferences
|
|
||||||
where
|
where
|
||||||
id = :id
|
id = :id
|
||||||
"#,
|
"#,
|
||||||
@ -478,11 +427,9 @@ impl State {
|
|||||||
":password_hash": phash,
|
":password_hash": phash,
|
||||||
":password_id": &pid,
|
":password_id": &pid,
|
||||||
":password_failure_count": &pcount,
|
":password_failure_count": &pcount,
|
||||||
":flags": &change.flags,
|
":config": &change.config,
|
||||||
":unix_uid": &change.unix_uid,
|
|
||||||
":id": &id,
|
":id": &id,
|
||||||
":permissions": &permissions,
|
":permissions": &permissions,
|
||||||
":preferences": &change.preferences,
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
let u = e.into_mut();
|
let u = e.into_mut();
|
||||||
@ -492,20 +439,16 @@ impl State {
|
|||||||
u.password_id += 1;
|
u.password_id += 1;
|
||||||
u.password_failure_count = 0;
|
u.password_failure_count = 0;
|
||||||
}
|
}
|
||||||
u.flags = change.flags;
|
u.config = change.config;
|
||||||
u.unix_uid = change.unix_uid;
|
|
||||||
u.permissions = change.permissions;
|
u.permissions = change.permissions;
|
||||||
u.preferences = change.preferences;
|
|
||||||
Ok(u)
|
Ok(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_user(&mut self, conn: &Connection, change: UserChange) -> Result<&User, Error> {
|
fn add_user(&mut self, conn: &Connection, change: UserChange) -> Result<&User, Error> {
|
||||||
let mut stmt = conn.prepare_cached(
|
let mut stmt = conn.prepare_cached(
|
||||||
r#"
|
r#"
|
||||||
insert into user (username, password_hash, flags, unix_uid, permissions,
|
insert into user (username, password_hash, config, permissions)
|
||||||
preferences)
|
values (:username, :password_hash, :config, :permissions)
|
||||||
values (:username, :password_hash, :flags, :unix_uid, :permissions,
|
|
||||||
:preferences)
|
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
let password_hash = change.set_password_hash.unwrap_or(None);
|
let password_hash = change.set_password_hash.unwrap_or(None);
|
||||||
@ -516,10 +459,8 @@ impl State {
|
|||||||
stmt.execute(named_params! {
|
stmt.execute(named_params! {
|
||||||
":username": &change.username[..],
|
":username": &change.username[..],
|
||||||
":password_hash": &password_hash,
|
":password_hash": &password_hash,
|
||||||
":flags": &change.flags,
|
":config": &change.config,
|
||||||
":unix_uid": &change.unix_uid,
|
|
||||||
":permissions": &permissions,
|
":permissions": &permissions,
|
||||||
":preferences": &change.preferences,
|
|
||||||
})?;
|
})?;
|
||||||
let id = conn.last_insert_rowid() as i32;
|
let id = conn.last_insert_rowid() as i32;
|
||||||
self.users_by_name.insert(change.username.clone(), id);
|
self.users_by_name.insert(change.username.clone(), id);
|
||||||
@ -531,14 +472,12 @@ impl State {
|
|||||||
Ok(e.insert(User {
|
Ok(e.insert(User {
|
||||||
id,
|
id,
|
||||||
username: change.username,
|
username: change.username,
|
||||||
flags: change.flags,
|
config: change.config,
|
||||||
password_hash,
|
password_hash,
|
||||||
password_id: 0,
|
password_id: 0,
|
||||||
password_failure_count: 0,
|
password_failure_count: 0,
|
||||||
unix_uid: change.unix_uid,
|
|
||||||
dirty: false,
|
dirty: false,
|
||||||
permissions: change.permissions,
|
permissions: change.permissions,
|
||||||
preferences: change.preferences,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -583,7 +522,7 @@ impl State {
|
|||||||
.users_by_id
|
.users_by_id
|
||||||
.get_mut(id)
|
.get_mut(id)
|
||||||
.expect("users_by_name implies users_by_id");
|
.expect("users_by_name implies users_by_id");
|
||||||
if u.disabled() {
|
if u.config.disabled {
|
||||||
bail!("user {:?} is disabled", username);
|
bail!("user {:?} is disabled", username);
|
||||||
}
|
}
|
||||||
let new_hash = {
|
let new_hash = {
|
||||||
@ -633,7 +572,7 @@ impl State {
|
|||||||
.users_by_id
|
.users_by_id
|
||||||
.get_mut(&uid)
|
.get_mut(&uid)
|
||||||
.ok_or_else(|| format_err!("no such uid {:?}", uid))?;
|
.ok_or_else(|| format_err!("no such uid {:?}", uid))?;
|
||||||
if u.disabled() {
|
if u.config.disabled {
|
||||||
bail!("user is disabled");
|
bail!("user is disabled");
|
||||||
}
|
}
|
||||||
State::make_session_int(
|
State::make_session_int(
|
||||||
@ -739,7 +678,7 @@ impl State {
|
|||||||
s.last_use = req;
|
s.last_use = req;
|
||||||
s.use_count += 1;
|
s.use_count += 1;
|
||||||
s.dirty = true;
|
s.dirty = true;
|
||||||
if u.disabled() {
|
if u.config.disabled {
|
||||||
bail_t!(Unauthenticated, "user {:?} is disabled", &u.username);
|
bail_t!(Unauthenticated, "user {:?} is disabled", &u.username);
|
||||||
}
|
}
|
||||||
Ok((s, u))
|
Ok((s, u))
|
||||||
@ -1207,7 +1146,7 @@ mod tests {
|
|||||||
// Disable the user.
|
// Disable the user.
|
||||||
{
|
{
|
||||||
let mut c = state.users_by_id().get(&uid).unwrap().change();
|
let mut c = state.users_by_id().get(&uid).unwrap().change();
|
||||||
c.disable();
|
c.config.disabled = true;
|
||||||
state.apply(&conn, c).unwrap();
|
state.apply(&conn, c).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1331,13 +1270,19 @@ mod tests {
|
|||||||
db::init(&mut conn).unwrap();
|
db::init(&mut conn).unwrap();
|
||||||
let mut state = State::init(&conn).unwrap();
|
let mut state = State::init(&conn).unwrap();
|
||||||
let mut change = UserChange::add_user("slamb".to_owned());
|
let mut change = UserChange::add_user("slamb".to_owned());
|
||||||
change.preferences.0.insert("foo".to_string(), 42.into());
|
change
|
||||||
|
.config
|
||||||
|
.preferences
|
||||||
|
.insert("foo".to_string(), 42.into());
|
||||||
let u = state.apply(&conn, change).unwrap();
|
let u = state.apply(&conn, change).unwrap();
|
||||||
let mut change = u.change();
|
let mut change = u.change();
|
||||||
change.preferences.0.insert("bar".to_string(), 26.into());
|
change
|
||||||
|
.config
|
||||||
|
.preferences
|
||||||
|
.insert("bar".to_string(), 26.into());
|
||||||
let u = state.apply(&conn, change).unwrap();
|
let u = state.apply(&conn, change).unwrap();
|
||||||
assert_eq!(u.preferences.0.get("foo"), Some(&42.into()));
|
assert_eq!(u.config.preferences.get("foo"), Some(&42.into()));
|
||||||
assert_eq!(u.preferences.0.get("bar"), Some(&26.into()));
|
assert_eq!(u.config.preferences.get("bar"), Some(&26.into()));
|
||||||
let uid = u.id;
|
let uid = u.id;
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -1347,7 +1292,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let state = State::init(&conn).unwrap();
|
let state = State::init(&conn).unwrap();
|
||||||
let u = state.users_by_id().get(&uid).unwrap();
|
let u = state.users_by_id().get(&uid).unwrap();
|
||||||
assert_eq!(u.preferences.0.get("foo"), Some(&42.into()));
|
assert_eq!(u.config.preferences.get("foo"), Some(&42.into()));
|
||||||
assert_eq!(u.preferences.0.get("bar"), Some(&26.into()));
|
assert_eq!(u.config.preferences.get("bar"), Some(&26.into()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ use std::{collections::BTreeMap, path::PathBuf};
|
|||||||
|
|
||||||
use rusqlite::types::{FromSqlError, ValueRef};
|
use rusqlite::types::{FromSqlError, ValueRef};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Map, Value};
|
use serde_json::Value;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -74,6 +74,7 @@ pub struct GlobalConfig {
|
|||||||
///
|
///
|
||||||
/// If an update causes this to be exceeded, older times will be garbage
|
/// If an update causes this to be exceeded, older times will be garbage
|
||||||
/// collected to stay within the limit.
|
/// collected to stay within the limit.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub max_signal_changes: Option<u32>,
|
pub max_signal_changes: Option<u32>,
|
||||||
|
|
||||||
/// Information about signal types.
|
/// Information about signal types.
|
||||||
@ -85,7 +86,7 @@ pub struct GlobalConfig {
|
|||||||
pub signals: BTreeMap<u32, SignalConfig>,
|
pub signals: BTreeMap<u32, SignalConfig>,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub unknown: Map<String, Value>,
|
pub unknown: BTreeMap<String, Value>,
|
||||||
}
|
}
|
||||||
sql!(GlobalConfig);
|
sql!(GlobalConfig);
|
||||||
|
|
||||||
@ -96,7 +97,7 @@ pub struct SampleFileDirConfig {
|
|||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub unknown: Map<String, Value>,
|
pub unknown: BTreeMap<String, Value>,
|
||||||
}
|
}
|
||||||
sql!(SampleFileDirConfig);
|
sql!(SampleFileDirConfig);
|
||||||
|
|
||||||
@ -120,7 +121,7 @@ pub struct SignalTypeConfig {
|
|||||||
pub values: BTreeMap<u8, SignalTypeValueConfig>,
|
pub values: BTreeMap<u8, SignalTypeValueConfig>,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub unknown: Map<String, Value>,
|
pub unknown: BTreeMap<String, Value>,
|
||||||
}
|
}
|
||||||
sql!(SignalTypeConfig);
|
sql!(SignalTypeConfig);
|
||||||
|
|
||||||
@ -137,7 +138,7 @@ pub struct SignalTypeValueConfig {
|
|||||||
pub color: String,
|
pub color: String,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub unknown: Map<String, Value>,
|
pub unknown: BTreeMap<String, Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignalTypeValueConfig {
|
impl SignalTypeValueConfig {
|
||||||
@ -172,7 +173,7 @@ pub struct CameraConfig {
|
|||||||
pub password: String,
|
pub password: String,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub unknown: Map<String, Value>,
|
pub unknown: BTreeMap<String, Value>,
|
||||||
}
|
}
|
||||||
sql!(CameraConfig);
|
sql!(CameraConfig);
|
||||||
|
|
||||||
@ -230,7 +231,7 @@ pub struct StreamConfig {
|
|||||||
pub flush_if_sec: u32,
|
pub flush_if_sec: u32,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub unknown: Map<String, Value>,
|
pub unknown: BTreeMap<String, Value>,
|
||||||
}
|
}
|
||||||
sql!(StreamConfig);
|
sql!(StreamConfig);
|
||||||
|
|
||||||
@ -274,6 +275,35 @@ pub struct SignalConfig {
|
|||||||
pub camera_associations: BTreeMap<i32, String>,
|
pub camera_associations: BTreeMap<i32, String>,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub unknown: Map<String, Value>,
|
pub unknown: BTreeMap<String, Value>,
|
||||||
}
|
}
|
||||||
sql!(SignalConfig);
|
sql!(SignalConfig);
|
||||||
|
|
||||||
|
/// User configuration, used in the `config` column of the `user` table.
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UserConfig {
|
||||||
|
/// If true, no method of authentication will succeed for this user.
|
||||||
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
pub disabled: bool,
|
||||||
|
|
||||||
|
/// If set, a Unix UID that is accepted for authentication when using HTTP over
|
||||||
|
/// a Unix domain socket.
|
||||||
|
///
|
||||||
|
/// (Additionally, the UID running Moonfire NVR can authenticate as anyone;
|
||||||
|
/// there's no point in trying to do otherwise.) This might be an easy
|
||||||
|
/// bootstrap method once configuration happens through a web UI rather than
|
||||||
|
/// text UI.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub unix_uid: Option<u64>,
|
||||||
|
|
||||||
|
/// Preferences controlled by the user.
|
||||||
|
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||||
|
pub preferences: UserPreferences,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub unknown: BTreeMap<String, Value>,
|
||||||
|
}
|
||||||
|
sql!(UserConfig);
|
||||||
|
|
||||||
|
pub type UserPreferences = BTreeMap<String, Value>;
|
||||||
|
@ -289,11 +289,18 @@ create table user (
|
|||||||
id integer primary key,
|
id integer primary key,
|
||||||
username unique not null,
|
username unique not null,
|
||||||
|
|
||||||
-- Bitwise mask of flags:
|
-- A json.UserConfig.
|
||||||
-- 1: disabled. If set, no method of authentication for this user will succeed.
|
config text,
|
||||||
flags integer not null,
|
|
||||||
|
|
||||||
-- If set, a hash for password authentication, as generated by `libpasta::hash_password`.
|
-- If set, a hash for password authentication, as generated by
|
||||||
|
-- `libpasta::hash_password`. This is separate from config for two reasons:
|
||||||
|
-- * It should never be sent over the wire, because password hashes are
|
||||||
|
-- almost as sensitive as passwords themselves. Keeping it separate avoids
|
||||||
|
-- complicating the protocol for retrieving the config and updating it
|
||||||
|
-- with optimistic concurrency control.
|
||||||
|
-- * It may be updated while authenticating to upgrade the password hash
|
||||||
|
-- format, and the conflicting writes again might complicate the update
|
||||||
|
-- protocol.
|
||||||
password_hash text,
|
password_hash text,
|
||||||
|
|
||||||
-- A counter which increments with every password reset or clear.
|
-- A counter which increments with every password reset or clear.
|
||||||
@ -303,19 +310,9 @@ create table user (
|
|||||||
-- This could be used to automatically disable the password on hitting a threshold.
|
-- This could be used to automatically disable the password on hitting a threshold.
|
||||||
password_failure_count integer not null default 0,
|
password_failure_count integer not null default 0,
|
||||||
|
|
||||||
-- If set, a Unix UID that is accepted for authentication when using HTTP over
|
|
||||||
-- a Unix domain socket. (Additionally, the UID running Moonfire NVR can authenticate
|
|
||||||
-- as anyone; there's no point in trying to do otherwise.) This might be an easy
|
|
||||||
-- bootstrap method once configuration happens through a web UI rather than text UI.
|
|
||||||
unix_uid integer,
|
|
||||||
|
|
||||||
-- Permissions available for newly created tokens or when authenticating via
|
-- Permissions available for newly created tokens or when authenticating via
|
||||||
-- unix_uid above. A serialized "Permissions" protobuf.
|
-- unix_uid above. A serialized "Permissions" protobuf.
|
||||||
permissions blob not null default X'',
|
permissions blob not null default X''
|
||||||
|
|
||||||
-- Preferences controlled by the user. A JSON object, or null to represent
|
|
||||||
-- the empty object. Can be returned and modified through the API.
|
|
||||||
preferences text
|
|
||||||
);
|
);
|
||||||
|
|
||||||
-- A single session, whether for browser or robot use.
|
-- A single session, whether for browser or robot use.
|
||||||
|
@ -11,7 +11,9 @@ use url::Url;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
json::{CameraConfig, GlobalConfig, SampleFileDirConfig, SignalConfig, SignalTypeConfig},
|
json::{
|
||||||
|
CameraConfig, GlobalConfig, SampleFileDirConfig, SignalConfig, SignalTypeConfig, UserConfig,
|
||||||
|
},
|
||||||
SqlUuid,
|
SqlUuid,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,6 +71,57 @@ fn copy_sample_file_dir(tx: &rusqlite::Transaction) -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn copy_users(tx: &rusqlite::Transaction) -> Result<(), Error> {
|
||||||
|
let mut stmt = tx.prepare(
|
||||||
|
r#"
|
||||||
|
select
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
flags,
|
||||||
|
password_hash,
|
||||||
|
password_id,
|
||||||
|
password_failure_count,
|
||||||
|
unix_uid,
|
||||||
|
permissions
|
||||||
|
from old_user
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
let mut insert = tx.prepare(
|
||||||
|
r#"
|
||||||
|
insert into user (id, username, config, password_hash, password_id,
|
||||||
|
password_failure_count, permissions)
|
||||||
|
values (:id, :username, :config, :password_hash, :password_id,
|
||||||
|
:password_failure_count, :permissions)
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
let mut rows = stmt.query(params![])?;
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
let id: i32 = row.get(0)?;
|
||||||
|
let username: String = row.get(1)?;
|
||||||
|
let flags: i32 = row.get(2)?;
|
||||||
|
let password_hash: String = row.get(3)?;
|
||||||
|
let password_id: i32 = row.get(4)?;
|
||||||
|
let password_failure_count: i32 = row.get(5)?;
|
||||||
|
let unix_uid: Option<i64> = row.get(6)?;
|
||||||
|
let permissions: Vec<u8> = row.get(7)?;
|
||||||
|
let config = UserConfig {
|
||||||
|
disabled: (flags & 1) != 0,
|
||||||
|
unix_uid: unix_uid.map(u64::try_from).transpose()?,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
insert.execute(named_params! {
|
||||||
|
":id": id,
|
||||||
|
":username": username,
|
||||||
|
":config": config,
|
||||||
|
":password_hash": password_hash,
|
||||||
|
":password_id": password_id,
|
||||||
|
":password_failure_count": password_failure_count,
|
||||||
|
":permissions": permissions,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_signal_types(tx: &rusqlite::Transaction) -> Result<(), Error> {
|
fn copy_signal_types(tx: &rusqlite::Transaction) -> Result<(), Error> {
|
||||||
let mut types_ = FnvHashMap::default();
|
let mut types_ = FnvHashMap::default();
|
||||||
let mut stmt = tx.prepare("select type_uuid, value, name from signal_type_enum")?;
|
let mut stmt = tx.prepare("select type_uuid, value, name from signal_type_enum")?;
|
||||||
@ -288,7 +341,8 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
|||||||
tx.execute_batch(
|
tx.execute_batch(
|
||||||
r#"
|
r#"
|
||||||
alter table open add boot_uuid check (length(boot_uuid) = 16);
|
alter table open add boot_uuid check (length(boot_uuid) = 16);
|
||||||
alter table user add preferences text;
|
alter table user rename to old_user;
|
||||||
|
alter table user_session rename to old_user_session;
|
||||||
alter table camera rename to old_camera;
|
alter table camera rename to old_camera;
|
||||||
alter table stream rename to old_stream;
|
alter table stream rename to old_stream;
|
||||||
alter table signal rename to old_signal;
|
alter table signal rename to old_signal;
|
||||||
@ -338,6 +392,42 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
|||||||
uuid blob primary key check (length(uuid) = 16),
|
uuid blob primary key check (length(uuid) = 16),
|
||||||
config text
|
config text
|
||||||
) without rowid;
|
) without rowid;
|
||||||
|
|
||||||
|
create table user (
|
||||||
|
id integer primary key,
|
||||||
|
username unique not null,
|
||||||
|
config text,
|
||||||
|
password_hash text,
|
||||||
|
password_id integer not null default 0,
|
||||||
|
password_failure_count integer not null default 0,
|
||||||
|
permissions blob not null default X''
|
||||||
|
);
|
||||||
|
|
||||||
|
create table user_session (
|
||||||
|
session_id_hash blob primary key not null,
|
||||||
|
user_id integer references user (id) not null,
|
||||||
|
seed blob not null,
|
||||||
|
flags integer not null,
|
||||||
|
domain text,
|
||||||
|
description text,
|
||||||
|
creation_password_id integer,
|
||||||
|
creation_time_sec integer not null,
|
||||||
|
creation_user_agent text,
|
||||||
|
creation_peer_addr blob,
|
||||||
|
revocation_time_sec integer,
|
||||||
|
revocation_user_agent text,
|
||||||
|
revocation_peer_addr blob,
|
||||||
|
revocation_reason integer,
|
||||||
|
revocation_reason_detail text,
|
||||||
|
last_use_time_sec integer,
|
||||||
|
last_use_user_agent text,
|
||||||
|
last_use_peer_addr blob,
|
||||||
|
use_count not null default 0,
|
||||||
|
permissions blob not null default X''
|
||||||
|
) without rowid;
|
||||||
|
|
||||||
|
drop index user_session_uid;
|
||||||
|
create index user_session_uid on user_session (user_id);
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
copy_meta(tx)?;
|
copy_meta(tx)?;
|
||||||
@ -346,8 +436,11 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
|||||||
copy_signal_types(tx)?;
|
copy_signal_types(tx)?;
|
||||||
copy_signals(tx)?;
|
copy_signals(tx)?;
|
||||||
copy_streams(tx)?;
|
copy_streams(tx)?;
|
||||||
|
copy_users(tx)?;
|
||||||
tx.execute_batch(
|
tx.execute_batch(
|
||||||
r#"
|
r#"
|
||||||
|
insert into user_session select * from old_user_session;
|
||||||
|
|
||||||
drop index recording_cover;
|
drop index recording_cover;
|
||||||
|
|
||||||
alter table recording rename to old_recording;
|
alter table recording rename to old_recording;
|
||||||
@ -416,6 +509,8 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
|||||||
drop table old_sample_file_dir;
|
drop table old_sample_file_dir;
|
||||||
drop table old_meta;
|
drop table old_meta;
|
||||||
drop table old_signal;
|
drop table old_signal;
|
||||||
|
drop table old_user_session;
|
||||||
|
drop table old_user;
|
||||||
drop table signal_type_enum;
|
drop table signal_type_enum;
|
||||||
drop table signal_camera;
|
drop table signal_camera;
|
||||||
"#,
|
"#,
|
||||||
|
@ -509,7 +509,7 @@ impl VideoSampleEntry {
|
|||||||
pub struct ToplevelUser {
|
pub struct ToplevelUser {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub preferences: db::auth::UserPreferences,
|
pub preferences: db::json::UserPreferences,
|
||||||
pub session: Option<Session>,
|
pub session: Option<Session>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -523,5 +523,5 @@ pub struct PostUser {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct UserSubset {
|
pub struct UserSubset {
|
||||||
pub preferences: Option<db::auth::UserPreferences>,
|
pub preferences: Option<db::json::UserPreferences>,
|
||||||
}
|
}
|
||||||
|
@ -1041,14 +1041,14 @@ impl Service {
|
|||||||
.get(&id)
|
.get(&id)
|
||||||
.ok_or_else(|| format_err_t!(Internal, "can't find currently authenticated user"))?;
|
.ok_or_else(|| format_err_t!(Internal, "can't find currently authenticated user"))?;
|
||||||
if let Some(precondition) = r.precondition {
|
if let Some(precondition) = r.precondition {
|
||||||
if matches!(precondition.preferences, Some(p) if p != user.preferences) {
|
if matches!(precondition.preferences, Some(p) if p != user.config.preferences) {
|
||||||
bail_t!(FailedPrecondition, "preferences mismatch");
|
bail_t!(FailedPrecondition, "preferences mismatch");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(update) = r.update {
|
if let Some(update) = r.update {
|
||||||
let mut change = user.change();
|
let mut change = user.change();
|
||||||
if let Some(preferences) = update.preferences {
|
if let Some(preferences) = update.preferences {
|
||||||
change.preferences = preferences;
|
change.config.preferences = preferences;
|
||||||
}
|
}
|
||||||
db.apply_user_change(change).map_err(internal_server_err)?;
|
db.apply_user_change(change).map_err(internal_server_err)?;
|
||||||
}
|
}
|
||||||
@ -1290,7 +1290,7 @@ impl Service {
|
|||||||
user: Some(json::ToplevelUser {
|
user: Some(json::ToplevelUser {
|
||||||
id: s.user_id,
|
id: s.user_id,
|
||||||
name: u.username.clone(),
|
name: u.username.clone(),
|
||||||
preferences: u.preferences.clone(),
|
preferences: u.config.preferences.clone(),
|
||||||
session: Some(json::Session { csrf: s.csrf() }),
|
session: Some(json::Session { csrf: s.csrf() }),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user