mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-03-30 17:23:43 -04:00
retrieve and set users' permissions
This commit is contained in:
parent
be4e11c506
commit
dffec68b2f
@ -10,6 +10,11 @@ Each release is tagged in Git and on the Docker repository
|
|||||||
|
|
||||||
* use Retina 0.4.3, which is newly compatible with rtsp-simple-server v0.19.3
|
* 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).
|
and some TP-Link cameras. Fixes [#238](https://github.com/scottlamb/moonfire-nvr/issues/238).
|
||||||
|
* expanded API interface for examining and updating users:
|
||||||
|
* `admin_users` permission for operating on arbitrary users.
|
||||||
|
* `GET /users/<id>` endpoint
|
||||||
|
* expanded `POST /users/<id>` endpoint, including password and
|
||||||
|
permissions.
|
||||||
|
|
||||||
## 0.7.5 (2022-05-09)
|
## 0.7.5 (2022-05-09)
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ Status: **current**.
|
|||||||
* [Request 1](#request-1)
|
* [Request 1](#request-1)
|
||||||
* [Request 2](#request-2)
|
* [Request 2](#request-2)
|
||||||
* [Request 3](#request-3)
|
* [Request 3](#request-3)
|
||||||
|
* [`GET /api/users/<id>`](#get-apiusersid)
|
||||||
* [`POST /api/users/<id>`](#post-apiusersid)
|
* [`POST /api/users/<id>`](#post-apiusersid)
|
||||||
|
|
||||||
## Objective
|
## Objective
|
||||||
@ -822,6 +823,19 @@ Response:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `GET /api/users/<id>`
|
||||||
|
|
||||||
|
Retrieves the user. Requires the `admin_users` permission if the caller is
|
||||||
|
not authenticated as the user in question.
|
||||||
|
|
||||||
|
Returns a HTTP status 200 on success with a JSON dict:
|
||||||
|
|
||||||
|
* `preferences`: a JSON dictionary.
|
||||||
|
* `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`.
|
||||||
|
|
||||||
### `POST /api/users/<id>`
|
### `POST /api/users/<id>`
|
||||||
|
|
||||||
Allows updating the given user. Requires the `admin_users` permission if the
|
Allows updating the given user. Requires the `admin_users` permission if the
|
||||||
@ -840,6 +854,7 @@ Currently the following fields are supported for `update` and `precondition`:
|
|||||||
* `password`, a cleartext string. When updating the password, the previous
|
* `password`, a cleartext string. When updating the password, the previous
|
||||||
password must be supplied as a precondition, unless the caller has
|
password must be supplied as a precondition, unless the caller has
|
||||||
`admin_users` permission.
|
`admin_users` permission.
|
||||||
|
* `permissions`, which always requires `admin_users` permission to update.
|
||||||
|
|
||||||
Returns HTTP status 204 (No Content) on success.
|
Returns HTTP status 204 (No Content) on success.
|
||||||
|
|
||||||
|
@ -8,6 +8,8 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::json::Permissions;
|
||||||
|
|
||||||
fn default_db_dir() -> PathBuf {
|
fn default_db_dir() -> PathBuf {
|
||||||
"/var/lib/moonfire-nvr/db".into()
|
"/var/lib/moonfire-nvr/db".into()
|
||||||
}
|
}
|
||||||
@ -82,32 +84,3 @@ pub enum AddressConfig {
|
|||||||
// TODO: SystemdFileDescriptorName(String), see
|
// TODO: SystemdFileDescriptorName(String), see
|
||||||
// https://www.freedesktop.org/software/systemd/man/systemd.socket.html
|
// https://www.freedesktop.org/software/systemd/man/systemd.socket.html
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JSON analog of `Permissions` defined in `db/proto/schema.proto`.
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct Permissions {
|
|
||||||
#[serde(default)]
|
|
||||||
view_video: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
read_camera_configs: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
update_signals: bool,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
admin_users: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Permissions {
|
|
||||||
pub fn as_proto(&self) -> db::schema::Permissions {
|
|
||||||
db::schema::Permissions {
|
|
||||||
view_video: self.view_video,
|
|
||||||
read_camera_configs: self.read_camera_configs,
|
|
||||||
update_signals: self.update_signals,
|
|
||||||
admin_users: self.admin_users,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// Copyright (C) 2022 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
// Copyright (C) 2022 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
||||||
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
||||||
|
|
||||||
use crate::cmds::run::config::Permissions;
|
|
||||||
use crate::streamer;
|
use crate::streamer;
|
||||||
use crate::web;
|
use crate::web;
|
||||||
use crate::web::accept::Listener;
|
use crate::web::accept::Listener;
|
||||||
@ -369,7 +368,7 @@ async fn inner(
|
|||||||
allow_unauthenticated_permissions: b
|
allow_unauthenticated_permissions: b
|
||||||
.allow_unauthenticated_permissions
|
.allow_unauthenticated_permissions
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(Permissions::as_proto),
|
.map(db::Permissions::from),
|
||||||
trust_forward_hdrs: b.trust_forward_headers,
|
trust_forward_hdrs: b.trust_forward_headers,
|
||||||
time_zone_name: time_zone_name.clone(),
|
time_zone_name: time_zone_name.clone(),
|
||||||
privileged_unix_uid: b.own_uid_is_privileged.then(|| own_euid),
|
privileged_unix_uid: b.own_uid_is_privileged.then(|| own_euid),
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
||||||
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
||||||
|
|
||||||
|
//! JSON/TOML-compatible serde types for use in the web API and `moonfire-nvr.toml`.
|
||||||
|
|
||||||
use base::time::{Duration, Time};
|
use base::time::{Duration, Time};
|
||||||
use db::auth::SessionHash;
|
use db::auth::SessionHash;
|
||||||
use failure::{format_err, Error};
|
use failure::{format_err, Error};
|
||||||
@ -519,7 +521,7 @@ pub struct PostUser<'a> {
|
|||||||
pub precondition: Option<UserSubset<'a>>,
|
pub precondition: Option<UserSubset<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct UserSubset<'a> {
|
pub struct UserSubset<'a> {
|
||||||
pub preferences: Option<db::json::UserPreferences>,
|
pub preferences: Option<db::json::UserPreferences>,
|
||||||
@ -530,6 +532,8 @@ pub struct UserSubset<'a> {
|
|||||||
/// `Some(None)` indicates the password should be absent.
|
/// `Some(None)` indicates the password should be absent.
|
||||||
#[serde(borrow, default, deserialize_with = "deserialize_some")]
|
#[serde(borrow, default, deserialize_with = "deserialize_some")]
|
||||||
pub password: Option<Option<&'a str>>,
|
pub password: Option<Option<&'a str>>,
|
||||||
|
|
||||||
|
pub permissions: Option<Permissions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any value that is present is considered Some value, including null.
|
// Any value that is present is considered Some value, including null.
|
||||||
@ -541,3 +545,43 @@ where
|
|||||||
{
|
{
|
||||||
Deserialize::deserialize(deserializer).map(Some)
|
Deserialize::deserialize(deserializer).map(Some)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// API/config analog of `Permissions` defined in `db/proto/schema.proto`.
|
||||||
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Permissions {
|
||||||
|
#[serde(default)]
|
||||||
|
view_video: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
read_camera_configs: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
update_signals: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
admin_users: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
update_signals: p.update_signals,
|
||||||
|
admin_users: p.admin_users,
|
||||||
|
special_fields: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
update_signals: p.update_signals,
|
||||||
|
admin_users: p.admin_users,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,6 +14,7 @@ use self::accept::ConnData;
|
|||||||
use self::path::Path;
|
use self::path::Path;
|
||||||
use crate::body::Body;
|
use crate::body::Body;
|
||||||
use crate::json;
|
use crate::json;
|
||||||
|
use crate::json::UserSubset;
|
||||||
use crate::mp4;
|
use crate::mp4;
|
||||||
use base::{bail_t, clock::Clocks, format_err_t, ErrorKind};
|
use base::{bail_t, clock::Clocks, format_err_t, ErrorKind};
|
||||||
use core::borrow::Borrow;
|
use core::borrow::Borrow;
|
||||||
@ -469,11 +470,30 @@ impl Service {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
match *req.method() {
|
match *req.method() {
|
||||||
|
Method::GET | Method::HEAD => self.get_user(req, id).await,
|
||||||
Method::POST => self.post_user(req, caller, id).await,
|
Method::POST => self.post_user(req, caller, id).await,
|
||||||
_ => Err(plain_response(StatusCode::METHOD_NOT_ALLOWED, "POST expected").into()),
|
_ => Err(plain_response(StatusCode::METHOD_NOT_ALLOWED, "POST expected").into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_user(&self, req: Request<hyper::Body>, id: i32) -> ResponseResult {
|
||||||
|
let db = self.db.lock();
|
||||||
|
let user = db
|
||||||
|
.users_by_id()
|
||||||
|
.get(&id)
|
||||||
|
.ok_or_else(|| format_err_t!(NotFound, "can't find requested user"))?;
|
||||||
|
let out = UserSubset {
|
||||||
|
preferences: Some(user.config.preferences.clone()),
|
||||||
|
password: Some(if user.has_password() {
|
||||||
|
Some("(censored)")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
permissions: Some((&user.permissions).into()),
|
||||||
|
};
|
||||||
|
serve_json(&req, &out)
|
||||||
|
}
|
||||||
|
|
||||||
async fn post_user(
|
async fn post_user(
|
||||||
&self,
|
&self,
|
||||||
mut req: Request<hyper::Body>,
|
mut req: Request<hyper::Body>,
|
||||||
@ -485,7 +505,7 @@ impl Service {
|
|||||||
let mut db = self.db.lock();
|
let mut db = self.db.lock();
|
||||||
let user = db
|
let user = db
|
||||||
.get_user_by_id_mut(id)
|
.get_user_by_id_mut(id)
|
||||||
.ok_or_else(|| format_err_t!(Internal, "can't find requested user"))?;
|
.ok_or_else(|| format_err_t!(NotFound, "can't find requested user"))?;
|
||||||
if r.update.as_ref().map(|u| u.password).is_some()
|
if r.update.as_ref().map(|u| u.password).is_some()
|
||||||
&& r.precondition.as_ref().map(|p| p.password).is_none()
|
&& r.precondition.as_ref().map(|p| p.password).is_none()
|
||||||
&& !caller.permissions.admin_users
|
&& !caller.permissions.admin_users
|
||||||
@ -495,6 +515,12 @@ impl Service {
|
|||||||
"to change password, must supply previous password or have admin_users permission"
|
"to change password, must supply previous password or have admin_users permission"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if r.update.as_ref().map(|u| &u.permissions).is_some() && !caller.permissions.admin_users {
|
||||||
|
bail_t!(
|
||||||
|
Unauthenticated,
|
||||||
|
"to change permissions, must have admin_users permission"
|
||||||
|
);
|
||||||
|
}
|
||||||
match (r.csrf, caller.user.and_then(|u| u.session)) {
|
match (r.csrf, caller.user.and_then(|u| u.session)) {
|
||||||
(None, Some(_)) => bail_t!(Unauthenticated, "csrf must be supplied"),
|
(None, Some(_)) => bail_t!(Unauthenticated, "csrf must be supplied"),
|
||||||
(Some(csrf), Some(session)) if !csrf_matches(csrf, session.csrf) => {
|
(Some(csrf), Some(session)) if !csrf_matches(csrf, session.csrf) => {
|
||||||
@ -511,6 +537,11 @@ impl Service {
|
|||||||
bail_t!(FailedPrecondition, "password mismatch"); // or Unauthenticated?
|
bail_t!(FailedPrecondition, "password mismatch"); // or Unauthenticated?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(ref p) = precondition.permissions {
|
||||||
|
if user.permissions != db::Permissions::from(p) {
|
||||||
|
bail_t!(FailedPrecondition, "permissions mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(update) = r.update {
|
if let Some(update) = r.update {
|
||||||
let mut change = user.change();
|
let mut change = user.change();
|
||||||
@ -522,6 +553,9 @@ impl Service {
|
|||||||
Some(None) => change.clear_password(),
|
Some(None) => change.clear_password(),
|
||||||
Some(Some(p)) => change.set_password(p.to_owned()),
|
Some(Some(p)) => change.set_password(p.to_owned()),
|
||||||
}
|
}
|
||||||
|
if let Some(ref permissions) = update.permissions {
|
||||||
|
change.permissions = permissions.into();
|
||||||
|
}
|
||||||
db.apply_user_change(change).map_err(internal_server_err)?;
|
db.apply_user_change(change).map_err(internal_server_err)?;
|
||||||
}
|
}
|
||||||
Ok(plain_response(StatusCode::NO_CONTENT, &b""[..]))
|
Ok(plain_response(StatusCode::NO_CONTENT, &b""[..]))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user