1use std::convert::Infallible;
16
17use axum::{
18 Json,
19 extract::{Path, Query},
20 http::StatusCode,
21 response::{
22 IntoResponse,
23 sse::{Event, KeepAlive, Sse},
24 },
25};
26use futures::{AsyncBufReadExt, StreamExt};
27use manta_backend_dispatcher::types::{K8sAuth, K8sDetails};
28
29use super::{
30 ErrorResponse, RequestCtx, SiteHeader, require_k8s_url, require_vault,
31 serialize_or_500, to_handler_error, validate_repo_list_lengths,
32};
33use crate::server::audit::AuditNote;
34use crate::service;
35
36pub use manta_shared::types::api::queries::{DeleteSessionQuery, SessionQuery};
41
42#[utoipa::path(get, path = "/sessions", tag = "sessions",
44 params(SessionQuery, SiteHeader),
45 security(("bearerAuth" = [])),
46 responses(
47 (status = 200, description = "List of sessions", body = serde_json::Value),
50 (status = 400, description = "Bad request", body = ErrorResponse),
51 (status = 401, description = "Unauthorized", body = ErrorResponse),
52 (status = 500, description = "Internal error", body = ErrorResponse),
53 )
54)]
55#[tracing::instrument(skip_all)]
56pub async fn get_sessions(
57 ctx: RequestCtx,
58 Query(q): Query<SessionQuery>,
59) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
60 let infra = ctx.infra();
61
62 let xnames: Vec<String> = q
63 .xnames
64 .map(|s| {
65 s.split(',')
66 .map(str::trim)
67 .filter(|v| !v.is_empty())
68 .map(str::to_string)
69 .collect()
70 })
71 .unwrap_or_default();
72
73 let params = service::session::GetSessionParams {
74 group: q.hsm_group,
75 xnames,
76 min_age: q.min_age,
77 max_age: q.max_age,
78 session_type: q.session_type,
79 status: q.status,
80 name: q.name,
81 limit: q.limit,
82 };
83
84 let sessions = service::session::get_sessions(&infra, &ctx.token, ¶ms)
85 .await
86 .map_err(to_handler_error)?;
87
88 Ok(Json(sessions))
89}
90
91#[utoipa::path(delete, path = "/sessions/{name}", tag = "sessions",
97 params(("name" = String, Path, description = "Session name"), DeleteSessionQuery, SiteHeader),
98 security(("bearerAuth" = [])),
99 responses(
100 (status = 200, description = "Session deleted or deletion preview", body = serde_json::Value),
102 (status = 401, description = "Unauthorized", body = ErrorResponse),
103 (status = 404, description = "Not found", body = ErrorResponse),
104 (status = 500, description = "Internal error", body = ErrorResponse),
105 )
106)]
107#[tracing::instrument(skip_all)]
108pub async fn delete_session(
109 ctx: RequestCtx,
110 note: AuditNote,
111 Path(name): Path<String>,
112 Query(q): Query<DeleteSessionQuery>,
113) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
114 note
115 .action("session.delete")
116 .target("session", &name)
117 .dry_run(q.dry_run);
118
119 tracing::info!("delete_session name={} dry_run={}", name, q.dry_run);
120 let infra = ctx.infra();
121
122 let deletion_ctx =
123 service::session::prepare_session_deletion(&infra, &ctx.token, &name, None)
124 .await
125 .map_err(to_handler_error)?;
126
127 if q.dry_run {
128 return Ok((StatusCode::OK, Json(serialize_or_500(&deletion_ctx)?)));
129 }
130
131 service::session::execute_session_deletion(
132 &infra,
133 &ctx.token,
134 &deletion_ctx,
135 false,
136 )
137 .await
138 .map_err(to_handler_error)?;
139
140 Ok((StatusCode::OK, Json(serde_json::json!({ "deleted": name }))))
141}
142
143pub use manta_shared::types::api::session::CreateSessionRequest;
148
149#[utoipa::path(post, path = "/sessions", tag = "sessions",
151 params(SiteHeader),
152 request_body = CreateSessionRequest,
153 security(("bearerAuth" = [])),
154 responses(
155 (status = 201, description = "Session created", body = manta_shared::types::api::responses::CreateSessionResponse),
156 (status = 400, description = "Bad request", body = ErrorResponse),
157 (status = 401, description = "Unauthorized", body = ErrorResponse),
158 (status = 500, description = "Internal error", body = ErrorResponse),
159 (status = 501, description = "Vault not configured", body = ErrorResponse),
160 )
161)]
162#[tracing::instrument(skip_all)]
163pub async fn create_session(
164 ctx: RequestCtx,
165 note: AuditNote,
166 Json(body): Json<CreateSessionRequest>,
167) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
168 note
169 .action("session.create")
170 .targets("session", body.cfs_conf_sess_name.as_deref())
171 .targets("group", body.hsm_group.as_deref())
172 .targets("hosts", body.ansible_limit.as_deref())
173 .detail("repo_names", &body.repo_names)
174 .detail("playbook", &body.playbook_yaml_file_name);
175
176 validate_repo_list_lengths(&body.repo_names, &body.repo_last_commit_ids)?;
177 tracing::info!("create_session repos={:?}", body.repo_names);
178 let infra = ctx.infra();
179
180 if let Some(ref hsm_group) = body.hsm_group {
182 service::authorization::validate_user_group_access(
183 &infra, &ctx.token, hsm_group,
184 )
185 .await
186 .map_err(to_handler_error)?;
187 }
188
189 if let Some(ref ansible_limit) = body.ansible_limit {
192 service::authorization::validate_ansible_limit_membership_access(
193 &infra,
194 &ctx.token,
195 ansible_limit,
196 )
197 .await
198 .map_err(to_handler_error)?;
199 }
200
201 let vault_base_url = require_vault(infra.vault_base_url)?;
202
203 let gitea_token =
204 crate::server::common::vault::http_client::get_shasta_vcs_token(
205 &ctx.token,
206 vault_base_url,
207 infra.site_name,
208 )
209 .await
210 .map_err(to_handler_error)?;
211
212 let repo_name_refs: Vec<&str> = body
213 .repo_names
214 .iter()
215 .map(std::string::String::as_str)
216 .collect();
217 let repo_commit_refs: Vec<&str> = body
218 .repo_last_commit_ids
219 .iter()
220 .map(std::string::String::as_str)
221 .collect();
222
223 let (session_name, config_name) = service::session::create_cfs_session(
224 &infra,
225 &ctx.token,
226 &gitea_token,
227 service::session::CreateCfsSessionParams {
228 cfs_conf_sess_name: body.cfs_conf_sess_name.as_deref(),
229 playbook_yaml_file_name: body.playbook_yaml_file_name.as_deref(),
230 group: body.hsm_group.as_deref(),
231 repo_names: &repo_name_refs,
232 repo_last_commit_ids: &repo_commit_refs,
233 ansible_limit: body.ansible_limit.as_deref(),
234 ansible_verbosity: body.ansible_verbosity.as_deref(),
235 ansible_passthrough: body.ansible_passthrough.as_deref(),
236 },
237 )
238 .await
239 .map_err(to_handler_error)?;
240 note
241 .produced("session", &session_name)
242 .produced("configuration", &config_name);
243
244 Ok((
245 StatusCode::CREATED,
246 Json(serde_json::json!({
247 "session_name": session_name,
248 "configuration_name": config_name,
249 })),
250 ))
251}
252
253pub use manta_shared::types::api::queries::SessionLogsQuery;
258
259#[utoipa::path(get, path = "/sessions/{name}/logs", tag = "sessions",
261 params(("name" = String, Path, description = "Session name"), SessionLogsQuery, SiteHeader),
262 security(("bearerAuth" = [])),
263 responses(
264 (status = 200, description = "SSE log stream"),
265 (status = 401, description = "Unauthorized", body = ErrorResponse),
266 (status = 500, description = "Internal error", body = ErrorResponse),
267 (status = 501, description = "Vault or k8s not configured", body = ErrorResponse),
268 )
269)]
270#[tracing::instrument(skip_all)]
271pub async fn get_session_logs(
272 ctx: RequestCtx,
273 Path(name): Path<String>,
274 Query(q): Query<SessionLogsQuery>,
275) -> Result<
276 Sse<impl futures::Stream<Item = Result<Event, Infallible>>>,
277 (StatusCode, Json<ErrorResponse>),
278> {
279 let infra = ctx.infra();
280
281 let k8s_api_url = require_k8s_url(infra.k8s_api_url)?;
282 let vault_base_url = require_vault(infra.vault_base_url)?;
283
284 service::session::validate_session_access(&infra, &ctx.token, &name)
290 .await
291 .map_err(to_handler_error)?;
292
293 let k8s = K8sDetails {
294 api_url: k8s_api_url.to_string(),
295 authentication: K8sAuth::Vault {
296 base_url: vault_base_url.to_string(),
297 },
298 };
299
300 let logs_stream = service::session::stream_logs(
301 &infra,
302 &ctx.token,
303 &name,
304 q.timestamps,
305 &k8s,
306 )
307 .await
308 .map_err(to_handler_error)?;
309
310 let sse_stream = logs_stream.lines().map(|result| {
311 Ok::<Event, Infallible>(
312 Event::default().data(result.unwrap_or_else(|e| format!("error: {e}"))),
313 )
314 });
315
316 Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
317}