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

@@ -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""[..]))