//! `/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 and its entry. //! * `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`. //! * `DELETE /admin/users/{name}/tokens`: every token of the user no longer taken //! (`login::revoke_tokens`). //! * `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. //! //! A user manages its own keys (`keys`, `authorized_keys` lines), whatever it may do //! otherwise: //! //! * `GET /auth/keys`: the caller's keys, its entry, and the envelope holding it under a //! strict policy (`signed`), to sign the next one against. //! * `PUT /auth/keys/{id}`: one added or replaced (the line as the body); //! `DELETE /auth/keys/{id}` removes one, the last only of a user with a password on //! this node (`--htpasswd`, `password` in `GET /auth/keys`), which logs in by it. Under //! a strict policy `409`: the user signs its entry with the keys changed, nothing else //! (`continuum-signed key-add`). //! //! 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 crate::document::signatures::Sign; 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 { /// As a capability, granted `by` at `since`. pub(crate) fn capability( &self, by: &str, since: u64, ) -> Result { let rights = self.rights()?; Ok(crate::access::Capability { path: self.path.clone(), rights: rights .iter() .map(|right| right.as_str().to_owned()) .collect(), by: by.to_owned(), since, }) } 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, with its name. fn shown(doc: &impl crate::document::ReadDoc, name: &str) -> Option { let mut entry = store::read_plain(doc, ®istry::user_path(name)).ok()??; if let Some(object) = entry.as_object_mut() { object.insert("name".to_owned(), json!(name)); } Some(entry) } /// `GET /auth/keys`. pub(crate) async fn own_keys( State(state): State, Extension(user): Extension, Extension(auth): Extension, ) -> Result, ApiError> { let replica = state.doc.read().await; let path = registry::user_path(&user.0); let entry = store::read_plain(&replica.doc, &path) .ok() .flatten() .ok_or_else(|| ApiError(StatusCode::NOT_FOUND, Some("no entry of yours".to_owned())))?; let signed = match store::read_path(&replica.doc, &path) { Ok(Some(JsonValue::String(text))) if text.starts_with("cv1.") => Some(text), _ => None, }; Ok(Json(json!({ "keys": entry.get("keys").cloned().unwrap_or_else(|| json!({})), "entry": entry, "signed": signed, "password": auth.has_password(&user.0), }))) } /// `PUT /auth/keys/{id}`. pub(crate) async fn put_own_key( State(state): State, Extension(user): Extension, Path(id): Path, body: String, ) -> Result { let line = crate::access::sshsig::PublicKey::kept_line(body.trim()).map_err(|why| bad(&why))?; own_key(&state, &user.0, id, Some(line), false).await } /// `DELETE /auth/keys/{id}`. pub(crate) async fn delete_own_key( State(state): State, Extension(user): Extension, Extension(auth): Extension, Path(id): Path, ) -> Result { let password = auth.has_password(&user.0); own_key(&state, &user.0, id, None, password).await } /// `user`'s key `id` set to `line`, or removed: one change, as `user`. Its last key only /// with `password` (it has one here, and logs in by it). async fn own_key( state: &AppState, user: &str, id: String, line: Option, password: bool, ) -> Result { if !registry::valid_name(&id) { return Err(bad("not a key's name: 1 to 64 of A-Z a-z 0-9 . _ @ -")); } if !state.is_ready().await { return Err(StatusCode::SERVICE_UNAVAILABLE.into()); } let entry = registry::user_path(user); let mut path = entry.clone(); path.extend(["keys".to_owned(), id.clone()]); let adding = line.is_some(); let id_kept = id.clone(); state .mutate_doc_for(Some(user.to_owned()), Sign::Protocol, move |doc| { if crate::access::policy::of(&*doc).strict() { return Err(ApiError( StatusCode::CONFLICT, Some( "users sign their entries (strict policy): sign yours with the keys \ changed (continuum-signed key-add, key-remove)" .to_owned(), ), )); } if store::read_path(&*doc, &entry)?.is_none() { return Err(ApiError( StatusCode::NOT_FOUND, Some("no entry of yours".to_owned()), )); } if line.is_none() && !password { let mut keys = entry.clone(); keys.push("keys".to_owned()); let others = store::keys_at(&*doc, &keys)? .unwrap_or_default() .into_iter() .filter(|other| *other != id_kept) .count(); if others == 0 { return Err(ApiError( StatusCode::CONFLICT, Some( "your last key, and you have no password on this node: add \ another first" .to_owned(), ), )); } } match line { Some(line) => store::write_path(doc, &path, &json!(line))?, None => match store::delete_path(doc, &path, false) { Ok(()) => {} Err(crate::document::store::StoreError::NotFound) => { return Err(ApiError(StatusCode::NOT_FOUND, None)); } Err(error) => return Err(error.into()), }, } Ok(()) }) .await?; info!(user = %user, key = %id, "{}", if adding { "Set own key" } else { "Removed own key" }); Ok(StatusCode::NO_CONTENT) } /// `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()), Sign::Protocol, move |doc| { if crate::access::policy::of(&*doc).strict() { return Err(ApiError( StatusCode::CONFLICT, Some( "users sign their entries (strict policy): PUT each signed to \ /store/_continuum/users/ (docs/access.md)" .to_owned(), ), )); } 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) } /// `DELETE /admin/users/{name}/tokens`: every token of the user, of every login, no /// longer taken (`login::revoke_tokens`). pub(crate) async fn revoke_tokens( State(state): State, Extension(user): Extension, Extension(access): Extension, Path(name): Path, ) -> Result { may_manage(&state, &user.0, &access)?; super::login::revoke_tokens(&state, &name, &user.0).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) }