use std::sync::Arc;
use axum::http::StatusCode;
use kellnr_common::account_name::UserName;
use kellnr_common::canonical_name::CanonicalName;
use kellnr_db::provider::DbResult;
use kellnr_db::{CrateVisibility, DbProvider};
use crate::error::ApiError;
use crate::identity::Identity;
pub fn require_admin(user: &Identity) -> Result<(), ApiError> {
if user.is_admin {
Ok(())
} else {
Err(ApiError::new(
"Admin access required",
StatusCode::FORBIDDEN,
))
}
}
pub async fn require_owner(
crate_name: &CanonicalName,
user: &Identity,
db: &Arc<dyn DbProvider>,
) -> Result<(), ApiError> {
if user.is_admin || db.is_owner(crate_name, &user.name).await? {
Ok(())
} else {
Err(ApiError::new(
"Not the owner of the crate",
StatusCode::FORBIDDEN,
))
}
}
pub fn require_writable(user: &Identity) -> Result<(), ApiError> {
if !user.is_admin && user.is_read_only {
Err(ApiError::new(
"Read-only users cannot modify the registry",
StatusCode::FORBIDDEN,
))
} else {
Ok(())
}
}
pub async fn has_restricted_crate_access(
crate_name: &CanonicalName,
user: &UserName,
is_admin: bool,
db: &Arc<dyn DbProvider>,
) -> DbResult<bool> {
Ok(is_admin
|| db.is_crate_user(crate_name, user).await?
|| db.is_crate_group_user(crate_name, user).await?
|| db.is_owner(crate_name, user).await?)
}
pub async fn can_read_crate(
crate_name: &CanonicalName,
user: Option<&Identity>,
db: &Arc<dyn DbProvider>,
) -> DbResult<bool> {
if !db.is_download_restricted(crate_name).await? {
return Ok(true);
}
match user {
Some(user) => has_restricted_crate_access(crate_name, &user.name, user.is_admin, db).await,
None => Ok(false),
}
}
pub fn crate_visibility(user: Option<&Identity>) -> CrateVisibility {
match user {
None => CrateVisibility::Unrestricted,
Some(user) if user.is_admin => CrateVisibility::All,
Some(user) => CrateVisibility::User(user.name.clone()),
}
}
#[cfg(test)]
mod tests {
use kellnr_common::account_name::GroupName;
use kellnr_db::UserFlags;
use kellnr_db::test_utils::TestCrateBuilder;
use kellnr_test_db::TestDb;
use super::*;
use crate::identity::Source;
fn name() -> CanonicalName {
CanonicalName::try_from("restricted").unwrap()
}
fn user_name() -> UserName {
UserName::new("user").unwrap()
}
fn user(is_admin: bool) -> Identity {
Identity {
name: user_name(),
is_admin,
is_read_only: false,
source: Source::Session,
}
}
async fn crate_db(restricted: bool) -> TestDb {
let db = TestDb::sqlite().await;
TestCrateBuilder::new(&db)
.name("restricted")
.build()
.await
.unwrap();
db.change_download_restricted(&name(), restricted)
.await
.unwrap();
db.add_user(&user_name(), "pwd", UserFlags::default())
.await
.unwrap();
db
}
async fn can_read(db: &TestDb, user: Option<&Identity>) -> bool {
can_read_crate(&name(), user, &db.provider()).await.unwrap()
}
#[test]
fn crate_visibility_depends_on_caller() {
assert_eq!(CrateVisibility::Unrestricted, crate_visibility(None));
assert_eq!(CrateVisibility::All, crate_visibility(Some(&user(true))));
assert_eq!(
CrateVisibility::User(UserName::new("user").unwrap()),
crate_visibility(Some(&user(false)))
);
}
#[tokio::test]
async fn unrestricted_crate_is_readable_by_anyone() {
let db = crate_db(false).await;
assert!(can_read(&db, None).await);
}
#[tokio::test]
async fn restricted_crate_is_hidden_from_anonymous() {
let db = crate_db(true).await;
assert!(!can_read(&db, None).await);
}
#[tokio::test]
async fn restricted_crate_is_readable_by_admin() {
let db = crate_db(true).await;
assert!(can_read(&db, Some(&user(true))).await);
}
#[tokio::test]
async fn restricted_crate_is_hidden_from_user_without_access() {
let db = crate_db(true).await;
assert!(!can_read(&db, Some(&user(false))).await);
}
#[tokio::test]
async fn restricted_crate_is_readable_by_crate_user() {
let db = crate_db(true).await;
db.add_crate_user(&name(), &user_name()).await.unwrap();
assert!(can_read(&db, Some(&user(false))).await);
}
#[tokio::test]
async fn restricted_crate_is_readable_by_crate_group_member() {
let db = crate_db(true).await;
let group = GroupName::new("group").unwrap();
db.add_group(&group).await.unwrap();
db.add_group_user(&group, &user_name()).await.unwrap();
db.add_crate_group(&name(), &group).await.unwrap();
assert!(can_read(&db, Some(&user(false))).await);
}
#[tokio::test]
async fn restricted_crate_is_readable_by_owner() {
let db = crate_db(true).await;
db.add_owner(&name(), &user_name()).await.unwrap();
assert!(can_read(&db, Some(&user(false))).await);
}
}
#[cfg(test)]
mod require_tests {
use super::*;
use crate::identity::Source;
fn user(is_admin: bool, is_read_only: bool) -> Identity {
Identity {
name: UserName::new("user").unwrap(),
is_admin,
is_read_only,
source: Source::Token(None),
}
}
fn status(result: Result<(), ApiError>) -> Option<StatusCode> {
result.err().map(|e| e.status())
}
#[test]
fn only_admins_pass_require_admin() {
assert_eq!(status(require_admin(&user(true, false))), None);
assert_eq!(
status(require_admin(&user(false, false))),
Some(StatusCode::FORBIDDEN)
);
}
#[test]
fn read_only_users_are_forbidden_to_write() {
assert_eq!(
status(require_writable(&user(false, true))),
Some(StatusCode::FORBIDDEN)
);
assert_eq!(status(require_writable(&user(false, false))), None);
assert_eq!(status(require_writable(&user(true, true))), None);
}
}