Skip to main content

haste_repository/pg/
mfa.rs

1use crate::{
2    admin::TenantModelAdmin,
3    pg::{PGConnection, StoreError},
4    types::mfa::{
5        MFAKey, UserMFACredential, UserMFACredentialCreate, UserMFACredentialUpdate,
6        UserMFASearchClaims,
7    },
8};
9use haste_fhir_model::r4::generated::terminology::IssueType;
10use haste_fhir_operation_error::OperationOutcomeError;
11use haste_jwt::TenantId;
12use sqlx::{PgExecutor, QueryBuilder};
13
14async fn create_user_mfa_credential<'a, 'e, E>(
15    executor: E,
16    tenant: &'a TenantId,
17    new_mfa_credentials: UserMFACredentialCreate,
18) -> Result<UserMFACredential, OperationOutcomeError>
19where
20    E: PgExecutor<'e>,
21{
22    let type_: &str = new_mfa_credentials.credential_type.into();
23    let totp_algorithm = new_mfa_credentials
24        .totp_algorithm
25        .unwrap_or_else(|| "SHA1".to_string());
26
27    let user_mfa_credential = sqlx::query_as::<_, UserMFACredential>(
28        r"
29            INSERT INTO user_mfa_credential (tenant, user_id, credential_type, secret_ciphertext, secret_nonce, key_id, totp_algorithm, totp_digits, totp_period, totp_skew)
30            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
31            RETURNING
32                id::TEXT,
33                tenant,
34                user_id,
35                credential_type,
36                secret_ciphertext,
37                secret_nonce,
38                key_id,
39                totp_algorithm,
40                totp_digits,
41                totp_period,
42                totp_skew,
43                created_at,
44                is_active
45        ",
46    )
47    .bind(tenant.as_ref())
48    .bind(new_mfa_credentials.user_id.as_ref())
49    .bind(type_)
50    .bind(new_mfa_credentials.secret_ciphertext)
51    .bind(new_mfa_credentials.secret_nonce)
52    .bind(new_mfa_credentials.key_id)
53    .bind(totp_algorithm)
54    .bind(new_mfa_credentials.totp_digits.unwrap_or(6))
55    .bind(new_mfa_credentials.totp_period.unwrap_or(30))
56    .bind(new_mfa_credentials.totp_skew.unwrap_or(1))
57    .fetch_one(executor)
58    .await
59    .map_err(StoreError::SQLXError)?;
60
61    Ok(user_mfa_credential)
62}
63
64async fn read_user_mfa<'a, 'e, E>(
65    executor: E,
66    tenant: &'a TenantId,
67    key: &'a MFAKey,
68) -> Result<Option<UserMFACredential>, OperationOutcomeError>
69where
70    E: PgExecutor<'e>,
71{
72    let user_mfa = sqlx::query_as::<_, UserMFACredential>(
73        r"
74            SELECT
75                id::TEXT,
76                tenant,
77                user_id,
78                credential_type,
79                secret_ciphertext,
80                secret_nonce,
81                key_id,
82                totp_algorithm,
83                totp_digits,
84                totp_period,
85                totp_skew,
86                created_at,
87                is_active
88            FROM user_mfa_credential
89            WHERE tenant = $1 AND id::text = $2 AND user_id = $3
90        ",
91    )
92    .bind(tenant.as_ref())
93    .bind(&key.mfa_id().0)
94    .bind(key.user_id().as_ref())
95    .fetch_optional(executor)
96    .await
97    .map_err(StoreError::SQLXError)?;
98
99    Ok(user_mfa)
100}
101
102async fn delete_user_mfa<'a, 'e, E>(
103    executor: E,
104    tenant: &'a TenantId,
105    key: &'a MFAKey,
106) -> Result<(), OperationOutcomeError>
107where
108    E: PgExecutor<'e>,
109{
110    let rows_affected = sqlx::query(
111        r"
112            DELETE FROM user_mfa_credential
113            WHERE tenant = $1 AND id::text = $2 AND user_id = $3
114        ",
115    )
116    .bind(tenant.as_ref())
117    .bind(&key.mfa_id().0)
118    .bind(key.user_id().as_ref())
119    .execute(executor)
120    .await
121    .map_err(|_e| {
122        OperationOutcomeError::error(
123            IssueType::not_found(),
124            format!(
125                "User MFA credential '{}' not found or is system created and cannot be deleted.",
126                key.mfa_id().0
127            ),
128        )
129    })?
130    .rows_affected();
131
132    if rows_affected == 0 {
133        return Err(OperationOutcomeError::error(
134            IssueType::not_found(),
135            format!(
136                "User MFA credential '{}' not found or is system created and cannot be deleted.",
137                key.mfa_id().0
138            ),
139        ));
140    }
141
142    Ok(())
143}
144
145async fn search_user_mfa<'a, 'e, E>(
146    executor: E,
147    tenant: &'a TenantId,
148    clauses: &'a UserMFASearchClaims,
149) -> Result<Vec<UserMFACredential>, OperationOutcomeError>
150where
151    E: PgExecutor<'e>,
152{
153    let mut query_builder: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
154        r"SELECT
155            id::TEXT,
156            tenant,
157            user_id,
158            credential_type,
159            secret_ciphertext,
160            secret_nonce,
161            key_id,
162            totp_algorithm,
163            totp_digits,
164            totp_period,
165            totp_skew,
166            created_at,
167            is_active FROM user_mfa_credential WHERE ",
168    );
169
170    let mut and_clauses = query_builder.separated(" AND ");
171
172    and_clauses
173        .push(" tenant = ")
174        .push_bind_unseparated(tenant.as_ref());
175
176    and_clauses
177        .push(" user_id = ")
178        .push_bind_unseparated(clauses.user_id.as_ref());
179
180    if let Some(is_active) = clauses.is_active {
181        and_clauses
182            .push(" is_active = ")
183            .push_bind_unseparated(is_active);
184    }
185
186    let query = query_builder.build_query_as::<UserMFACredential>();
187
188    let user_mfas: Vec<UserMFACredential> =
189        query.fetch_all(executor).await.map_err(StoreError::from)?;
190
191    Ok(user_mfas)
192}
193
194async fn update_user_mfa<'a, 'e, E>(
195    executor: E,
196    tenant: &'a TenantId,
197    model: UserMFACredentialUpdate,
198) -> Result<UserMFACredential, OperationOutcomeError>
199where
200    E: PgExecutor<'e>,
201{
202    let mut query_builder = QueryBuilder::new(
203        r"
204            UPDATE user_mfa_credential SET
205        ",
206    );
207
208    let mut set_statements = query_builder.separated(", ");
209
210    set_statements
211        .push(" is_active = ")
212        .push_bind_unseparated(model.is_active);
213
214    query_builder.push(" WHERE ");
215
216    let mut where_statements = query_builder.separated(" AND ");
217    where_statements
218        .push(" tenant = ")
219        .push_bind_unseparated(tenant.as_ref())
220        .push(" id::text = ")
221        .push_bind_unseparated(model.id)
222        .push(" user_id = ")
223        .push_bind_unseparated(model.user_id.as_ref());
224
225    query_builder.push(
226        r" RETURNING
227            id::TEXT,
228            tenant,
229            user_id,
230            credential_type,
231            secret_ciphertext,
232            secret_nonce,
233            key_id,
234            totp_algorithm,
235            totp_digits,
236            totp_period,
237            totp_skew,
238            created_at,
239            is_active",
240    );
241
242    let query = query_builder.build_query_as::<UserMFACredential>();
243
244    let user_mfa_credentials = query
245        .fetch_one(executor)
246        .await
247        .map_err(StoreError::SQLXError)?;
248
249    Ok(user_mfa_credentials)
250}
251
252impl
253    TenantModelAdmin<
254        UserMFACredentialCreate,
255        UserMFACredential,
256        UserMFASearchClaims,
257        UserMFACredentialUpdate,
258        MFAKey,
259    > for PGConnection
260{
261    async fn create(
262        &self,
263        tenant: &TenantId,
264        new_user_mfa_credential: UserMFACredentialCreate,
265    ) -> Result<UserMFACredential, OperationOutcomeError> {
266        match self {
267            PGConnection::Pool(pool, _) => {
268                create_user_mfa_credential(pool, tenant, new_user_mfa_credential).await
269            }
270            PGConnection::Transaction(tx, _) => {
271                let mut tx = tx.lock().await;
272
273                create_user_mfa_credential(&mut **tx, tenant, new_user_mfa_credential).await
274            }
275        }
276    }
277
278    async fn read(
279        &self,
280        tenant: &TenantId,
281        id: &MFAKey,
282    ) -> Result<Option<UserMFACredential>, haste_fhir_operation_error::OperationOutcomeError> {
283        match self {
284            PGConnection::Pool(pool, _) => read_user_mfa(pool, tenant, id).await,
285            PGConnection::Transaction(tx, _) => {
286                let mut tx = tx.lock().await;
287                read_user_mfa(&mut **tx, tenant, id).await
288            }
289        }
290    }
291
292    async fn update(
293        &self,
294        tenant: &TenantId,
295        model: UserMFACredentialUpdate,
296    ) -> Result<UserMFACredential, haste_fhir_operation_error::OperationOutcomeError> {
297        match self {
298            PGConnection::Pool(pool, _) => update_user_mfa(pool, tenant, model).await,
299            PGConnection::Transaction(tx, _) => {
300                let mut tx = tx.lock().await;
301                update_user_mfa(&mut **tx, tenant, model).await
302            }
303        }
304    }
305
306    async fn delete(
307        &self,
308        tenant: &TenantId,
309        id: &MFAKey,
310    ) -> Result<(), haste_fhir_operation_error::OperationOutcomeError> {
311        match self {
312            PGConnection::Pool(pool, _) => delete_user_mfa(pool, tenant, id).await,
313            PGConnection::Transaction(tx, _) => {
314                let mut tx = tx.lock().await;
315                delete_user_mfa(&mut **tx, tenant, id).await
316            }
317        }
318    }
319
320    async fn search(
321        &self,
322        tenant: &TenantId,
323        claims: &UserMFASearchClaims,
324    ) -> Result<Vec<UserMFACredential>, OperationOutcomeError> {
325        match self {
326            PGConnection::Pool(pool, _) => search_user_mfa(pool, tenant, claims).await,
327            PGConnection::Transaction(tx, _) => {
328                let mut tx = tx.lock().await;
329                search_user_mfa(&mut **tx, tenant, claims).await
330            }
331        }
332    }
333}