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