use crate::data::Data; use crate::schema::{Email, Permissions}; use failure::{bail, ensure, Error}; use regex::Regex; use std::collections::HashSet; static CHECKS: &[fn(&Data, &mut Vec)] = &[ validate_wg_names, validate_subteam_of, validate_team_leads, validate_team_members, validate_inactive_members, validate_list_email_addresses, validate_list_extra_people, validate_list_extra_teams, validate_list_addresses, validate_people_addresses, validate_discord_name, validate_duplicate_permissions, validate_permissions, ]; pub(crate) fn validate(data: &Data) -> Result<(), Error> { let mut errors = Vec::new(); for check in CHECKS { check(data, &mut errors); } if !errors.is_empty() { errors.sort(); errors.dedup_by(|a, b| a == b); for err in &errors { eprintln!("validation error: {}", err); } bail!("{} validation errors found", errors.len()); } Ok(()) } /// Ensure working group names start with `wg-` fn validate_wg_names(data: &Data, errors: &mut Vec) { wrapper(data.teams(), errors, |team, _| { match (team.is_wg() == team.name().starts_with("wg-"), team.is_wg()) { (false, true) => bail!( "working group `{}`'s name doesn't start with wg-", team.name() ), (false, false) => bail!( "team `{}` seems like a working group but has `wg = false`", team.name() ), (true, _) => {} } Ok(()) }); } /// Ensure `subteam-of` points to an existing team fn validate_subteam_of(data: &Data, errors: &mut Vec) { let team_names: HashSet<_> = data.teams().map(|t| t.name()).collect(); wrapper(data.teams(), errors, |team, _| { if let Some(subteam_of) = team.subteam_of() { ensure!( team_names.contains(subteam_of), "team `{}` doesn't exist", subteam_of ); } Ok(()) }); } /// Ensure team leaders are part of the teams they lead fn validate_team_leads(data: &Data, errors: &mut Vec) { wrapper(data.teams(), errors, |team, errors| { let members = team.members(data)?; wrapper(team.leads().iter(), errors, |lead, _| { if !members.contains(lead) { bail!( "`{}` leads team `{}`, but is not a member of it", lead, team.name() ); } Ok(()) }); Ok(()) }); } /// Ensure t_eam members are people fn validate_team_members(data: &Data, errors: &mut Vec) { wrapper(data.teams(), errors, |team, errors| { wrapper(team.members(data)?.iter(), errors, |member, _| { if data.person(member).is_none() { bail!( "person `{}` is member of team `{}` but doesn't exist", member, team.name() ); } Ok(()) }); Ok(()) }); } /// Ensure every person is part of at least a team fn validate_inactive_members(data: &Data, errors: &mut Vec) { let mut active_members = HashSet::new(); wrapper(data.teams(), errors, |team, _| { let members = team.members(data)?; for member in members { active_members.insert(member); } Ok(()) }); let all_members = data.people().map(|p| p.github()).collect::>(); wrapper( all_members.difference(&active_members), errors, |person, _| { if !data.person(person).unwrap().permissions().has_any() { bail!( "person `{}` is not a member of any team and has no permissions", person ); } Ok(()) }, ); } /// Ensure every member of a team with a mailing list has an email address fn validate_list_email_addresses(data: &Data, errors: &mut Vec) { wrapper(data.teams(), errors, |team, errors| { if team.lists(data)?.is_empty() { return Ok(()); } wrapper(team.members(data)?.iter(), errors, |member, _| { if let Some(member) = data.person(member) { if let Email::Missing = member.email() { bail!( "person `{}` is a member of a mailing list but has no email address", member.github() ); } } Ok(()) }); Ok(()) }); } /// Ensure members of extra-people in a list are real people fn validate_list_extra_people(data: &Data, errors: &mut Vec) { wrapper(data.teams(), errors, |team, errors| { wrapper(team.raw_lists().iter(), errors, |list, _| { for person in &list.extra_people { if data.person(person).is_none() { bail!( "person `{}` does not exist (in list `{}`)", person, list.address ); } } Ok(()) }); Ok(()) }); } /// Ensure members of extra-people in a list are real people fn validate_list_extra_teams(data: &Data, errors: &mut Vec) { wrapper(data.teams(), errors, |team, errors| { wrapper(team.raw_lists().iter(), errors, |list, _| { for list_team in &list.extra_teams { if data.team(list_team).is_none() { bail!( "team `{}` does not exist (in list `{}`)", list_team, list.address ); } } Ok(()) }); Ok(()) }); } /// Ensure the list addresses are correct fn validate_list_addresses(data: &Data, errors: &mut Vec) { let email_re = Regex::new(r"^[a-zA-Z0-9_\.-]+@([a-zA-Z0-9_\.-]+)$").unwrap(); let config = data.config().allowed_mailing_lists_domains(); wrapper(data.teams(), errors, |team, errors| { wrapper(team.raw_lists().iter(), errors, |list, _| { if let Some(captures) = email_re.captures(&list.address) { if !config.contains(&captures[1]) { bail!("list address on a domain we don't own: `{}`", list.address); } } else { bail!("invalid list address: `{}`", list.address); } Ok(()) }); Ok(()) }); } /// Ensure people email addresses are correct fn validate_people_addresses(data: &Data, errors: &mut Vec) { wrapper(data.people(), errors, |person, _| { if let Email::Present(email) = person.email() { if !email.contains('@') { bail!("invalid email address of `{}`: {}", person.github(), email); } } Ok(()) }); } /// Ensure the Discord name is formatted properly fn validate_discord_name(data: &Data, errors: &mut Vec) { // https://discordapp.com/developers/docs/resources/user#usernames-and-nicknames let name_re = Regex::new(r"^[^@#:`]{2,32}#[0-9]{4}$").unwrap(); wrapper(data.people(), errors, |person, _| { if let Some(name) = person.discord() { if !name_re.is_match(name) { bail!( "user `{}` has an invalid discord name: {}", person.github(), name ); } } Ok(()) }) } /// Ensure members of teams with permissions don't explicitly have those permissions fn validate_duplicate_permissions(data: &Data, errors: &mut Vec) { wrapper(data.teams(), errors, |team, errors| { wrapper(team.members(&data)?.iter(), errors, |member, _| { if let Some(person) = data.person(member) { for permission in Permissions::AVAILABLE { if team.permissions().has(permission) && person.permissions().has(permission) { bail!( "user `{}` has the permission `{}` both explicitly and through \ the `{}` team", member, permission, team.name() ); } } } Ok(()) }); Ok(()) }); } /// Ensure the permissions are valid fn validate_permissions(data: &Data, errors: &mut Vec) { wrapper(data.teams(), errors, |team, _| { team.permissions() .validate(format!("team `{}`", team.name()))?; Ok(()) }); wrapper(data.people(), errors, |person, _| { person .permissions() .validate(format!("user `{}`", person.github()))?; Ok(()) }); } fn wrapper(iter: I, errors: &mut Vec, mut func: F) where I: Iterator, F: FnMut(T, &mut Vec) -> Result<(), Error>, { for item in iter { if let Err(err) = func(item, errors) { errors.push(err.to_string()); } } }