Filtering refactoring (#806)

* switching computers

* fmt clippy sqlx prepare

* merge fixes
This commit is contained in:
Wyatt Verchere
2023-12-21 16:36:30 -08:00
committed by GitHub
parent b46f3bf2c4
commit 76e00c2432
18 changed files with 293 additions and 392 deletions

View File

@@ -7,6 +7,7 @@ use crate::database::{models, Project, Version};
use crate::models::users::User;
use crate::routes::ApiError;
use actix_web::web;
use itertools::Itertools;
use sqlx::PgPool;
pub trait ValidateAuthorized {
@@ -28,179 +29,168 @@ where
}
}
pub async fn is_authorized(
pub async fn is_visible_project(
project_data: &Project,
user_option: &Option<User>,
pool: &web::Data<PgPool>,
) -> Result<bool, ApiError> {
let mut authorized = !project_data.status.is_hidden();
if let Some(user) = &user_option {
if !authorized {
if user.role.is_mod() {
authorized = true;
} else {
let user_id: models::ids::UserId = user.id.into();
let project_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)",
project_data.team_id as database::models::ids::TeamId,
user_id as database::models::ids::UserId,
)
.fetch_one(&***pool)
.await?
.exists;
let organization_exists =
if let Some(organization_id) = project_data.organization_id {
sqlx::query!(
"SELECT EXISTS(
SELECT 1
FROM organizations o JOIN team_members tm ON tm.team_id = o.team_id
WHERE o.id = $1 AND tm.user_id = $2
)",
organization_id as database::models::ids::OrganizationId,
user_id as database::models::ids::UserId,
)
.fetch_one(&***pool)
.await?
.exists
} else {
None
};
authorized =
project_exists.unwrap_or(false) || organization_exists.unwrap_or(false);
}
}
}
Ok(authorized)
filter_visible_project_ids(vec![project_data], user_option, pool)
.await
.map(|x| !x.is_empty())
}
pub async fn filter_authorized_projects(
projects: Vec<QueryProject>,
pub async fn is_team_member_project(
project_data: &Project,
user_option: &Option<User>,
pool: &web::Data<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: &web::Data<PgPool>,
) -> 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,
)
.await
.unwrap();
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: &web::Data<PgPool>,
) -> 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 !project.inner.status.is_hidden()
if !project.status.is_hidden()
|| user_option
.as_ref()
.map(|x| x.role.is_mod())
.unwrap_or(false)
{
return_projects.push(project.into());
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() {
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
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
",
&check_projects
.iter()
.map(|x| x.inner.team_id.0)
.collect::<Vec<_>>(),
&check_projects
.iter()
.filter_map(|x| x.inner.organization_id.map(|x| x.0))
.collect::<Vec<_>>(),
user_id as database::models::ids::UserId,
)
.fetch_many(&***pool)
.try_for_each(|e| {
if let Some(row) = e.right() {
check_projects.retain(|x| {
let bool =
Some(x.inner.id.0) == row.id && Some(x.inner.team_id.0) == row.team_id;
if bool {
return_projects.push(x.clone().into());
}
!bool
});
}
futures::future::ready(Ok(()))
})
.await?;
}
return_projects
.extend(filter_enlisted_projects_ids(check_projects, user_option, pool).await?);
}
Ok(return_projects)
}
pub async fn is_authorized_version(
// 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: &web::Data<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_many(&***pool)
.try_for_each(|e| {
if let Some(row) = e.right() {
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);
}
}
}
futures::future::ready(Ok(()))
})
.await?;
}
Ok(return_projects)
}
pub async fn is_visible_version(
version_data: &Version,
user_option: &Option<User>,
pool: &web::Data<PgPool>,
redis: &RedisPool,
) -> Result<bool, ApiError> {
let mut authorized = !version_data.status.is_hidden();
filter_visible_version_ids(vec![version_data], user_option, pool, redis)
.await
.map(|x| !x.is_empty())
}
if let Some(user) = &user_option {
if !authorized {
if user.role.is_mod() {
authorized = true;
} else {
let user_id: models::ids::UserId = user.id.into();
pub async fn is_team_member_version(
version_data: &Version,
user_option: &Option<User>,
pool: &web::Data<PgPool>,
redis: &RedisPool,
) -> Result<bool, ApiError> {
filter_enlisted_version_ids(vec![version_data], user_option, pool, redis)
.await
.map(|x| !x.is_empty())
}
let version_exists = sqlx::query!(
"SELECT EXISTS(
SELECT 1 FROM mods m
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2
WHERE m.id = $1
)",
version_data.project_id as database::models::ids::ProjectId,
user_id as database::models::ids::UserId,
)
.fetch_one(&***pool)
.await?
.exists;
let version_organization_exists = sqlx::query!(
"SELECT EXISTS(
SELECT 1 FROM mods m
INNER JOIN organizations o ON m.organization_id = o.id
INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2
WHERE m.id = $1
)",
version_data.project_id as database::models::ids::ProjectId,
user_id as database::models::ids::UserId,
)
.fetch_one(&***pool)
.await?
.exists;
authorized = version_exists
.or(version_organization_exists)
.unwrap_or(false);
}
}
}
Ok(authorized)
pub async fn filter_visible_versions(
mut versions: Vec<QueryVersion>,
user_option: &Option<User>,
pool: &web::Data<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
.unwrap();
versions.retain(|x| filtered_version_ids.contains(&x.inner.id));
Ok(versions.into_iter().map(|x| x.into()).collect())
}
impl ValidateAuthorized for models::OAuthClient {
@@ -220,62 +210,111 @@ impl ValidateAuthorized for models::OAuthClient {
}
}
pub async fn filter_authorized_versions(
versions: Vec<QueryVersion>,
pub async fn filter_visible_version_ids(
versions: Vec<&Version>,
user_option: &Option<User>,
pool: &web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<Vec<crate::models::projects::Version>, ApiError> {
redis: &RedisPool,
) -> Result<Vec<crate::database::models::VersionId>, ApiError> {
let mut return_versions = Vec::new();
let mut check_versions = Vec::new();
let project_ids = versions
.iter()
.map(|x| x.inner.project_id)
.collect::<Vec<_>>();
// 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<_>>();
let authorized_projects = filter_authorized_projects(
Project::get_many_ids(&project_ids, &***pool, &redis).await?,
// 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,
)
.await?;
let authorized_project_ids: Vec<_> = authorized_projects.iter().map(|x| x.id.into()).collect();
// 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 {
if !version.inner.status.is_hidden()
// 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)
|| (user_option.is_some() && authorized_project_ids.contains(&version.inner.project_id))
|| enlisted_version_ids.contains(&version.id)
{
return_versions.push(version.into());
return_versions.push(version.id);
} else if user_option.is_some() {
check_versions.push(version);
}
}
Ok(return_versions)
}
pub async fn is_authorized_collection(
pub async fn filter_enlisted_version_ids(
versions: Vec<&Version>,
user_option: &Option<User>,
pool: &web::Data<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();
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_authorized_collections(
pub async fn filter_visible_collections(
collections: Vec<Collection>,
user_option: &Option<User>,
pool: &web::Data<PgPool>,
) -> Result<Vec<crate::models::collections::Collection>, ApiError> {
let mut return_collections = Vec::new();
let mut check_collections = Vec::new();
@@ -293,37 +332,12 @@ pub async fn filter_authorized_collections(
}
}
if !check_collections.is_empty() {
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 {
let user_id: models::ids::UserId = user.id.into();
use futures::TryStreamExt;
sqlx::query!(
"
SELECT c.id id, c.user_id user_id FROM collections c
WHERE c.user_id = $2 AND c.id = ANY($1)
",
&check_collections.iter().map(|x| x.id.0).collect::<Vec<_>>(),
user_id as database::models::ids::UserId,
)
.fetch_many(&***pool)
.try_for_each(|e| {
if let Some(row) = e.right() {
check_collections.retain(|x| {
let bool = x.id.0 == row.id && x.user_id.0 == row.user_id;
if bool {
return_collections.push(x.clone().into());
}
!bool
});
}
futures::future::ready(Ok(()))
})
.await?;
if user.role.is_mod() || user.id == collection.user_id.into() {
return_collections.push(collection.into());
}
}
}

View File

@@ -4,7 +4,8 @@ pub mod oauth;
pub mod templates;
pub mod validate;
pub use checks::{
filter_authorized_projects, filter_authorized_versions, is_authorized, is_authorized_version,
filter_enlisted_projects_ids, filter_enlisted_version_ids, filter_visible_collections,
filter_visible_project_ids, filter_visible_projects,
};
use serde::{Deserialize, Serialize};
// pub use pat::{generate_pat, PersonalAccessToken};