use std::collections::HashSet;
#[derive(Debug, Clone, Copy)]
pub struct EntityCategoryRule {
pub categories: &'static [&'static str],
pub attributes: &'static [&'static str],
pub conflicts: &'static [&'static str],
pub only_required: bool,
}
impl EntityCategoryRule {
fn matches(&self, sp_categories: &HashSet<&str>) -> bool {
if self.conflicts.iter().any(|c| sp_categories.contains(c)) {
return false;
}
self.categories.iter().all(|c| sp_categories.contains(c))
}
}
#[derive(Debug, Clone, Copy)]
pub struct EntityCategoryPolicy {
pub name: &'static str,
pub rules: &'static [EntityCategoryRule],
}
pub const COCO_V1: &str = "http://www.geant.net/uri/dataprotection-code-of-conduct/v1";
pub const COCO_V2: &str = "https://refeds.org/category/code-of-conduct/v2";
pub const REFEDS_RESEARCH_AND_SCHOLARSHIP: &str =
"http://refeds.org/category/research-and-scholarship";
pub const INCOMMON_RESEARCH_AND_SCHOLARSHIP: &str =
"http://id.incommon.org/category/research-and-scholarship";
pub const MYACADEMICID_ESI: &str = "https://myacademicid.org/entity-categories/esi";
pub const REFEDS_PERSONALIZED: &str = "https://refeds.org/category/personalized";
pub const REFEDS_PSEUDONYMOUS: &str = "https://refeds.org/category/pseudonymous";
pub const REFEDS_ANONYMOUS: &str = "https://refeds.org/category/anonymous";
pub const SWAMID_RESEARCH_AND_EDUCATION: &str =
"http://www.swamid.se/category/research-and-education";
pub const SWAMID_SFS_1993_1153: &str = "http://www.swamid.se/category/sfs-1993-1153";
pub const SWAMID_EU: &str = "http://www.swamid.se/category/eu-adequate-protection";
pub const SWAMID_NREN: &str = "http://www.swamid.se/category/nren-service";
pub const SWAMID_HEI: &str = "http://www.swamid.se/category/hei-service";
pub const AT_EGOV_PVP2: &str = "http://www.ref.gv.at/ns/names/agiz/pvp/egovtoken";
pub const AT_EGOV_PVP2_CHARGE: &str = "http://www.ref.gv.at/ns/names/agiz/pvp/egovtoken-charge";
pub const SUBJECT_ID_ATTR: &str = "urn:oasis:names:tc:SAML:attribute:subject-id";
pub const PAIRWISE_ID_ATTR: &str = "urn:oasis:names:tc:SAML:attribute:pairwise-id";
pub const SUBJECT_ID_REQ_ATTR: &str = "urn:oasis:names:tc:SAML:profiles:subject-id:req";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SubjectIdReq {
#[default]
None,
SubjectId,
PairwiseId,
Any,
}
impl SubjectIdReq {
pub fn from_metadata_values(values: &[String]) -> Self {
for value in values {
let normalized = value.trim().to_ascii_lowercase();
match normalized.as_str() {
"any" => return SubjectIdReq::Any,
"none" => return SubjectIdReq::None,
"subject-id" => return SubjectIdReq::SubjectId,
"pairwise-id" => return SubjectIdReq::PairwiseId,
_ => continue,
}
}
SubjectIdReq::None
}
}
pub fn prefer_pairwise_over_subject_id(req: SubjectIdReq, released: &mut HashSet<String>) {
if req != SubjectIdReq::Any {
return;
}
if released.contains("pairwise-id") && released.contains("subject-id") {
released.remove("subject-id");
}
}
const COCO_V1_ATTRIBUTES: &[&str] = &[
"eduPersonPrincipalName",
"eduPersonScopedAffiliation",
"eduPersonAffiliation",
"mail",
"displayName",
"cn",
"schacHomeOrganization",
];
const REFEDS_RS_ATTRIBUTES: &[&str] = &[
"eduPersonPrincipalName",
"eduPersonScopedAffiliation",
"mail",
"givenName",
"sn",
"displayName",
];
const SWAMID_NAME_ORG_OTHER: &[&str] = &[
"givenName",
"displayName",
"sn",
"cn",
"c",
"o",
"co",
"norEduOrgAcronym",
"schacHomeOrganization",
"schacHomeOrganizationType",
"eduPersonPrincipalName",
"eduPersonScopedAffiliation",
"mail",
"eduPersonAssurance",
];
const SWAMID_R_AND_S: &[&str] = &[
"eduPersonPrincipalName",
"eduPersonUniqueID",
"mail",
"displayName",
"givenName",
"sn",
"eduPersonAssurance",
"eduPersonScopedAffiliation",
];
const GEANT_COCO: &[&str] = &[
"pairwise-id",
"subject-id",
"eduPersonTargetedID",
"eduPersonPrincipalName",
"eduPersonOrcid",
"norEduPersonNIN",
"personalIdentityNumber",
"schacDateOfBirth",
"mail",
"mailLocalAddress",
"displayName",
"cn",
"givenName",
"sn",
"norEduPersonLegalName",
"eduPersonAssurance",
"eduPersonScopedAffiliation",
"eduPersonAffiliation",
"o",
"norEduOrgAcronym",
"c",
"co",
"schacHomeOrganization",
"schacHomeOrganizationType",
];
const ESI_ATTRIBUTES: &[&str] = &["schacPersonalUniqueCode"];
const ESI_AND_COCO: &[&str] = &[
"schacPersonalUniqueCode",
"pairwise-id",
"subject-id",
"eduPersonTargetedID",
"eduPersonPrincipalName",
"eduPersonOrcid",
"norEduPersonNIN",
"personalIdentityNumber",
"schacDateOfBirth",
"mail",
"mailLocalAddress",
"displayName",
"cn",
"givenName",
"sn",
"norEduPersonLegalName",
"eduPersonAssurance",
"eduPersonScopedAffiliation",
"eduPersonAffiliation",
"o",
"norEduOrgAcronym",
"c",
"co",
"schacHomeOrganization",
"schacHomeOrganizationType",
];
const REFEDS_PERSONALIZED_ACCESS: &[&str] = &[
"subject-id",
"mail",
"displayName",
"givenName",
"sn",
"eduPersonScopedAffiliation",
"eduPersonAssurance",
"schacHomeOrganization",
];
const REFEDS_PSEUDONYMOUS_ACCESS: &[&str] = &[
"pairwise-id",
"eduPersonScopedAffiliation",
"eduPersonAssurance",
"schacHomeOrganization",
];
const REFEDS_ANONYMOUS_ACCESS: &[&str] = &["eduPersonScopedAffiliation", "schacHomeOrganization"];
const EGOVTOKEN: &[&str] = &[
"PVP-VERSION",
"PVP-PRINCIPAL-NAME",
"PVP-GIVENNAME",
"PVP-BIRTHDATE",
"PVP-USERID",
"PVP-GID",
"PVP-BPK",
"PVP-MAIL",
"PVP-TEL",
"PVP-PARTICIPANT-ID",
"PVP-PARTICIPANT-OKZ",
"PVP-OU-OKZ",
"PVP-OU",
"PVP-OU-GV-OU-ID",
"PVP-FUNCTION",
"PVP-ROLES",
];
const CHARGEATTR: &[&str] = &[
"PVP-INVOICE-RECPT-ID",
"PVP-COST-CENTER-ID",
"PVP-CHARGE-CODE",
];
pub static EDUGAIN: EntityCategoryPolicy = EntityCategoryPolicy {
name: "edugain",
rules: &[
EntityCategoryRule {
categories: &[],
attributes: &["eduPersonTargetedID"],
only_required: false,
conflicts: &[],
},
EntityCategoryRule {
categories: &[COCO_V1],
attributes: COCO_V1_ATTRIBUTES,
only_required: true,
conflicts: &[],
},
],
};
pub static REFEDS: EntityCategoryPolicy = EntityCategoryPolicy {
name: "refeds",
rules: &[
EntityCategoryRule {
categories: &[],
attributes: &["eduPersonTargetedID"],
only_required: false,
conflicts: &[],
},
EntityCategoryRule {
categories: &[REFEDS_RESEARCH_AND_SCHOLARSHIP],
attributes: REFEDS_RS_ATTRIBUTES,
only_required: false,
conflicts: &[],
},
],
};
pub static INCOMMON: EntityCategoryPolicy = EntityCategoryPolicy {
name: "incommon",
rules: &[
EntityCategoryRule {
categories: &[],
attributes: &["eduPersonTargetedID"],
only_required: false,
conflicts: &[],
},
EntityCategoryRule {
categories: &[INCOMMON_RESEARCH_AND_SCHOLARSHIP],
attributes: REFEDS_RS_ATTRIBUTES,
only_required: false,
conflicts: &[],
},
],
};
pub static SWAMID: EntityCategoryPolicy = EntityCategoryPolicy {
name: "swamid",
rules: &[
EntityCategoryRule {
categories: &[SWAMID_SFS_1993_1153],
attributes: &["norEduPersonNIN", "eduPersonAssurance"],
only_required: false,
conflicts: &[],
},
EntityCategoryRule {
categories: &[SWAMID_RESEARCH_AND_EDUCATION, SWAMID_EU],
attributes: SWAMID_NAME_ORG_OTHER,
only_required: false,
conflicts: &[],
},
EntityCategoryRule {
categories: &[SWAMID_RESEARCH_AND_EDUCATION, SWAMID_NREN],
attributes: SWAMID_NAME_ORG_OTHER,
only_required: false,
conflicts: &[],
},
EntityCategoryRule {
categories: &[SWAMID_RESEARCH_AND_EDUCATION, SWAMID_HEI],
attributes: SWAMID_NAME_ORG_OTHER,
only_required: false,
conflicts: &[],
},
EntityCategoryRule {
categories: &[REFEDS_RESEARCH_AND_SCHOLARSHIP],
attributes: SWAMID_R_AND_S,
only_required: false,
conflicts: &[],
},
EntityCategoryRule {
categories: &[COCO_V1],
attributes: GEANT_COCO,
only_required: true,
conflicts: &[],
},
EntityCategoryRule {
categories: &[COCO_V2],
attributes: GEANT_COCO,
only_required: true,
conflicts: &[],
},
EntityCategoryRule {
categories: &[MYACADEMICID_ESI],
attributes: ESI_ATTRIBUTES,
only_required: false,
conflicts: &[],
},
EntityCategoryRule {
categories: &[MYACADEMICID_ESI, COCO_V1],
attributes: ESI_AND_COCO,
only_required: true,
conflicts: &[],
},
EntityCategoryRule {
categories: &[MYACADEMICID_ESI, COCO_V2],
attributes: ESI_AND_COCO,
only_required: true,
conflicts: &[],
},
REFEDS_PERSONALIZED_RULE,
REFEDS_PSEUDONYMOUS_RULE,
REFEDS_ANONYMOUS_RULE,
],
};
const REFEDS_PERSONALIZED_RULE: EntityCategoryRule = EntityCategoryRule {
categories: &[REFEDS_PERSONALIZED],
attributes: REFEDS_PERSONALIZED_ACCESS,
conflicts: &[REFEDS_PSEUDONYMOUS, REFEDS_ANONYMOUS],
only_required: false,
};
const REFEDS_PSEUDONYMOUS_RULE: EntityCategoryRule = EntityCategoryRule {
categories: &[REFEDS_PSEUDONYMOUS],
attributes: REFEDS_PSEUDONYMOUS_ACCESS,
conflicts: &[REFEDS_ANONYMOUS],
only_required: false,
};
const REFEDS_ANONYMOUS_RULE: EntityCategoryRule = EntityCategoryRule {
categories: &[REFEDS_ANONYMOUS],
attributes: REFEDS_ANONYMOUS_ACCESS,
conflicts: &[],
only_required: false,
};
pub static REFEDS_ACCESS_RULES: EntityCategoryPolicy = EntityCategoryPolicy {
name: "refeds-access",
rules: &[
REFEDS_PERSONALIZED_RULE,
REFEDS_PSEUDONYMOUS_RULE,
REFEDS_ANONYMOUS_RULE,
],
};
pub static AT_EGOV_PVP2_POLICY: EntityCategoryPolicy = EntityCategoryPolicy {
name: "at_egov_pvp2",
rules: &[
EntityCategoryRule {
categories: &[AT_EGOV_PVP2],
attributes: EGOVTOKEN,
only_required: false,
conflicts: &[],
},
EntityCategoryRule {
categories: &[AT_EGOV_PVP2_CHARGE],
attributes: CHARGEATTR,
only_required: false,
conflicts: &[],
},
],
};
pub fn releasable_attributes(
policies: &[&EntityCategoryPolicy],
sp_entity_categories: &[String],
required_local_names: &[String],
) -> HashSet<String> {
let ecs: HashSet<&str> = sp_entity_categories.iter().map(String::as_str).collect();
let mut released: HashSet<String> = HashSet::new();
for policy in policies {
for rule in policy.rules {
if !rule.matches(&ecs) {
continue;
}
let attrs = rule
.attributes
.iter()
.map(|a| a.to_lowercase())
.filter(|a| {
!rule.only_required
|| rule.categories.is_empty()
|| required_local_names.contains(a)
});
released.extend(attrs);
}
}
released
}
#[cfg(test)]
mod tests {
use super::*;
fn cats(uris: &[&str]) -> Vec<String> {
uris.iter().map(|s| s.to_string()).collect()
}
fn lower(names: &[&str]) -> Vec<String> {
names.iter().map(|s| s.to_lowercase()).collect()
}
#[test]
fn test_refeds_rs_release() {
let released =
releasable_attributes(&[&REFEDS], &cats(&[REFEDS_RESEARCH_AND_SCHOLARSHIP]), &[]);
assert!(released.contains("mail"));
assert!(released.contains("edupersonprincipalname"));
assert!(released.contains("edupersontargetedid"));
}
#[test]
fn test_no_category_only_default_rule() {
let released = releasable_attributes(&[&REFEDS], &[], &[]);
assert_eq!(released.len(), 1);
assert!(released.contains("edupersontargetedid"));
}
#[test]
fn test_coco_only_required() {
let released = releasable_attributes(
&[&EDUGAIN],
&cats(&[COCO_V1]),
&lower(&["mail", "displayName"]),
);
assert!(released.contains("mail"));
assert!(released.contains("displayname"));
assert!(!released.contains("cn"));
assert!(!released.contains("edupersonaffiliation"));
}
#[test]
fn test_swamid_tuple_key_requires_both() {
let released =
releasable_attributes(&[&SWAMID], &cats(&[SWAMID_RESEARCH_AND_EDUCATION]), &[]);
assert!(released.is_empty());
let released = releasable_attributes(
&[&SWAMID],
&cats(&[SWAMID_RESEARCH_AND_EDUCATION, SWAMID_EU]),
&[],
);
assert!(released.contains("givenname"));
assert!(released.contains("schachomeorganization"));
}
#[test]
fn test_refeds_access_combines_with_other_categories() {
let released = releasable_attributes(
&[&REFEDS, &REFEDS_ACCESS_RULES],
&cats(&[REFEDS_RESEARCH_AND_SCHOLARSHIP, REFEDS_ANONYMOUS]),
&[],
);
assert!(released.contains("edupersonscopedaffiliation"));
assert!(released.contains("schachomeorganization"));
assert!(released.contains("mail"));
assert!(released.contains("givenname"));
}
#[test]
fn test_refeds_access_prefers_more_restrictive_category() {
let released = releasable_attributes(
&[&REFEDS_ACCESS_RULES],
&cats(&[REFEDS_PERSONALIZED, REFEDS_PSEUDONYMOUS]),
&[],
);
assert!(!released.contains("subject-id"));
assert!(!released.contains("displayname"));
assert!(released.contains("pairwise-id"));
assert!(released.contains("edupersonscopedaffiliation"));
let released = releasable_attributes(
&[&REFEDS_ACCESS_RULES],
&cats(&[REFEDS_PERSONALIZED, REFEDS_ANONYMOUS]),
&[],
);
assert!(!released.contains("subject-id"));
assert!(!released.contains("displayname"));
assert!(!released.contains("givenname"));
assert!(released.contains("edupersonscopedaffiliation"));
assert!(released.contains("schachomeorganization"));
let released = releasable_attributes(
&[&REFEDS_ACCESS_RULES],
&cats(&[REFEDS_PSEUDONYMOUS, REFEDS_ANONYMOUS]),
&[],
);
assert!(!released.contains("pairwise-id"));
assert!(released.contains("edupersonscopedaffiliation"));
}
#[test]
fn test_refeds_personalized_alone_releases_its_attributes() {
let released =
releasable_attributes(&[&REFEDS_ACCESS_RULES], &cats(&[REFEDS_PERSONALIZED]), &[]);
assert!(released.contains("subject-id"));
assert!(released.contains("displayname"));
assert!(released.contains("edupersonscopedaffiliation"));
}
#[test]
fn test_swamid_default_includes_refeds_access() {
let released = releasable_attributes(&[&SWAMID], &cats(&[REFEDS_PSEUDONYMOUS]), &[]);
assert!(released.contains("pairwise-id"));
assert!(released.contains("schachomeorganization"));
}
#[test]
fn test_subject_id_req_parsing() {
assert_eq!(
SubjectIdReq::from_metadata_values(&["any".to_string()]),
SubjectIdReq::Any
);
assert_eq!(
SubjectIdReq::from_metadata_values(&[" Any ".to_string()]),
SubjectIdReq::Any
);
assert_eq!(
SubjectIdReq::from_metadata_values(&["none".to_string()]),
SubjectIdReq::None
);
assert_eq!(
SubjectIdReq::from_metadata_values(&["pairwise-id".to_string()]),
SubjectIdReq::PairwiseId
);
assert_eq!(
SubjectIdReq::from_metadata_values(&["SUBJECT-ID".to_string()]),
SubjectIdReq::SubjectId
);
assert_eq!(SubjectIdReq::from_metadata_values(&[]), SubjectIdReq::None);
assert_eq!(
SubjectIdReq::from_metadata_values(&["bogus".to_string()]),
SubjectIdReq::None
);
}
#[test]
fn test_prefer_pairwise_over_subject_id() {
let mut set: HashSet<String> = ["subject-id", "pairwise-id", "mail"]
.iter()
.map(|s| s.to_string())
.collect();
prefer_pairwise_over_subject_id(SubjectIdReq::Any, &mut set);
assert!(!set.contains("subject-id"));
assert!(set.contains("pairwise-id"));
assert!(set.contains("mail"));
let mut set: HashSet<String> = ["subject-id", "pairwise-id"]
.iter()
.map(|s| s.to_string())
.collect();
prefer_pairwise_over_subject_id(SubjectIdReq::None, &mut set);
assert!(set.contains("subject-id"));
assert!(set.contains("pairwise-id"));
let mut set: HashSet<String> = ["subject-id"].iter().map(|s| s.to_string()).collect();
prefer_pairwise_over_subject_id(SubjectIdReq::Any, &mut set);
assert!(set.contains("subject-id"));
}
#[test]
fn test_at_egov() {
let released = releasable_attributes(&[&AT_EGOV_PVP2_POLICY], &cats(&[AT_EGOV_PVP2]), &[]);
assert!(released.contains("pvp-mail"));
assert!(!released.contains("pvp-charge-code"));
}
}