From 42fe054d4634738035401b1521f303b16d1418ff Mon Sep 17 00:00:00 2001 From: Scott Lamb Date: Sat, 31 Dec 2022 12:08:26 -0500 Subject: [PATCH] make `GET /api/` return current permissions This is useful for e.g. deciding whether or not to present the user admin UI in navigation. As part of this change, I adjusted the casing in Permissions, and then all the toml stuff for consistency. Noted in changelog. --- CHANGELOG.md | 2 ++ design/api.md | 49 +++++++++++++++++++++++++---------- design/signal.md | 35 +++++++++++++++++++++++++ guide/build.md | 4 +-- guide/install.md | 4 +-- guide/secure.md | 4 +-- server/db/proto/schema.proto | 22 +++++----------- server/src/cmds/run/config.rs | 4 ++- server/src/cmds/run/mod.rs | 2 +- server/src/json.rs | 13 ++++++---- server/src/web/mod.rs | 1 + server/src/web/users.rs | 10 +++---- 12 files changed, 103 insertions(+), 47 deletions(-) create mode 100644 design/signal.md diff --git a/CHANGELOG.md b/CHANGELOG.md index eb702e7..4a3244f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Each release is tagged in Git and on the Docker repository ## unreleased +* expect camelCase in `moonfire-nvr.toml` file, for consistency with the JSON + API. You'll need to adjust your config file when upgrading. * use Retina 0.4.3, which is newly compatible with rtsp-simple-server v0.19.3 and some TP-Link cameras. Fixes [#238](https://github.com/scottlamb/moonfire-nvr/issues/238). * expanded API interface for examining and updating users: diff --git a/design/api.md b/design/api.md index 0917b11..a19cc8d 100644 --- a/design/api.md +++ b/design/api.md @@ -2,8 +2,8 @@ Status: **current**. -* [Objective](#objective) -* [Detailed design](#detailed-design) +* [Summary](#summary) +* [Endpoints](#endpoints) * [Authentication](#authentication) * [`POST /api/login`](#post-apilogin) * [`POST /api/logout`](#post-apilogout) @@ -28,11 +28,13 @@ Status: **current**. * [`GET /api/users/`](#get-apiusersid) * [`POST /api/users/`](#post-apiusersid) * [`DELETE /api/users/`](#delete-apiusersid) +* [Types](#types) + * [Permissions](#permissions) -## Objective +## Summary -Allow a JavaScript-based web interface to list cameras and view recordings. -Support external analytics. +A JavaScript-based web interface to list cameras and view recordings. +Supports external analytics. In the future, this is likely to be expanded: @@ -40,8 +42,6 @@ In the future, this is likely to be expanded: * commandline tool over a UNIX-domain socket (at least for bootstrapping web authentication) -## Detailed design - *Note:* italicized terms in this document are defined in the [glossary](glossary.md). Currently the API is considered an internal contract between the server and the @@ -56,6 +56,8 @@ developed tools. All requests for JSON data should be sent with the header `Accept: application/json` (exactly). +## Endpoints + ### Authentication #### `POST /api/login` @@ -184,6 +186,7 @@ The `application/json` response will have a JSON object as follows: considered to have motion when this signal is in this state. * `color` (optional): a recommended color to use in UIs to represent this state, as in the [HTML specification](https://html.spec.whatwg.org/#colours). +* `permissions`: the caller's current `Permissions` object (defined below). * `user`: an object, present only when authenticated: * `name`: a human-readable name * `id`: an integer @@ -840,12 +843,13 @@ a `users` key with a map of id to username. Requires the `admin_users` permission. -Adds a user. Expects a JSON dictionary with the parameters for the user: +Adds a user. Expects a JSON object with the parameters for the user: * `username`: a string, which must be unique. -* `permissions`: a JSON dictionary of permissions. +* `permissions`: see `Permissions` below. * `password` (optional): a string. -* `preferences` (optional): a JSON dictionary. +* `preferences` (optional): an arbitrary JSON object. Interpretation is + up to clients. Returns status 204 (No Content) on success. @@ -856,11 +860,11 @@ not authenticated as the user in question. Returns a HTTP status 200 on success with a JSON dict: -* `preferences`: a JSON dictionary. +* `preferences`: a JSON object. * `password`: absent (no password set) or a placeholder string to indicate the password is set. Passwords are stored hashed, so the cleartext can not be retrieved. -* `permissions`. +* `permissions`: see `Permissions` below. #### `POST /api/users/` @@ -876,11 +880,14 @@ Expects a JSON object: Currently the following fields are supported for `update` and `precondition`: -* `preferences`, a JSON dictionary. +* `preferences`, a JSON object. * `password`, a cleartext string. When updating the password, the previous password must be supplied as a precondition, unless the caller has `admin_users` permission. -* `permissions`, which always requires `admin_users` permission to update. +* `permissions`, a `Permissions` as described below, which always requires + `admin_users` permission to update. Note that updating a user's permissions + currently neither adds nor limits permissions of existing sessions; it only + changes what is available to newly created sessions. Returns HTTP status 204 (No Content) on success. @@ -890,6 +897,20 @@ Deletes the given user. Requires the `admin_users` permission. Returns HTTP status 204 (No Content) on success. +## Types + +### Permissions + +A JSON object of permissions to perform various actions: + +* `adminUsers`: bool +* `readCameraConfigs`: bool, read camera configs including credentials +* `updateSignals`: bool +* `viewVideo`: bool + +See endpoints above for more details on the contexts in which these are +required. + [media-segment]: https://w3c.github.io/media-source/isobmff-byte-stream-format.html#iso-media-segments [init-segment]: https://w3c.github.io/media-source/isobmff-byte-stream-format.html#iso-init-segments [rfc-6381]: https://tools.ietf.org/html/rfc6381 diff --git a/design/signal.md b/design/signal.md new file mode 100644 index 0000000..7f6da69 --- /dev/null +++ b/design/signal.md @@ -0,0 +1,35 @@ +# Moonfire NVR Signals + +Status: **draft**. + +"Signals" are what Moonfire NVR uses to describe non-video timeseries data +such as "was motion detected?" or "what mode was my burglar alarm in?" They are +intended to be displayed in the UI with the video scrub bar to aid in finding +a relevant portion of video. + +## Objective + +Goals: + +* represent simple results of on-camera and on-NVR motion detection, e.g.: + `true`, `false`, or `unknown`. +* represent external signals such as burglar alarm state, e.g.: + `off`, `stay`, `away`, `alarm`, or `unknown`. + +Non-goals: + +* provide meaningful data when the NVR has inaccurate system time. +* support internal state necessary for on-NVR motion detection. (This will + be considered separately.) +* support fine-grained outputs such as "what are the bounding boxes of all + detected faces?", "what cells have motion?", audio volume, or audio + spectograms. + +## Overview + +hmm, two ideas: + +* just use timestamps everywhere. allow adding/updating historical data. +* only allow updating the current open. initially, just support setting + current time. then support extending from a previous request. no ability + to fill in while NVR is down. diff --git a/guide/build.md b/guide/build.md index d9f700a..03e7d85 100644 --- a/guide/build.md +++ b/guide/build.md @@ -295,11 +295,11 @@ You'll also need a `/etc/moonfire-nvr.toml`: ```toml [[binds]] ipv4 = "0.0.0.0:8080" -allow_unauthenticated_permissions = { view_video = true } +allowUnauthenticatedPermissions = { viewVideo = true } [[binds]] unix = "/var/lib/moonfire-nvr/sock" -own_uid_is_privileged = true +ownUidIsPrivileged = true ``` Note this configuration is insecure. You can change that via replacing the diff --git a/guide/install.md b/guide/install.md index 4d403f7..a1b06ab 100644 --- a/guide/install.md +++ b/guide/install.md @@ -71,11 +71,11 @@ $ sudo chmod a+rx /usr/local/bin/nvr ```toml [[binds]] ipv4 = "0.0.0.0:8080" -allow_unauthenticated_permissions = { view_video = true } +allowUnauthenticatedPermissions = { viewVideo = true } [[binds]] unix = "/var/lib/moonfire-nvr/sock" -own_uid_is_privileged = true +ownUidIsPrivileged = true ``` `/usr/local/bin/nvr`: diff --git a/guide/secure.md b/guide/secure.md index fc9858c..2ac531e 100644 --- a/guide/secure.md +++ b/guide/secure.md @@ -165,13 +165,13 @@ If you follow the recommended Docker setup, your `/etc/moonfire-nvr.json` will contain this line: ```toml -allow_unauthenticated_permissions = { view_video = true } +allowUnauthenticatedPermissions = { viewVideo = true } ``` Replace it with the following: ```toml -trust_forward_headers = true +trustForwardHeaders = true ``` This change has two effects: diff --git a/server/db/proto/schema.proto b/server/db/proto/schema.proto index 8b14a4e..a24e783 100644 --- a/server/db/proto/schema.proto +++ b/server/db/proto/schema.proto @@ -2,6 +2,11 @@ // Copyright (C) 2018 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt. // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.'; +// Protobuf portion of the Moonfire NVR schema. In general Moonfire's schema +// uses a SQLite3 database with some fields in JSON representation. The protobuf +// stuff is just high-cardinality things that must be compact, e.g. permissions +// that can be stuffed into every user session. + syntax = "proto3"; // Metadata stored in sample file dirs as `/meta`. This is checked @@ -55,25 +60,12 @@ message DirMeta { Open in_progress_open = 4; } -// Permissions to perform actions, currently all simple bools. +// Permissions to perform actions. See description in design/api.md. // -// 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. +// This protobuf form is stored in user and session rows. message Permissions { bool view_video = 1; bool read_camera_configs = 2; - bool update_signals = 3; - - // Administrate user accounts: create, delete accounts; modify passwords of - // accounts other than the caller's own. bool admin_users = 4; } diff --git a/server/src/cmds/run/config.rs b/server/src/cmds/run/config.rs index c8619d2..6913c0e 100644 --- a/server/src/cmds/run/config.rs +++ b/server/src/cmds/run/config.rs @@ -21,6 +21,7 @@ fn default_ui_dir() -> PathBuf { /// Top-level configuration file object. #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] pub struct ConfigFile { pub binds: Vec, @@ -42,6 +43,7 @@ pub struct ConfigFile { /// Per-bind configuration. #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] pub struct BindConfig { /// The address to bind to. #[serde(flatten)] @@ -70,8 +72,8 @@ pub struct BindConfig { } #[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] #[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] pub enum AddressConfig { /// IPv4 address such as `0.0.0.0:8080` or `127.0.0.1:8080`. Ipv4(std::net::SocketAddrV4), diff --git a/server/src/cmds/run/mod.rs b/server/src/cmds/run/mod.rs index 6a1ad90..ba26ba5 100644 --- a/server/src/cmds/run/mod.rs +++ b/server/src/cmds/run/mod.rs @@ -367,7 +367,7 @@ async fn inner( ui_dir: Some(&config.ui_dir), allow_unauthenticated_permissions: b .allow_unauthenticated_permissions - .as_ref() + .clone() .map(db::Permissions::from), trust_forward_hdrs: b.trust_forward_headers, time_zone_name: time_zone_name.clone(), diff --git a/server/src/json.rs b/server/src/json.rs index 803f4df..c15ce50 100644 --- a/server/src/json.rs +++ b/server/src/json.rs @@ -25,6 +25,8 @@ pub struct TopLevel<'a> { #[serde(serialize_with = "TopLevel::serialize_cameras")] pub cameras: (&'a db::LockedDatabase, bool, bool), + pub permissions: Permissions, + #[serde(skip_serializing_if = "Option::is_none")] pub user: Option, @@ -553,8 +555,9 @@ where } /// API/config analog of `Permissions` defined in `db/proto/schema.proto`. -#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] +#[serde(rename_all = "camelCase")] pub struct Permissions { #[serde(default)] view_video: bool, @@ -569,8 +572,8 @@ pub struct Permissions { admin_users: bool, } -impl From<&Permissions> for db::schema::Permissions { - fn from(p: &Permissions) -> Self { +impl From for db::schema::Permissions { + fn from(p: Permissions) -> Self { Self { view_video: p.view_video, read_camera_configs: p.read_camera_configs, @@ -581,8 +584,8 @@ impl From<&Permissions> for db::schema::Permissions { } } -impl From<&db::schema::Permissions> for Permissions { - fn from(p: &db::schema::Permissions) -> Self { +impl From for Permissions { + fn from(p: db::schema::Permissions) -> Self { Self { view_video: p.view_video, read_camera_configs: p.read_camera_configs, diff --git a/server/src/web/mod.rs b/server/src/web/mod.rs index 0978546..b48990f 100644 --- a/server/src/web/mod.rs +++ b/server/src/web/mod.rs @@ -347,6 +347,7 @@ impl Service { user: caller.user, signals: (&db, days), signal_types: &db, + permissions: caller.permissions.into(), }, ) } diff --git a/server/src/web/users.rs b/server/src/web/users.rs index a9fb0ce..7c08960 100644 --- a/server/src/web/users.rs +++ b/server/src/web/users.rs @@ -57,7 +57,7 @@ impl Service { if let Some(preferences) = r.preferences.take() { change.config.preferences = preferences; } - if let Some(ref permissions) = r.permissions.take() { + if let Some(permissions) = r.permissions.take() { change.permissions = permissions.into(); } if r != Default::default() { @@ -101,7 +101,7 @@ impl Service { } else { None }), - permissions: Some((&user.permissions).into()), + permissions: Some(user.permissions.clone().into()), }; serve_json(&req, &out) } @@ -145,7 +145,7 @@ impl Service { (_, _) => {} } if let Some(mut precondition) = r.precondition { - if matches!(precondition.username.take(), Some(n) if n != &user.username) { + if matches!(precondition.username.take(), Some(n) if n != user.username) { bail_t!(FailedPrecondition, "username mismatch"); } if matches!(precondition.preferences.take(), Some(ref p) if p != &user.config.preferences) @@ -158,7 +158,7 @@ impl Service { } } if let Some(p) = precondition.permissions.take() { - if user.permissions != db::Permissions::from(&p) { + if user.permissions != db::Permissions::from(p) { bail_t!(FailedPrecondition, "permissions mismatch"); } } @@ -193,7 +193,7 @@ impl Service { change.username = n.to_string(); } if let Some(permissions) = update.permissions.take() { - change.permissions = (&permissions).into(); + change.permissions = permissions.into(); } // Safety valve in case something is added to UserSubset and forgotten here.