use serde::{Deserialize, Serialize};
use crate::rights::{Mode, Resource};
pub const CONTRACT_VERSION: u32 = 1;
pub const CONTRACT_UNSUPPORTED: &str = "membership_contract_unsupported";
pub const LOG_MISMATCH: &str = "membership_log_mismatch";
pub const REQUEST_MALFORMED: &str = "membership_request_malformed";
pub const SUBJECT_KIND_MISMATCH: &str = "membership_subject_kind_mismatch";
pub const OUTSIDE_WORKSPACE: &str = "membership_outside_workspace";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipGrantLog))]
pub struct GrantLog {
pub identity: String,
pub epoch: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipSubject))]
pub struct Subject {
pub id: String,
pub kind: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipRequest))]
pub struct MembershipRequest {
pub contract: u32,
pub log: GrantLog,
pub workspace: Resource,
pub subject: Subject,
pub resource: Resource,
pub action: String,
pub at_least: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipDecision))]
pub struct MembershipDecision {
pub contract: u32,
pub log: GrantLog,
pub request: MembershipRequest,
pub revision: Option<u64>,
pub verdict: Verdict,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipVerdict))]
pub enum Verdict {
Allowed {
grant: String,
path: Vec<String>,
scope: Resource,
},
Held {
grant: String,
scope: Resource,
mode: Mode,
},
Refused {
refusal: String,
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Refused {
pub name: &'static str,
pub reason: String,
}
fn malformed(value: &str) -> bool {
value.is_empty() || value.chars().any(char::is_control)
}
pub fn validate(request: &MembershipRequest, served: &GrantLog) -> Result<(), Refused> {
check(
request.contract,
&request.log,
served,
&[
("workspace kind", request.workspace.kind.as_str()),
("workspace id", request.workspace.id.as_str()),
("subject id", request.subject.id.as_str()),
("subject kind", request.subject.kind.as_str()),
("resource kind", request.resource.kind.as_str()),
("resource id", request.resource.id.as_str()),
("action", request.action.as_str()),
],
)
}
pub(crate) fn check(
contract: u32,
log: &GrantLog,
served: &GrantLog,
members: &[(&'static str, &str)],
) -> Result<(), Refused> {
if contract != CONTRACT_VERSION {
return Err(Refused {
name: CONTRACT_UNSUPPORTED,
reason: format!(
"contract {contract} is not served; this provider speaks {CONTRACT_VERSION}"
),
});
}
if log != served {
return Err(Refused {
name: LOG_MISMATCH,
reason: "the request names another grant log or reset epoch".to_owned(),
});
}
let shaped =
std::iter::once(("log identity", log.identity.as_str())).chain(members.iter().copied());
for (member, value) in shaped {
if malformed(value) {
return Err(Refused {
name: REQUEST_MALFORMED,
reason: format!("the {member} is empty or contains control characters"),
});
}
}
Ok(())
}
#[must_use]
pub fn refused_before_lookup(
request: &MembershipRequest,
served: &GrantLog,
name: &str,
reason: &str,
) -> MembershipDecision {
MembershipDecision {
contract: CONTRACT_VERSION,
log: served.clone(),
request: request.clone(),
revision: None,
verdict: Verdict::Refused {
refusal: name.to_owned(),
reason: reason.to_owned(),
},
}
}