aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/users.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/users.rs')
-rw-r--r--src/api/users.rs103
1 files changed, 103 insertions, 0 deletions
diff --git a/src/api/users.rs b/src/api/users.rs
index 8972566..122fc4b 100644
--- a/src/api/users.rs
+++ b/src/api/users.rs
@@ -18,6 +18,15 @@
//! 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. 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
@@ -129,6 +138,100 @@ fn shown(doc: &impl crate::document::ReadDoc, name: &str) -> Option<JsonValue> {
Some(entry)
}
+/// `GET /auth/keys`.
+pub(crate) async fn own_keys(
+ State(state): State<AppState>,
+ Extension(user): Extension<AuthenticatedUser>,
+) -> Result<Json<JsonValue>, 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,
+ })))
+}
+
+/// `PUT /auth/keys/{id}`.
+pub(crate) async fn put_own_key(
+ State(state): State<AppState>,
+ Extension(user): Extension<AuthenticatedUser>,
+ Path(id): Path<String>,
+ body: String,
+) -> Result<StatusCode, ApiError> {
+ let key = crate::access::sshsig::PublicKey::parse(body.trim()).map_err(|why| bad(&why))?;
+ own_key(&state, &user.0, id, Some(key.line())).await
+}
+
+/// `DELETE /auth/keys/{id}`.
+pub(crate) async fn delete_own_key(
+ State(state): State<AppState>,
+ Extension(user): Extension<AuthenticatedUser>,
+ Path(id): Path<String>,
+) -> Result<StatusCode, ApiError> {
+ own_key(&state, &user.0, id, None).await
+}
+
+/// `user`'s key `id` set to `line`, or removed: one change, as `user`.
+async fn own_key(
+ state: &AppState,
+ user: &str,
+ id: String,
+ line: Option<String>,
+) -> Result<StatusCode, ApiError> {
+ 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();
+ 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()),
+ ));
+ }
+ 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<AppState>) -> Json<JsonValue> {
let replica = state.doc.read().await;