add concept of user/session permissions

(I also considered the names "capabilities" and "scopes", but I think
"permissions" is the most widely understood.)

This is increasingly necessary as the web API becomes more capable.
Among other things, it allows:

* non-administrator users who can view but not access camera passwords
  or change any state
* workers that update signal state based on cameras' built-in motion
  detection or a security system's events but don't need to view videos
* control over what can be done without authenticating

Currently session permissions are just copied from user permissions, but
you can also imagine admin sessions vs not, as a checkbox when signing
in. This would match the standard Unix workflow of using a
non-administrative session most of the time.

Relevant to my current signals work (#28) and to the addition of an
administrative API (#35, including #66).
This commit is contained in:
Scott Lamb 2019-06-19 15:17:50 -07:00
parent d8b8d5d5e0
commit fda7e4ca2b
23 changed files with 336 additions and 741 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@ cameras.sql
node_modules node_modules
prep.config prep.config
settings-nvr-local.js settings-nvr-local.js
db/schema.rs
target target
ui-dist ui-dist
yarn-error.log yarn-error.log

29
Cargo.lock generated
View File

@ -954,7 +954,8 @@ dependencies = [
"odds 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "odds 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl 0.10.23 (registry+https://github.com/rust-lang/crates.io-index)", "openssl 0.10.23 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"protobuf 2.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
"protobuf-codegen-pure 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
"regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rusqlite 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusqlite 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1000,6 +1001,7 @@ dependencies = [
"mylog 0.1.0 (git+https://github.com/scottlamb/mylog)", "mylog 0.1.0 (git+https://github.com/scottlamb/mylog)",
"openssl 0.10.23 (registry+https://github.com/rust-lang/crates.io-index)", "openssl 0.10.23 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
"reffers 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "reffers 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.17 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.17 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1296,8 +1298,25 @@ dependencies = [
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "2.6.1" version = "3.0.0-pre"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/stepancheg/rust-protobuf#76c8892a410fa7a3d74041332c20fb2b1a74f71f"
[[package]]
name = "protobuf-codegen"
version = "3.0.0-pre"
source = "git+https://github.com/stepancheg/rust-protobuf#76c8892a410fa7a3d74041332c20fb2b1a74f71f"
dependencies = [
"protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
]
[[package]]
name = "protobuf-codegen-pure"
version = "3.0.0-pre"
source = "git+https://github.com/stepancheg/rust-protobuf#76c8892a410fa7a3d74041332c20fb2b1a74f71f"
dependencies = [
"protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
"protobuf-codegen 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
]
[[package]] [[package]]
name = "publicsuffix" name = "publicsuffix"
@ -2453,7 +2472,9 @@ dependencies = [
"checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" "checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" "checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
"checksum protobuf 2.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a151c11a92df0059d6ab446fafa3b21a1210aad4bc2293e1c946e8132b10db01" "checksum protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)" = "<none>"
"checksum protobuf-codegen 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)" = "<none>"
"checksum protobuf-codegen-pure 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)" = "<none>"
"checksum publicsuffix 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5afecba86dcf1e4fd610246f89899d1924fe12e1e89f555eb7c7f710f3c5ad1d" "checksum publicsuffix 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5afecba86dcf1e4fd610246f89899d1924fe12e1e89f555eb7c7f710f3c5ad1d"
"checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db" "checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db"
"checksum rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" "checksum rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"

View File

@ -41,6 +41,7 @@ memmap = "0.7"
mylog = { git = "https://github.com/scottlamb/mylog" } mylog = { git = "https://github.com/scottlamb/mylog" }
openssl = "0.10" openssl = "0.10"
parking_lot = { version = "0.8", features = [] } parking_lot = { version = "0.8", features = [] }
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
reffers = "0.5.1" reffers = "0.5.1"
regex = "1.0" regex = "1.0"
ring = "0.14.6" ring = "0.14.6"

View File

@ -26,7 +26,7 @@ mylog = { git = "https://github.com/scottlamb/mylog" }
odds = { version = "0.3.1", features = ["std-vec"] } odds = { version = "0.3.1", features = ["std-vec"] }
openssl = "0.10" openssl = "0.10"
parking_lot = { version = "0.8", features = [] } parking_lot = { version = "0.8", features = [] }
protobuf = "2.0" protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
regex = "1.0" regex = "1.0"
rusqlite = "0.18" rusqlite = "0.18"
smallvec = "0.6" smallvec = "0.6"
@ -34,3 +34,6 @@ tempdir = "0.3"
time = "0.1" time = "0.1"
uuid = { version = "0.7", features = ["std", "v4"] } uuid = { version = "0.7", features = ["std", "v4"] }
itertools = "0.8.0" itertools = "0.8.0"
[build-dependencies]
protobuf-codegen-pure = { git = "https://github.com/stepancheg/rust-protobuf" }

View File

@ -31,11 +31,13 @@
use log::info; use log::info;
use base::strutil; use base::strutil;
use blake2_rfc::blake2b::blake2b; use blake2_rfc::blake2b::blake2b;
use crate::schema::Permissions;
use failure::{Error, bail, format_err}; use failure::{Error, bail, format_err};
use fnv::FnvHashMap; use fnv::FnvHashMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use libpasta; use libpasta;
use parking_lot::Mutex; use parking_lot::Mutex;
use protobuf::Message;
use rusqlite::{Connection, Transaction, types::ToSql}; use rusqlite::{Connection, Transaction, types::ToSql};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt; use std::fmt;
@ -68,6 +70,7 @@ pub struct User {
pub password_id: i32, pub password_id: i32,
pub password_failure_count: i64, pub password_failure_count: i64,
pub unix_uid: Option<i32>, pub unix_uid: Option<i32>,
pub permissions: Permissions,
/// 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
@ -79,10 +82,11 @@ impl User {
pub fn change(&self) -> UserChange { pub fn change(&self) -> UserChange {
UserChange { UserChange {
id: Some(self.id), id: Some(self.id),
username: self.username.to_string(), username: self.username.clone(),
flags: self.flags, flags: self.flags,
set_password_hash: None, set_password_hash: None,
unix_uid: self.unix_uid, unix_uid: self.unix_uid,
permissions: self.permissions.clone(),
} }
} }
@ -103,6 +107,7 @@ pub struct UserChange {
pub flags: i32, pub flags: i32,
set_password_hash: Option<Option<String>>, set_password_hash: Option<Option<String>>,
pub unix_uid: Option<i32>, pub unix_uid: Option<i32>,
pub permissions: Permissions,
} }
impl UserChange { impl UserChange {
@ -113,6 +118,7 @@ impl UserChange {
flags: 0, flags: 0,
set_password_hash: None, set_password_hash: None,
unix_uid: None, unix_uid: None,
permissions: Permissions::default(),
} }
} }
@ -215,6 +221,8 @@ pub struct Session {
revocation_reason: Option<i32>, // see RevocationReason enum revocation_reason: Option<i32>, // see RevocationReason enum
revocation_reason_detail: Option<String>, revocation_reason_detail: Option<String>,
pub permissions: Permissions,
last_use: Request, last_use: Request,
use_count: i32, use_count: i32,
dirty: bool, dirty: bool,
@ -342,7 +350,8 @@ impl State {
password_hash, password_hash,
password_id, password_id,
password_failure_count, password_failure_count,
unix_uid unix_uid,
permissions
from from
user user
"#)?; "#)?;
@ -350,6 +359,8 @@ impl State {
while let Some(row) = rows.next()? { while let Some(row) = rows.next()? {
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();
permissions.merge_from_bytes(row.get_raw_checked(7)?.as_blob()?)?;
state.users_by_id.insert(id, User { state.users_by_id.insert(id, User {
id, id,
username: name.clone(), username: name.clone(),
@ -359,6 +370,7 @@ impl State {
password_failure_count: row.get(5)?, password_failure_count: row.get(5)?,
unix_uid: row.get(6)?, unix_uid: row.get(6)?,
dirty: false, dirty: false,
permissions,
}); });
state.users_by_name.insert(name, id); state.users_by_name.insert(name, id);
} }
@ -385,7 +397,8 @@ impl State {
password_id = :password_id, password_id = :password_id,
password_failure_count = :password_failure_count, password_failure_count = :password_failure_count,
flags = :flags, flags = :flags,
unix_uid = :unix_uid unix_uid = :unix_uid,
permissions = :permissions
where where
id = :id id = :id
"#)?; "#)?;
@ -402,6 +415,7 @@ impl State {
}, },
Some(h) => (h, e.get().password_id + 1, 0), Some(h) => (h, e.get().password_id + 1, 0),
}; };
let permissions = change.permissions.write_to_bytes().expect("proto3->vec is infallible");
stmt.execute_named(&[ stmt.execute_named(&[
(":username", &&change.username[..]), (":username", &&change.username[..]),
(":password_hash", phash), (":password_hash", phash),
@ -410,6 +424,7 @@ impl State {
(":flags", &change.flags), (":flags", &change.flags),
(":unix_uid", &change.unix_uid), (":unix_uid", &change.unix_uid),
(":id", &id), (":id", &id),
(":permissions", &permissions),
])?; ])?;
} }
let u = e.into_mut(); let u = e.into_mut();
@ -421,20 +436,23 @@ impl State {
} }
u.flags = change.flags; u.flags = change.flags;
u.unix_uid = change.unix_uid; u.unix_uid = change.unix_uid;
u.permissions = change.permissions;
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(r#" let mut stmt = conn.prepare_cached(r#"
insert into user (username, password_hash, flags, unix_uid) insert into user (username, password_hash, flags, unix_uid, permissions)
values (:username, :password_hash, :flags, :unix_uid) values (:username, :password_hash, :flags, :unix_uid, :permissions)
"#)?; "#)?;
let password_hash = change.set_password_hash.unwrap_or(None); let password_hash = change.set_password_hash.unwrap_or(None);
let permissions = change.permissions.write_to_bytes().expect("proto3->vec is infallible");
stmt.execute_named(&[ stmt.execute_named(&[
(":username", &&change.username[..]), (":username", &&change.username[..]),
(":password_hash", &password_hash), (":password_hash", &password_hash),
(":flags", &change.flags), (":flags", &change.flags),
(":unix_uid", &change.unix_uid), (":unix_uid", &change.unix_uid),
(":permissions", &permissions),
])?; ])?;
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);
@ -452,6 +470,7 @@ impl State {
password_failure_count: 0, password_failure_count: 0,
unix_uid: change.unix_uid, unix_uid: change.unix_uid,
dirty: false, dirty: false,
permissions: change.permissions,
})) }))
} }
@ -503,12 +522,13 @@ impl State {
} }
let password_id = u.password_id; let password_id = u.password_id;
State::make_session(conn, req, u, domain, Some(password_id), session_flags, State::make_session(conn, req, u, domain, Some(password_id), session_flags,
&mut self.sessions) &mut self.sessions, u.permissions.clone())
} }
fn make_session<'s>(conn: &Connection, creation: Request, user: &mut User, domain: Vec<u8>, fn make_session<'s>(conn: &Connection, creation: Request, user: &mut User, domain: Vec<u8>,
creation_password_id: Option<i32>, flags: i32, creation_password_id: Option<i32>, flags: i32,
sessions: &'s mut FnvHashMap<SessionHash, Session>) sessions: &'s mut FnvHashMap<SessionHash, Session>,
permissions: Permissions)
-> Result<(RawSessionId, &'s Session), Error> { -> Result<(RawSessionId, &'s Session), Error> {
let mut session_id = RawSessionId::new(); let mut session_id = RawSessionId::new();
::openssl::rand::rand_bytes(&mut session_id.0).unwrap(); ::openssl::rand::rand_bytes(&mut session_id.0).unwrap();
@ -518,13 +538,16 @@ impl State {
let mut stmt = conn.prepare_cached(r#" let mut stmt = conn.prepare_cached(r#"
insert into user_session (session_id_hash, user_id, seed, flags, domain, insert into user_session (session_id_hash, user_id, seed, flags, domain,
creation_password_id, creation_time_sec, creation_password_id, creation_time_sec,
creation_user_agent, creation_peer_addr) creation_user_agent, creation_peer_addr,
permissions)
values (:session_id_hash, :user_id, :seed, :flags, :domain, values (:session_id_hash, :user_id, :seed, :flags, :domain,
:creation_password_id, :creation_time_sec, :creation_password_id, :creation_time_sec,
:creation_user_agent, :creation_peer_addr) :creation_user_agent, :creation_peer_addr,
:permissions)
"#)?; "#)?;
let addr = creation.addr_buf(); let addr = creation.addr_buf();
let addr: Option<&[u8]> = addr.as_ref().map(|a| a.as_ref()); let addr: Option<&[u8]> = addr.as_ref().map(|a| a.as_ref());
let permissions_blob = permissions.write_to_bytes().expect("proto3->vec is infallible");
stmt.execute_named(&[ stmt.execute_named(&[
(":session_id_hash", &&hash.0[..]), (":session_id_hash", &&hash.0[..]),
(":user_id", &user.id), (":user_id", &user.id),
@ -535,6 +558,7 @@ impl State {
(":creation_time_sec", &creation.when_sec), (":creation_time_sec", &creation.when_sec),
(":creation_user_agent", &creation.user_agent), (":creation_user_agent", &creation.user_agent),
(":creation_peer_addr", &addr), (":creation_peer_addr", &addr),
(":permissions", &permissions_blob),
])?; ])?;
let e = match sessions.entry(hash) { let e = match sessions.entry(hash) {
::std::collections::hash_map::Entry::Occupied(_) => panic!("duplicate session hash!"), ::std::collections::hash_map::Entry::Occupied(_) => panic!("duplicate session hash!"),
@ -547,6 +571,7 @@ impl State {
creation_password_id, creation_password_id,
creation, creation,
seed: Seed(seed), seed: Seed(seed),
permissions,
..Default::default() ..Default::default()
}); });
Ok((session_id, session)) Ok((session_id, session))
@ -692,7 +717,8 @@ fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Erro
last_use_time_sec, last_use_time_sec,
last_use_user_agent, last_use_user_agent,
last_use_peer_addr, last_use_peer_addr,
use_count use_count,
permissions
from from
user_session user_session
where where
@ -703,6 +729,8 @@ fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Erro
let creation_addr: FromSqlIpAddr = row.get(8)?; let creation_addr: FromSqlIpAddr = row.get(8)?;
let revocation_addr: FromSqlIpAddr = row.get(11)?; let revocation_addr: FromSqlIpAddr = row.get(11)?;
let last_use_addr: FromSqlIpAddr = row.get(16)?; let last_use_addr: FromSqlIpAddr = row.get(16)?;
let mut permissions = Permissions::new();
permissions.merge_from_bytes(row.get_raw_checked(18)?.as_blob()?)?;
Ok(Session { Ok(Session {
user_id: row.get(0)?, user_id: row.get(0)?,
seed: row.get(1)?, seed: row.get(1)?,
@ -729,6 +757,7 @@ fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Erro
}, },
use_count: row.get(17)?, use_count: row.get(17)?,
dirty: false, dirty: false,
permissions,
}) })
} }
@ -961,4 +990,35 @@ mod tests {
let e = state.authenticate_session(&conn, req.clone(), &sid.hash()).unwrap_err(); let e = state.authenticate_session(&conn, req.clone(), &sid.hash()).unwrap_err();
assert_eq!(format!("{}", e), "no such session"); assert_eq!(format!("{}", e), "no such session");
} }
#[test]
fn permissions() {
testutil::init();
let mut conn = Connection::open_in_memory().unwrap();
db::init(&mut conn).unwrap();
let mut state = State::init(&conn).unwrap();
let mut change = UserChange::add_user("slamb".to_owned());
change.permissions.view_video = true;
let u = state.apply(&conn, change).unwrap();
assert!(u.permissions.view_video);
assert!(!u.permissions.update_signals);
let mut change = u.change();
assert!(change.permissions.view_video);
assert!(!change.permissions.update_signals);
change.permissions.update_signals = true;
let u = state.apply(&conn, change).unwrap();
assert!(u.permissions.view_video);
assert!(u.permissions.update_signals);
let uid = u.id;
{
let tx = conn.transaction().unwrap();
state.flush(&tx).unwrap();
tx.commit().unwrap();
}
let state = State::init(&conn).unwrap();
let u = state.users_by_id().get(&uid).unwrap();
assert!(u.permissions.view_video);
assert!(u.permissions.update_signals);
}
} }

