From 02dab8a94dce1a93e8ca50232cf75597fee327b0 Mon Sep 17 00:00:00 2001 From: Pietro Albini Date: Wed, 6 Feb 2019 19:27:17 +0100 Subject: add permissions support and perf permissions --- README.md | 24 +++++++++++++++++++ people/RalfJung.toml | 5 ++++ people/Xanewok.toml | 3 +++ people/davidtwco.toml | 5 ++++ people/nnethercote.toml | 5 ++++ rust_team_data/src/v1.rs | 5 ++++ src/main.rs | 16 +++++++++++++ src/permissions.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ src/schema.rs | 16 +++++++++++++ src/static_api.rs | 16 +++++++++++++ src/validate.rs | 33 ++++++++++++++++++++++++-- teams/compiler.toml | 3 +++ teams/infra.toml | 3 +++ teams/lang.toml | 3 +++ teams/libs.toml | 3 +++ 15 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 people/RalfJung.toml create mode 100644 people/davidtwco.toml create mode 100644 people/nnethercote.toml create mode 100644 src/permissions.rs diff --git a/README.md b/README.md index 11b756d..05071a0 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,12 @@ You can get a list of all the email addresses subscribed to a list: $ cargo run dump-list all@rust-lang.org ``` +You can get a list of all the users with a permission: + +``` +$ cargo run dump-permission perf +``` + ## Schema ### People @@ -50,6 +56,9 @@ github = "johndoe" # GitHub username of the person (required) # This will, for example, avoid adding the person to the mailing lists. email = "john@doe.com" # Email address used for mailing lists (optional) irc-nickname = "jdoe" # Nickname of the person on IRC, if different than the GitHub one (optional) + +[permissions] +# Optional, see the permissions documentation ``` The file must be named the same as the GitHub username. @@ -80,6 +89,9 @@ members = [ "rust-timer", ] +[permissions] +# Optional, see the permissions documentation + # Define the mailing lists used by the team # It's optional, and there can be more than one [[lists]] @@ -109,3 +121,15 @@ extra-teams = [ "bots-nursery", ] ``` + +### Permissions + +Permissions can be applied either to a single person or to a whole team, and +they grant access to some pieces of rust-lang tooling. The following +permissions are available: + +```toml +[permissions] +# Optional, grants access to the @rust-timer GitHub bot +perf = true +``` diff --git a/people/RalfJung.toml b/people/RalfJung.toml new file mode 100644 index 0000000..d226397 --- /dev/null +++ b/people/RalfJung.toml @@ -0,0 +1,5 @@ +name = "Ralf Jung" +github = "RalfJung" + +[permissions] +perf = true diff --git a/people/Xanewok.toml b/people/Xanewok.toml index 011f120..309ec14 100644 --- a/people/Xanewok.toml +++ b/people/Xanewok.toml @@ -1,3 +1,6 @@ name = "Igor Matuszewski" github = "Xanewok" email = "xanewok@gmail.com" + +[permissions] +perf = true diff --git a/people/davidtwco.toml b/people/davidtwco.toml new file mode 100644 index 0000000..7c4e675 --- /dev/null +++ b/people/davidtwco.toml @@ -0,0 +1,5 @@ +name = "David Wood" +github = "davidtwco" + +[permissions] +perf = true diff --git a/people/nnethercote.toml b/people/nnethercote.toml new file mode 100644 index 0000000..3e3a4fe --- /dev/null +++ b/people/nnethercote.toml @@ -0,0 +1,5 @@ +name = "Nicholas Nethercote" +github = "nnethercote" + +[permissions] +perf = true diff --git a/rust_team_data/src/v1.rs b/rust_team_data/src/v1.rs index d79d758..bb69449 100644 --- a/rust_team_data/src/v1.rs +++ b/rust_team_data/src/v1.rs @@ -59,3 +59,8 @@ pub struct List { pub struct Lists { pub lists: IndexMap, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Permission { + pub github_users: Vec, +} diff --git a/src/main.rs b/src/main.rs index 1f0592e..7ea1fad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ #![allow(clippy::new_ret_no_self)] mod data; +#[macro_use] +mod permissions; mod schema; mod static_api; mod validate; @@ -21,6 +23,11 @@ enum Cli { DumpTeam { name: String }, #[structopt(name = "dump-list", help = "print all the emails in a list")] DumpList { name: String }, + #[structopt( + name = "dump-permission", + help = "print all the people with a permission" + )] + DumpPermission { name: String }, } fn main() { @@ -68,6 +75,15 @@ fn run() -> Result<(), Error> { println!("{}", email); } } + Cli::DumpPermission { ref name } => { + if !crate::schema::Permissions::AVAILABLE.contains(&name.as_str()) { + failure::bail!("unknown permission: {}", name); + } + let allowed = crate::permissions::allowed_github_users(&data, name)?; + for github_username in &allowed { + println!("{}", github_username); + } + } } Ok(()) diff --git a/src/permissions.rs b/src/permissions.rs new file mode 100644 index 0000000..f181e21 --- /dev/null +++ b/src/permissions.rs @@ -0,0 +1,62 @@ +use crate::data::Data; +use failure::Error; +use std::collections::HashSet; + +#[macro_export] +macro_rules! permissions { + ($vis:vis struct $name:ident { $($key:ident,)* }) => { + #[derive(serde_derive::Deserialize, Debug)] + #[serde(rename_all = "kebab-case", deny_unknown_fields)] + $vis struct $name { + $( + #[serde(default)] + $key: bool, + )* + } + + impl Default for $name { + fn default() -> Self { + $name { + $($key: false,)* + } + } + } + + impl $name { + $vis const AVAILABLE: &'static [&'static str] = &[$(stringify!($key),)*]; + + $vis fn has(&self, permission: &str) -> bool { + $( + if permission == stringify!($key) { + return self.$key; + } + )* + false + } + + $vis fn has_any(&self) -> bool { + false $(|| self.$key)* + } + } + } +} + +pub(crate) fn allowed_github_users( + data: &Data, + permission: &str, +) -> Result, Error> { + let mut github_users = HashSet::new(); + for team in data.teams() { + if team.permissions().has(permission) { + for member in team.members(&data)? { + github_users.insert(member.to_string()); + } + } + } + for person in data.people() { + if person.permissions().has(permission) { + github_users.insert(person.github().to_string()); + } + } + Ok(github_users) +} diff --git a/src/schema.rs b/src/schema.rs index 96b1217..adb67a4 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -45,6 +45,8 @@ pub(crate) struct Person { #[serde(default)] email: EmailField, discord: Option, + #[serde(default)] + permissions: Permissions, } impl Person { @@ -78,6 +80,10 @@ impl Person { self.discord.as_ref().map(|e| e.as_str()) } + pub(crate) fn permissions(&self) -> &Permissions { + &self.permissions + } + pub(crate) fn validate(&self) -> Result<(), Error> { if let EmailField::Disabled(true) = &self.email { bail!("`email = true` is not valid (for person {})", self.github); @@ -96,6 +102,8 @@ pub(crate) struct Team { #[serde(default)] children: Vec, people: TeamPeople, + #[serde(default)] + permissions: Permissions, website: Option, #[serde(default)] lists: Vec, @@ -190,6 +198,10 @@ impl Team { } Ok(lists) } + + pub(crate) fn permissions(&self) -> &Permissions { + &self.permissions + } } #[derive(serde_derive::Deserialize, Debug)] @@ -203,6 +215,10 @@ struct TeamPeople { include_wg_leads: bool, } +permissions!(pub(crate) struct Permissions { + perf, +}); + pub(crate) struct DiscordInvite<'a> { pub(crate) url: &'a str, pub(crate) channel: &'a str, diff --git a/src/static_api.rs b/src/static_api.rs index 33bac41..24bd989 100644 --- a/src/static_api.rs +++ b/src/static_api.rs @@ -1,4 +1,5 @@ use crate::data::Data; +use crate::schema::Permissions; use failure::Error; use indexmap::IndexMap; use log::info; @@ -23,6 +24,7 @@ impl<'a> Generator<'a> { pub(crate) fn generate(&self) -> Result<(), Error> { self.generate_teams()?; self.generate_lists()?; + self.generate_permissions()?; Ok(()) } @@ -96,6 +98,20 @@ impl<'a> Generator<'a> { Ok(()) } + fn generate_permissions(&self) -> Result<(), Error> { + for perm in Permissions::AVAILABLE { + let mut github_users = crate::permissions::allowed_github_users(&self.data, perm)? + .into_iter() + .collect::>(); + github_users.sort(); + self.add( + &format!("v1/permissions/{}.json", perm), + &v1::Permission { github_users }, + )?; + } + Ok(()) + } + fn add(&self, path: &str, obj: &T) -> Result<(), Error> { info!("writing API object {}...", path); let dest = self.dest.join(path); diff --git a/src/validate.rs b/src/validate.rs index 6606064..cce801f 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1,5 +1,5 @@ use crate::data::Data; -use crate::schema::Email; +use crate::schema::{Email, Permissions}; use failure::{bail, ensure, Error}; use regex::Regex; use std::collections::HashSet; @@ -18,6 +18,7 @@ pub(crate) fn validate(data: &Data) -> Result<(), Error> { validate_list_addresses(data, &mut errors); validate_people_addresses(data, &mut errors); validate_discord_name(data, &mut errors); + validate_duplicate_permissions(data, &mut errors); if !errors.is_empty() { errors.sort(); @@ -117,7 +118,13 @@ fn validate_inactive_members(data: &Data, errors: &mut Vec) { all_members.difference(&active_members), errors, |person, _| { - bail!("person `{}` is not a member of any team", person); + if !data.person(person).unwrap().permissions().has_any() { + bail!( + "person `{}` is not a member of any team and has no permissions", + person + ); + } + Ok(()) }, ); } @@ -229,6 +236,28 @@ fn validate_discord_name(data: &Data, errors: &mut Vec) { }) } +/// 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, _| { + let person = data.person(member).unwrap(); + 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(()) + }); +} + fn wrapper(iter: I, errors: &mut Vec, mut func: F) where I: Iterator, diff --git a/teams/compiler.toml b/teams/compiler.toml index 65028cf..a8aa774 100644 --- a/teams/compiler.toml +++ b/teams/compiler.toml @@ -16,6 +16,9 @@ members = [ "Zoxc", ] +[permissions] +perf = true + [website] name = "Compiler team" description = "compiler internals, optimizations" diff --git a/teams/infra.toml b/teams/infra.toml index 5c17068..3ef95ae 100644 --- a/teams/infra.toml +++ b/teams/infra.toml @@ -16,6 +16,9 @@ members = [ "shepmaster", ] +[permissions] +perf = true + [website] name = "Infrastructure team" description = "infrastructure supporting the Rust project itself: CI, releases, bots, metrics" diff --git a/teams/lang.toml b/teams/lang.toml index d8d7ce1..e332036 100644 --- a/teams/lang.toml +++ b/teams/lang.toml @@ -14,6 +14,9 @@ members = [ "withoutboats", ] +[permissions] +perf = true + [website] name = "Language team" description = "designing new language features" diff --git a/teams/libs.toml b/teams/libs.toml index 06075dd..ef3f749 100644 --- a/teams/libs.toml +++ b/teams/libs.toml @@ -15,6 +15,9 @@ members = [ "Amanieu", ] +[permissions] +perf = true + [website] page = "library" name = "Library team" -- cgit v1.2.3