use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InstanceProjectLabel {
Unlabeled,
Single(String),
Multiple(Vec<String>),
}
impl InstanceProjectLabel {
pub fn from_labels(labels: &HashMap<String, String>) -> Self {
if let Some(value) = labels
.get("project_id")
.or_else(|| labels.get("project"))
.map(|s| s.trim())
.filter(|s| !s.is_empty())
{
return Self::Single(value.to_string());
}
if let Some(values) = labels
.get("projects")
.map(|s| s.trim())
.filter(|s| !s.is_empty())
{
let list: Vec<String> = values
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
if !list.is_empty() {
return Self::Multiple(list);
}
}
Self::Unlabeled
}
pub fn directly_permits(&self, project_id: &str) -> bool {
match self {
Self::Unlabeled => false,
Self::Single(label) => label == "*" || label == project_id,
Self::Multiple(list) => list.iter().any(|l| l == "*" || l == project_id),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProjectRoutingMode {
Permissive,
Strict,
StrictWithDefault { name: String },
}
impl Default for ProjectRoutingMode {
fn default() -> Self {
Self::Permissive
}
}
impl ProjectRoutingMode {
pub fn parse(value: &str) -> Self {
let trimmed = value.trim();
if trimmed.is_empty() {
return Self::Permissive;
}
let lowered = trimmed.to_ascii_lowercase();
if let Some(rest) = lowered.strip_prefix("strict_with_default:") {
let name = rest.trim();
if name.is_empty() {
return Self::Strict;
}
return Self::StrictWithDefault {
name: name.to_string(),
};
}
match lowered.as_str() {
"permissive" | "compat" | "lax" => Self::Permissive,
"strict" | "isolated" | "tenant" => Self::Strict,
_ => Self::Permissive,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProjectAccessDecision {
Allowed,
NotProvisioned { reason: String },
}
impl ProjectAccessDecision {
pub fn is_allowed(&self) -> bool {
matches!(self, Self::Allowed)
}
}
pub fn evaluate_instance_for_project(
project_id: &str,
instance_labels: &HashMap<String, String>,
mode: &ProjectRoutingMode,
) -> ProjectAccessDecision {
let label = InstanceProjectLabel::from_labels(instance_labels);
let normalized = normalize_project_id(project_id);
if !matches!(label, InstanceProjectLabel::Unlabeled) {
return if label.directly_permits(normalized) {
ProjectAccessDecision::Allowed
} else {
ProjectAccessDecision::NotProvisioned {
reason: format!(
"instance is labeled for a different project; project '{normalized}' is not in the allow-list"
),
}
};
}
match mode {
ProjectRoutingMode::Permissive => ProjectAccessDecision::Allowed,
ProjectRoutingMode::Strict => {
if normalized.is_empty() || normalized == "default" {
ProjectAccessDecision::Allowed
} else {
ProjectAccessDecision::NotProvisioned {
reason: format!(
"unlabeled instance is restricted to the default project under strict routing; project '{normalized}' is not provisioned"
),
}
}
}
ProjectRoutingMode::StrictWithDefault { name } => {
if normalized == name.as_str() {
ProjectAccessDecision::Allowed
} else {
ProjectAccessDecision::NotProvisioned {
reason: format!(
"unlabeled instance is bound to default project '{name}' under strict routing; project '{normalized}' is not provisioned"
),
}
}
}
}
}
fn normalize_project_id(project_id: &str) -> &str {
let p = project_id.trim();
if p.is_empty() { "default" } else { p }
}
#[cfg(test)]
mod tests {
use super::*;
fn labels(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn label_parser_prefers_project_id_then_project_then_list() {
let one = labels(&[("project_id", "a"), ("project", "b"), ("projects", "c,d")]);
assert_eq!(
InstanceProjectLabel::from_labels(&one),
InstanceProjectLabel::Single("a".to_string())
);
let two = labels(&[("project", "b"), ("projects", "c,d")]);
assert_eq!(
InstanceProjectLabel::from_labels(&two),
InstanceProjectLabel::Single("b".to_string())
);
let three = labels(&[("projects", "c, d , e")]);
assert_eq!(
InstanceProjectLabel::from_labels(&three),
InstanceProjectLabel::Multiple(
vec!["c".to_string(), "d".to_string(), "e".to_string(),]
)
);
let empty = labels(&[("project_id", " ")]);
assert_eq!(
InstanceProjectLabel::from_labels(&empty),
InstanceProjectLabel::Unlabeled,
"blank label must not promote to wildcard"
);
let none = labels(&[]);
assert_eq!(
InstanceProjectLabel::from_labels(&none),
InstanceProjectLabel::Unlabeled
);
}
#[test]
fn explicit_label_blocks_other_projects_in_every_mode() {
let l = labels(&[("project_id", "a")]);
for mode in [
ProjectRoutingMode::Permissive,
ProjectRoutingMode::Strict,
ProjectRoutingMode::StrictWithDefault {
name: "default".to_string(),
},
] {
assert!(
evaluate_instance_for_project("a", &l, &mode).is_allowed(),
"project A must reach its labeled instance under {:?}",
mode
);
let decision = evaluate_instance_for_project("b", &l, &mode);
assert!(!decision.is_allowed(), "project B blocked under {:?}", mode);
match decision {
ProjectAccessDecision::NotProvisioned { reason } => {
assert!(
reason.contains("'b' is not in the allow-list"),
"got: {reason}"
);
}
_ => unreachable!(),
}
}
}
#[test]
fn wildcard_label_permits_every_project() {
let l = labels(&[("project_id", "*")]);
for project in ["a", "b", "default", "anything-goes"] {
assert!(
evaluate_instance_for_project(project, &l, &ProjectRoutingMode::Strict)
.is_allowed(),
"wildcard must let {project} in even under Strict"
);
}
}
#[test]
fn unlabeled_instance_under_permissive_matches_every_project() {
let l = labels(&[]);
for project in ["", "default", "a", "b"] {
assert!(
evaluate_instance_for_project(project, &l, &ProjectRoutingMode::Permissive)
.is_allowed()
);
}
}
#[test]
fn unlabeled_instance_under_strict_only_serves_default() {
let l = labels(&[]);
assert!(evaluate_instance_for_project("", &l, &ProjectRoutingMode::Strict).is_allowed());
assert!(
evaluate_instance_for_project("default", &l, &ProjectRoutingMode::Strict).is_allowed()
);
let decision = evaluate_instance_for_project("project-a", &l, &ProjectRoutingMode::Strict);
assert!(!decision.is_allowed());
match decision {
ProjectAccessDecision::NotProvisioned { reason } => {
assert!(
reason.contains("restricted to the default project"),
"got: {reason}"
);
}
_ => unreachable!(),
}
}
#[test]
fn strict_with_default_binds_unlabeled_to_named_project() {
let l = labels(&[]);
let mode = ProjectRoutingMode::StrictWithDefault {
name: "alpha".to_string(),
};
assert!(evaluate_instance_for_project("alpha", &l, &mode).is_allowed());
assert!(!evaluate_instance_for_project("default", &l, &mode).is_allowed());
assert!(!evaluate_instance_for_project("beta", &l, &mode).is_allowed());
}
#[test]
fn env_parser_recognises_canonical_tokens() {
assert_eq!(
ProjectRoutingMode::parse("permissive"),
ProjectRoutingMode::Permissive
);
assert_eq!(
ProjectRoutingMode::parse("Strict"),
ProjectRoutingMode::Strict
);
assert_eq!(
ProjectRoutingMode::parse("strict_with_default:alpha"),
ProjectRoutingMode::StrictWithDefault {
name: "alpha".to_string()
}
);
assert_eq!(
ProjectRoutingMode::parse(""),
ProjectRoutingMode::Permissive
);
assert_eq!(
ProjectRoutingMode::parse("garbage"),
ProjectRoutingMode::Permissive
);
assert_eq!(
ProjectRoutingMode::parse("strict_with_default:"),
ProjectRoutingMode::Strict
);
}
#[test]
fn projects_list_handles_wildcard_mixed_with_names() {
let l = labels(&[("projects", "alpha, *, beta")]);
for project in ["alpha", "beta", "gamma", "anything"] {
assert!(
evaluate_instance_for_project(project, &l, &ProjectRoutingMode::Strict)
.is_allowed()
);
}
}
#[test]
fn projects_list_without_wildcard_rejects_outside_projects() {
let l = labels(&[("projects", "alpha,beta")]);
assert!(
evaluate_instance_for_project("alpha", &l, &ProjectRoutingMode::Permissive)
.is_allowed()
);
assert!(
!evaluate_instance_for_project("gamma", &l, &ProjectRoutingMode::Permissive)
.is_allowed()
);
}
}