View File

@ -37,6 +37,7 @@ use crate::recording;
use failure::Error; use failure::Error;
use fnv::FnvHashMap; use fnv::FnvHashMap;
use log::error; use log::error;
use protobuf::prelude::MessageField;
use rusqlite::types::ToSql; use rusqlite::types::ToSql;
use crate::schema; use crate::schema;
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
@ -69,7 +70,7 @@ pub fn run(conn: &rusqlite::Connection, opts: &Options) -> Result<(), Error> {
meta.db_uuid.extend_from_slice(&db_uuid.as_bytes()[..]); meta.db_uuid.extend_from_slice(&db_uuid.as_bytes()[..]);
meta.dir_uuid.extend_from_slice(&dir_uuid.0.as_bytes()[..]); meta.dir_uuid.extend_from_slice(&dir_uuid.0.as_bytes()[..]);
{ {
let o = meta.mut_last_complete_open(); let o = meta.last_complete_open.mut_message();
o.id = open_id; o.id = open_id;
o.uuid.extend_from_slice(&open_uuid.0.as_bytes()[..]); o.uuid.extend_from_slice(&open_uuid.0.as_bytes()[..]);
} }

View File

@ -66,6 +66,7 @@ use log::{error, info, trace};
use lru_cache::LruCache; use lru_cache::LruCache;
use openssl::hash; use openssl::hash;
use parking_lot::{Mutex,MutexGuard}; use parking_lot::{Mutex,MutexGuard};
use protobuf::prelude::MessageField;
use rusqlite::types::ToSql; use rusqlite::types::ToSql;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::collections::{BTreeMap, VecDeque}; use std::collections::{BTreeMap, VecDeque};
@ -326,7 +327,7 @@ impl SampleFileDir {
meta.db_uuid.extend_from_slice(&db_uuid.as_bytes()[..]); meta.db_uuid.extend_from_slice(&db_uuid.as_bytes()[..]);
meta.dir_uuid.extend_from_slice(&self.uuid.as_bytes()[..]); meta.dir_uuid.extend_from_slice(&self.uuid.as_bytes()[..]);
if let Some(o) = self.last_complete_open { if let Some(o) = self.last_complete_open {
let open = meta.mut_last_complete_open(); let open = meta.last_complete_open.mut_message();
open.id = o.id; open.id = o.id;
open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]); open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]);
} }
@ -1061,7 +1062,7 @@ impl LockedDatabase {
if dir.dir.is_some() { continue } if dir.dir.is_some() { continue }
let mut meta = dir.meta(&self.uuid); let mut meta = dir.meta(&self.uuid);
if let Some(o) = self.open.as_ref() { if let Some(o) = self.open.as_ref() {
let open = meta.mut_in_progress_open(); let open = meta.in_progress_open.mut_message();
open.id = o.id; open.id = o.id;
open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]); open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]);
} }
@ -1540,7 +1541,7 @@ impl LockedDatabase {
{ {
meta.db_uuid.extend_from_slice(&self.uuid.as_bytes()[..]); meta.db_uuid.extend_from_slice(&self.uuid.as_bytes()[..]);
meta.dir_uuid.extend_from_slice(uuid_bytes); meta.dir_uuid.extend_from_slice(uuid_bytes);
let open = meta.mut_in_progress_open(); let open = meta.in_progress_open.mut_message();
open.id = o.id; open.id = o.id;
open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]); open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]);
} }

View File

@ -47,4 +47,5 @@ pub mod writer;
pub mod testutil; pub mod testutil;
pub use crate::db::*; pub use crate::db::*;
pub use crate::schema::Permissions;
pub use crate::signal::Signal; pub use crate::signal::Signal;

View File

