Skip to main content

systemprompt_cli/commands/admin/config/
security.rs

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