systemprompt_cli/session/creation/
helpers.rs1use std::sync::Arc;
10
11use anyhow::{Context, Result};
12use chrono::Duration as ChronoDuration;
13use systemprompt_agent::repository::context::ContextRepository;
14use systemprompt_cloud::{
15 CliSession, CloudCredentials, CredentialsBootstrap, SessionBinding, SessionIdentity, SessionKey,
16};
17use systemprompt_config::SecretsBootstrap;
18use systemprompt_database::{Database, DbPool, PoolConfig};
19use systemprompt_identifiers::{ContextId, Email, ProfileName, SessionId, SessionToken};
20use systemprompt_models::auth::{Permission, RateLimitTier, UserType};
21use systemprompt_security::{SessionGenerator, SessionParams};
22use systemprompt_users::{UserRepository, UserService};
23
24use crate::session::resolution::ProfileContext;
25
26pub(super) struct ResolvedSecrets {
27 pub database_url: String,
28 pub database_write_url: Option<String>,
29}
30
31pub(super) fn load_secrets() -> Result<ResolvedSecrets> {
32 let secrets = SecretsBootstrap::get().map_err(|e| {
33 anyhow::anyhow!(
34 "Secrets not initialized: {}\n\nEnsure your profile has a valid secrets \
35 configuration.\nCheck that secrets.json exists or environment variables are set.",
36 e
37 )
38 })?;
39
40 Ok(ResolvedSecrets {
41 database_url: secrets.database_url.clone(),
42 database_write_url: secrets.database_write_url.clone(),
43 })
44}
45
46pub(super) async fn connect_database(secrets: &ResolvedSecrets) -> Result<DbPool> {
47 let db = Database::connect(
48 &secrets.database_url,
49 secrets.database_write_url.as_deref(),
50 &PoolConfig::default(),
51 )
52 .await
53 .context("Failed to connect to database")?;
54 Ok(DbPool::from(Arc::new(db)))
55}
56
57pub async fn get_or_create_admin(
58 db_pool: &DbPool,
59 email: &str,
60 context_type: &str,
61) -> Result<systemprompt_users::User> {
62 let email = Email::try_new(email).map_err(|e| {
63 anyhow::anyhow!("refusing to provision an admin for an invalid address: {e}")
64 })?;
65 let email = email.as_str();
66
67 let user_service = UserService::new(Arc::new(UserRepository::new(db_pool)?));
68
69 if let Some(user) = user_service
70 .find_by_email(email)
71 .await
72 .context("Failed to query user by email")?
73 {
74 if user.is_admin() {
75 return Ok(user);
76 }
77
78 tracing::info!(email = %email, context = %context_type, "Promoting existing user to admin");
79
80 return user_service
81 .assign_roles(&user.id, &["admin".to_owned()])
82 .await
83 .context("Failed to assign admin role to existing user");
84 }
85
86 let name = email.split('@').next().unwrap_or("admin").to_owned();
87
88 tracing::info!(email = %email, name = %name, context = %context_type, "Auto-provisioning user");
89
90 let user = match user_service
91 .create_if_absent(&name, email, None, None)
92 .await
93 .with_context(|| format!("Failed to create user in {context_type} database"))?
94 {
95 Some(user) => user,
96 None => user_service
97 .find_by_email(email)
98 .await
99 .context("Failed to query user by email")?
100 .with_context(|| format!("User {email} vanished between provisioning and lookup"))?,
101 };
102
103 user_service
104 .assign_roles(&user.id, &["admin".to_owned()])
105 .await
106 .context("Failed to assign admin role to new user")
107}
108
109pub fn generate_admin_token(
110 issuer: &str,
111 user: &systemprompt_users::User,
112 session_id: &SessionId,
113) -> Result<SessionToken> {
114 let generator = SessionGenerator::new(issuer);
115 generator
116 .generate(&SessionParams {
117 user_id: &user.id,
118 session_id,
119 email: &user.email,
120 duration: ChronoDuration::hours(crate::session::api::DEFAULT_CLI_SESSION_HOURS),
121 user_type: UserType::Admin,
122 permissions: vec![Permission::Admin],
123 roles: vec!["admin".to_owned()],
124 attributes: std::collections::BTreeMap::new(),
125 rate_limit_tier: RateLimitTier::Admin,
126 })
127 .context("Failed to generate session token")
128}
129
130pub async fn create_cli_context(
131 db_pool: DbPool,
132 user: &systemprompt_users::User,
133 session_id: &SessionId,
134 profile_name: &str,
135) -> Result<ContextId> {
136 let context_repo = ContextRepository::new(&db_pool)?;
137 context_repo
138 .get_or_create_cli_context(
139 &user.id,
140 session_id,
141 &format!("CLI Session - {}", profile_name),
142 )
143 .await
144 .context("Failed to create CLI context")
145}
146
147pub(super) struct SessionComponents {
148 pub session_token: SessionToken,
149 pub session_id: SessionId,
150 pub context_id: ContextId,
151}
152
153pub(super) fn build_cli_session(
154 profile_ctx: &ProfileContext<'_>,
155 session_key: &SessionKey,
156 components: SessionComponents,
157 admin_user: &systemprompt_users::User,
158 issuer: &str,
159) -> Result<CliSession> {
160 let profile_name = ProfileName::try_new(profile_ctx.name)
161 .map_err(|e| anyhow::anyhow!("Invalid profile name: {}", e))?;
162 let email =
163 Email::try_new(&admin_user.email).map_err(|e| anyhow::anyhow!("Invalid email: {}", e))?;
164
165 Ok(CliSession::builder(
166 SessionBinding::new(profile_name, issuer.to_owned()),
167 components.session_token,
168 components.session_id,
169 components.context_id,
170 SessionIdentity::new(admin_user.id.clone(), email, UserType::Admin),
171 )
172 .with_session_key(session_key)
173 .with_profile_path(profile_ctx.path.clone())
174 .build())
175}
176
177pub async fn resolve_local_admin(
178 db_pool: &DbPool,
179 admin_name: &str,
180) -> Result<systemprompt_users::User> {
181 let user_service = UserService::new(Arc::new(UserRepository::new(db_pool)?));
182
183 let user = user_service
184 .find_by_name(admin_name)
185 .await
186 .context("Failed to query the local admin user by name")?
187 .with_context(|| {
188 format!(
189 "Local admin user '{admin_name}' not found.\n\nRun 'systemprompt admin bootstrap \
190 --email <your email>' to create it with a real address."
191 )
192 })?;
193
194 if !user.is_active() {
195 anyhow::bail!("Local admin user '{admin_name}' exists but is not active.");
196 }
197 if !user.is_admin() {
198 anyhow::bail!(
199 "User '{admin_name}' exists but does not hold the admin role. Run 'systemprompt admin \
200 bootstrap' to repair it."
201 );
202 }
203
204 Ok(user)
205}
206
207#[doc(hidden)]
208pub async fn resolve_credentialed_user_email(session_email_hint: Option<&str>) -> Result<Email> {
209 if let Some(email) = session_email_hint {
210 return Email::try_new(email).context("session email hint is not a valid email address");
211 }
212
213 CredentialsBootstrap::try_init()
214 .await
215 .context("Failed to initialize credentials. Run 'systemprompt cloud auth login'.")?;
216
217 let creds = CredentialsBootstrap::require().map_err(|_e| {
218 anyhow::anyhow!(
219 "Cloud authentication required for new sessions.\n\nRun 'systemprompt cloud auth \
220 login' to authenticate."
221 )
222 })?;
223 Ok(creds.user_email.clone())
224}
225
226#[doc(hidden)]
227pub async fn resolve_admin_with_fallback(
228 db_pool: &DbPool,
229 user_email: &str,
230 session_email_hint: Option<&str>,
231 context_type: &str,
232) -> Result<systemprompt_users::User> {
233 match get_or_create_admin(db_pool, user_email, context_type).await {
234 Ok(user) => Ok(user),
235 Err(e) if session_email_hint.is_some() => {
236 tracing::warn!(
237 email = %user_email,
238 error = %e,
239 "Session user lookup failed, falling back to cloud credentials"
240 );
241 if let Err(init_err) = CredentialsBootstrap::try_init().await {
242 tracing::debug!(error = %init_err, "Credentials init failed during fallback");
243 }
244 if let Ok(creds) = CredentialsBootstrap::require()
245 && creds.user_email.as_str() != user_email
246 {
247 return get_or_create_admin(db_pool, creds.user_email.as_str(), context_type).await;
248 }
249 Err(e)
250 },
251 Err(e) => Err(e),
252 }
253}
254
255pub async fn resolve_tenant_admin_with_fallback(
256 db_pool: &DbPool,
257 creds: &CloudCredentials,
258 user_email: &str,
259 session_email_hint: Option<&str>,
260) -> Result<systemprompt_users::User> {
261 match get_or_create_admin(db_pool, user_email, "tenant").await {
262 Ok(user) => Ok(user),
263 Err(e) if session_email_hint.is_some() && creds.user_email.as_str() != user_email => {
264 tracing::warn!(
265 email = %user_email,
266 error = %e,
267 "Session user lookup failed, falling back to cloud credentials"
268 );
269 get_or_create_admin(db_pool, creds.user_email.as_str(), "tenant").await
270 },
271 Err(e) => Err(e),
272 }
273}