@ -60,3 +60,22 @@ message DirMeta {
// guaranteed that no data has yet been written by this open. // guaranteed that no data has yet been written by this open.
Open in_progress_open = 4; Open in_progress_open = 4;
} }
// Permissions to perform actions, currently all simple bools.
//
// These indicate actions which may be unnecessary in some contexts. Some
// basic access - like listing the cameras - is currently always allowed.
// See design/api.md for a description of what requires these permissions.
//
// These are used in a few contexts:
// * a session - affects what can be done when using that session to
// authenticate.
// * a user - when a new session is created, it inherits these permissions.
// * on the commandline - to specify what permissions are available for
// unauthenticated access.
message Permissions {
bool view_video = 1;
bool read_camera_configs = 2;
bool update_signals = 3;
}

View File

@ -1,642 +0,0 @@
// This file is generated by rust-protobuf 2.0.4. Do not edit
// @generated
// https://github.com/Manishearth/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy)]
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(box_pointers)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unsafe_code)]
#![allow(unused_imports)]
#![allow(unused_results)]
use protobuf::Message as Message_imported_for_functions;
use protobuf::ProtobufEnum as ProtobufEnum_imported_for_functions;
#[derive(PartialEq,Clone,Default)]
pub struct DirMeta {
// message fields
pub db_uuid: ::std::vec::Vec<u8>,
pub dir_uuid: ::std::vec::Vec<u8>,
pub last_complete_open: ::protobuf::SingularPtrField<DirMeta_Open>,
pub in_progress_open: ::protobuf::SingularPtrField<DirMeta_Open>,
// special fields
unknown_fields: ::protobuf::UnknownFields,
cached_size: ::protobuf::CachedSize,
}
impl DirMeta {
pub fn new() -> DirMeta {
::std::default::Default::default()
}
// bytes db_uuid = 1;
pub fn clear_db_uuid(&mut self) {
self.db_uuid.clear();
}
// Param is passed by value, moved
pub fn set_db_uuid(&mut self, v: ::std::vec::Vec<u8>) {
self.db_uuid = v;
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_db_uuid(&mut self) -> &mut ::std::vec::Vec<u8> {
&mut self.db_uuid
}
// Take field
pub fn take_db_uuid(&mut self) -> ::std::vec::Vec<u8> {
::std::mem::replace(&mut self.db_uuid, ::std::vec::Vec::new())
}
pub fn get_db_uuid(&self) -> &[u8] {
&self.db_uuid
}
// bytes dir_uuid = 2;
pub fn clear_dir_uuid(&mut self) {
self.dir_uuid.clear();
}
// Param is passed by value, moved
pub fn set_dir_uuid(&mut self, v: ::std::vec::Vec<u8>) {
self.dir_uuid = v;
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_dir_uuid(&mut self) -> &mut ::std::vec::Vec<u8> {
&mut self.dir_uuid
}
// Take field
pub fn take_dir_uuid(&mut self) -> ::std::vec::Vec<u8> {
::std::mem::replace(&mut self.dir_uuid, ::std::vec::Vec::new())
}
pub fn get_dir_uuid(&self) -> &[u8] {
&self.dir_uuid
}
// .DirMeta.Open last_complete_open = 3;
pub fn clear_last_complete_open(&mut self) {
self.last_complete_open.clear();
}
pub fn has_last_complete_open(&self) -> bool {
self.last_complete_open.is_some()
}
// Param is passed by value, moved
pub fn set_last_complete_open(&mut self, v: DirMeta_Open) {
self.last_complete_open = ::protobuf::SingularPtrField::some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_last_complete_open(&mut self) -> &mut DirMeta_Open {
if self.last_complete_open.is_none() {
self.last_complete_open.set_default();
}
self.last_complete_open.as_mut().unwrap()
}
// Take field
pub fn take_last_complete_open(&mut self) -> DirMeta_Open {
self.last_complete_open.take().unwrap_or_else(|| DirMeta_Open::new())
}
pub fn get_last_complete_open(&self) -> &DirMeta_Open {
self.last_complete_open.as_ref().unwrap_or_else(|| DirMeta_Open::default_instance())
}
// .DirMeta.Open in_progress_open = 4;
pub fn clear_in_progress_open(&mut self) {
self.in_progress_open.clear();
}
pub fn has_in_progress_open(&self) -> bool {
self.in_progress_open.is_some()
}
// Param is passed by value, moved
pub fn set_in_progress_open(&mut self, v: DirMeta_Open) {
self.in_progress_open = ::protobuf::SingularPtrField::some(v);
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_in_progress_open(&mut self) -> &mut DirMeta_Open {
if self.in_progress_open.is_none() {
self.in_progress_open.set_default();
}
self.in_progress_open.as_mut().unwrap()
}
// Take field
pub fn take_in_progress_open(&mut self) -> DirMeta_Open {
self.in_progress_open.take().unwrap_or_else(|| DirMeta_Open::new())
}
pub fn get_in_progress_open(&self) -> &DirMeta_Open {
self.in_progress_open.as_ref().unwrap_or_else(|| DirMeta_Open::default_instance())
}
}
impl ::protobuf::Message for DirMeta {
fn is_initialized(&self) -> bool {
for v in &self.last_complete_open {
if !v.is_initialized() {
return false;
}
};
for v in &self.in_progress_open {
if !v.is_initialized() {
return false;
}
};
true
}
fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream) -> ::protobuf::ProtobufResult<()> {
while !is.eof()? {
let (field_number, wire_type) = is.read_tag_unpack()?;
match field_number {
1 => {
::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.db_uuid)?;
},
2 => {
::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.dir_uuid)?;
},
3 => {
::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.last_complete_open)?;
},
4 => {
::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.in_progress_open)?;
},
_ => {
::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
},
};
}
::std::result::Result::Ok(())
}
// Compute sizes of nested messages
#[allow(unused_variables)]
fn compute_size(&self) -> u32 {
let mut my_size = 0;
if !self.db_uuid.is_empty() {
my_size += ::protobuf::rt::bytes_size(1, &self.db_uuid);
}
if !self.dir_uuid.is_empty() {
my_size += ::protobuf::rt::bytes_size(2, &self.dir_uuid);
}
if let Some(ref v) = self.last_complete_open.as_ref() {
let len = v.compute_size();
my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
}
if let Some(ref v) = self.in_progress_open.as_ref() {
let len = v.compute_size();
my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
}
my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
self.cached_size.set(my_size);
my_size
}
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream) -> ::protobuf::ProtobufResult<()> {
if !self.db_uuid.is_empty() {
os.write_bytes(1, &self.db_uuid)?;
}
if !self.dir_uuid.is_empty() {
os.write_bytes(2, &self.dir_uuid)?;
}
if let Some(ref v) = self.last_complete_open.as_ref() {
os.write_tag(3, ::protobuf::wire_format::WireTypeLengthDelimited)?;
os.write_raw_varint32(v.get_cached_size())?;
v.write_to_with_cached_sizes(os)?;
}
if let Some(ref v) = self.in_progress_open.as_ref() {
os.write_tag(4, ::protobuf::wire_format::WireTypeLengthDelimited)?;
os.write_raw_varint32(v.get_cached_size())?;
v.write_to_with_cached_sizes(os)?;
}
os.write_unknown_fields(self.get_unknown_fields())?;
::std::result::Result::Ok(())
}
fn get_cached_size(&self) -> u32 {
self.cached_size.get()
}
fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
&self.unknown_fields
}
fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
&mut self.unknown_fields
}
fn as_any(&self) -> &dyn (::std::any::Any) {
self as &dyn (::std::any::Any)
}
fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
self as &mut dyn (::std::any::Any)
}
fn into_any(self: Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
self
}
fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
Self::descriptor_static()
}
fn new() -> DirMeta {
DirMeta::new()
}
fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const ::protobuf::reflect::MessageDescriptor,
};
unsafe {
descriptor.get(|| {
let mut fields = ::std::vec::Vec::new();
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>(
"db_uuid",
|m: &DirMeta| { &m.db_uuid },
|m: &mut DirMeta| { &mut m.db_uuid },
));
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>(
"dir_uuid",
|m: &DirMeta| { &m.dir_uuid },
|m: &mut DirMeta| { &mut m.dir_uuid },
));
fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<DirMeta_Open>>(
"last_complete_open",
|m: &DirMeta| { &m.last_complete_open },
|m: &mut DirMeta| { &mut m.last_complete_open },
));
fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<DirMeta_Open>>(
"in_progress_open",
|m: &DirMeta| { &m.in_progress_open },
|m: &mut DirMeta| { &mut m.in_progress_open },
));
::protobuf::reflect::MessageDescriptor::new::<DirMeta>(
"DirMeta",
fields,
file_descriptor_proto()
)
})
}
}
fn default_instance() -> &'static DirMeta {
static mut instance: ::protobuf::lazy::Lazy<DirMeta> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const DirMeta,
};
unsafe {
instance.get(DirMeta::new)
}
}
}
impl ::protobuf::Clear for DirMeta {
fn clear(&mut self) {
self.clear_db_uuid();
self.clear_dir_uuid();
self.clear_last_complete_open();
self.clear_in_progress_open();
self.unknown_fields.clear();
}
}
impl ::std::fmt::Debug for DirMeta {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::protobuf::text_format::fmt(self, f)
}
}
impl ::protobuf::reflect::ProtobufValue for DirMeta {
fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
::protobuf::reflect::ProtobufValueRef::Message(self)
}
}
#[derive(PartialEq,Clone,Default)]
pub struct DirMeta_Open {
// message fields
pub id: u32,
pub uuid: ::std::vec::Vec<u8>,
// special fields
unknown_fields: ::protobuf::UnknownFields,
cached_size: ::protobuf::CachedSize,
}
impl DirMeta_Open {
pub fn new() -> DirMeta_Open {
::std::default::Default::default()
}
// uint32 id = 1;
pub fn clear_id(&mut self) {
self.id = 0;
}
// Param is passed by value, moved
pub fn set_id(&mut self, v: u32) {
self.id = v;
}
pub fn get_id(&self) -> u32 {
self.id
}
// bytes uuid = 2;
pub fn clear_uuid(&mut self) {
self.uuid.clear();
}
// Param is passed by value, moved
pub fn set_uuid(&mut self, v: ::std::vec::Vec<u8>) {
self.uuid = v;
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_uuid(&mut self) -> &mut ::std::vec::Vec<u8> {
&mut self.uuid
}
// Take field
pub fn take_uuid(&mut self) -> ::std::vec::Vec<u8> {
::std::mem::replace(&mut self.uuid, ::std::vec::Vec::new())
}
pub fn get_uuid(&self) -> &[u8] {
&self.uuid
}
}
impl ::protobuf::Message for DirMeta_Open {
fn is_initialized(&self) -> bool {
true
}
fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream) -> ::protobuf::ProtobufResult<()> {
while !is.eof()? {
let (field_number, wire_type) = is.read_tag_unpack()?;
match field_number {
1 => {
if wire_type != ::protobuf::wire_format::WireTypeVarint {
return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
}
let tmp = is.read_uint32()?;
self.id = tmp;
},
2 => {
::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.uuid)?;
},
_ => {
::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
},
};
}
::std::result::Result::Ok(())
}
// Compute sizes of nested messages
#[allow(unused_variables)]
fn compute_size(&self) -> u32 {
let mut my_size = 0;
if self.id != 0 {
my_size += ::protobuf::rt::value_size(1, self.id, ::protobuf::wire_format::WireTypeVarint);
}
if !self.uuid.is_empty() {
my_size += ::protobuf::rt::bytes_size(2, &self.uuid);
}
my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
self.cached_size.set(my_size);
my_size
}
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream) -> ::protobuf::ProtobufResult<()> {
if self.id != 0 {
os.write_uint32(1, self.id)?;
}
if !self.uuid.is_empty() {
os.write_bytes(2, &self.uuid)?;
}
os.write_unknown_fields(self.get_unknown_fields())?;
::std::result::Result::Ok(())
}
fn get_cached_size(&self) -> u32 {
self.cached_size.get()
}
fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
&self.unknown_fields
}
fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
&mut self.unknown_fields
}
fn as_any(&self) -> &dyn (::std::any::Any) {
self as &dyn (::std::any::Any)
}
fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
self as &mut dyn (::std::any::Any)
}
fn into_any(self: Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
self
}
fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
Self::descriptor_static()
}
fn new() -> DirMeta_Open {
DirMeta_Open::new()
}
fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const ::protobuf::reflect::MessageDescriptor,
};
unsafe {
descriptor.get(|| {
let mut fields = ::std::vec::Vec::new();
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeUint32>(
"id",
|m: &DirMeta_Open| { &m.id },
|m: &mut DirMeta_Open| { &mut m.id },
));
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>(
"uuid",
|m: &DirMeta_Open| { &m.uuid },
|m: &mut DirMeta_Open| { &mut m.uuid },
));
::protobuf::reflect::MessageDescriptor::new::<DirMeta_Open>(
"DirMeta_Open",
fields,
file_descriptor_proto()
)
})
}
}
fn default_instance() -> &'static DirMeta_Open {
static mut instance: ::protobuf::lazy::Lazy<DirMeta_Open> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const DirMeta_Open,
};
unsafe {
instance.get(DirMeta_Open::new)
}
}
}
impl ::protobuf::Clear for DirMeta_Open {
fn clear(&mut self) {
self.clear_id();
self.clear_uuid();
self.unknown_fields.clear();
}
}
impl ::std::fmt::Debug for DirMeta_Open {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::protobuf::text_format::fmt(self, f)
}
}
impl ::protobuf::reflect::ProtobufValue for DirMeta_Open {
fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
::protobuf::reflect::ProtobufValueRef::Message(self)
}
}
static file_descriptor_proto_data: &'static [u8] = b"\
\n\x0cschema.proto\"\xdf\x01\n\x07DirMeta\x12\x17\n\x07db_uuid\x18\x01\
\x20\x01(\x0cR\x06dbUuid\x12\x19\n\x08dir_uuid\x18\x02\x20\x01(\x0cR\x07\
dirUuid\x12;\n\x12last_complete_open\x18\x03\x20\x01(\x0b2\r.DirMeta.Ope\
nR\x10lastCompleteOpen\x127\n\x10in_progress_open\x18\x04\x20\x01(\x0b2\
\r.DirMeta.OpenR\x0einProgressOpen\x1a*\n\x04Open\x12\x0e\n\x02id\x18\
\x01\x20\x01(\rR\x02id\x12\x12\n\x04uuid\x18\x02\x20\x01(\x0cR\x04uuidJ\
\xc1\x17\n\x06\x12\x04\x1e\0=\x01\n\xc2\x0b\n\x01\x0c\x12\x03\x1e\0\x122\
\xb7\x0b\x20This\x20file\x20is\x20part\x20of\x20Moonfire\x20NVR,\x20a\
\x20security\x20camera\x20digital\x20video\x20recorder.\n\x20Copyright\
\x20(C)\x202018\x20Scott\x20Lamb\x20<slamb@slamb.org>\n\n\x20This\x20pro\
gram\x20is\x20free\x20software:\x20you\x20can\x20redistribute\x20it\x20a\
nd/or\x20modify\n\x20it\x20under\x20the\x20terms\x20of\x20the\x20GNU\x20\
General\x20Public\x20License\x20as\x20published\x20by\n\x20the\x20Free\
\x20Software\x20Foundation,\x20either\x20version\x203\x20of\x20the\x20Li\
cense,\x20or\n\x20(at\x20your\x20option)\x20any\x20later\x20version.\n\n\
\x20In\x20addition,\x20as\x20a\x20special\x20exception,\x20the\x20copyri\
ght\x20holders\x20give\n\x20permission\x20to\x20link\x20the\x20code\x20o\
f\x20portions\x20of\x20this\x20program\x20with\x20the\n\x20OpenSSL\x20li\
brary\x20under\x20certain\x20conditions\x20as\x20described\x20in\x20each\
\n\x20individual\x20source\x20file,\x20and\x20distribute\x20linked\x20co\
mbinations\x20including\n\x20the\x20two.\n\n\x20You\x20must\x20obey\x20t\
he\x20GNU\x20General\x20Public\x20License\x20in\x20all\x20respects\x20fo\
r\x20all\n\x20of\x20the\x20code\x20used\x20other\x20than\x20OpenSSL.\x20\
If\x20you\x20modify\x20file(s)\x20with\x20this\n\x20exception,\x20you\
\x20may\x20extend\x20this\x20exception\x20to\x20your\x20version\x20of\
\x20the\n\x20file(s),\x20but\x20you\x20are\x20not\x20obligated\x20to\x20\
do\x20so.\x20If\x20you\x20do\x20not\x20wish\x20to\x20do\n\x20so,\x20dele\
te\x20this\x20exception\x20statement\x20from\x20your\x20version.\x20If\
\x20you\x20delete\n\x20this\x20exception\x20statement\x20from\x20all\x20\
source\x20files\x20in\x20the\x20program,\x20then\n\x20also\x20delete\x20\
it\x20here.\n\n\x20This\x20program\x20is\x20distributed\x20in\x20the\x20\
hope\x20that\x20it\x20will\x20be\x20useful,\n\x20but\x20WITHOUT\x20ANY\
\x20WARRANTY;\x20without\x20even\x20the\x20implied\x20warranty\x20of\n\
\x20MERCHANTABILITY\x20or\x20FITNESS\x20FOR\x20A\x20PARTICULAR\x20PURPOS\
E.\x20\x20See\x20the\n\x20GNU\x20General\x20Public\x20License\x20for\x20\
more\x20details.\n\n\x20You\x20should\x20have\x20received\x20a\x20copy\
\x20of\x20the\x20GNU\x20General\x20Public\x20License\n\x20along\x20with\
\x20this\x20program.\x20\x20If\x20not,\x20see\x20<http://www.gnu.org/lic\
enses/>.\n\n\xf1\x01\n\x02\x04\0\x12\x04$\0=\x01\x1a\xe4\x01\x20Metadata\
\x20stored\x20in\x20sample\x20file\x20dirs\x20as\x20\"<dir>/meta\".\x20T\
his\x20is\x20checked\n\x20against\x20the\x20metadata\x20stored\x20within\
\x20the\x20database\x20to\x20detect\x20inconsistencies\n\x20between\x20t\
he\x20directory\x20and\x20database,\x20such\x20as\x20those\x20described\
\x20in\n\x20design/schema.md.\n\n\n\n\x03\x04\0\x01\x12\x03$\x08\x0f\n\
\xcf\x01\n\x04\x04\0\x02\0\x12\x03(\x02\x14\x1a\xc1\x01\x20A\x20uuid\x20\
associated\x20with\x20the\x20database,\x20in\x20binary\x20form.\x20dir_u\
uid\x20is\x20strictly\n\x20more\x20powerful,\x20but\x20it\x20improves\
\x20diagnostics\x20to\x20know\x20if\x20the\x20directory\n\x20belongs\x20\
to\x20the\x20expected\x20database\x20at\x20all\x20or\x20not.\n\n\r\n\x05\
\x04\0\x02\0\x04\x12\x04(\x02$\x11\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03(\
\x02\x07\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03(\x08\x0f\n\x0c\n\x05\x04\0\
\x02\0\x03\x12\x03(\x12\x13\n;\n\x04\x04\0\x02\x01\x12\x03+\x02\x15\x1a.\
\x20A\x20uuid\x20associated\x20with\x20the\x20directory\x20itself.\n\n\r\
\n\x05\x04\0\x02\x01\x04\x12\x04+\x02(\x14\n\x0c\n\x05\x04\0\x02\x01\x05\
\x12\x03+\x02\x07\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03+\x08\x10\n\x0c\n\
\x05\x04\0\x02\x01\x03\x12\x03+\x13\x14\nE\n\x04\x04\0\x03\0\x12\x04.\
\x021\x03\x1a7\x20Corresponds\x20to\x20an\x20entry\x20in\x20the\x20`open\
`\x20database\x20table.\n\n\x0c\n\x05\x04\0\x03\0\x01\x12\x03.\n\x0e\n\r\
\n\x06\x04\0\x03\0\x02\0\x12\x03/\x04\x12\n\x0f\n\x07\x04\0\x03\0\x02\0\
\x04\x12\x04/\x04.\x10\n\x0e\n\x07\x04\0\x03\0\x02\0\x05\x12\x03/\x04\n\
\n\x0e\n\x07\x04\0\x03\0\x02\0\x01\x12\x03/\x0b\r\n\x0e\n\x07\x04\0\x03\
\0\x02\0\x03\x12\x03/\x10\x11\n\r\n\x06\x04\0\x03\0\x02\x01\x12\x030\x04\
\x13\n\x0f\n\x07\x04\0\x03\0\x02\x01\x04\x12\x040\x04/\x12\n\x0e\n\x07\
\x04\0\x03\0\x02\x01\x05\x12\x030\x04\t\n\x0e\n\x07\x04\0\x03\0\x02\x01\
\x01\x12\x030\n\x0e\n\x0e\n\x07\x04\0\x03\0\x02\x01\x03\x12\x030\x11\x12\
\n\xb0\x02\n\x04\x04\0\x02\x02\x12\x037\x02\x1e\x1a\xa2\x02\x20The\x20la\
st\x20open\x20that\x20was\x20known\x20to\x20be\x20recorded\x20in\x20the\
\x20database\x20as\x20completed.\n\x20Absent\x20if\x20this\x20has\x20nev\
er\x20happened.\x20Note\x20this\x20can\x20backtrack\x20in\x20exactly\x20\
one\n\x20scenario:\x20when\x20deleting\x20the\x20directory,\x20after\x20\
all\x20associated\x20files\x20have\n\x20been\x20deleted,\x20last_complet\
e_open\x20can\x20be\x20moved\x20to\x20in_progress_open.\n\n\r\n\x05\x04\
\0\x02\x02\x04\x12\x047\x021\x03\n\x0c\n\x05\x04\0\x02\x02\x06\x12\x037\
\x02\x06\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x037\x07\x19\n\x0c\n\x05\x04\
\0\x02\x02\x03\x12\x037\x1c\x1d\n\xd6\x01\n\x04\x04\0\x02\x03\x12\x03<\
\x02\x1c\x1a\xc8\x01\x20The\x20last\x20run\x20which\x20is\x20in\x20progr\
ess,\x20if\x20different\x20from\x20last_complete_open.\n\x20This\x20may\
\x20or\x20may\x20not\x20have\x20been\x20recorded\x20in\x20the\x20databas\
e,\x20but\x20it's\n\x20guaranteed\x20that\x20no\x20data\x20has\x20yet\
\x20been\x20written\x20by\x20this\x20open.\n\n\r\n\x05\x04\0\x02\x03\x04\
\x12\x04<\x027\x1e\n\x0c\n\x05\x04\0\x02\x03\x06\x12\x03<\x02\x06\n\x0c\
\n\x05\x04\0\x02\x03\x01\x12\x03<\x07\x17\n\x0c\n\x05\x04\0\x02\x03\x03\
\x12\x03<\x1a\x1bb\x06proto3\
";
static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const ::protobuf::descriptor::FileDescriptorProto,
};
fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
::protobuf::parse_from_bytes(file_descriptor_proto_data).unwrap()
}
pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
unsafe {
file_descriptor_proto_lazy.get(|| {
parse_descriptor_proto()
})
}
}

