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
|
||||
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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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""[..]))
|
||||
|
|
Loading…
Reference in New Issue