//! `/admin/users`: the swarm's users and what each may do (`access.md`), managed //! through the API rather than by writing the registry's entries through the store. //! //! * `GET /admin/users`: every user, its entry and whether it may manage users. //! * `GET /admin/users/{name}`: one. //! * `POST /admin/users/{name}`: adds a user, with the capabilities of the body //! (`{"caps": [{"path": [...], "rights": ["read", "write"]}, …]}`, none if absent); //! `409` if it exists. //! * `DELETE /admin/users/{name}`: removes a user (`disabled` keeps who wrote what //! readable: usually the better choice). //! * `PUT /admin/users/{name}/disabled`: `true` or `false`. //! * `POST /admin/users/{name}/caps`: grants a capability (`{"path": […], "rights": //! […]}`), answered with its ID; `DELETE /admin/users/{name}/caps/{id}` revokes one. //! * `POST /admin/users`: adds several at once, by name, each with the capabilities //! `POST /admin/users/{name}` takes (`{"alice": {"caps": […]}, "bob": {…}}`); `409` if //! one exists. How a swarm without a registry gets one, the caller among those who //! may manage users. //! //! Looking needs `read` on `_continuum/users`, changing `write` on it (in a swarm //! without a registry, being in `--protocol-writers`). Every change is one change to //! the document, logged, named by who made it, and refused (`409`) where it would lock //! its maker out (`access::registry_kept`). use axum::{ Extension, Json, extract::{Path, State}, http::StatusCode, response::{IntoResponse, Response}, }; use serde::Deserialize; use serde_json::{Value as JsonValue, json}; use tracing::info; use super::ApiError; use super::access::{Access, Before, registry_kept}; use super::auth::AuthenticatedUser; use crate::access::{Right, registry}; use crate::document::store; use crate::state::AppState; /// A capability as a request gives it. #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub(crate) struct CapRequest { path: Vec, rights: Vec, } impl CapRequest { fn rights(&self) -> Result, ApiError> { if self.rights.is_empty() { return Err(bad("a capability gives at least one right")); } self.rights .iter() .map(|right| match right.as_str() { "read" => Ok(Right::Read), "write" => Ok(Right::Write), other => Err(bad(&format!("no right {other:?}: read or write"))), }) .collect() } } #[derive(Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] pub(crate) struct AddRequest { #[serde(default)] caps: Vec, } fn bad(message: &str) -> ApiError { ApiError(StatusCode::BAD_REQUEST, Some(message.to_owned())) } fn named(name: &str) -> Result<(), ApiError> { if registry::valid_name(name) { Ok(()) } else { Err(bad("not a user name: 1 to 64 of A-Z a-z 0-9 . _ @ -")) } } /// `403` unless the caller may change the registry: `write` on `_continuum/users`, or /// in a swarm without a registry, being in `--protocol-writers` (as for writes to /// protocol data through the store). fn may_manage(state: &AppState, user: &str, access: &Access) -> Result<(), ApiError> { let allowed = match access { Access::Everything => state.protocol_writers.contains(user), Access::Caps(caps) => caps.allows(Right::Write, ®istry::users_dir()), }; if allowed { Ok(()) } else { Err(ApiError( StatusCode::FORBIDDEN, Some("may not manage users (docs/access.md)".to_owned()), )) } } /// A user's entry as the API shows it: as recorded, and whether it may manage users. fn shown(doc: &impl crate::document::ReadDoc, name: &str) -> Option { let mut entry = store::read_path(doc, ®istry::user_path(name)).ok()??; let manages = registry::caps_of(doc, name).allows(Right::Write, ®istry::users_dir()); if let Some(object) = entry.as_object_mut() { object.insert("name".to_owned(), json!(name)); object.insert("manages".to_owned(), json!(manages)); } Some(entry) } /// `GET /admin/users`. pub(crate) async fn list(State(state): State) -> Json { let replica = state.doc.read().await; let users: Vec = registry::names(&replica.doc) .iter() .filter_map(|name| shown(&replica.doc, name)) .collect(); Json(json!({ "registry": registry::exists(&replica.doc), "users": users })) } /// `GET /admin/users/{name}`. pub(crate) async fn get( State(state): State, Path(name): Path, ) -> Result, ApiError> { let replica = state.doc.read().await; shown(&replica.doc, &name) .map(Json) .ok_or(ApiError(StatusCode::NOT_FOUND, None)) } /// Writes to the registry what `f` writes, as `user`, logged as `what`: one change, /// dropped whole if it fails or would lock `user` out (`registry_kept`). async fn change( state: &AppState, user: &str, what: String, f: impl FnOnce(&mut crate::document::Transaction<'_>, &str, u64) -> Result<(), ApiError> + Send + 'static, ) -> Result<(), ApiError> { if !state.is_ready().await { return Err(StatusCode::SERVICE_UNAVAILABLE.into()); } let by = format!("user:{user}"); let writer = user.to_owned(); let since = state.clock.now_millis().as_u64(); state .mutate_doc_for(Some(user.to_owned()), move |doc| { let before = Before::of(&*doc, &writer); f(doc, &by, since)?; registry_kept(&*doc, ®istry::users_dir(), before, &writer) }) .await?; info!(user = %user, "{what}"); Ok(()) } /// `POST /admin/users/{name}`. pub(crate) async fn add( State(state): State, Extension(user): Extension, Extension(access): Extension, Path(name): Path, body: Option>, ) -> Result { may_manage(&state, &user.0, &access)?; named(&name)?; let request = body.map(|Json(request)| request).unwrap_or_default(); let caps: Vec<(Vec, Vec)> = request .caps .iter() .map(|cap| Ok((cap.path.clone(), cap.rights()?))) .collect::>()?; let path = registry::user_path(&name); change( &state, &user.0, format!("Added user {name}"), move |doc, by, since| { if store::read_path(&*doc, &path)?.is_some() { return Err(ApiError( StatusCode::CONFLICT, Some("the user exists".to_owned()), )); } let caps: Vec<(Vec, &[Right])> = caps .iter() .map(|(path, rights)| (path.clone(), rights.as_slice())) .collect(); store::write_path(doc, &path, ®istry::entry(by, since, &caps))?; Ok(()) }, ) .await?; Ok(StatusCode::CREATED.into_response()) } /// `DELETE /admin/users/{name}`. pub(crate) async fn remove( State(state): State, Extension(user): Extension, Extension(access): Extension, Path(name): Path, ) -> Result { may_manage(&state, &user.0, &access)?; let path = registry::user_path(&name); change( &state, &user.0, format!("Removed user {name}"), move |doc, _, _| { store::delete_path(doc, &path, false)?; Ok(()) }, ) .await?; Ok(StatusCode::NO_CONTENT) } /// `PUT /admin/users/{name}/disabled`. pub(crate) async fn set_disabled( State(state): State, Extension(user): Extension, Extension(access): Extension, Path(name): Path, Json(disabled): Json, ) -> Result { may_manage(&state, &user.0, &access)?; let entry = registry::user_path(&name); let what = format!( "{} user {name}", if disabled { "Disabled" } else { "Enabled" } ); change(&state, &user.0, what, move |doc, _, _| { if store::read_path(&*doc, &entry)?.is_none() { return Err(ApiError(StatusCode::NOT_FOUND, None)); } let mut path = entry; path.push("disabled".to_owned()); store::write_path(doc, &path, &json!(disabled))?; Ok(()) }) .await?; Ok(StatusCode::NO_CONTENT) } /// `POST /admin/users/{name}/caps`. pub(crate) async fn grant( State(state): State, Extension(user): Extension, Extension(access): Extension, Path(name): Path, Json(cap): Json, ) -> Result { may_manage(&state, &user.0, &access)?; let rights = cap.rights()?; let id = registry::new_id(); let entry = registry::user_path(&name); let mut path = entry.clone(); path.extend(["caps".to_owned(), id.clone()]); let what = format!("Granted user {name} {:?} on {:?}", cap.rights, cap.path); change(&state, &user.0, what, move |doc, by, since| { if store::read_path(&*doc, &entry)?.is_none() { return Err(ApiError(StatusCode::NOT_FOUND, None)); } store::write_path( doc, &path, ®istry::capability(by, since, &cap.path, &rights), )?; Ok(()) }) .await?; Ok((StatusCode::CREATED, Json(json!({ "id": id }))).into_response()) } /// `DELETE /admin/users/{name}/caps/{id}`. pub(crate) async fn revoke( State(state): State, Extension(user): Extension, Extension(access): Extension, Path((name, id)): Path<(String, String)>, ) -> Result { may_manage(&state, &user.0, &access)?; let mut path = registry::user_path(&name); path.extend(["caps".to_owned(), id.clone()]); change( &state, &user.0, format!("Revoked capability {id} of user {name}"), move |doc, _, _| { store::delete_path(doc, &path, false)?; Ok(()) }, ) .await?; Ok(StatusCode::NO_CONTENT) } /// `POST /admin/users`. pub(crate) async fn add_several( State(state): State, Extension(user): Extension, Extension(access): Extension, Json(request): Json>, ) -> Result { may_manage(&state, &user.0, &access)?; if request.is_empty() { return Err(bad("nobody to add")); } let mut users = Vec::new(); for (name, add) in &request { named(name)?; let caps: Vec<(Vec, Vec)> = add .caps .iter() .map(|cap| Ok((cap.path.clone(), cap.rights()?))) .collect::>()?; users.push((name.clone(), caps)); } let what = format!("Added users {:?}", request.keys().collect::>()); change(&state, &user.0, what, move |doc, by, since| { for (name, caps) in &users { let path = registry::user_path(name); if store::read_path(&*doc, &path)?.is_some() { return Err(ApiError( StatusCode::CONFLICT, Some(format!("user {name} exists")), )); } let caps: Vec<(Vec, &[Right])> = caps .iter() .map(|(path, rights)| (path.clone(), rights.as_slice())) .collect(); store::write_path(doc, &path, ®istry::entry(by, since, &caps))?; } Ok(()) }) .await?; Ok(StatusCode::CREATED) }