retrieve and set users' permissions

This commit is contained in:
Scott Lamb 2022-12-24 12:38:13 -05:00
parent be4e11c506
commit dffec68b2f
6 changed files with 103 additions and 33 deletions

View File

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

View File

@ -22,6 +22,7 @@ Status: **current**.
* [Request 1](#request-1)
* [Request 2](#request-2)
* [Request 3](#request-3)
* [`GET /api/users/<id>`](#get-apiusersid)
* [`POST /api/users/<id>`](#post-apiusersid)
## 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>`
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 must be supplied as a precondition, unless the caller has
`admin_users` permission.
* `permissions`, which always requires `admin_users` permission to update.
Returns HTTP status 204 (No Content) on success.

View File

@ -8,6 +8,8 @@ use std::path::PathBuf;
use serde::Deserialize;
use crate::json::Permissions;
fn default_db_dir() -> PathBuf {
"/var/lib/moonfire-nvr/db".into()
}
@ -82,32 +84,3 @@ pub enum AddressConfig {
// TODO: SystemdFileDescriptorName(String), see
// 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()
}
}
}

View File

@ -2,7 +2,6 @@
// 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.
use crate::cmds::run::config::Permissions;
use crate::streamer;
use crate::web;
use crate::web::accept::Listener;
@ -369,7 +368,7 @@ async fn inner(
allow_unauthenticated_permissions: b
.allow_unauthenticated_permissions
.as_ref()
.map(Permissions::as_proto),
.map(db::Permissions::from),
trust_forward_hdrs: b.trust_forward_headers,
time_zone_name: time_zone_name.clone(),
privileged_unix_uid: b.own_uid_is_privileged.then(|| own_euid),

View File

@ -2,6 +2,8 @@
// 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.
//! JSON/TOML-compatible serde types for use in the web API and `moonfire-nvr.toml`.
use base::time::{Duration, Time};
use db::auth::SessionHash;
use failure::{format_err, Error};
@ -519,7 +521,7 @@ pub struct PostUser<'a> {
pub precondition: Option<UserSubset<'a>>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UserSubset<'a> {
pub preferences: Option<db::json::UserPreferences>,
@ -530,6 +532,8 @@ pub struct UserSubset<'a> {
/// `Some(None)` indicates the password should be absent.
#[serde(borrow, default, deserialize_with = "deserialize_some")]
pub password: Option<Option<&'a str>>,
pub permissions: Option<Permissions>,
}
// Any value that is present is considered Some value, including null.
@ -541,3 +545,43 @@ where
{
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,
}
}
}

View File

@ -14,6 +14,7 @@ use self::accept::ConnData;
use self::path::Path;
use crate::body::Body;
use crate::json;
use crate::json::UserSubset;
use crate::mp4;
use base::{bail_t, clock::Clocks, format_err_t, ErrorKind};
use core::borrow::Borrow;
@ -469,11 +470,30 @@ impl Service {
);
}
match *req.method() {
Method::GET | Method::HEAD => self.get_user(req, id).await,
Method::POST => self.post_user(req, caller, id).await,
_ => 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(
&self,
mut req: Request<hyper::Body>,
@ -485,7 +505,7 @@ impl Service {
let mut db = self.db.lock();
let user = db
.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()
&& r.precondition.as_ref().map(|p| p.password).is_none()
&& !caller.permissions.admin_users
@ -495,6 +515,12 @@ impl Service {
"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)) {
(None, Some(_)) => bail_t!(Unauthenticated, "csrf must be supplied"),
(Some(csrf), Some(session)) if !csrf_matches(csrf, session.csrf) => {
@ -511,6 +537,11 @@ impl Service {
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 {
let mut change = user.change();
@ -522,6 +553,9 @@ impl Service {
Some(None) => change.clear_password(),
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)?;
}
Ok(plain_response(StatusCode::NO_CONTENT, &b""[..]))