Scott Lamb dfa949815b
tweaks to api and docs
In particular, the docs now talk about the CSRF protection. This is
increasing relevant as we start having more mutation endpoints. And
make the signals api expect a csrf for session auth to match the newer
users api.
2023-01-05 12:21:35 -06:00

227 lines
8.5 KiB
Rust

// This file is part of Moonfire NVR, a security camera network video recorder.
// 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.
//! User management: `/api/users/*`.
use base::{bail_t, format_err_t};
use http::{Method, Request, StatusCode};
use crate::json::{self, PutUsersResponse, UserSubset, UserSummary};
use super::{
bad_req, extract_json_body, plain_response, require_csrf_if_session, serve_json, Caller,
ResponseResult, Service,
};
impl Service {
pub(super) async fn users(&self, req: Request<hyper::Body>, caller: Caller) -> ResponseResult {
match *req.method() {
Method::GET | Method::HEAD => self.get_users(req, caller).await,
Method::PUT => self.put_users(req, caller).await,
_ => Err(
plain_response(StatusCode::METHOD_NOT_ALLOWED, "GET, HEAD, or PUT expected").into(),
),
}
}
async fn get_users(&self, req: Request<hyper::Body>, caller: Caller) -> ResponseResult {
if !caller.permissions.admin_users {
bail_t!(Unauthenticated, "must have admin_users permission");
}
let users = self
.db
.lock()
.users_by_id()
.iter()
.map(|(&id, user)| UserSummary {
id,
username: user.username.clone(),
})
.collect();
serve_json(&req, &json::GetUsersResponse { users })
}
async fn put_users(&self, mut req: Request<hyper::Body>, caller: Caller) -> ResponseResult {
if !caller.permissions.admin_users {
bail_t!(Unauthenticated, "must have admin_users permission");
}
let r = extract_json_body(&mut req).await?;
let mut r: json::PutUsers =
serde_json::from_slice(&r).map_err(|e| bad_req(e.to_string()))?;
require_csrf_if_session(&caller, r.csrf)?;
let username = r
.user
.username
.take()
.ok_or_else(|| format_err_t!(InvalidArgument, "username must be specified"))?;
let mut change = db::UserChange::add_user(username.to_owned());
if let Some(Some(pwd)) = r.user.password.take() {
change.set_password(pwd.to_owned());
}
if let Some(preferences) = r.user.preferences.take() {
change.config.preferences = preferences;
}
if let Some(permissions) = r.user.permissions.take() {
change.permissions = permissions.into();
}
if r.user != Default::default() {
bail_t!(Unimplemented, "unsupported user fields: {:#?}", r);
}
let mut l = self.db.lock();
let user = l.apply_user_change(change)?;
serve_json(&req, &PutUsersResponse { id: user.id })
}
pub(super) async fn user(
&self,
req: Request<hyper::Body>,
caller: Caller,
id: i32,
) -> ResponseResult {
match *req.method() {
Method::GET | Method::HEAD => self.get_user(req, caller, id).await,
Method::DELETE => self.delete_user(req, caller, id).await,
Method::POST => self.post_user(req, caller, id).await,
_ => Err(plain_response(
StatusCode::METHOD_NOT_ALLOWED,
"GET, HEAD, DELETE, or POST expected",
)
.into()),
}
}
async fn get_user(&self, req: Request<hyper::Body>, caller: Caller, id: i32) -> ResponseResult {
require_same_or_admin(&caller, id)?;
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 {
username: Some(&user.username),
preferences: Some(user.config.preferences.clone()),
password: Some(if user.has_password() {
Some("(censored)")
} else {
None
}),
permissions: Some(user.permissions.clone().into()),
};
serve_json(&req, &out)
}
async fn delete_user(
&self,
mut req: Request<hyper::Body>,
caller: Caller,
id: i32,
) -> ResponseResult {
if !caller.permissions.admin_users {
bail_t!(Unauthenticated, "must have admin_users permission");
}
let r = extract_json_body(&mut req).await?;
let r: json::DeleteUser = serde_json::from_slice(&r).map_err(|e| bad_req(e.to_string()))?;
require_csrf_if_session(&caller, r.csrf)?;
let mut l = self.db.lock();
l.delete_user(id)?;
Ok(plain_response(StatusCode::NO_CONTENT, &b""[..]))
}
async fn post_user(
&self,
mut req: Request<hyper::Body>,
caller: Caller,
id: i32,
) -> ResponseResult {
require_same_or_admin(&caller, id)?;
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
.get_user_by_id_mut(id)
.ok_or_else(|| format_err_t!(NotFound, "can't find requested user"))?;
if r.update.as_ref().and_then(|u| u.password).is_some()
&& r.precondition.as_ref().and_then(|p| p.password).is_none()
&& !caller.permissions.admin_users
{
bail_t!(
Unauthenticated,
"to change password, must supply previous password or have admin_users permission"
);
}
require_csrf_if_session(&caller, r.csrf)?;
if let Some(mut precondition) = r.precondition {
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)
{
bail_t!(FailedPrecondition, "preferences mismatch");
}
if let Some(p) = precondition.password.take() {
if !user.check_password(p)? {
bail_t!(FailedPrecondition, "password mismatch"); // or Unauthenticated?
}
}
if let Some(p) = precondition.permissions.take() {
if user.permissions != db::Permissions::from(p) {
bail_t!(FailedPrecondition, "permissions mismatch");
}
}
// Safety valve in case something is added to UserSubset and forgotten here.
if precondition != Default::default() {
bail_t!(
Unimplemented,
"preconditions not supported: {:#?}",
&precondition
);
}
}
if let Some(mut update) = r.update {
let mut change = user.change();
// First, set up updates which non-admins are allowed to perform on themselves.
if let Some(preferences) = update.preferences.take() {
change.config.preferences = preferences;
}
match update.password.take() {
None => {}
Some(None) => change.clear_password(),
Some(Some(p)) => change.set_password(p.to_owned()),
}
// Requires admin_users if there's anything else.
if update != Default::default() && !caller.permissions.admin_users {
bail_t!(Unauthenticated, "must have admin_users permission");
}
if let Some(n) = update.username.take() {
change.username = n.to_string();
}
if let Some(permissions) = update.permissions.take() {
change.permissions = permissions.into();
}
// Safety valve in case something is added to UserSubset and forgotten here.
if update != Default::default() {
bail_t!(Unimplemented, "updates not supported: {:#?}", &update);
}
// Then apply all together.
db.apply_user_change(change)?;
}
Ok(plain_response(StatusCode::NO_CONTENT, &b""[..]))
}
}
fn require_same_or_admin(caller: &Caller, id: i32) -> Result<(), base::Error> {
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"
);
}
Ok(())
}