server/command/
ssh_key.rs1use chrono::{DateTime, Utc};
9use serde::Serialize;
10use sqlx::Connection;
11use sqlx::types::Uuid;
12use std::fmt::Debug;
13use supp_macro::command;
14
15use super::{CmdError, CmdResult};
16use crate::{config::ConfigError, db::get_connection};
17
18async fn require_verified(conn: &mut sqlx::PgConnection, user_id: Uuid) -> Result<(), CmdError> {
23 match sqlx::query_file_scalar!("sql/select/users/verified.sql", user_id)
24 .fetch_optional(conn)
25 .await?
26 {
27 Some(true) => Ok(()),
28 _ => Err(CmdError::Disabled),
29 }
30}
31
32#[derive(Debug, Clone, Serialize)]
36pub struct SshKeyRecord {
37 pub id: Uuid,
38 pub user_id: Uuid,
39 pub key_type: String,
40 pub key_blob: Vec<u8>,
41 pub fingerprint: String,
42 pub annotation: String,
43 pub created_at: DateTime<Utc>,
44 pub last_used_at: Option<DateTime<Utc>>,
45}
46
47command! {
53 AddSshKey {
54 #[required]
55 user_id: Uuid,
56 #[required]
57 key_type: String,
58 #[required]
59 key_blob: Vec<u8>,
60 #[required]
61 fingerprint: String,
62 #[optional]
63 annotation: String,
64 } => {
65 let mut conn = get_connection().await.map_err(|err| {
66 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
67 ConfigError::DB
68 })?;
69 let mut tx = conn.begin().await?;
70 require_verified(&mut tx, user_id).await?;
71 let annotation = annotation.unwrap_or_default();
72 let row = sqlx::query_file!(
73 "sql/insert/ssh_keys/add.sql",
74 user_id,
75 key_type,
76 key_blob,
77 fingerprint,
78 annotation,
79 )
80 .fetch_one(&mut *tx)
81 .await
82 .map_err(refuse_a_key_held_elsewhere)?;
83 sqlx::query_file!("sql/update/users/set_ssh_enabled.sql", user_id, true)
84 .execute(&mut *tx)
85 .await?;
86 tx.commit().await?;
87 Ok(Some(CmdResult::Uuid(row.id)))
88 }
89}
90
91fn refuse_a_key_held_elsewhere(error: sqlx::Error) -> CmdError {
96 if let sqlx::Error::Database(db) = &error
97 && db.is_unique_violation()
98 && db.constraint() == Some("user_ssh_keys_fingerprint_key")
99 {
100 return CmdError::Args(
101 t!("this SSH key is already registered to another account").to_string(),
102 );
103 }
104 CmdError::DB(error)
105}
106
107command! {
108 ListSshKeys {
109 #[required]
110 user_id: Uuid,
111 } => {
112 let mut conn = get_connection().await.map_err(|err| {
113 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
114 ConfigError::DB
115 })?;
116 let rows = sqlx::query_file!(
117 "sql/select/ssh_keys/list_for_user.sql",
118 user_id
119 )
120 .fetch_all(&mut *conn)
121 .await?;
122 let keys = rows
123 .into_iter()
124 .map(|r| SshKeyRecord {
125 id: r.id,
126 user_id: r.user_id,
127 key_type: r.key_type,
128 key_blob: r.key_blob,
129 fingerprint: r.fingerprint,
130 annotation: r.annotation,
131 created_at: r.created_at,
132 last_used_at: r.last_used_at,
133 })
134 .collect();
135 Ok(Some(CmdResult::SshKeys(keys)))
136 }
137}
138
139command! {
144 RemoveSshKey {
145 #[required]
146 user_id: Uuid,
147 #[required]
148 fingerprint: String,
149 } => {
150 let mut conn = get_connection().await.map_err(|err| {
151 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
152 ConfigError::DB
153 })?;
154 let mut tx = conn.begin().await?;
155 require_verified(&mut tx, user_id).await?;
156 sqlx::query_file!(
157 "sql/delete/ssh_keys/by_fingerprint.sql",
158 user_id,
159 fingerprint
160 )
161 .execute(&mut *tx)
162 .await?;
163 let remaining = sqlx::query_file!(
164 "sql/select/ssh_keys/count_for_user.sql",
165 user_id
166 )
167 .fetch_one(&mut *tx)
168 .await?
169 .count
170 .unwrap_or(0);
171 if remaining == 0 {
172 sqlx::query_file!("sql/update/users/set_ssh_enabled.sql", user_id, false)
173 .execute(&mut *tx)
174 .await?;
175 }
176 tx.commit().await?;
177 Ok(Some(CmdResult::Bool(true)))
178 }
179}
180
181command! {
186 UserHasSshKey {
187 #[required]
188 user_id: Uuid,
189 } => {
190 let mut conn = get_connection().await.map_err(|err| {
191 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
192 ConfigError::DB
193 })?;
194 let count = sqlx::query_file!(
195 "sql/select/ssh_keys/count_for_user.sql",
196 user_id
197 )
198 .fetch_one(&mut *conn)
199 .await?
200 .count
201 .unwrap_or(0);
202 Ok(Some(CmdResult::Bool(count > 0)))
203 }
204}
205
206command! {
211 LookupUserBySshKey {
212 #[required]
213 fingerprint: String,
214 } => {
215 let mut conn = get_connection().await.map_err(|err| {
216 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
217 ConfigError::DB
218 })?;
219 let Some(row) = sqlx::query_file!(
221 "sql/select/ssh_keys/lookup_user.sql",
222 fingerprint
223 )
224 .fetch_optional(&mut *conn)
225 .await?
226 else {
227 return Ok(None);
228 };
229 if !row.ssh_enabled || !row.verified {
230 return Ok(None);
231 }
232 sqlx::query_file!(
233 "sql/update/ssh_keys/touch_last_used.sql",
234 fingerprint,
235 row.user_id
236 )
237 .execute(&mut *conn)
238 .await?;
239 Ok(Some(CmdResult::Uuid(row.user_id)))
240 }
241}
242
243#[cfg(test)]
244mod command_tests;