You've already forked pages
forked from didirus/AstralRinth
I made a typo on PR https://github.com/modrinth/code/pull/4426 by making the corresponding SQL query filter by projects with an unexisting `public` status, instead of `approved`. During my testing, I used the `archived` status, so I didn't notice it back then.
399 lines
12 KiB
Rust
399 lines
12 KiB
Rust
use crate::database;
|
|
use crate::database::models::project_item::ProjectQueryResult;
|
|
use crate::database::models::version_item::VersionQueryResult;
|
|
use crate::database::models::{DBCollection, DBOrganization, DBTeamMember};
|
|
use crate::database::redis::RedisPool;
|
|
use crate::database::{DBProject, DBVersion, models};
|
|
use crate::models::users::User;
|
|
use crate::routes::ApiError;
|
|
use futures::TryStreamExt;
|
|
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: &DBProject,
|
|
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: &DBProject,
|
|
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<ProjectQueryResult>,
|
|
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<&DBProject>,
|
|
user_option: &Option<User>,
|
|
pool: &PgPool,
|
|
hide_unlisted: bool,
|
|
) -> Result<Vec<crate::database::models::DBProjectId>, 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().is_some_and(|x| x.role.is_mod())
|
|
{
|
|
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<&DBProject>,
|
|
user_option: &Option<User>,
|
|
pool: &PgPool,
|
|
) -> Result<Vec<crate::database::models::DBProjectId>, ApiError> {
|
|
let mut return_projects = vec![];
|
|
|
|
if let Some(user) = user_option {
|
|
let user_id: models::ids::DBUserId = user.id.into();
|
|
|
|
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::DBUserId,
|
|
)
|
|
.fetch(pool)
|
|
.map_ok(|row| {
|
|
for x in &projects {
|
|
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: &DBVersion,
|
|
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: &DBVersion,
|
|
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<VersionQueryResult>,
|
|
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::DBOAuthClient {
|
|
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<&DBVersion>,
|
|
user_option: &Option<User>,
|
|
pool: &PgPool,
|
|
redis: &RedisPool,
|
|
) -> Result<Vec<crate::database::models::DBVersionId>, ApiError> {
|
|
let mut return_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(
|
|
DBProject::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().is_some_and(|x| x.role.is_mod())
|
|
|| enlisted_version_ids.contains(&version.id)
|
|
{
|
|
return_versions.push(version.id);
|
|
}
|
|
}
|
|
|
|
Ok(return_versions)
|
|
}
|
|
|
|
pub async fn filter_enlisted_version_ids(
|
|
versions: Vec<&DBVersion>,
|
|
user_option: &Option<User>,
|
|
pool: &PgPool,
|
|
redis: &RedisPool,
|
|
) -> Result<Vec<crate::database::models::DBVersionId>, 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(
|
|
DBProject::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().is_some_and(|x| x.role.is_mod())
|
|
|| (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: &DBCollection,
|
|
user_option: &Option<User>,
|
|
hide_unlisted: bool,
|
|
) -> Result<bool, ApiError> {
|
|
let mut authorized = (if hide_unlisted {
|
|
collection_data.status.is_searchable()
|
|
} else {
|
|
!collection_data.status.is_hidden()
|
|
}) && !collection_data.projects.is_empty();
|
|
if let Some(user) = &user_option
|
|
&& !authorized
|
|
&& (user.role.is_mod() || user.id == collection_data.user_id.into())
|
|
{
|
|
authorized = true;
|
|
}
|
|
Ok(authorized)
|
|
}
|
|
|
|
pub async fn filter_visible_collections(
|
|
collections: Vec<DBCollection>,
|
|
user_option: &Option<User>,
|
|
hide_unlisted: bool,
|
|
) -> Result<Vec<crate::models::collections::Collection>, ApiError> {
|
|
let mut return_collections = Vec::new();
|
|
let mut check_collections = Vec::new();
|
|
|
|
for collection in collections {
|
|
if ((if hide_unlisted {
|
|
collection.status.is_searchable()
|
|
} else {
|
|
!collection.status.is_hidden()
|
|
}) && !collection.projects.is_empty())
|
|
|| user_option.as_ref().is_some_and(|x| x.role.is_mod())
|
|
{
|
|
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
|
|
&& (user.role.is_mod() || user.id == collection.user_id.into())
|
|
{
|
|
return_collections.push(collection.into());
|
|
}
|
|
}
|
|
|
|
Ok(return_collections)
|
|
}
|
|
|
|
pub async fn is_visible_organization(
|
|
organization: &DBOrganization,
|
|
viewing_user: &Option<User>,
|
|
pool: &PgPool,
|
|
redis: &RedisPool,
|
|
) -> Result<bool, ApiError> {
|
|
let members =
|
|
DBTeamMember::get_from_team_full(organization.team_id, pool, redis)
|
|
.await?;
|
|
|
|
// This is meant to match the same projects as the `Project::is_searchable` method, but we're not using
|
|
// it here because that'd entail pulling in all projects for the organization
|
|
let has_searchable_projects = sqlx::query_scalar!(
|
|
"SELECT TRUE FROM mods WHERE organization_id = $1 AND status IN ('approved', 'archived') LIMIT 1",
|
|
organization.id as database::models::ids::DBOrganizationId
|
|
)
|
|
.fetch_optional(pool)
|
|
.await?
|
|
.flatten()
|
|
.unwrap_or(false);
|
|
|
|
let visible = has_searchable_projects
|
|
|| members.iter().filter(|member| member.accepted).count() > 1
|
|
|| viewing_user.as_ref().is_some_and(|viewing_user| {
|
|
viewing_user.role.is_mod()
|
|
|| members
|
|
.iter()
|
|
.any(|member| member.user_id == viewing_user.id.into())
|
|
});
|
|
|
|
Ok(visible)
|
|
}
|