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.
This commit is contained in:
Scott Lamb
2023-01-05 12:11:28 -06:00
parent 42fe054d46
commit dfa949815b
6 changed files with 162 additions and 67 deletions

View File

@@ -9,7 +9,6 @@ use db::auth::SessionHash;
use failure::{format_err, Error};
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::BTreeMap;
use std::ops::Not;
use uuid::Uuid;
@@ -123,12 +122,15 @@ pub struct LoginRequest<'a> {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogoutRequest<'a> {
#[serde(borrow)]
pub csrf: &'a str,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PostSignalsRequest {
pub struct PostSignalsRequest<'a> {
#[serde(borrow)]
pub csrf: Option<&'a str>,
pub signal_ids: Vec<u32>,
pub states: Vec<u16>,
pub start: PostSignalsTimeBase,
@@ -516,15 +518,33 @@ pub struct ToplevelUser {
pub session: Option<Session>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct PutUsers<'a> {
#[serde(borrow)]
pub csrf: Option<&'a str>,
pub user: UserSubset<'a>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct PostUser<'a> {
#[serde(borrow)]
pub csrf: Option<&'a str>,
pub update: Option<UserSubset<'a>>,
pub precondition: Option<UserSubset<'a>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct DeleteUser<'a> {
#[serde(borrow)]
pub csrf: Option<&'a str>,
}
#[derive(Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
@@ -598,7 +618,13 @@ impl From<db::schema::Permissions> for Permissions {
/// Response to `GET /api/users/`.
#[derive(Serialize)]
pub struct GetUsersResponse {
pub users: BTreeMap<i32, String>,
pub users: Vec<UserSummary>,
}
#[derive(Serialize)]
pub struct UserSummary {
pub id: i32,
pub username: String,
}
/// Response to `PUT /api/users/`.

View File

@@ -153,6 +153,16 @@ async fn extract_json_body(req: &mut Request<hyper::Body>) -> Result<Bytes, Http
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}", e)))
}
fn require_csrf_if_session(caller: &Caller, csrf: Option<&str>) -> Result<(), base::Error> {
match (csrf, caller.user.as_ref().and_then(|u| u.session.as_ref())) {
(None, Some(_)) => bail_t!(Unauthenticated, "csrf must be supplied"),
(Some(csrf), Some(session)) if !csrf_matches(csrf, session.csrf) => {
bail_t!(Unauthenticated, "incorrect csrf");
}
(_, _) => Ok(()),
}
}
pub struct Config<'a> {
pub db: Arc<db::Database>,
pub ui_dir: Option<&'a std::path::Path>,

View File

@@ -12,8 +12,8 @@ use url::form_urlencoded;
use crate::json;
use super::{
bad_req, extract_json_body, from_base_error, plain_response, serve_json, Caller,
ResponseResult, Service,
bad_req, extract_json_body, from_base_error, plain_response, require_csrf_if_session,
serve_json, Caller, ResponseResult, Service,
};
use std::borrow::Borrow;
@@ -42,6 +42,7 @@ impl Service {
let r = extract_json_body(&mut req).await?;
let r: json::PostSignalsRequest =
serde_json::from_slice(&r).map_err(|e| bad_req(e.to_string()))?;
require_csrf_if_session(&caller, r.csrf)?;
let now = recording::Time::new(self.db.clocks().realtime());
let mut l = self.db.lock();
let start = match r.start {

View File

@@ -7,11 +7,11 @@
use base::{bail_t, format_err_t};
use http::{Method, Request, StatusCode};
use crate::json::{self, PutUsersResponse, UserSubset};
use crate::json::{self, PutUsersResponse, UserSubset, UserSummary};
use super::{
bad_req, csrf_matches, extract_json_body, plain_response, serve_json, Caller, ResponseResult,
Service,
bad_req, extract_json_body, plain_response, require_csrf_if_session, serve_json, Caller,
ResponseResult, Service,
};
impl Service {
@@ -34,7 +34,10 @@ impl Service {
.lock()
.users_by_id()
.iter()
.map(|(&id, user)| (id, user.username.clone()))
.map(|(&id, user)| UserSummary {
id,
username: user.username.clone(),
})
.collect();
serve_json(&req, &json::GetUsersResponse { users })
}
@@ -44,23 +47,25 @@ impl Service {
bail_t!(Unauthenticated, "must have admin_users permission");
}
let r = extract_json_body(&mut req).await?;
let mut r: json::UserSubset =
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.password.take() {
if let Some(Some(pwd)) = r.user.password.take() {
change.set_password(pwd.to_owned());
}
if let Some(preferences) = r.preferences.take() {
if let Some(preferences) = r.user.preferences.take() {
change.config.preferences = preferences;
}
if let Some(permissions) = r.permissions.take() {
if let Some(permissions) = r.user.permissions.take() {
change.permissions = permissions.into();
}
if r != Default::default() {
if r.user != Default::default() {
bail_t!(Unimplemented, "unsupported user fields: {:#?}", r);
}
let mut l = self.db.lock();
@@ -76,7 +81,7 @@ impl Service {
) -> ResponseResult {
match *req.method() {
Method::GET | Method::HEAD => self.get_user(req, caller, id).await,
Method::DELETE => self.delete_user(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,
@@ -106,10 +111,18 @@ impl Service {
serve_json(&req, &out)
}
async fn delete_user(&self, caller: Caller, id: i32) -> ResponseResult {
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""[..]))
@@ -137,13 +150,7 @@ impl Service {
"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");
}
(_, _) => {}
}
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");