use super::{
BTreeMap, BTreeSet, CONTRACT_VERSION, LysSession, MembershipConfig, MembershipRefusal,
PageBounds, Resource, ServerError, Subject, invalid, malformed,
};
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct MembershipSubject {
pub(super) id: String,
pub(super) kind: String,
}
impl MembershipSubject {
pub fn from_session(session: &dyn LysSession) -> Result<Self, MembershipRefusal> {
let kind = session
.holder_kind()
.ok_or(MembershipRefusal::HolderKindUnknown)?;
let id = session.holder();
if malformed(id) || malformed(kind) {
return Err(MembershipRefusal::HolderMalformed);
}
Ok(Self {
id: id.to_owned(),
kind: kind.to_owned(),
})
}
#[must_use]
pub fn kind(&self) -> &str {
&self.kind
}
#[must_use]
pub fn to_contract(&self) -> Subject {
Subject {
id: self.id.clone(),
kind: self.kind.clone(),
}
}
pub(super) fn is(&self, subject: &Subject) -> bool {
self.id == subject.id && self.kind == subject.kind
}
}
impl std::fmt::Debug for MembershipSubject {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("MembershipSubject")
.field("id", &crate::auth_pass::ByteLength(self.id.len()))
.field("kind", &self.kind)
.finish()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MappedChannel {
pub(super) workspace: Resource,
pub(super) resource: Resource,
}
impl MappedChannel {
#[must_use]
pub const fn workspace(&self) -> &Resource {
&self.workspace
}
#[must_use]
pub const fn resource(&self) -> &Resource {
&self.resource
}
}
#[derive(Debug)]
pub(super) struct ChannelMap {
pub(super) channels: BTreeMap<String, MappedChannel>,
pub(super) conversations: BTreeMap<u64, String>,
pub(super) by_resource: BTreeMap<String, String>,
pub(super) workspaces: BTreeSet<String>,
}
pub(super) fn channel_map(config: &MembershipConfig) -> Result<ChannelMap, ServerError> {
if config.channels.is_empty() {
return Err(invalid(
"auth.lys.membership.channels: at least one protected channel is required",
));
}
let mut channels = BTreeMap::new();
let mut by_resource = BTreeMap::new();
let mut workspaces = BTreeSet::new();
for entry in &config.channels {
for (member, value) in [
("channel", entry.channel.as_str()),
("workspace", entry.workspace.as_str()),
("resource", entry.resource.as_str()),
] {
if malformed(value) {
return Err(invalid(format!(
"auth.lys.membership.channels: a {member} is empty or contains control characters"
)));
}
}
let mapped = MappedChannel {
workspace: Resource {
kind: config.workspace_kind.clone(),
id: entry.workspace.clone(),
},
resource: Resource {
kind: config.channel_kind.clone(),
id: entry.resource.clone(),
},
};
if channels.insert(entry.channel.clone(), mapped).is_some() {
return Err(invalid(
"auth.lys.membership.channels: a channel is listed twice",
));
}
if by_resource
.insert(entry.resource.clone(), entry.channel.clone())
.is_some()
{
return Err(invalid(
"auth.lys.membership.channels: a placed resource is mapped to two channels",
));
}
workspaces.insert(entry.workspace.clone());
}
let mut conversations = BTreeMap::new();
for entry in &config.conversations {
if !channels.contains_key(&entry.channel) {
return Err(invalid(
"auth.lys.membership.conversations: a conversation names an unlisted channel",
));
}
if conversations
.insert(entry.conversation, entry.channel.clone())
.is_some()
{
return Err(invalid(
"auth.lys.membership.conversations: a conversation is listed twice",
));
}
}
Ok(ChannelMap {
channels,
conversations,
by_resource,
workspaces,
})
}
pub(super) fn validated_bounds(config: &MembershipConfig) -> Result<PageBounds, ServerError> {
if config.contract != CONTRACT_VERSION {
return Err(invalid(format!(
"auth.lys.membership.contract {} is not served; this liminal speaks {CONTRACT_VERSION}",
config.contract
)));
}
for (member, value) in [
("log_identity", config.log_identity.as_str()),
("workspace_kind", config.workspace_kind.as_str()),
("channel_kind", config.channel_kind.as_str()),
] {
if malformed(value) {
return Err(invalid(format!(
"auth.lys.membership.{member} is empty or contains control characters"
)));
}
}
if config.workspace_kind == config.channel_kind {
return Err(invalid(
"auth.lys.membership: workspace_kind and channel_kind must differ",
));
}
if config.page_rows == 0 || config.page_bytes == 0 || config.max_decisions == 0 {
return Err(invalid(
"auth.lys.membership: page_rows, page_bytes and max_decisions must each be positive",
));
}
Ok(PageBounds {
rows: config.page_rows,
bytes: config.page_bytes,
})
}