use serde_json::json;
use super::identity::{PrincipalType, classify_principal_id, split_principal_id};
use super::{RelayError, relay_error};
#[derive(Clone, Copy, Debug)]
pub(super) enum Capability {
Look,
Send,
List,
Raww,
}
impl Capability {
pub(super) fn label(self) -> &'static str {
match self {
Self::Look => "look.inspect",
Self::Send => "send.deliver",
Self::List => "list.read",
Self::Raww => "raww.write",
}
}
}
#[derive(Clone, Copy, Debug)]
pub(super) enum Addressing {
SingleTarget,
MultiTarget,
BundleEnumerate,
}
#[derive(Clone, Copy, Debug)]
pub(super) struct OperationProfile {
pub capability: Capability,
pub addressing: Addressing,
}
#[derive(Clone, Debug)]
pub(super) struct ResolvedTarget {
pub namespace: String,
pub session_id: Option<String>,
}
#[derive(Clone, Debug)]
pub(super) struct ResolvedRoute {
pub dispatch_namespace: String,
pub requester_session: String,
pub targets: Vec<ResolvedTarget>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ScopeTier {
Own,
Home,
All,
}
impl ScopeTier {
fn rank(self) -> u8 {
match self {
Self::Own => 0,
Self::Home => 1,
Self::All => 2,
}
}
}
impl ResolvedTarget {
pub(super) fn is_relay_wide(&self) -> bool {
self.session_id
.as_deref()
.and_then(classify_principal_id)
.is_some_and(|class| {
matches!(
class,
PrincipalType::User | PrincipalType::Application | PrincipalType::Relay
)
})
}
fn reachable_at_home_tier(&self, dispatch_namespace: &str) -> bool {
self.is_relay_wide() || self.namespace == dispatch_namespace
}
fn tier(&self, dispatch_namespace: &str, requester_session: &str) -> ScopeTier {
if !self.reachable_at_home_tier(dispatch_namespace) {
return ScopeTier::All;
}
match self.session_id.as_deref() {
Some(session_id) if session_id == requester_session && !self.is_relay_wide() => {
ScopeTier::Own
}
_ => ScopeTier::Home,
}
}
}
pub(super) fn requester_home_namespace<'a>(
requester_session: &'a str,
dispatch_namespace: &'a str,
) -> &'a str {
match split_principal_id(requester_session) {
Some((_, namespace)) => namespace,
None => dispatch_namespace,
}
}
pub(super) fn required_tier(route: &ResolvedRoute) -> ScopeTier {
route
.targets
.iter()
.map(|target| target.tier(&route.dispatch_namespace, &route.requester_session))
.max_by_key(|tier| tier.rank())
.unwrap_or(ScopeTier::Own)
}
fn resolve_target(dispatch_namespace: &str, target: &str) -> Result<ResolvedTarget, RelayError> {
let requested = target.trim();
match classify_principal_id(requested) {
Some(PrincipalType::User) => Ok(ResolvedTarget {
namespace: dispatch_namespace.to_string(),
session_id: Some(requested.to_string()),
}),
Some(PrincipalType::Session) => {
let (session_id, namespace) = split_principal_id(requested)
.expect("session classification implies a parseable suffix");
Ok(ResolvedTarget {
namespace: namespace.to_string(),
session_id: Some(session_id.to_string()),
})
}
Some(PrincipalType::Application | PrincipalType::Relay) => {
let namespace = split_principal_id(requested)
.map(|(_, namespace)| namespace)
.unwrap_or_default();
Err(relay_error(
"validation_unsupported_namespace",
"target namespace names no routable recipient for this operation",
Some(json!({ "target": requested, "namespace": namespace })),
))
}
None => Err(relay_error(
"validation_unqualified_target",
"target must be a fully-qualified principal id (id@namespace)",
Some(json!({ "target": requested })),
)),
}
}
pub(super) fn resolve_send_route(
dispatch_namespace: &str,
requester_session: &str,
targets: &[String],
) -> Result<ResolvedRoute, RelayError> {
let resolved = targets
.iter()
.map(|target| resolve_target(dispatch_namespace, target))
.collect::<Result<Vec<_>, _>>()?;
Ok(ResolvedRoute {
dispatch_namespace: dispatch_namespace.to_string(),
requester_session: requester_session.to_string(),
targets: resolved,
})
}
fn resolve_single_target_route(
dispatch_namespace: &str,
requester_session: &str,
target_session: &str,
) -> Result<ResolvedRoute, RelayError> {
let target = resolve_target(dispatch_namespace, target_session)?;
Ok(ResolvedRoute {
dispatch_namespace: dispatch_namespace.to_string(),
requester_session: requester_session.to_string(),
targets: vec![target],
})
}
pub(super) fn resolve_look_route(
dispatch_namespace: &str,
requester_session: &str,
target_session: &str,
) -> Result<ResolvedRoute, RelayError> {
resolve_single_target_route(dispatch_namespace, requester_session, target_session)
}
pub(super) fn resolve_raww_route(
dispatch_namespace: &str,
requester_session: &str,
target_session: &str,
) -> Result<ResolvedRoute, RelayError> {
resolve_single_target_route(dispatch_namespace, requester_session, target_session)
}
pub(super) fn resolve_list_route(
dispatch_namespace: &str,
requester_session: &str,
enumerate_bundle: &str,
) -> ResolvedRoute {
ResolvedRoute {
dispatch_namespace: dispatch_namespace.to_string(),
requester_session: requester_session.to_string(),
targets: vec![ResolvedTarget {
namespace: enumerate_bundle.to_string(),
session_id: None,
}],
}
}