View File

@ -328,7 +328,11 @@ create table user (
-- a Unix domain socket. (Additionally, the UID running Moonfire NVR can authenticate -- 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 -- 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. -- bootstrap method once configuration happens through a web UI rather than text UI.
unix_uid integer unix_uid integer,
-- Permissions available for newly created tokens or when authenticating via
-- unix_uid above. A serialized "Permissions" protobuf.
permissions blob
); );
-- A single session, whether for browser or robot use. -- A single session, whether for browser or robot use.
@ -391,7 +395,10 @@ create table user_session (
last_use_time_sec integer, -- sec since epoch last_use_time_sec integer, -- sec since epoch
last_use_user_agent text, -- User-Agent header from inbound HTTP request. last_use_user_agent text, -- User-Agent header from inbound HTTP request.
last_use_peer_addr blob, -- IPv4 or IPv6 address, or null for Unix socket. last_use_peer_addr blob, -- IPv4 or IPv6 address, or null for Unix socket.
use_count not null default 0 use_count not null default 0,
-- Permissions associated with this token; a serialized "Permissions" protobuf.
permissions blob
) without rowid; ) without rowid;
create index user_session_uid on user_session (user_id); create index user_session_uid on user_session (user_id);

View File

@ -33,6 +33,7 @@
use crate::dir; use crate::dir;
use failure::{Error, bail, format_err}; use failure::{Error, bail, format_err};
use libc; use libc;
use protobuf::prelude::MessageField;
use rusqlite::types::ToSql; use rusqlite::types::ToSql;
use crate::schema::DirMeta; use crate::schema::DirMeta;
use std::fs; use std::fs;
@ -113,7 +114,7 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
{ {
meta.db_uuid.extend_from_slice(db_uuid_bytes); meta.db_uuid.extend_from_slice(db_uuid_bytes);
meta.dir_uuid.extend_from_slice(dir_uuid_bytes); meta.dir_uuid.extend_from_slice(dir_uuid_bytes);
let open = meta.mut_last_complete_open(); let open = meta.last_complete_open.mut_message();
open.id = open_id; open.id = open_id;
open.uuid.extend_from_slice(&open_uuid_bytes); open.uuid.extend_from_slice(&open_uuid_bytes);
} }

