Skip to content

Commit

Permalink
models: add initial user model bits for defining admins
Browse files Browse the repository at this point in the history
Based on rust-lang#5376.

Co-authored-by: Carol (Nichols || Goulding) <carol.nichols@gmail.com>
  • Loading branch information
LawnGnome and carols10cents committed Apr 19, 2023
1 parent 3925cdd commit 0a2cb03
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,7 @@ export GH_CLIENT_SECRET=
# Credentials for connecting to the Sentry error reporting service.
# export SENTRY_DSN_API=
export SENTRY_ENV_API=local

# GitHub users that are admins on this instance, separated by commas. Whitespace
# will be ignored.
export GH_ADMIN_USERS=
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ tower-http = { version = "=0.4.0", features = ["fs", "catch-panic"] }
tracing = "=0.1.37"
tracing-subscriber = { version = "=0.3.16", features = ["env-filter"] }
url = "=2.3.1"
lazy_static = "1.4.0"

[dev-dependencies]
cargo-registry-index = { path = "cargo-registry-index", features = ["testing"] }
Expand Down
1 change: 1 addition & 0 deletions src/models/helpers.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod admin;
pub mod with_count;
81 changes: 81 additions & 0 deletions src/models/helpers/admin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::collections::HashSet;

use lazy_static::lazy_static;

use crate::util::errors::{forbidden, AppResult};

lazy_static! {
static ref AUTHORIZED_ADMIN_USERS: HashSet<String> =
parse_authorized_admin_users(dotenv::var("GH_ADMIN_USERS"));
}

const DEFAULT_ADMIN_USERS: [&str; 3] = ["carols10cents", "jtgeibel", "Turbo87"];

fn parse_authorized_admin_users(maybe_users: dotenv::Result<String>) -> HashSet<String> {
match maybe_users {
Ok(users) => users
.split(|c: char| !(c.is_ascii_alphanumeric() || c == '-'))
.filter(|user| !user.is_empty())
.map(String::from)
.collect(),
Err(_err) => DEFAULT_ADMIN_USERS.into_iter().map(String::from).collect(),
}
}

/// Return `Ok(())` if the given GitHub user name matches a known admin (set
/// either via the `$GH_ADMIN_USERS` environment variable, or the builtin
/// fallback list if that variable is unset), or a forbidden error otherwise.
pub fn is_authorized_admin(username: &str) -> AppResult<()> {
// This hack is here to allow tests to have a consistent set of admin users
// (in this case, just the contents of the `DEFAULT_ADMIN_USERS` constant
// above).

#[cfg(not(test))]
fn check_username(username: &str) -> bool {
AUTHORIZED_ADMIN_USERS.contains(username)
}

#[cfg(test)]
fn check_username(username: &str) -> bool {
DEFAULT_ADMIN_USERS.contains(&username)
}

if check_username(username) {
Ok(())
} else {
Err(forbidden())
}
}

#[cfg(test)]
mod tests {
use std::io::{self, ErrorKind};

use super::{is_authorized_admin, parse_authorized_admin_users, DEFAULT_ADMIN_USERS};

#[test]
fn test_is_authorized_admin() {
assert_ok!(is_authorized_admin("Turbo87"));
assert_err!(is_authorized_admin(""));
assert_err!(is_authorized_admin("foo"));
}

#[test]
fn test_parse_authorized_admin_users() {
fn assert_authorized(input: dotenv::Result<&str>, expected: &[&str]) {
assert_eq!(
parse_authorized_admin_users(input.map(String::from)),
expected.iter().map(|s| String::from(*s)).collect()
);
}

assert_authorized(Ok(""), &[]);
assert_authorized(Ok("foo"), &["foo"]);
assert_authorized(Ok("foo, bar"), &["foo", "bar"]);
assert_authorized(Ok(" foo bar "), &["foo", "bar"]);
assert_authorized(Ok("foo;bar"), &["foo", "bar"]);

let not_found_error = dotenv::Error::Io(io::Error::new(ErrorKind::NotFound, "not found"));
assert_authorized(Err(not_found_error), DEFAULT_ADMIN_USERS.as_slice());
}
}
49 changes: 48 additions & 1 deletion src/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use crate::app::App;
use crate::email::Emails;
use crate::util::errors::AppResult;

use crate::models::{ApiToken, Crate, CrateOwner, Email, NewEmail, Owner, OwnerKind, Rights};
use crate::models::{
helpers::admin, ApiToken, Crate, CrateOwner, Email, NewEmail, Owner, OwnerKind, Rights,
};
use crate::schema::{crate_owners, emails, users};

/// The model representing a row in the `users` database table.
Expand Down Expand Up @@ -177,4 +179,49 @@ impl User {
.first(conn)
.optional()
}

/// Attempt to turn this user into an AdminUser
pub fn admin(&self) -> AppResult<AdminUser> {
AdminUser::new(self)
}
}

pub struct AdminUser(User);

impl AdminUser {
pub fn new(user: &User) -> AppResult<Self> {
admin::is_authorized_admin(user.gh_login.as_str()).map(|_| Self(user.clone()))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn hardcoded_admins() {
let user = User {
id: 3,
gh_access_token: "arbitrary".into(),
gh_login: "literally_anything".into(),
name: None,
gh_avatar: None,
gh_id: 7,
account_lock_reason: None,
account_lock_until: None,
};
assert!(user.admin().is_err());

let sneaky_user = User {
gh_login: "carols10cents_plus_extra_stuff".into(),
..user
};
assert!(sneaky_user.admin().is_err());

let real_real_real = User {
gh_login: "carols10cents".into(),
..sneaky_user
};
assert!(real_real_real.admin().is_ok());
}
}

0 comments on commit 0a2cb03

Please sign in to comment.