Skip to main content

haste_health/commands/
admin.rs

1use clap::{Subcommand, ValueEnum};
2use haste_config::{Config, get_config};
3use haste_fhir_client::FHIRClient;
4use haste_fhir_model::r4::generated::{
5    resources::{
6        AccessPolicyV2, AccessPolicyV2Target, Bundle, BundleEntry, BundleEntryRequest,
7        ClientApplication, Resource,
8    },
9    terminology::{
10        AccessPolicyv2Engine, BundleType, ClientapplicationGrantType,
11        ClientapplicationResponseTypes, HttpVerb, IssueType, UserRole,
12    },
13    types::{FHIRString, FHIRUri, Reference},
14};
15use haste_fhir_operation_error::OperationOutcomeError;
16use haste_fhir_search::SearchEngine;
17use haste_jwt::{ProjectId, TenantId, claims::SubscriptionTier};
18use haste_repository::admin::Migrate;
19use haste_server::{
20    ServerEnvironmentVariables,
21    fhir_client::ServerCTX,
22    load_artifacts, services,
23    tenants::{create_tenant, create_user},
24};
25use std::sync::Arc;
26
27#[derive(Clone, Debug, ValueEnum)]
28pub enum UserSubscriptionChoice {
29    Free,
30    Professional,
31    Team,
32    Unlimited,
33}
34
35impl From<UserSubscriptionChoice> for SubscriptionTier {
36    fn from(choice: UserSubscriptionChoice) -> Self {
37        match choice {
38            UserSubscriptionChoice::Free => SubscriptionTier::Free,
39            UserSubscriptionChoice::Professional => SubscriptionTier::Professional,
40            UserSubscriptionChoice::Team => SubscriptionTier::Team,
41            UserSubscriptionChoice::Unlimited => SubscriptionTier::Unlimited,
42        }
43    }
44}
45
46#[derive(Subcommand, Debug)]
47pub enum ClientCommands {
48    Create {
49        #[arg(short, long)]
50        id: String,
51        #[arg(short, long)]
52        secret: String,
53        #[arg(short, long)]
54        tenant: String,
55        #[arg(short, long)]
56        project: String,
57    },
58}
59
60#[derive(Subcommand, Debug)]
61pub enum AdminCommands {
62    Tenant {
63        #[command(subcommand)]
64        command: TenantCommands,
65    },
66
67    User {
68        #[command(subcommand)]
69        command: UserCommands,
70    },
71
72    Client {
73        #[command(subcommand)]
74        command: ClientCommands,
75    },
76
77    Migrate {
78        #[command(subcommand)]
79        command: MigrationCommands,
80    },
81}
82
83#[derive(Subcommand, Debug)]
84pub enum MigrationCommands {
85    Artifacts {},
86    Repo {},
87    Search {},
88    All,
89}
90
91#[derive(Subcommand, Debug)]
92pub enum TenantCommands {
93    Create {
94        #[arg(short, long)]
95        id: String,
96        #[arg(short, long)]
97        subscription_tier: Option<UserSubscriptionChoice>,
98        #[arg(long)]
99        owner_email: String,
100        #[arg(long)]
101        owner_password: String,
102    },
103}
104
105#[derive(Subcommand, Debug)]
106pub enum UserCommands {
107    Create {
108        #[arg(short, long)]
109        email: String,
110        #[arg(short, long)]
111        password: String,
112        #[arg(short, long)]
113        tenant: String,
114    },
115}
116
117async fn migrate_repo(
118    config: Arc<dyn Config<ServerEnvironmentVariables>>,
119) -> Result<(), OperationOutcomeError> {
120    let services = services::create_services(config).await?;
121    services.repo.migrate().await?;
122    Ok(())
123}
124
125async fn migrate_search(
126    config: Arc<dyn Config<ServerEnvironmentVariables>>,
127) -> Result<(), OperationOutcomeError> {
128    let services = services::create_services(config).await?;
129    services
130        .search
131        .migrate(&haste_repository::types::SupportedFHIRVersions::R4)
132        .await?;
133    Ok(())
134}
135
136async fn migrate_artifacts(
137    config: Arc<dyn Config<ServerEnvironmentVariables>>,
138) -> Result<(), OperationOutcomeError> {
139    let initial = config
140        .get(ServerEnvironmentVariables::AllowArtifactMutations)
141        .unwrap_or("false".to_string());
142    config.set(
143        ServerEnvironmentVariables::AllowArtifactMutations,
144        "true".to_string(),
145    )?;
146    load_artifacts::load_artifacts(config.clone()).await?;
147    config.set(ServerEnvironmentVariables::AllowArtifactMutations, initial)?;
148    Ok(())
149}
150
151pub async fn admin(command: &AdminCommands) -> Result<(), OperationOutcomeError> {
152    let config = get_config::<ServerEnvironmentVariables>("environment".into());
153
154    match &command {
155        AdminCommands::Migrate { command } => match command {
156            MigrationCommands::Artifacts {} => migrate_artifacts(config).await,
157            MigrationCommands::Repo {} => migrate_repo(config).await,
158            MigrationCommands::Search {} => migrate_search(config).await,
159            MigrationCommands::All => {
160                migrate_repo(config.clone()).await?;
161                migrate_search(config.clone()).await?;
162                migrate_artifacts(config).await?;
163                Ok(())
164            }
165        },
166        AdminCommands::Tenant { command } => match command {
167            TenantCommands::Create {
168                id,
169                subscription_tier,
170                owner_email,
171                owner_password,
172            } => {
173                let services = services::create_services(config).await?;
174                let result = create_tenant(
175                    services.as_ref(),
176                    Some(id.clone()),
177                    id,
178                    &SubscriptionTier::from(
179                        subscription_tier
180                            .clone()
181                            .unwrap_or(UserSubscriptionChoice::Free),
182                    ),
183                    haste_fhir_model::r4::generated::resources::User {
184                        role: Box::new(UserRole::Owner(None)),
185                        email: Some(Box::new(
186                            haste_fhir_model::r4::generated::types::FHIRString {
187                                value: Some(owner_email.clone()),
188                                ..Default::default()
189                            },
190                        )),
191                        ..Default::default()
192                    },
193                    Some(owner_password),
194                )
195                .await;
196
197                if let Err(operation_outcome_error) = result.as_ref()
198                    && let Some(issue) = operation_outcome_error.outcome().issue.first()
199                    && matches!(issue.code.as_ref(), IssueType::Duplicate(None))
200                {
201                    println!("Tenant with ID '{}' already exists.", id);
202                    return Ok(());
203                }
204
205                result?;
206
207                Ok(())
208            }
209        },
210        AdminCommands::User { command } => match command {
211            UserCommands::Create {
212                email,
213                password,
214                tenant,
215            } => {
216                let services = services::create_services(config)
217                    .await?
218                    .transaction()
219                    .await?;
220
221                let tenant = TenantId::new(tenant.clone());
222
223                create_user(
224                    &services,
225                    &tenant,
226                    haste_fhir_model::r4::generated::resources::User {
227                        role: Box::new(UserRole::Admin(None)),
228                        email: Some(Box::new(
229                            haste_fhir_model::r4::generated::types::FHIRString {
230                                value: Some(email.clone()),
231                                ..Default::default()
232                            },
233                        )),
234                        ..Default::default()
235                    },
236                    Some(password),
237                )
238                .await?;
239
240                services.commit().await?;
241
242                Ok(())
243            }
244        },
245        AdminCommands::Client { command } => match command {
246            ClientCommands::Create {
247                tenant,
248                project,
249                id,
250                secret,
251            } => {
252                let services = services::create_services(config).await?;
253
254                let ctx = Arc::new(ServerCTX::system(
255                    TenantId::new(tenant.clone()),
256                    ProjectId::new(project.clone()),
257                    services.fhir_client.clone(),
258                    services.rate_limit.clone(),
259                ));
260
261                let transaction_bundle = Bundle {
262                    type_: Box::new(BundleType::Transaction(None)),
263                    entry: Some(vec![
264                        BundleEntry {
265                            fullUrl: Some(Box::new(FHIRUri {
266                                value: Some("access-policy".to_string()),
267                                ..Default::default()
268                            })),
269                            request: Some(BundleEntryRequest {
270                                method: Box::new(HttpVerb::POST(None)),
271                                url: Box::new(FHIRUri {
272                                    value: Some("AccessPolicyV2".to_string()),
273                                    ..Default::default()
274                                }),
275                                ..Default::default()
276                            }),
277                            resource: Some(Box::new(Resource::AccessPolicyV2(AccessPolicyV2 {
278                                name: Box::new(FHIRString {
279                                    value: Some("ADMIN".to_string()),
280                                    ..Default::default()
281                                }),
282                                engine: Box::new(AccessPolicyv2Engine::FullAccess(None)),
283                                target: Some(vec![AccessPolicyV2Target {
284                                    link: Box::new(Reference {
285                                        reference: Some(Box::new(FHIRString {
286                                            value: Some("client-app".to_string()),
287                                            ..Default::default()
288                                        })),
289                                        ..Default::default()
290                                    }),
291                                }]),
292                                ..Default::default()
293                            }))),
294                            ..Default::default()
295                        },
296                        BundleEntry {
297                            fullUrl: Some(Box::new(FHIRUri {
298                                value: Some("client-app".to_string()),
299                                ..Default::default()
300                            })),
301                            request: Some(BundleEntryRequest {
302                                method: Box::new(HttpVerb::PUT(None)),
303                                url: Box::new(FHIRUri {
304                                    value: Some(format!("ClientApplication/{}", id)),
305                                    ..Default::default()
306                                }),
307                                ..Default::default()
308                            }),
309                            resource: Some(Box::new(Resource::ClientApplication(
310                                ClientApplication {
311                                    id: Some(id.clone()),
312                                    secret: Some(Box::new(FHIRString {
313                                        value: Some(secret.clone()),
314                                        ..Default::default()
315                                    })),
316
317                                    scope: Some(Box::new(FHIRString {
318                                        value: Some("openid system/*.*".to_string()),
319                                        ..Default::default()
320                                    })),
321
322                                    name: Box::new(FHIRString {
323                                        value: Some("CLI".to_string()),
324                                        ..Default::default()
325                                    }),
326
327                                    grantType: vec![Box::new(
328                                        ClientapplicationGrantType::Client_credentials(None),
329                                    )],
330
331                                    responseTypes: Box::new(ClientapplicationResponseTypes::Token(
332                                        None,
333                                    )),
334
335                                    ..Default::default()
336                                },
337                            ))),
338                            ..Default::default()
339                        },
340                    ]),
341                    ..Default::default()
342                };
343
344                services
345                    .fhir_client
346                    .transaction(ctx, transaction_bundle)
347                    .await?;
348
349                Ok(())
350            }
351        },
352    }
353}