View File

@ -37,10 +37,11 @@ use crate::dir;
use failure::Error; use failure::Error;
use libc; use libc;
use crate::schema; use crate::schema;
use protobuf::prelude::MessageField;
use rusqlite::types::ToSql;
use std::io::{self, Write}; use std::io::{self, Write};
use std::mem; use std::mem;
use std::sync::Arc; use std::sync::Arc;
use rusqlite::types::ToSql;
use uuid::Uuid; use uuid::Uuid;
/// Opens the sample file dir. /// Opens the sample file dir.
@ -68,7 +69,7 @@ fn open_sample_file_dir(tx: &rusqlite::Transaction) -> Result<Arc<dir::SampleFil
meta.db_uuid.extend_from_slice(&db_uuid.0.as_bytes()[..]); meta.db_uuid.extend_from_slice(&db_uuid.0.as_bytes()[..]);
meta.dir_uuid.extend_from_slice(&s_uuid.0.as_bytes()[..]); meta.dir_uuid.extend_from_slice(&s_uuid.0.as_bytes()[..]);
{ {
let open = meta.mut_last_complete_open(); let open = meta.last_complete_open.mut_message();
open.id = o_id as u32; open.id = o_id as u32;
open.uuid.extend_from_slice(&o_uuid.0.as_bytes()[..]); open.uuid.extend_from_slice(&o_uuid.0.as_bytes()[..]);
} }

View File

@ -62,6 +62,9 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
time_90k integer primary key, time_90k integer primary key,
changes blob changes blob
); );
alter table user add column permissions blob;
alter table user_session add column permissions blob;
"#)?; "#)?;
Ok(()) Ok(())
} }

View File

