use liminal::protocol::{CONVERSATION_REPLY_REQUESTED_FLAG, Frame, MessageEnvelope};
use super::service_route::{SERVICE_REFUSED_CODE, parse_request};
use super::state::{ConnectionProcessState, FrameAction};
use super::supervisor::runtime::ConnectionRuntime;
use crate::auth_lys::session::LysSession;
use crate::auth_lys::{
AccessAction, AccessResource, DeliberateDecision, HotAction, ResourceKind, SubjectBinding,
};
use crate::services::lifecycle::{Lifecycle, LifecycleError, ServiceRecord};
pub(super) const PREFIX: &str = "services.";
pub(super) fn is_lifecycle(subject: &str) -> bool {
subject
.strip_prefix(PREFIX)
.is_some_and(|verb| VERBS.contains(&verb))
}
const VERBS: [&str; 6] = ["list", "create", "deploy", "hold", "wake", "retire"];
pub(super) fn lifecycle_frame_response(
runtime: &ConnectionRuntime,
state: &ConnectionProcessState,
flags: u8,
stream_id: u32,
conversation_id: u64,
subject: &str,
envelope: &MessageEnvelope,
) -> FrameAction {
let refused = |message: String| {
FrameAction::Respond(Frame::ConversationError {
flags: 0,
stream_id,
conversation_id,
reason_code: SERVICE_REFUSED_CODE,
message: Some(message),
})
};
let answer = match serve(runtime, state, subject, &envelope.payload) {
Ok(answer) => answer,
Err(message) => return refused(message),
};
if flags & CONVERSATION_REPLY_REQUESTED_FLAG == 0 {
return FrameAction::NoResponse;
}
FrameAction::Respond(Frame::ConversationMessage {
flags: CONVERSATION_REPLY_REQUESTED_FLAG,
stream_id,
conversation_id,
envelope: MessageEnvelope::new(
envelope.schema_id,
envelope.causal_context.clone(),
answer.to_string().into_bytes(),
),
})
}
fn serve(
runtime: &ConnectionRuntime,
state: &ConnectionProcessState,
subject: &str,
payload: &[u8],
) -> Result<serde_json::Value, String> {
let verb = subject.strip_prefix(PREFIX).unwrap_or_default();
let lifecycle = runtime.lifecycle().ok_or_else(|| {
"lifecycle_unavailable: no [[auth.lys.templates]] are configured".to_owned()
})?;
let session = state
.lys_session
.as_deref()
.ok_or_else(|| "lifecycle_needs_a_pass: connect with a Lys pass".to_owned())?;
let (_, network) = runtime
.lys_admitter()
.ok_or_else(|| "no lys admitter is installed".to_owned())?;
let (operation, body) = parse_request(payload)?;
let body: serde_json::Value = if body.is_empty() {
serde_json::Value::Null
} else {
serde_json::from_slice(&body).map_err(|error| format!("payload is not JSON: {error}"))?
};
let now = super::apply::unix_seconds_now();
let now_ms = now.saturating_mul(1000);
let holder = session.holder().to_owned();
if verb == "list" {
return lifecycle
.list(|record| visible(session, network, &holder, record, now))
.map_err(|error| error.to_string());
}
let field = |name: &str| {
body.get(name)
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
.ok_or_else(|| format!("payload names no {name}"))
};
let (resource, act): (AccessResource, Act) = if verb == "create" {
let template = field("template")?;
let name = field("name")?;
(
AccessResource {
kind: ResourceKind::Network,
id: network.to_owned(),
},
Box::new(move |lifecycle| {
lifecycle.create(&template, &name, &holder, &operation, now_ms)
}),
)
} else {
let id = field("id")?;
let verb = verb.to_owned();
(
AccessResource {
kind: ResourceKind::Service,
id: id.clone(),
},
Box::new(move |lifecycle| match verb.as_str() {
"deploy" => lifecycle.deploy(&id, &holder),
"wake" => lifecycle.wake(&id, &holder),
"hold" => lifecycle.hold(&id, &holder, now_ms),
_ => lifecycle.retire(&id, &holder, now_ms),
}),
)
};
let decision = session
.decide_live(&resource, AccessAction::DeployService, now)
.map_err(|error| {
serde_json::to_string(&error.refusal).unwrap_or_else(|_| error.cause.to_string())
})?;
let grant = match decision {
DeliberateDecision::Outright(receipt) => receipt.grant,
DeliberateDecision::ByDraft { grant, draft } => {
return Ok(
serde_json::json!({"outcome": "held", "mode": "by_draft", "grant": grant, "draft": draft}),
);
}
DeliberateDecision::ByTwo { grant, draft } => {
return Ok(
serde_json::json!({"outcome": "held", "mode": "by_two", "grant": grant, "draft": draft}),
);
}
};
let record = act(lifecycle).map_err(|error| error.to_string())?;
Ok(serde_json::json!({"outcome": "acted", "grant": grant, "service": record}))
}
type Act = Box<dyn FnOnce(&std::sync::Arc<Lifecycle>) -> Result<ServiceRecord, LifecycleError>>;
fn visible(
session: &dyn LysSession,
network: &str,
holder: &str,
record: &ServiceRecord,
now: u64,
) -> bool {
record.created_by == holder
|| record.subjects.iter().any(|subject| {
SubjectBinding::new(network.to_owned(), subject.clone(), subject.clone())
.is_ok_and(|binding| session.decide(&binding, HotAction::Call, now).is_ok())
})
}