Josiah Glosson 62de07e4e6
Version updates (#3626)
* Update some Labrinth dependencies

* Update some Labrinth dependencies

* Update some Labrinth dependencies

* Update zip in Labrinth

* Update itertools in Labrinth

* Update validator in labrinth

* Update thiserror in labrinth

* Update rust_decimal, redis, and deadpool-redis in labrinth

* Update totp-rs and spdx in labrinth

* Update maxminddb and tar in labrinth

* Update sentry and sentry-actix in labrinth

* Update image in labrinth

* Update lettre in labrinth

* Update derive-new and rust_iso3166 in labrinth

* Update async-stripe and json-patch in labrinth

* Update clap and iana-time-zone in labrinth

* Update labrinth to Rust 2024

* Cargo fmt

* Just do a full cargo update

* Update daedelus to Rust 2024

* Update daedelus_client to Rust 2024

* Set the formatting edition to 2024

* Fix formatting

IntelliJ messed up my formatting
2025-05-09 12:27:55 +00:00

374 lines
11 KiB
Rust

use crate::database;
use crate::database::models::Collection;
use crate::database::models::project_item::QueryProject;
use crate::database::models::version_item::QueryVersion;
use crate::database::redis::RedisPool;
use crate::database::{Project, Version, models};
use crate::models::users::User;
use crate::routes::ApiError;
use itertools::Itertools;
use sqlx::PgPool;
pub trait ValidateAuthorized {
fn validate_authorized(
&self,
user_option: Option<&User>,
) -> Result<(), ApiError>;
}
pub trait ValidateAllAuthorized {
fn validate_all_authorized(
self,
user_option: Option<&User>,
) -> Result<(), ApiError>;
}
impl<'a, T, A> ValidateAllAuthorized for T
where
T: IntoIterator<Item = &'a A>,
A: ValidateAuthorized + 'a,
{
fn validate_all_authorized(
self,
user_option: Option<&User>,
) -> Result<(), ApiError> {
self.into_iter()
.try_for_each(|c| c.validate_authorized(user_option))
}
}
pub async fn is_visible_project(
project_data: &Project,
user_option: &Option<User>,
pool: &PgPool,
hide_unlisted: bool,
) -> Result<bool, ApiError> {
filter_visible_project_ids(
vec![project_data],
user_option,
pool,
hide_unlisted,
)
.await
.map(|x| !x.is_empty())
}
pub async fn is_team_member_project(
project_data: &Project,
user_option: &Option<User>,
pool: &PgPool,
) -> Result<bool, ApiError> {
filter_enlisted_projects_ids(vec![project_data], user_option, pool)
.await
.map(|x| !x.is_empty())
}
pub async fn filter_visible_projects(
mut projects: Vec<QueryProject>,
user_option: &Option<User>,
pool: &PgPool,
hide_unlisted: bool,
) -> Result<Vec<crate::models::projects::Project>, ApiError> {
let filtered_project_ids = filter_visible_project_ids(
projects.iter().map(|x| &x.inner).collect_vec(),
user_option,
pool,
hide_unlisted,
)
.await?;
projects.retain(|x| filtered_project_ids.contains(&x.inner.id));
Ok(projects.into_iter().map(|x| x.into()).collect())
}
// Filters projects for which we can see, meaning one of the following is true:
// - it's not hidden
// - the user is enlisted on the project's team (filter_enlisted_projects)
// - the user is a mod
// This is essentially whether you can know of the project's existence
pub async fn filter_visible_project_ids(
projects: Vec<&Project>,
user_option: &Option<User>,
pool: &PgPool,
hide_unlisted: bool,
) -> Result<Vec<crate::database::models::ProjectId>, ApiError> {
let mut return_projects = Vec::new();
let mut check_projects = Vec::new();
// Return projects that are not hidden or we are a mod of
for project in projects {
if (if hide_unlisted {
project.status.is_searchable()
} else {
!project.status.is_hidden()
}) || user_option
.as_ref()
.map(|x| x.role.is_mod())
.unwrap_or(false)
{
return_projects.push(project.id);
} else if user_option.is_some() {
check_projects.push(project);
}
}
// For hidden projects, return a filtered list of projects for which we are enlisted on the team
if !check_projects.is_empty() {
return_projects.extend(
filter_enlisted_projects_ids(check_projects, user_option, pool)
.await?,
);
}
Ok(return_projects)
}
// Filters out projects for which we are a member of the team (or a mod)
// These are projects we have internal access to and can potentially see even if they are hidden
// This is useful for getting visibility of versions, or seeing analytics or sensitive team-restricted data of a project
pub async fn filter_enlisted_projects_ids(
projects: Vec<&Project>,
user_option: &Option<User>,
pool: &PgPool,
) -> Result<Vec<crate::database::models::ProjectId>, ApiError> {
let mut return_projects = vec![];
if let Some(user) = user_option {
let user_id: models::ids::UserId = user.id.into();
use futures::TryStreamExt;
sqlx::query!(
"
SELECT m.id id, m.team_id team_id FROM team_members tm
INNER JOIN mods m ON m.team_id = tm.team_id
LEFT JOIN organizations o ON o.team_id = tm.team_id
WHERE tm.team_id = ANY($1) AND tm.user_id = $3
UNION
SELECT m.id id, m.team_id team_id FROM team_members tm
INNER JOIN organizations o ON o.team_id = tm.team_id
INNER JOIN mods m ON m.organization_id = o.id
WHERE o.id = ANY($2) AND tm.user_id = $3
",
&projects.iter().map(|x| x.team_id.0).collect::<Vec<_>>(),
&projects
.iter()
.filter_map(|x| x.organization_id.map(|x| x.0))
.collect::<Vec<_>>(),
user_id as database::models::ids::UserId,
)
.fetch(pool)
.map_ok(|row| {
for x in projects.iter() {
let bool =
Some(x.id.0) == row.id && Some(x.team_id.0) == row.team_id;
if bool {
return_projects.push(x.id);
}
}
})
.try_collect::<Vec<()>>()
.await?;
}
Ok(return_projects)
}
pub async fn is_visible_version(
version_data: &Version,
user_option: &Option<User>,
pool: &PgPool,
redis: &RedisPool,
) -> Result<bool, ApiError> {
filter_visible_version_ids(vec![version_data], user_option, pool, redis)
.await
.map(|x| !x.is_empty())
}
pub async fn is_team_member_version(
version_data: &Version,
user_option: &Option<User>,
pool: &PgPool,
redis: &RedisPool,
) -> Result<bool, ApiError> {
filter_enlisted_version_ids(vec![version_data], user_option, pool, redis)
.await
.map(|x| !x.is_empty())
}
pub async fn filter_visible_versions(
mut versions: Vec<QueryVersion>,
user_option: &Option<User>,
pool: &PgPool,
redis: &RedisPool,
) -> Result<Vec<crate::models::projects::Version>, ApiError> {
let filtered_version_ids = filter_visible_version_ids(
versions.iter().map(|x| &x.inner).collect_vec(),
user_option,
pool,
redis,
)
.await?;
versions.retain(|x| filtered_version_ids.contains(&x.inner.id));
Ok(versions.into_iter().map(|x| x.into()).collect())
}
impl ValidateAuthorized for models::OAuthClient {
fn validate_authorized(
&self,
user_option: Option<&User>,
) -> Result<(), ApiError> {
if let Some(user) = user_option {
return if user.role.is_mod() || user.id == self.created_by.into() {
Ok(())
} else {
Err(ApiError::CustomAuthentication(
"You don't have sufficient permissions to interact with this OAuth application"
.to_string(),
))
};
}
Ok(())
}
}
pub async fn filter_visible_version_ids(
versions: Vec<&Version>,
user_option: &Option<User>,
pool: &PgPool,
redis: &RedisPool,
) -> Result<Vec<crate::database::models::VersionId>, ApiError> {
let mut return_versions = Vec::new();
let mut check_versions = Vec::new();
// First, filter out versions belonging to projects we can't see
// (ie: a hidden project, but public version, should still be hidden)
// Gets project ids of versions
let project_ids = versions.iter().map(|x| x.project_id).collect::<Vec<_>>();
// Get visible projects- ones we are allowed to see public versions for.
let visible_project_ids = filter_visible_project_ids(
Project::get_many_ids(&project_ids, pool, redis)
.await?
.iter()
.map(|x| &x.inner)
.collect(),
user_option,
pool,
false,
)
.await?;
// Then, get enlisted versions (Versions that are a part of a project we are a member of)
let enlisted_version_ids =
filter_enlisted_version_ids(versions.clone(), user_option, pool, redis)
.await?;
// Return versions that are not hidden, we are a mod of, or we are enlisted on the team of
for version in versions {
// We can see the version if:
// - it's not hidden and we can see the project
// - we are a mod
// - we are enlisted on the team of the mod
if (!version.status.is_hidden()
&& visible_project_ids.contains(&version.project_id))
|| user_option
.as_ref()
.map(|x| x.role.is_mod())
.unwrap_or(false)
|| enlisted_version_ids.contains(&version.id)
{
return_versions.push(version.id);
} else if user_option.is_some() {
check_versions.push(version);
}
}
Ok(return_versions)
}
pub async fn filter_enlisted_version_ids(
versions: Vec<&Version>,
user_option: &Option<User>,
pool: &PgPool,
redis: &RedisPool,
) -> Result<Vec<crate::database::models::VersionId>, ApiError> {
let mut return_versions = Vec::new();
// Get project ids of versions
let project_ids = versions.iter().map(|x| x.project_id).collect::<Vec<_>>();
// Get enlisted projects- ones we are allowed to see hidden versions for.
let authorized_project_ids = filter_enlisted_projects_ids(
Project::get_many_ids(&project_ids, pool, redis)
.await?
.iter()
.map(|x| &x.inner)
.collect(),
user_option,
pool,
)
.await?;
for version in versions {
if user_option
.as_ref()
.map(|x| x.role.is_mod())
.unwrap_or(false)
|| (user_option.is_some()
&& authorized_project_ids.contains(&version.project_id))
{
return_versions.push(version.id);
}
}
Ok(return_versions)
}
pub async fn is_visible_collection(
collection_data: &Collection,
user_option: &Option<User>,
) -> Result<bool, ApiError> {
let mut authorized = !collection_data.status.is_hidden()
&& !collection_data.projects.is_empty();
if let Some(user) = &user_option {
if !authorized
&& (user.role.is_mod() || user.id == collection_data.user_id.into())
{
authorized = true;
}
}
Ok(authorized)
}
pub async fn filter_visible_collections(
collections: Vec<Collection>,
user_option: &Option<User>,
) -> Result<Vec<crate::models::collections::Collection>, ApiError> {
let mut return_collections = Vec::new();
let mut check_collections = Vec::new();
for collection in collections {
if !collection.status.is_hidden()
|| user_option
.as_ref()
.map(|x| x.role.is_mod())
.unwrap_or(false)
{
return_collections.push(collection.into());
} else if user_option.is_some() {
check_collections.push(collection);
}
}
for collection in check_collections {
// Collections are simple- if we are the owner or a mod, we can see it
if let Some(user) = user_option {
if user.role.is_mod() || user.id == collection.user_id.into() {
return_collections.push(collection.into());
}
}
}
Ok(return_collections)
}