@ -53,11 +53,15 @@ request parameters:
* `days`: a boolean indicating if the days parameter described below * `days`: a boolean indicating if the days parameter described below
should be included. should be included.
* `cameraConfigs`: a boolean indicating if the `camera.config` parameter
described below should be included. This requires the
`read_camera_configs` permission as described in `schema.proto`.
Example request URI: Example request URI (with added whitespace between parameters):
``` ```
/api/?days=true /api/?days=true
&cameraConfigs=true
``` ```
The `application/json` response will have a dict as follows: The `application/json` response will have a dict as follows:
@ -68,6 +72,11 @@ The `application/json` response will have a dict as follows:
* `uuid`: in text format * `uuid`: in text format
* `shortName`: a short name (typically one or two words) * `shortName`: a short name (typically one or two words)
* `description`: a longer description (typically a phrase or paragraph) * `description`: a longer description (typically a phrase or paragraph)
* `config`: (only included if request parameter `cameraConfigs` is true)
a dictionary describing the configuration of the camera:
* `username`
* `password`
* `host`
* `streams`: a dict of stream type ("main" or "sub") to a dictionary * `streams`: a dict of stream type ("main" or "sub") to a dictionary
describing the stream: describing the stream:
* `retainBytes`: the configured total number of bytes of completed * `retainBytes`: the configured total number of bytes of completed
@ -81,9 +90,10 @@ The `application/json` response will have a dict as follows:
be lesser if there are gaps in the recorded data. be lesser if there are gaps in the recorded data.
* `totalSampleFileBytes`: the total number of bytes of sample data * `totalSampleFileBytes`: the total number of bytes of sample data
(the `mdat` portion of a `.mp4` file). (the `mdat` portion of a `.mp4` file).
* `days`: object representing calendar days (in the server's time * `days`: (only included if request pararameter `days` is true)
zone) with non-zero total duration of recordings for that day. The dictionary representing calendar days (in the server's time zone)
keys are of the form `YYYY-mm-dd`; the values are objects with the with non-zero total duration of recordings for that day. The keys
are of the form `YYYY-mm-dd`; the values are objects with the
following attributes: following attributes:
* `totalDuration90k` is the total duration recorded during that * `totalDuration90k` is the total duration recorded during that
day. If a recording spans a day boundary, some portion of it day. If a recording spans a day boundary, some portion of it
@ -124,6 +134,11 @@ Example response:
"uuid": "fd20f7a2-9d69-4cb3-94ed-d51a20c3edfe", "uuid": "fd20f7a2-9d69-4cb3-94ed-d51a20c3edfe",
"shortName": "driveway", "shortName": "driveway",
"description": "Hikvision DS-2CD2032 overlooking the driveway from east", "description": "Hikvision DS-2CD2032 overlooking the driveway from east",
"config": {
"host": "192.168.1.100",
"user": "admin",
"password": "12345",
},
"streams": { "streams": {
"main": { "main": {
"retainBytes": 536870912000, "retainBytes": 536870912000,
@ -193,7 +208,9 @@ Example response:
### `GET /api/cameras/<uuid>/` ### `GET /api/cameras/<uuid>/`
Returns information for the camera with the given URL. Returns information for the camera with the given URL. As in the like section
of `GET /api/` with the `days` parameter set and the `cameraConfigs` parameter
unset.
Example response: Example response:
@ -311,6 +328,8 @@ Example response:
### `GET /api/cameras/<uuid>/<stream>/view.mp4` ### `GET /api/cameras/<uuid>/<stream>/view.mp4`
Requires the `view_video` permission.
Returns a `.mp4` file, with an etag and support for range requests. The MIME Returns a `.mp4` file, with an etag and support for range requests. The MIME
type will be `video/mp4`, with a `codecs` parameter as specified in type will be `video/mp4`, with a `codecs` parameter as specified in
[RFC 6381][rfc-6381]. [RFC 6381][rfc-6381].
@ -525,6 +544,8 @@ This represents the following observations:
### `POST /api/signals` ### `POST /api/signals`
Requires the `update_signals` permission.
Alters the state of a signal. Alters the state of a signal.
A typical client might be a subscriber of a camera's built-in motion A typical client might be a subscriber of a camera's built-in motion

View File

@ -84,7 +84,8 @@ Moonfire NVR can be run as a systemd service. Create
[Service] [Service]
ExecStart=/usr/local/bin/moonfire-nvr run \ ExecStart=/usr/local/bin/moonfire-nvr run \
--db-dir=/var/lib/moonfire-nvr/db \ --db-dir=/var/lib/moonfire-nvr/db \
--http-addr=0.0.0.0:8080 --http-addr=0.0.0.0:8080 \
--allow_unauthenticated_scopes='view_video: true'
Environment=TZ=:/etc/localtime Environment=TZ=:/etc/localtime
Environment=MOONFIRE_FORMAT=google-systemd Environment=MOONFIRE_FORMAT=google-systemd
Environment=MOONFIRE_LOG=info Environment=MOONFIRE_LOG=info

View File

@ -154,12 +154,14 @@ In your `/etc/systemd/system/moonfire-nvr.service` file, look for these lines:
``` ```
ExecStart=/usr/local/bin/moonfire-nvr run \ ExecStart=/usr/local/bin/moonfire-nvr run \
--db-dir=/var/lib/moonfire-nvr/db \ --db-dir=/var/lib/moonfire-nvr/db \
--http-addr=0.0.0.0:8080 --http-addr=0.0.0.0:8080 \
--allow-unauthenticated-permissions='view_video: true'
``` ```
Add `--require-auth --trust-forward-hdrs`. This change has two effects: Replace the last line with `--trust-forward-hdrs`. This change has two effects:
* `--require-auth` means that web users must authenticate. * No `--allow-unauthenticated-permissions` means that web users must
authenticate.
* `--trust-forward-hdrs` means that Moonfire NVR will look for `X-Real-IP` * `--trust-forward-hdrs` means that Moonfire NVR will look for `X-Real-IP`
and `X-Forwarded-Proto` headers as added by the webserver configuration and `X-Forwarded-Proto` headers as added by the webserver configuration
in the next section. in the next section.

View File

@ -106,7 +106,8 @@ After=network-online.target
ExecStart=${SERVICE_BIN} run \\ ExecStart=${SERVICE_BIN} run \\
--db-dir=${DB_DIR} \\ --db-dir=${DB_DIR} \\
--ui-dir=${LIB_DIR}/ui \\ --ui-dir=${LIB_DIR}/ui \\
--http-addr=0.0.0.0:${NVR_PORT} --http-addr=0.0.0.0:${NVR_PORT} \\
--allow-unauthenticated-permissions='view_video: true'
Environment=TZ=:/etc/localtime Environment=TZ=:/etc/localtime
Environment=MOONFIRE_FORMAT=google-systemd Environment=MOONFIRE_FORMAT=google-systemd
Environment=MOONFIRE_LOG=info Environment=MOONFIRE_LOG=info

View File

@ -31,6 +31,7 @@
use cursive::Cursive; use cursive::Cursive;
use cursive::traits::{Boxable, Identifiable}; use cursive::traits::{Boxable, Identifiable};
use cursive::views; use cursive::views;
use log::info;
use std::sync::Arc; use std::sync::Arc;
/// Builds a `UserChange` from an active `edit_user_dialog`. /// Builds a `UserChange` from an active `edit_user_dialog`.
@ -50,6 +51,13 @@ fn get_change(siv: &mut Cursive, db: &db::LockedDatabase, id: Option<i32>,
}, },
PasswordChange::Clear => change.clear_password(), PasswordChange::Clear => change.clear_password(),
}; };
for (id, ref mut b) in &mut [
("perm_view_video", &mut change.permissions.view_video),
("perm_read_camera_configs", &mut change.permissions.read_camera_configs),
("perm_update_signals", &mut change.permissions.update_signals)] {
**b = siv.find_id::<views::Checkbox>(id).unwrap().is_checked();
info!("{}: {}", id, **b);
}
change change
} }
@ -112,9 +120,7 @@ fn select_set(siv: &mut Cursive) {
/// Adds or updates a user. /// Adds or updates a user.
/// (The former if `item` is None; the latter otherwise.) /// (The former if `item` is None; the latter otherwise.)
fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>) { fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>) {
let username; let (username, id_str, has_password, permissions);
let id_str;
let has_password;
let mut pw_group = views::RadioGroup::new(); let mut pw_group = views::RadioGroup::new();
{ {
let l = db.lock(); let l = db.lock();
@ -122,6 +128,7 @@ fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>
username = u.map(|u| u.username.clone()).unwrap_or(String::new()); username = u.map(|u| u.username.clone()).unwrap_or(String::new());
id_str = item.map(|id| id.to_string()).unwrap_or("<new>".to_string()); id_str = item.map(|id| id.to_string()).unwrap_or("<new>".to_string());
has_password = u.map(|u| u.has_password()).unwrap_or(false); has_password = u.map(|u| u.has_password()).unwrap_or(false);
permissions = u.map(|u| u.permissions.clone()).unwrap_or(db::Permissions::default());
} }
let top_list = views::ListView::new() let top_list = views::ListView::new()
.child("id", views::TextView::new(id_str)) .child("id", views::TextView::new(id_str))
@ -156,6 +163,18 @@ fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>
.full_width())); .full_width()));
} }
layout.add_child(views::DummyView);
layout.add_child(views::TextView::new("permissions"));
let mut perms = views::ListView::new();
for (name, b) in &[("view_video", permissions.view_video),
("read_camera_configs", permissions.read_camera_configs),
("update_signals", permissions.update_signals)] {
let mut checkbox = views::Checkbox::new();
checkbox.set_checked(*b);
perms.add_child(name, checkbox.with_id(format!("perm_{}", name)));
}
layout.add_child(perms);
let dialog = views::Dialog::around(layout); let dialog = views::Dialog::around(layout);
let dialog = if let Some(id) = item { let dialog = if let Some(id) = item {
dialog.title("Edit user") dialog.title("Edit user")

View File

@ -33,7 +33,7 @@ use crate::stream;
use crate::streamer; use crate::streamer;
use crate::web; use crate::web;
use db::{dir, writer}; use db::{dir, writer};
use failure::{Error, bail}; use failure::{Error, bail, format_err};
use fnv::FnvHashMap; use fnv::FnvHashMap;
use futures::{Future, Stream}; use futures::{Future, Stream};
use log::{error, info, warn}; use log::{error, info, warn};
@ -68,7 +68,13 @@ Options:
--http-addr=ADDR Set the bind address for the unencrypted HTTP server. --http-addr=ADDR Set the bind address for the unencrypted HTTP server.
[default: 0.0.0.0:8080] [default: 0.0.0.0:8080]
--read-only Forces read-only mode / disables recording. --read-only Forces read-only mode / disables recording.
--require-auth Requires authentication to access the web interface. --allow-unauthenticated-permissions=PERMISSIONS
Allow unauthenticated access to the web interface,
with the given permissions (may be empty).
PERMISSIONS should be a text Permissions protobuf
such as "view_videos: true". NOTE: even an empty
string allows some basic access that would be
rejected if the argument were omitted.
--trust-forward-hdrs Trust X-Real-IP: and X-Forwarded-Proto: headers on --trust-forward-hdrs Trust X-Real-IP: and X-Forwarded-Proto: headers on
the incoming request. Set this only after ensuring the incoming request. Set this only after ensuring
your proxy server is configured to set them and that your proxy server is configured to set them and that
@ -82,7 +88,7 @@ struct Args {
flag_http_addr: String, flag_http_addr: String,
flag_ui_dir: String, flag_ui_dir: String,
flag_read_only: bool, flag_read_only: bool,
flag_require_auth: bool, flag_allow_unauthenticated_permissions: Option<String>,
flag_trust_forward_hdrs: bool, flag_trust_forward_hdrs: bool,
} }
@ -186,10 +192,14 @@ pub fn run() -> Result<(), Error> {
let time_zone_name = resolve_zone()?; let time_zone_name = resolve_zone()?;
info!("Resolved timezone: {}", &time_zone_name); info!("Resolved timezone: {}", &time_zone_name);
let allow_unauthenticated_permissions = args.flag_allow_unauthenticated_permissions
.map(|s| protobuf::text_format::parse_from_str(&s))
.transpose()
.map_err(|_| format_err!("Unable to parse --allow-unauthenticated-permissions"))?;
let s = web::Service::new(web::Config { let s = web::Service::new(web::Config {
db: db.clone(), db: db.clone(),
ui_dir: Some(&args.flag_ui_dir), ui_dir: Some(&args.flag_ui_dir),
require_auth: args.flag_require_auth, allow_unauthenticated_permissions,
trust_forward_hdrs: args.flag_trust_forward_hdrs, trust_forward_hdrs: args.flag_trust_forward_hdrs,
time_zone_name, time_zone_name,
})?; })?;

View File

@ -42,9 +42,9 @@ pub struct TopLevel<'a> {
pub time_zone_name: &'a str, pub time_zone_name: &'a str,
// Use a custom serializer which presents the map's values as a sequence and includes the // Use a custom serializer which presents the map's values as a sequence and includes the
// "days" attribute or not, according to the bool in the tuple. // "days" and "camera_configs" attributes or not, according to the respective bools.
#[serde(serialize_with = "TopLevel::serialize_cameras")] #[serde(serialize_with = "TopLevel::serialize_cameras")]
pub cameras: (&'a db::LockedDatabase, bool), pub cameras: (&'a db::LockedDatabase, bool, bool),
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub session: Option<Session>, pub session: Option<Session>,
@ -83,10 +83,21 @@ pub struct Camera<'a> {
pub short_name: &'a str, pub short_name: &'a str,
pub description: &'a str, pub description: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<CameraConfig<'a>>,
#[serde(serialize_with = "Camera::serialize_streams")] #[serde(serialize_with = "Camera::serialize_streams")]
pub streams: [Option<Stream<'a>>; 2], pub streams: [Option<Stream<'a>>; 2],
} }
#[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")]
pub struct CameraConfig<'a> {
pub host: &'a str,
pub username: &'a str,
pub password: &'a str,
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all="camelCase")] #[serde(rename_all="camelCase")]
pub struct Stream<'a> { pub struct Stream<'a> {
@ -163,11 +174,20 @@ pub struct SignalTypeState<'a> {
} }
impl<'a> Camera<'a> { impl<'a> Camera<'a> {
pub fn wrap(c: &'a db::Camera, db: &'a db::LockedDatabase, include_days: bool) -> Result<Self, Error> { pub fn wrap(c: &'a db::Camera, db: &'a db::LockedDatabase, include_days: bool,
include_config: bool) -> Result<Self, Error> {
Ok(Camera { Ok(Camera {
uuid: c.uuid, uuid: c.uuid,
short_name: &c.short_name, short_name: &c.short_name,
description: &c.description, description: &c.description,
config: match include_config {
false => None,
true => Some(CameraConfig {
host: &c.host,
username: &c.username,
password: &c.password,
}),
},
streams: [ streams: [
Stream::wrap(db, c.streams[0], include_days)?, Stream::wrap(db, c.streams[0], include_days)?,
Stream::wrap(db, c.streams[1], include_days)?, Stream::wrap(db, c.streams[1], include_days)?,
@ -295,16 +315,18 @@ struct StreamDayValue {
} }
impl<'a> TopLevel<'a> { impl<'a> TopLevel<'a> {
/// Serializes cameras as a list (rather than a map), optionally including the `days` field. /// Serializes cameras as a list (rather than a map), optionally including the `days` and
fn serialize_cameras<S>(cameras: &(&db::LockedDatabase, bool), /// `cameras` fields.
fn serialize_cameras<S>(cameras: &(&db::LockedDatabase, bool, bool),
serializer: S) -> Result<S::Ok, S::Error> serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer { where S: Serializer {
let (db, include_days) = *cameras; let (db, include_days, include_config) = *cameras;
let cs = db.cameras_by_id(); let cs = db.cameras_by_id();
let mut seq = serializer.serialize_seq(Some(cs.len()))?; let mut seq = serializer.serialize_seq(Some(cs.len()))?;
for (_, c) in cs { for (_, c) in cs {
seq.serialize_element( seq.serialize_element(
&Camera::wrap(c, db, include_days).map_err(|e| S::Error::custom(e))?)?; &Camera::wrap(c, db, include_days, include_config)
.map_err(|e| S::Error::custom(e))?)?;
} }
seq.end() seq.end()
} }

View File

@ -179,6 +179,7 @@ fn internal_server_err<E: Into<Error>>(err: E) -> Response<Body> {
fn from_base_error(err: base::Error) -> Response<Body> { fn from_base_error(err: base::Error) -> Response<Body> {
let status_code = match err.kind() { let status_code = match err.kind() {
ErrorKind::PermissionDenied | ErrorKind::Unauthenticated => StatusCode::UNAUTHORIZED,
ErrorKind::InvalidArgument => StatusCode::BAD_REQUEST, ErrorKind::InvalidArgument => StatusCode::BAD_REQUEST,
ErrorKind::NotFound => StatusCode::NOT_FOUND, ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
@ -241,13 +242,21 @@ struct UiFile {
path: PathBuf, path: PathBuf,
} }
struct Caller {
permissions: db::Permissions,
session: Option<json::Session>,
}
impl Caller {
}
struct ServiceInner { struct ServiceInner {
db: Arc<db::Database>, db: Arc<db::Database>,
dirs_by_stream_id: Arc<FnvHashMap<i32, Arc<SampleFileDir>>>, dirs_by_stream_id: Arc<FnvHashMap<i32, Arc<SampleFileDir>>>,
ui_files: HashMap<String, UiFile>, ui_files: HashMap<String, UiFile>,
pool: futures_cpupool::CpuPool, pool: futures_cpupool::CpuPool,
time_zone_name: String, time_zone_name: String,
require_auth: bool, allow_unauthenticated_permissions: Option<db::Permissions>,
trust_forward_hdrs: bool, trust_forward_hdrs: bool,
} }
@ -264,24 +273,32 @@ fn serve_json<T: serde::ser::Serialize>(req: &Request<hyper::Body>, out: &T) ->
} }
impl ServiceInner { impl ServiceInner {
fn top_level(&self, req: &Request<::hyper::Body>, session: Option<json::Session>) fn top_level(&self, req: &Request<::hyper::Body>, caller: Caller) -> ResponseResult {
-> ResponseResult {
let mut days = false; let mut days = false;
let mut camera_configs = false;
if let Some(q) = req.uri().query() { if let Some(q) = req.uri().query() {
for (key, value) in form_urlencoded::parse(q.as_bytes()) { for (key, value) in form_urlencoded::parse(q.as_bytes()) {
let (key, value): (_, &str) = (key.borrow(), value.borrow()); let (key, value): (_, &str) = (key.borrow(), value.borrow());
match key { match key {
"days" => days = value == "true", "days" => days = value == "true",
"cameraConfigs" => camera_configs = value == "true",
_ => {}, _ => {},
}; };
} }
} }
if camera_configs {
if !caller.permissions.read_camera_configs {
return Err(plain_response(StatusCode::UNAUTHORIZED,
"read_camera_configs required"));
}
}
let db = self.db.lock(); let db = self.db.lock();
serve_json(req, &json::TopLevel { serve_json(req, &json::TopLevel {
time_zone_name: &self.time_zone_name, time_zone_name: &self.time_zone_name,
cameras: (&db, days), cameras: (&db, days, camera_configs),
session, session: caller.session,
signals: (&db, days), signals: (&db, days),
signal_types: &db, signal_types: &db,
}) })
@ -291,7 +308,7 @@ impl ServiceInner {
let db = self.db.lock(); let db = self.db.lock();
let camera = db.get_camera(uuid) let camera = db.get_camera(uuid)
.ok_or_else(|| not_found(format!("no such camera {}", uuid)))?; .ok_or_else(|| not_found(format!("no such camera {}", uuid)))?;
serve_json(req, &json::Camera::wrap(camera, &db, true).map_err(internal_server_err)?) serve_json(req, &json::Camera::wrap(camera, &db, true, false).map_err(internal_server_err)?)
} }
fn stream_recordings(&self, req: &Request<::hyper::Body>, uuid: Uuid, type_: db::StreamType) fn stream_recordings(&self, req: &Request<::hyper::Body>, uuid: Uuid, type_: db::StreamType)
@ -372,9 +389,12 @@ impl ServiceInner {
Err(not_found("no such init segment")) Err(not_found("no such init segment"))
} }
fn stream_view_mp4(&self, req: &Request<::hyper::Body>, uuid: Uuid, fn stream_view_mp4(&self, req: &Request<::hyper::Body>, caller: Caller, uuid: Uuid,
stream_type: db::StreamType, mp4_type: mp4::Type, debug: bool) stream_type: db::StreamType, mp4_type: mp4::Type, debug: bool)
-> ResponseResult { -> ResponseResult {
if !caller.permissions.view_video {
return Err(plain_response(StatusCode::UNAUTHORIZED, "view_video required"));
}
let stream_id = { let stream_id = {
let db = self.db.lock(); let db = self.db.lock();
let camera = db.get_camera(uuid) let camera = db.get_camera(uuid)
@ -629,7 +649,11 @@ impl ServiceInner {
Ok(res) Ok(res)
} }
fn post_signals(&self, req: &Request<hyper::Body>, body: hyper::Chunk) -> ResponseResult { fn post_signals(&self, req: &Request<hyper::Body>, caller: Caller, body: hyper::Chunk)
-> ResponseResult {
if !caller.permissions.update_signals {
return Err(plain_response(StatusCode::UNAUTHORIZED, "update_signals required"));
}
let r: json::PostSignalsRequest = serde_json::from_slice(&body) let r: json::PostSignalsRequest = serde_json::from_slice(&body)
.map_err(|e| bad_req(e.to_string()))?; .map_err(|e| bad_req(e.to_string()))?;
let mut l = self.db.lock(); let mut l = self.db.lock();
@ -676,24 +700,39 @@ impl ServiceInner {
serve_json(req, &signals) serve_json(req, &signals)
} }
fn authenticated(&self, req: &Request<hyper::Body>) -> Result<Option<json::Session>, Error> { fn authenticate(&self, req: &Request<hyper::Body>, unauth_path: bool)
-> Result<Caller, base::Error> {
if let Some(sid) = extract_sid(req) { if let Some(sid) = extract_sid(req) {
let authreq = self.authreq(req); let authreq = self.authreq(req);
match self.db.lock().authenticate_session(authreq.clone(), &sid.hash()) {
Ok((s, u)) => {
return Ok(Some(json::Session {
username: u.username.clone(),
csrf: s.csrf(),
}))
},
Err(_) => {
// TODO: real error handling! this assumes all errors are due to lack of // TODO: real error handling! this assumes all errors are due to lack of
// authentication, when they could be logic errors in SQL or such. // authentication, when they could be logic errors in SQL or such.
return Ok(None); if let Ok((s, u)) = self.db.lock().authenticate_session(authreq.clone(), &sid.hash()) {
return Ok(Caller {
permissions: s.permissions.clone(),
session: Some(json::Session {
username: u.username.clone(),
csrf: s.csrf(),
}),
});
} }
} }
if let Some(s) = self.allow_unauthenticated_permissions.as_ref() {
return Ok(Caller {
permissions: s.clone(),
session: None,
});
} }
Ok(None)
if unauth_path {
return Ok(Caller {
permissions: db::Permissions::default(),
session: None,
})
}
bail_t!(Unauthenticated, "unauthenticated");
} }
} }
@ -783,9 +822,9 @@ fn with_json_body(mut req: Request<hyper::Body>)
pub struct Config<'a> { pub struct Config<'a> {
pub db: Arc<db::Database>, pub db: Arc<db::Database>,
pub ui_dir: Option<&'a str>, pub ui_dir: Option<&'a str>,
pub require_auth: bool,
pub trust_forward_hdrs: bool, pub trust_forward_hdrs: bool,
pub time_zone_name: String, pub time_zone_name: String,
pub allow_unauthenticated_permissions: Option<db::Permissions>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -820,7 +859,7 @@ impl Service {
dirs_by_stream_id, dirs_by_stream_id,
ui_files, ui_files,
pool: futures_cpupool::Builder::new().pool_size(1).name_prefix("static").create(), pool: futures_cpupool::Builder::new().pool_size(1).name_prefix("static").create(),
require_auth: config.require_auth, allow_unauthenticated_permissions: config.allow_unauthenticated_permissions,
trust_forward_hdrs: config.trust_forward_hdrs, trust_forward_hdrs: config.trust_forward_hdrs,
time_zone_name: config.time_zone_name, time_zone_name: config.time_zone_name,
}))) })))
@ -867,8 +906,11 @@ impl Service {
} }
} }
fn stream_live_m4s(&self, _req: &Request<::hyper::Body>, uuid: Uuid, fn stream_live_m4s(&self, _req: &Request<::hyper::Body>, caller: Caller, uuid: Uuid,
stream_type: db::StreamType) -> ResponseResult { stream_type: db::StreamType) -> ResponseResult {
if !caller.permissions.view_video {
return Err(plain_response(StatusCode::UNAUTHORIZED, "view_video required"));
}
let stream_id; let stream_id;
let open_id; let open_id;
let (sub_tx, sub_rx) = futures::sync::mpsc::unbounded(); let (sub_tx, sub_rx) = futures::sync::mpsc::unbounded();
@ -952,14 +994,14 @@ impl Service {
.unwrap()) .unwrap())
} }
fn signals(&self, req: Request<hyper::Body>) fn signals(&self, req: Request<hyper::Body>, caller: Caller)
-> Box<dyn Future<Item = Response<Body>, Error = Response<Body>> + Send + 'static> { -> Box<dyn Future<Item = Response<Body>, Error = Response<Body>> + Send + 'static> {
use http::method::Method; use http::method::Method;
match *req.method() { match *req.method() {
Method::POST => Box::new(with_json_body(req) Method::POST => Box::new(with_json_body(req)
.and_then({ .and_then({
let s = self.0.clone(); let s = self.0.clone();
move |(req, b)| s.post_signals(&req, b) move |(req, b)| s.post_signals(&req, caller, b)
})), })),
Method::GET | Method::HEAD => Box::new(future::result(self.0.get_signals(&req))), Method::GET | Method::HEAD => Box::new(future::result(self.0.get_signals(&req))),
_ => Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED, _ => Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
@ -992,36 +1034,33 @@ impl ::hyper::service::Service for Service {
} }
let p = Path::decode(req.uri().path()); let p = Path::decode(req.uri().path());
let require_auth = self.0.require_auth && match p { let always_allow_unauthenticated = match p {
Path::NotFound | Path::Request | Path::Login | Path::Logout | Path::Static => false, Path::NotFound | Path::Request | Path::Login | Path::Logout | Path::Static => true,
_ => true, _ => false,
}; };
debug!("request on: {}: {:?}, require_auth={}", req.uri(), p, require_auth); debug!("request on: {}: {:?}", req.uri(), p);
let session = match self.0.authenticated(&req) { let caller = match self.0.authenticate(&req, always_allow_unauthenticated) {
Ok(s) => s, Ok(c) => c,
Err(e) => return Box::new(future::ok(internal_server_err(e))), Err(e) => return Box::new(future::ok(from_base_error(e))),
}; };
if require_auth && session.is_none() {
return Box::new(future::ok(
plain_response(StatusCode::UNAUTHORIZED, "unauthorized")));
}
match p { match p {
Path::InitSegment(sha1, debug) => wrap_r(true, self.0.init_segment(sha1, debug, &req)), Path::InitSegment(sha1, debug) => wrap_r(true, self.0.init_segment(sha1, debug, &req)),
Path::TopLevel => wrap_r(true, self.0.top_level(&req, session)), Path::TopLevel => wrap_r(true, self.0.top_level(&req, caller)),
Path::Request => wrap_r(true, self.0.request(&req)), Path::Request => wrap_r(true, self.0.request(&req)),
Path::Camera(uuid) => wrap_r(true, self.0.camera(&req, uuid)), Path::Camera(uuid) => wrap_r(true, self.0.camera(&req, uuid)),
Path::StreamRecordings(uuid, type_) => { Path::StreamRecordings(uuid, type_) => {
wrap_r(true, self.0.stream_recordings(&req, uuid, type_)) wrap_r(true, self.0.stream_recordings(&req, uuid, type_))
}, },
Path::StreamViewMp4(uuid, type_, debug) => { Path::StreamViewMp4(uuid, type_, debug) => {
wrap_r(true, self.0.stream_view_mp4(&req, uuid, type_, mp4::Type::Normal, debug)) wrap_r(true, self.0.stream_view_mp4(&req, caller, uuid, type_, mp4::Type::Normal,
},
Path::StreamViewMp4Segment(uuid, type_, debug) => {
wrap_r(true, self.0.stream_view_mp4(&req, uuid, type_, mp4::Type::MediaSegment,
debug)) debug))
}, },
Path::StreamViewMp4Segment(uuid, type_, debug) => {
wrap_r(true, self.0.stream_view_mp4(&req, caller, uuid, type_,
mp4::Type::MediaSegment, debug))
},
Path::StreamLiveMp4Segments(uuid, type_) => { Path::StreamLiveMp4Segments(uuid, type_) => {
wrap_r(true, self.stream_live_m4s(&req, uuid, type_)) wrap_r(true, self.stream_live_m4s(&req, caller, uuid, type_))
}, },
Path::NotFound => wrap(true, future::err(not_found("path not understood"))), Path::NotFound => wrap(true, future::err(not_found("path not understood"))),
Path::Login => wrap(true, with_form_body(req).and_then({ Path::Login => wrap(true, with_form_body(req).and_then({
@ -1032,7 +1071,7 @@ impl ::hyper::service::Service for Service {
let s = self.clone(); let s = self.clone();
move |(req, b)| { s.0.logout(&req, b) } move |(req, b)| { s.0.logout(&req, b) }
})), })),
Path::Signals => wrap(true, self.signals(req)), Path::Signals => wrap(true, self.signals(req, caller)),
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())), Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
} }
} }
@ -1057,14 +1096,14 @@ mod tests {
} }
impl Server { impl Server {
fn new(require_auth: bool) -> Server { fn new(allow_unauthenticated_permissions: Option<db::Permissions>) -> Server {
let db = TestDb::new(base::clock::RealClocks {}); let db = TestDb::new(base::clock::RealClocks {});
let (shutdown_tx, shutdown_rx) = futures::sync::oneshot::channel::<()>(); let (shutdown_tx, shutdown_rx) = futures::sync::oneshot::channel::<()>();
let addr = "127.0.0.1:0".parse().unwrap(); let addr = "127.0.0.1:0".parse().unwrap();
let service = super::Service::new(super::Config { let service = super::Service::new(super::Config {
db: db.db.clone(), db: db.db.clone(),
ui_dir: None, ui_dir: None,
require_auth, allow_unauthenticated_permissions,
trust_forward_hdrs: true, trust_forward_hdrs: true,
time_zone_name: "".to_owned(), time_zone_name: "".to_owned(),
}).unwrap(); }).unwrap();
@ -1213,7 +1252,7 @@ mod tests {
#[test] #[test]
fn unauthorized_without_cookie() { fn unauthorized_without_cookie() {
testutil::init(); testutil::init();
let s = Server::new(true); let s = Server::new(None);
let cli = reqwest::Client::new(); let cli = reqwest::Client::new();
let resp = cli.get(&format!("{}/api/", &s.base_url)).send().unwrap(); let resp = cli.get(&format!("{}/api/", &s.base_url)).send().unwrap();
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED); assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
@ -1222,7 +1261,7 @@ mod tests {
#[test] #[test]
fn login() { fn login() {
testutil::init(); testutil::init();
let s = Server::new(true); let s = Server::new(None);
let cli = reqwest::Client::new(); let cli = reqwest::Client::new();
let login_url = format!("{}/api/login", &s.base_url); let login_url = format!("{}/api/login", &s.base_url);
@ -1255,7 +1294,7 @@ mod tests {
#[test] #[test]
fn logout() { fn logout() {
testutil::init(); testutil::init();
let s = Server::new(true); let s = Server::new(None);
let cli = reqwest::Client::new(); let cli = reqwest::Client::new();
let mut p = HashMap::new(); let mut p = HashMap::new();
p.insert("username", "slamb"); p.insert("username", "slamb");
@ -1310,7 +1349,9 @@ mod tests {
#[test] #[test]
fn view_without_segments() { fn view_without_segments() {
testutil::init(); testutil::init();
let s = Server::new(false); let mut permissions = db::Permissions::new();
permissions.view_video = true;
let s = Server::new(Some(permissions));
let cli = reqwest::Client::new(); let cli = reqwest::Client::new();
let resp = cli.get( let resp = cli.get(
&format!("{}/api/cameras/{}/main/view.mp4", &s.base_url, s.db.test_camera_uuid)) &format!("{}/api/cameras/{}/main/view.mp4", &s.base_url, s.db.test_camera_uuid))

View File

@ -215,7 +215,7 @@ function fetch(selectedRange, videoLength) {
function updateSession(session) { function updateSession(session) {
let sessionBar = $('#session'); let sessionBar = $('#session');
sessionBar.empty(); sessionBar.empty();
if (session === null) { if (session === null || session === undefined) {
sessionBar.hide(); sessionBar.hide();
return; return;
} }
@ -247,7 +247,7 @@ function updateSession(session) {
*/ */
function onReceivedTopLevel(data) { function onReceivedTopLevel(data) {
if (data === null) { if (data === null) {
data = {cameras: [], session: null, timeZoneName: null}; data = {cameras: [], timeZoneName: null};
} }
newTimeZone(data.timeZoneName); newTimeZone(data.timeZoneName);