Skip to main content

systemprompt_cli/commands/admin/config/
security.rs

1use anyhow::{Context, Result, bail};
2use clap::{Args, Subcommand};
3use std::fs;
4use systemprompt_config::ProfileBootstrap;
5use systemprompt_logging::CliService;
6use systemprompt_models::Profile;
7
8use super::types::{SecurityConfigOutput, SecuritySetOutput};
9use crate::CliConfig;
10use crate::cli_settings::OutputFormat;
11use crate::shared::{CommandResult, render_result};
12
13#[derive(Debug, Subcommand)]
14pub enum SecurityCommands {
15    #[command(about = "Show security configuration")]
16    Show,
17
18    #[command(about = "Set security configuration value")]
19    Set(SetArgs),
20}
21
22#[derive(Debug, Clone, Args)]
23pub struct SetArgs {
24    #[arg(long, help = "JWT issuer")]
25    pub jwt_issuer: Option<String>,
26
27    #[arg(long, help = "Access token expiry in seconds")]
28    pub access_expiry: Option<i64>,
29
30    #[arg(long, help = "Refresh token expiry in seconds")]
31    pub refresh_expiry: Option<i64>,
32}
33
34pub fn execute(command: &SecurityCommands, config: &CliConfig) -> Result<()> {
35    match command {
36        SecurityCommands::Show => execute_show(),
37        SecurityCommands::Set(args) => execute_set(args, config),
38    }
39}
40
41fn execute_show() -> Result<()> {
42    let profile = ProfileBootstrap::get()?;
43
44    let output = SecurityConfigOutput {
45        jwt_issuer: profile.security.issuer.clone(),
46        access_token_expiry_seconds: profile.security.access_token_expiration,
47        refresh_token_expiry_seconds: profile.security.refresh_token_expiration,
48        audiences: profile
49            .security
50            .audiences
51            .iter()
52            .map(ToString::to_string)
53            .collect(),
54    };
55
56    render_result(&CommandResult::card(output).with_title("Security Configuration"));
57
58    Ok(())
59}
60
61fn execute_set(args: &SetArgs, config: &CliConfig) -> Result<()> {
62    if args.jwt_issuer.is_none() && args.access_expiry.is_none() && args.refresh_expiry.is_none() {
63        bail!("Must specify at least one option: --jwt-issuer, --access-expiry, --refresh-expiry");
64    }
65
66    let profile_path = ProfileBootstrap::get_path()?;
67    let mut profile = load_profile(profile_path)?;
68
69    let mut changes: Vec<SecuritySetOutput> = Vec::new();
70
71    if let Some(ref issuer) = args.jwt_issuer {
72        let old = profile.security.issuer.clone();
73        profile.security.issuer.clone_from(issuer);
74        changes.push(SecuritySetOutput {
75            field: "jwt_issuer".to_string(),
76            old_value: old,
77            new_value: issuer.clone(),
78            message: format!("Updated JWT issuer to {}", issuer),
79        });
80    }
81
82    if let Some(expiry) = args.access_expiry {
83        if expiry <= 0 {
84            bail!("Access token expiry must be positive");
85        }
86        let old = profile.security.access_token_expiration;
87        profile.security.access_token_expiration = expiry;
88        changes.push(SecuritySetOutput {
89            field: "access_token_expiration".to_string(),
90            old_value: old.to_string(),
91            new_value: expiry.to_string(),
92            message: format!("Updated access token expiry to {} seconds", expiry),
93        });
94    }
95
96    if let Some(expiry) = args.refresh_expiry {
97        if expiry <= 0 {
98            bail!("Refresh token expiry must be positive");
99        }
100        let old = profile.security.refresh_token_expiration;
101        profile.security.refresh_token_expiration = expiry;
102        changes.push(SecuritySetOutput {
103            field: "refresh_token_expiration".to_string(),
104            old_value: old.to_string(),
105            new_value: expiry.to_string(),
106            message: format!("Updated refresh token expiry to {} seconds", expiry),
107        });
108    }
109
110    save_profile(&profile, profile_path)?;
111
112    for change in &changes {
113        render_result(&CommandResult::text(change.clone()).with_title("Security Updated"));
114    }
115
116    if config.output_format() == OutputFormat::Table {
117        CliService::warning("Restart services for changes to take effect");
118    }
119
120    Ok(())
121}
122
123fn load_profile(path: &str) -> Result<Profile> {
124    let content =
125        fs::read_to_string(path).with_context(|| format!("Failed to read profile: {}", path))?;
126    let profile: Profile = serde_yaml::from_str(&content)
127        .with_context(|| format!("Failed to parse profile: {}", path))?;
128    Ok(profile)
129}
130
131fn save_profile(profile: &Profile, path: &str) -> Result<()> {
132    let content = serde_yaml::to_string(profile).context("Failed to serialize profile")?;
133    fs::write(path, content).with_context(|| format!("Failed to write profile: {}", path))?;
134    Ok(())
135}