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:
parent
a6bdf0bd80
commit
42fe054d46
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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`:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -347,6 +347,7 @@ impl Service {
|
|||
user: caller.user,
|
||||
signals: (&db, days),
|
||||
signal_types: &db,
|
||||
permissions: caller.permissions.into(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue