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.
This commit is contained in:
Scott Lamb 2022-12-31 12:08:26 -05:00
parent a6bdf0bd80
commit 42fe054d46
No known key found for this signature in database
12 changed files with 103 additions and 47 deletions

View File

@ -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:

View File

@ -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/<id>`](#get-apiusersid)
* [`POST /api/users/<id>`](#post-apiusersid)
* [`DELETE /api/users/<id>`](#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/<id>`
@ -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

35
design/signal.md Normal file
View File

@ -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.

View File

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

View File

@ -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`:

View File

@ -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:

View File

@ -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 `<dir>/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;
}

View File

@ -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<BindConfig>,
@ -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),

View File

@ -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(),

View File

@ -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<ToplevelUser>,
@ -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<Permissions> 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<db::schema::Permissions> for Permissions {
fn from(p: db::schema::Permissions) -> Self {
Self {
view_video: p.view_video,
read_camera_configs: p.read_camera_configs,

View File

@ -347,6 +347,7 @@ impl Service {
user: caller.user,
signals: (&db, days),
signal_types: &db,
permissions: caller.permissions.into(),
},
)
}

View File

@ -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.