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