mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-12-05 15:22:29 -05:00
extend POST /users/:id
Now you can set a password for a user while the server is running,
e.g. via the following command:
```shell
curl \
-H 'Content-Type: application/json' \
-d '{"update": {"password": "asdf"}}' \
--unix-socket /var/lib/moonfire-nvr/sock \
http://nvr/api/users/1
```
This commit is contained in:
@@ -95,6 +95,9 @@ pub struct Permissions {
|
||||
|
||||
#[serde(default)]
|
||||
update_signals: bool,
|
||||
|
||||
#[serde(default)]
|
||||
admin_users: bool,
|
||||
}
|
||||
|
||||
impl Permissions {
|
||||
@@ -103,6 +106,7 @@ impl 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use base::time::{Duration, Time};
|
||||
use db::auth::SessionHash;
|
||||
use failure::{format_err, Error};
|
||||
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::ops::Not;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -513,13 +513,31 @@ pub struct ToplevelUser {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PostUser {
|
||||
pub update: Option<UserSubset>,
|
||||
pub precondition: Option<UserSubset>,
|
||||
pub struct PostUser<'a> {
|
||||
pub csrf: Option<&'a str>,
|
||||
pub update: Option<UserSubset<'a>>,
|
||||
pub precondition: Option<UserSubset<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserSubset {
|
||||
pub struct UserSubset<'a> {
|
||||
pub preferences: Option<db::json::UserPreferences>,
|
||||
|
||||
/// An optional password value.
|
||||
///
|
||||
/// `None` indicates the password does not wish to check/update the password.
|
||||
/// `Some(None)` indicates the password should be absent.
|
||||
#[serde(borrow, default, deserialize_with = "deserialize_some")]
|
||||
pub password: Option<Option<&'a str>>,
|
||||
}
|
||||
|
||||
// Any value that is present is considered Some value, including null.
|
||||
// https://github.com/serde-rs/serde/issues/984#issuecomment-314143738
|
||||
fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer).map(Some)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ use self::path::Path;
|
||||
use crate::body::Body;
|
||||
use crate::json;
|
||||
use crate::mp4;
|
||||
use base::{bail_t, ErrorKind};
|
||||
use base::{clock::Clocks, format_err_t};
|
||||
use base::{bail_t, clock::Clocks, format_err_t, ErrorKind};
|
||||
use core::borrow::Borrow;
|
||||
use core::str::FromStr;
|
||||
use db::dir::SampleFileDir;
|
||||
@@ -82,7 +81,8 @@ fn from_base_error(err: base::Error) -> Response<Body> {
|
||||
let status_code = match err.kind() {
|
||||
Unauthenticated => StatusCode::UNAUTHORIZED,
|
||||
PermissionDenied => StatusCode::FORBIDDEN,
|
||||
InvalidArgument | FailedPrecondition => StatusCode::BAD_REQUEST,
|
||||
InvalidArgument => StatusCode::BAD_REQUEST,
|
||||
FailedPrecondition => StatusCode::PRECONDITION_FAILED,
|
||||
NotFound => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
@@ -462,33 +462,66 @@ impl Service {
|
||||
}
|
||||
|
||||
async fn user(&self, req: Request<hyper::Body>, caller: Caller, id: i32) -> ResponseResult {
|
||||
if caller.user.map(|u| u.id) != Some(id) {
|
||||
bail_t!(Unauthenticated, "must be authenticated as supplied user");
|
||||
if caller.user.as_ref().map(|u| u.id) != Some(id) && !caller.permissions.admin_users {
|
||||
bail_t!(
|
||||
Unauthenticated,
|
||||
"must be authenticated as supplied user or have admin_users permission"
|
||||
);
|
||||
}
|
||||
match *req.method() {
|
||||
Method::POST => self.post_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 post_user(&self, mut req: Request<hyper::Body>, id: i32) -> ResponseResult {
|
||||
async fn post_user(
|
||||
&self,
|
||||
mut req: Request<hyper::Body>,
|
||||
caller: Caller,
|
||||
id: i32,
|
||||
) -> ResponseResult {
|
||||
let r = extract_json_body(&mut req).await?;
|
||||
let r: json::PostUser = serde_json::from_slice(&r).map_err(|e| bad_req(e.to_string()))?;
|
||||
let mut db = self.db.lock();
|
||||
let user = db
|
||||
.users_by_id()
|
||||
.get(&id)
|
||||
.ok_or_else(|| format_err_t!(Internal, "can't find currently authenticated user"))?;
|
||||
if let Some(precondition) = r.precondition {
|
||||
if matches!(precondition.preferences, Some(p) if p != user.config.preferences) {
|
||||
.get_user_by_id_mut(id)
|
||||
.ok_or_else(|| format_err_t!(Internal, "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
|
||||
{
|
||||
bail_t!(
|
||||
Unauthenticated,
|
||||
"to change password, must supply previous password or 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) => {
|
||||
bail_t!(Unauthenticated, "incorrect csrf");
|
||||
}
|
||||
(_, _) => {}
|
||||
}
|
||||
if let Some(ref precondition) = r.precondition {
|
||||
if matches!(precondition.preferences, Some(ref p) if p != &user.config.preferences) {
|
||||
bail_t!(FailedPrecondition, "preferences mismatch");
|
||||
}
|
||||
if let Some(p) = precondition.password {
|
||||
if !user.check_password(p)? {
|
||||
bail_t!(FailedPrecondition, "password mismatch"); // or Unauthenticated?
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(update) = r.update {
|
||||
let mut change = user.change();
|
||||
if let Some(preferences) = update.preferences {
|
||||
change.config.preferences = preferences;
|
||||
}
|
||||
match update.password {
|
||||
None => {}
|
||||
Some(None) => change.clear_password(),
|
||||
Some(Some(p)) => change.set_password(p.to_owned()),
|
||||
}
|
||||
db.apply_user_change(change).map_err(internal_server_err)?;
|
||||
}
|
||||
Ok(plain_response(StatusCode::NO_CONTENT, &b""[..]))
|
||||
@@ -611,6 +644,7 @@ impl Service {
|
||||
view_video: true,
|
||||
read_camera_configs: true,
|
||||
update_signals: true,
|
||||
admin_users: true,
|
||||
..Default::default()
|
||||
},
|
||||
user: None,
|
||||
|
||||
Reference in New Issue
Block a user