From e893756c140151cbcc7564709410a26c2b561925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?= <108630700+fetchfern@users.noreply.github.com> Date: Thu, 5 Nov 2026 18:36:14 -0500 Subject: [PATCH 1/8] feat: staff PAT scopes --- apps/labrinth/src/auth/checks.rs | 68 +++++--- apps/labrinth/src/auth/mod.rs | 3 +- apps/labrinth/src/auth/oauth/mod.rs | 6 +- apps/labrinth/src/auth/validate.rs | 90 +++++----- apps/labrinth/src/models/v3/pats.rs | 33 ++++ apps/labrinth/src/models/v3/teams.rs | 44 +++-- apps/labrinth/src/models/v3/thread_issues.rs | 9 +- apps/labrinth/src/models/v3/threads.rs | 15 +- apps/labrinth/src/models/v3/user_limits.rs | 37 ++-- apps/labrinth/src/models/v3/users.rs | 15 +- .../routes/internal/admin/user_credentials.rs | 20 ++- .../src/routes/internal/admin/user_lock.rs | 17 +- .../routes/internal/admin/user_sessions.rs | 5 +- .../src/routes/internal/analytics_event.rs | 15 +- .../src/routes/internal/attribution.rs | 65 ++++--- apps/labrinth/src/routes/internal/billing.rs | 86 +++++----- .../src/routes/internal/delphi/mod.rs | 14 +- .../routes/internal/external_notifications.rs | 6 +- apps/labrinth/src/routes/internal/flows.rs | 22 +-- apps/labrinth/src/routes/internal/gdpr.rs | 7 +- .../moderation/copyright_archives/mod.rs | 18 +- .../internal/moderation/external_license.rs | 32 ++-- .../src/routes/internal/moderation/mod.rs | 83 +++++---- .../routes/internal/moderation/tech_review.rs | 58 +++---- .../internal/moderation/tech_review/global.rs | 10 +- .../internal/moderation/tech_review/rules.rs | 35 ++-- .../moderation/tech_review/rules_scan.rs | 14 +- .../internal/moderation/user_restrictions.rs | 12 +- apps/labrinth/src/routes/internal/pats.rs | 26 ++- .../src/routes/internal/server_ping.rs | 2 +- apps/labrinth/src/routes/internal/session.rs | 6 +- apps/labrinth/src/routes/maven.rs | 4 - apps/labrinth/src/routes/mod.rs | 5 +- apps/labrinth/src/routes/updates.rs | 1 - apps/labrinth/src/routes/v2/versions.rs | 1 - .../src/routes/v3/analytics_get/mod.rs | 22 +-- apps/labrinth/src/routes/v3/blocked_users.rs | 6 +- apps/labrinth/src/routes/v3/collections.rs | 30 ++-- apps/labrinth/src/routes/v3/content/mod.rs | 7 +- apps/labrinth/src/routes/v3/disclosures.rs | 21 ++- apps/labrinth/src/routes/v3/friends.rs | 6 +- apps/labrinth/src/routes/v3/images.rs | 28 ++- apps/labrinth/src/routes/v3/limits.rs | 6 +- apps/labrinth/src/routes/v3/notifications.rs | 55 +++--- apps/labrinth/src/routes/v3/oauth_clients.rs | 34 ++-- apps/labrinth/src/routes/v3/organizations.rs | 108 ++++++------ apps/labrinth/src/routes/v3/payouts.rs | 16 +- .../src/routes/v3/project_creation.rs | 25 ++- .../src/routes/v3/project_creation/new.rs | 29 ++-- apps/labrinth/src/routes/v3/projects/mod.rs | 161 ++++++++---------- .../src/routes/v3/projects/mutation.rs | 21 ++- .../src/routes/v3/projects/validate.rs | 9 +- apps/labrinth/src/routes/v3/reports.rs | 94 +++++----- apps/labrinth/src/routes/v3/teams.rs | 108 ++++++------ apps/labrinth/src/routes/v3/threads.rs | 91 +++++----- apps/labrinth/src/routes/v3/users.rs | 110 ++++++------ .../src/routes/v3/version_creation.rs | 34 ++-- apps/labrinth/src/routes/v3/version_file.rs | 26 ++- apps/labrinth/src/routes/v3/versions.rs | 32 ++-- apps/labrinth/src/util/validate.rs | 2 +- apps/labrinth/tests/limits.rs | 13 +- apps/labrinth/tests/oauth_clients.rs | 2 +- apps/labrinth/tests/pats.rs | 18 +- apps/labrinth/tests/project.rs | 6 +- apps/labrinth/tests/scopes.rs | 4 +- 65 files changed, 1047 insertions(+), 961 deletions(-) diff --git a/apps/labrinth/src/auth/checks.rs b/apps/labrinth/src/auth/checks.rs index f0c132b20c4..15ce7d49100 100644 --- a/apps/labrinth/src/auth/checks.rs +++ b/apps/labrinth/src/auth/checks.rs @@ -1,3 +1,4 @@ +use crate::auth::UserWithScopes; use crate::database; use crate::database::models::project_item::ProjectQueryResult; use crate::database::models::version_item::VersionQueryResult; @@ -5,6 +6,7 @@ use crate::database::models::{DBCollection, DBOrganization, DBTeamMember}; use crate::database::{DBProject, DBVersion, models}; use crate::database::{PgPool, ReadOnlyPgPool}; use crate::models::ids::FileId; +use crate::models::pats::Scopes; use crate::models::projects::{ MissingAttributionFile, OverrideSource, Version, }; @@ -30,14 +32,14 @@ pub fn require_verified_email(user: &User) -> Result<(), ApiError> { pub trait ValidateAuthorized { fn validate_authorized( &self, - user_option: Option<&User>, + user_option: Option<&UserWithScopes>, ) -> Result<(), ApiError>; } pub trait ValidateAllAuthorized { fn validate_all_authorized( self, - user_option: Option<&User>, + user_option: Option<&UserWithScopes>, ) -> Result<(), ApiError>; } @@ -48,7 +50,7 @@ where { fn validate_all_authorized( self, - user_option: Option<&User>, + user_option: Option<&UserWithScopes>, ) -> Result<(), ApiError> { self.into_iter() .try_for_each(|c| c.validate_authorized(user_option)) @@ -57,7 +59,7 @@ where pub async fn is_visible_project( project_data: &DBProject, - user_option: &Option, + user_option: &Option, pool: &PgPool, hide_unlisted: bool, ) -> Result { @@ -73,7 +75,7 @@ pub async fn is_visible_project( pub async fn is_team_member_project( project_data: &DBProject, - user_option: &Option, + user_option: &Option, pool: &PgPool, ) -> Result { filter_enlisted_projects_ids(vec![project_data], user_option, pool) @@ -83,7 +85,7 @@ pub async fn is_team_member_project( pub async fn filter_visible_projects( mut projects: Vec, - user_option: &Option, + user_option: &Option, pool: &PgPool, hide_unlisted: bool, ) -> Result, ApiError> { @@ -106,7 +108,7 @@ pub async fn filter_visible_projects( // 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_option: &Option, pool: &PgPool, hide_unlisted: bool, ) -> Result, ApiError> { @@ -119,7 +121,9 @@ pub async fn filter_visible_project_ids( project.status.is_searchable() } else { !project.status.is_hidden() - }) || user_option.as_ref().is_some_and(|x| x.role.is_mod()) + }) || user_option + .as_ref() + .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) { return_projects.push(project.id); } else if user_option.is_some() { @@ -144,13 +148,13 @@ pub async fn filter_visible_project_ids( // 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_option: &Option, pool: &PgPool, ) -> Result, ApiError> { let mut return_projects = vec![]; if let Some(user) = user_option { - let user_id: models::ids::DBUserId = user.id.into(); + let user_id: models::ids::DBUserId = user.user.id.into(); sqlx::query!( " @@ -190,7 +194,7 @@ pub async fn filter_enlisted_projects_ids( pub async fn is_visible_version( version_data: &DBVersion, - user_option: &Option, + user_option: &Option, pool: &PgPool, redis: &RedisPool, ) -> Result { @@ -201,7 +205,7 @@ pub async fn is_visible_version( pub async fn is_team_member_version( version_data: &DBVersion, - user_option: &Option, + user_option: &Option, pool: &PgPool, redis: &RedisPool, ) -> Result { @@ -212,7 +216,7 @@ pub async fn is_team_member_version( pub async fn filter_visible_versions( mut versions: Vec, - user_option: &Option, + user_option: &Option, ro_pool: &ReadOnlyPgPool, redis: &RedisPool, ) -> Result, ApiError> { @@ -272,10 +276,12 @@ pub async fn filter_visible_versions( impl ValidateAuthorized for models::DBOAuthClient { fn validate_authorized( &self, - user_option: Option<&User>, + user_option: Option<&UserWithScopes>, ) -> Result<(), ApiError> { if let Some(user) = user_option { - return if user.role.is_mod() || user.id == self.created_by.into() { + return if user.has_scopes(Scopes::MODERATION_WRITE) + || user.user.id == self.created_by.into() + { Ok(()) } else { Err(ApiError::Auth(eyre::eyre!( @@ -290,7 +296,7 @@ impl ValidateAuthorized for models::DBOAuthClient { pub async fn filter_visible_version_ids( versions: Vec<&DBVersion>, - user_option: &Option, + user_option: &Option, pool: &PgPool, redis: &RedisPool, ) -> Result, ApiError> { @@ -339,7 +345,9 @@ pub async fn filter_visible_version_ids( if (!version.status.is_hidden() && !is_withheld && visible_project_ids.contains(&version.project_id)) - || user_option.as_ref().is_some_and(|x| x.role.is_mod()) + || user_option + .as_ref() + .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) || enlisted_version_ids.contains(&version.id) { return_versions.push(version.id); @@ -351,7 +359,7 @@ pub async fn filter_visible_version_ids( pub async fn filter_enlisted_version_ids( versions: Vec<&DBVersion>, - user_option: &Option, + user_option: &Option, pool: &PgPool, redis: &RedisPool, ) -> Result, ApiError> { @@ -375,7 +383,9 @@ pub async fn filter_enlisted_version_ids( .wrap_api_err("filtering projects by membership")?; for version in versions { - if user_option.as_ref().is_some_and(|x| x.role.is_mod()) + if user_option + .as_ref() + .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) || (user_option.is_some() && authorized_project_ids.contains(&version.project_id)) { @@ -388,7 +398,7 @@ pub async fn filter_enlisted_version_ids( pub async fn is_visible_collection( collection_data: &DBCollection, - user_option: &Option, + user_option: &Option, hide_unlisted: bool, ) -> Result { let mut authorized = (if hide_unlisted { @@ -398,7 +408,8 @@ pub async fn is_visible_collection( }) && !collection_data.projects.is_empty(); if let Some(user) = &user_option && !authorized - && (user.role.is_mod() || user.id == collection_data.user_id.into()) + && (user.has_scopes(Scopes::MODERATION_READ) + || user.user.id == collection_data.user_id.into()) { authorized = true; } @@ -407,7 +418,7 @@ pub async fn is_visible_collection( pub async fn filter_visible_collections( collections: Vec, - user_option: &Option, + user_option: &Option, hide_unlisted: bool, ) -> Result, ApiError> { let mut return_collections = Vec::new(); @@ -419,7 +430,9 @@ pub async fn filter_visible_collections( } else { !collection.status.is_hidden() }) && !collection.projects.is_empty()) - || user_option.as_ref().is_some_and(|x| x.role.is_mod()) + || user_option + .as_ref() + .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) { return_collections.push(collection.into()); } else if user_option.is_some() { @@ -430,7 +443,8 @@ pub async fn filter_visible_collections( 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()) + && (user.has_scopes(Scopes::MODERATION_READ) + || user.user.id == collection.user_id.into()) { return_collections.push(collection.into()); } @@ -441,7 +455,7 @@ pub async fn filter_visible_collections( pub async fn is_visible_organization( organization: &DBOrganization, - viewing_user: &Option, + viewing_user: &Option, pool: &PgPool, redis: &RedisPool, ) -> Result { @@ -464,10 +478,10 @@ pub async fn is_visible_organization( 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() + viewing_user.has_scopes(Scopes::MODERATION_READ) || members .iter() - .any(|member| member.user_id == viewing_user.id.into()) + .any(|member| member.user_id == viewing_user.user.id.into()) }); Ok(visible) diff --git a/apps/labrinth/src/auth/mod.rs b/apps/labrinth/src/auth/mod.rs index 9ac7da77dc3..a63ff2970c9 100644 --- a/apps/labrinth/src/auth/mod.rs +++ b/apps/labrinth/src/auth/mod.rs @@ -9,8 +9,7 @@ pub use checks::{ }; use serde::{Deserialize, Serialize}; pub use validate::{ - check_is_moderator_from_headers, get_user_from_bearer_token, - get_user_from_headers, + UserWithScopes, get_user_from_bearer_token, get_user_from_headers, }; use crate::file_hosting::FileHostingError; diff --git a/apps/labrinth/src/auth/oauth/mod.rs b/apps/labrinth/src/auth/oauth/mod.rs index adb48931048..efe8e159441 100644 --- a/apps/labrinth/src/auth/oauth/mod.rs +++ b/apps/labrinth/src/auth/oauth/mod.rs @@ -85,7 +85,7 @@ pub async fn init_oauth( Scopes::USER_AUTH_WRITE, ) .await? - .1; + .into_user(); let client_id = oauth_info.client_id.into(); let client = DBOAuthClient::get(client_id, &**pool).await?; @@ -294,7 +294,7 @@ pub async fn request_token( )); } - let scopes = scopes - Scopes::restricted(); + let scopes = scopes - Scopes::restricted() - Scopes::staff(); let mut transaction = pool.begin().await?; let token_id = @@ -352,7 +352,7 @@ pub async fn accept_or_reject_client_scopes( Scopes::SESSION_ACCESS | Scopes::USER_AUTH_WRITE, ) .await? - .1; + .into_user(); let flow = DBFlow::take_if( &body.flow, diff --git a/apps/labrinth/src/auth/validate.rs b/apps/labrinth/src/auth/validate.rs index 2a7d64694d0..9132ee9b009 100644 --- a/apps/labrinth/src/auth/validate.rs +++ b/apps/labrinth/src/auth/validate.rs @@ -3,7 +3,7 @@ use crate::auth::AuthenticationError; use crate::database::models::{DBUser, user_item}; use crate::env::ENV; use crate::models::pats::Scopes; -use crate::models::users::User; +use crate::models::users::{Role, User}; use crate::queue::session::AuthQueue; use crate::routes::internal::session::get_session_metadata; use actix_web::HttpRequest; @@ -11,13 +11,38 @@ use actix_web::http::header::{AUTHORIZATION, HeaderValue}; use chrono::Utc; use xredis::RedisPool; +/// An authenticated user, along with the scopes their credentials grant. +/// +/// Staff access is decided by [`Scopes::staff`] scopes in `scopes`, never by +/// the user's role. +#[derive(Debug, Clone)] +pub struct UserWithScopes { + pub user: User, + pub scopes: Scopes, +} + +impl UserWithScopes { + pub fn into_user(self) -> User { + self.user + } + + pub fn to_user(&self) -> User { + self.user.clone() + } + + /// Whether the credentials grant all of `scopes`. + pub fn has_scopes(&self, scopes: Scopes) -> bool { + self.scopes.contains(scopes) + } +} + pub async fn get_maybe_user_from_headers<'a, E>( req: &HttpRequest, executor: E, redis: &RedisPool, session_queue: &AuthQueue, required_scopes: Scopes, -) -> Result, AuthenticationError> +) -> Result, AuthenticationError> where E: crate::database::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -43,7 +68,10 @@ where return Ok(None); } - Ok(Some((scopes, User::from_full(db_user)))) + Ok(Some(UserWithScopes { + user: User::from_full(db_user), + scopes, + })) } pub async fn get_full_user_from_headers<'a, E>( @@ -84,7 +112,7 @@ pub async fn get_user_from_headers<'a, E>( redis: &RedisPool, session_queue: &AuthQueue, required_scopes: Scopes, -) -> Result<(Scopes, User), AuthenticationError> +) -> Result where E: crate::database::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -97,7 +125,10 @@ where ) .await?; - Ok((scopes, User::from_full(db_user))) + Ok(UserWithScopes { + user: User::from_full(db_user), + scopes, + }) } pub async fn get_user_from_bearer_token<'a, E>( @@ -107,7 +138,7 @@ pub async fn get_user_from_bearer_token<'a, E>( redis: &RedisPool, session_queue: &AuthQueue, allow_expired: bool, -) -> Result<(Scopes, User), AuthenticationError> +) -> Result where E: crate::database::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -122,7 +153,10 @@ where .await? .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - Ok((scopes, User::from_full(db_user))) + Ok(UserWithScopes { + user: User::from_full(db_user), + scopes, + }) } pub async fn get_user_record_from_bearer_token<'a, 'b, E>( @@ -213,7 +247,7 @@ where session_queue.add_oauth_access_token(access_token.id).await; - user.map(|u| (access_token.scopes, u)) + user.map(|u| (access_token.scopes - Scopes::staff(), u)) } Some(("github" | "gho" | "ghp", _)) => { let user = AuthProvider::GitHub.get_user(token).await?; @@ -227,7 +261,9 @@ where ) .await?; - user.map(|x| ((Scopes::all() ^ Scopes::restricted()), x)) + user.map(|x| { + (Scopes::all() - Scopes::restricted() - Scopes::staff(), x) + }) } _ => return Err(AuthenticationError::InvalidAuthMethod), }; @@ -239,8 +275,13 @@ where return Err(AuthenticationError::AccountLocked); } - Ok(possible_user - .map(|(scopes, user)| (scopes - user.removed_perms(), user))) + Ok(possible_user.map(|(scopes, user)| { + let allowed_staff = + Scopes::staff_for_role(&Role::from_string(&user.role)); + let scopes = + scopes - (Scopes::staff() - allowed_staff) - user.removed_perms(); + (scopes, user) + })) } pub fn extract_authorization_header( @@ -258,30 +299,3 @@ pub fn extract_authorization_header( token_val }) } - -pub async fn check_is_moderator_from_headers<'a, 'b, E>( - req: &HttpRequest, - executor: E, - redis: &RedisPool, - session_queue: &AuthQueue, - required_scopes: Scopes, -) -> Result -where - E: crate::database::Executor<'a, Database = sqlx::Postgres> + Copy, -{ - let user = get_user_from_headers( - req, - executor, - redis, - session_queue, - required_scopes, - ) - .await? - .1; - - if user.role.is_mod() { - Ok(user) - } else { - Err(AuthenticationError::InvalidCredentials) - } -} diff --git a/apps/labrinth/src/models/v3/pats.rs b/apps/labrinth/src/models/v3/pats.rs index 3bb1b8cd5c2..7c37252c5af 100644 --- a/apps/labrinth/src/models/v3/pats.rs +++ b/apps/labrinth/src/models/v3/pats.rs @@ -1,5 +1,6 @@ use crate::bitflags_serde_impl; use crate::models::ids::PatId; +use crate::models::users::Role; use ariadne::ids::UserId; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -118,6 +119,15 @@ bitflags::bitflags! { // delete a shared instance version const SHARED_INSTANCE_VERSION_DELETE = 1 << 47; + // view content and data only visible to moderators + const MODERATION_READ = 1 << 48; + // perform moderation actions + const MODERATION_WRITE = 1 << 49; + // view content and data only visible to admins + const ADMIN_READ = 1 << 50; + // perform admin actions + const ADMIN_WRITE = 1 << 51; + const NONE = 0b0; } } @@ -173,6 +183,29 @@ impl Scopes { self.intersects(Self::restricted()) } + /// Scopes which grant moderation or admin access. + /// + /// A credential only holds these while its user has a role which allows + /// them (see [`Scopes::staff_for_role`]), and OAuth or GitHub tokens never + /// hold them. + pub fn staff() -> Scopes { + Scopes::MODERATION_READ + | Scopes::MODERATION_WRITE + | Scopes::ADMIN_READ + | Scopes::ADMIN_WRITE + } + + /// The staff scopes which a user with the given role may hold. + pub fn staff_for_role(role: &Role) -> Scopes { + match role { + Role::Developer => Scopes::NONE, + Role::Moderator => { + Scopes::MODERATION_READ | Scopes::MODERATION_WRITE + } + Role::Admin => Scopes::staff(), + } + } + pub fn parse_from_oauth_scopes( scopes: &str, ) -> Result { diff --git a/apps/labrinth/src/models/v3/teams.rs b/apps/labrinth/src/models/v3/teams.rs index 97d866f5ac4..cc921a2c458 100644 --- a/apps/labrinth/src/models/v3/teams.rs +++ b/apps/labrinth/src/models/v3/teams.rs @@ -1,5 +1,6 @@ use crate::bitflags_serde_impl; use crate::models::ids::TeamId; +use crate::models::pats::Scopes; use crate::models::users::User; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -57,38 +58,45 @@ impl Default for ProjectPermissions { } impl ProjectPermissions { - pub fn get_permissions_by_role( - role: &crate::models::users::Role, + /// Permissions on a project for a user whose credentials grant `scopes`. + pub fn get_permissions_by_scopes( + scopes: Scopes, project_team_member: &Option, // team member of the user in the project organization_team_member: &Option< crate::database::models::DBTeamMember, >, // team member of the user in the organization ) -> Option { - if role.is_admin() { + if scopes.contains(Scopes::ADMIN_WRITE) { return Some(ProjectPermissions::all()); } + let admin_read = if scopes.contains(Scopes::ADMIN_READ) { + ProjectPermissions::VIEW_ANALYTICS + | ProjectPermissions::VIEW_PAYOUTS + } else { + ProjectPermissions::empty() + }; + if let Some(member) = project_team_member && member.accepted { - return Some(member.permissions); + return Some(member.permissions | admin_read); } if let Some(member) = organization_team_member && member.accepted { - return Some(member.permissions); + return Some(member.permissions | admin_read); } - if role.is_mod() { - Some( - ProjectPermissions::EDIT_DETAILS - | ProjectPermissions::EDIT_BODY - | ProjectPermissions::UPLOAD_VERSION, - ) - } else { - None + let mut permissions = admin_read; + if scopes.contains(Scopes::MODERATION_WRITE) { + permissions |= ProjectPermissions::EDIT_DETAILS + | ProjectPermissions::EDIT_BODY + | ProjectPermissions::UPLOAD_VERSION; } + + (!permissions.is_empty()).then_some(permissions) } } @@ -133,11 +141,13 @@ impl Default for OrganizationPermissions { } impl OrganizationPermissions { - pub fn get_permissions_by_role( - role: &crate::models::users::Role, + /// Permissions on an organization for a user whose credentials grant + /// `scopes`. + pub fn get_permissions_by_scopes( + scopes: Scopes, team_member: &Option, ) -> Option { - if role.is_admin() { + if scopes.contains(Scopes::ADMIN_WRITE) { return Some(OrganizationPermissions::all()); } @@ -146,7 +156,7 @@ impl OrganizationPermissions { { return member.organization_permissions; } - if role.is_mod() { + if scopes.contains(Scopes::MODERATION_WRITE) { return Some( OrganizationPermissions::EDIT_DETAILS | OrganizationPermissions::ADD_PROJECT, diff --git a/apps/labrinth/src/models/v3/thread_issues.rs b/apps/labrinth/src/models/v3/thread_issues.rs index 11bba8d176d..b611653b637 100644 --- a/apps/labrinth/src/models/v3/thread_issues.rs +++ b/apps/labrinth/src/models/v3/thread_issues.rs @@ -3,14 +3,15 @@ reason = "utoipa generates a large array for the thread issue target schema" )] +use crate::auth::UserWithScopes; use crate::models::disclosures::ProjectDisclosure; use crate::models::exp::minecraft::Language; use crate::models::ids::{ FileId, GalleryImageId, TeamId, ThreadIssueFacetId, ThreadIssueId, VersionId, }; +use crate::models::pats::Scopes; use crate::models::projects::{Dependency, FileType, Project, Version}; -use crate::models::users::User; use crate::util::non_empty_vec::NonEmptyVec; use ariadne::ids::UserId; use chrono::{DateTime, Utc}; @@ -800,11 +801,13 @@ fn value_state(target: &TextTarget, current: &str) -> ThreadIssueValueState { impl ThreadIssue { pub fn from( data: crate::database::models::DBThreadIssue, - user: &User, + user: &UserWithScopes, ) -> Self { Self { id: data.id.into(), - created_by: user.role.is_mod().then(|| data.created_by.into()), + created_by: user + .has_scopes(Scopes::MODERATION_READ) + .then(|| data.created_by.into()), why: data.why, facets: data.facets, verdict: data.verdict, diff --git a/apps/labrinth/src/models/v3/threads.rs b/apps/labrinth/src/models/v3/threads.rs index 5bef100510c..39a7046d7ea 100644 --- a/apps/labrinth/src/models/v3/threads.rs +++ b/apps/labrinth/src/models/v3/threads.rs @@ -1,7 +1,9 @@ +use crate::auth::UserWithScopes; use crate::database::models::delphi_report_item::DelphiVerdict; use crate::models::ids::{ ImageId, ProjectId, ReportId, ThreadId, ThreadMessageId, }; +use crate::models::pats::Scopes; use crate::models::projects::ProjectStatus; use crate::models::thread_issues::ThreadIssue; use crate::models::users::User; @@ -122,7 +124,7 @@ impl Thread { pub fn from( data: crate::database::models::DBThread, users: Vec, - user: &User, + user: &UserWithScopes, ) -> Self { let thread_type = data.type_; @@ -134,7 +136,10 @@ impl Thread { messages: data .messages .into_iter() - .filter(|x| user.role.is_mod() || !x.body.is_private()) + .filter(|x| { + user.has_scopes(Scopes::MODERATION_READ) + || !x.body.is_private() + }) .map(|x| ThreadMessage::from(x, user)) .collect(), issues: data @@ -150,11 +155,13 @@ impl Thread { impl ThreadMessage { pub fn from( data: crate::database::models::DBThreadMessage, - user: &User, + user: &UserWithScopes, ) -> Self { Self { id: data.id.into(), - author_id: if data.hide_identity && !user.role.is_mod() { + author_id: if data.hide_identity + && !user.has_scopes(Scopes::MODERATION_READ) + { None } else { data.author_id.map(|x| x.into()) diff --git a/apps/labrinth/src/models/v3/user_limits.rs b/apps/labrinth/src/models/v3/user_limits.rs index 1f395ce3859..e45ed75b57a 100644 --- a/apps/labrinth/src/models/v3/user_limits.rs +++ b/apps/labrinth/src/models/v3/user_limits.rs @@ -3,8 +3,9 @@ use chrono::{DateTime, TimeDelta, Utc}; use serde::{Deserialize, Serialize}; use crate::{ + auth::UserWithScopes, database::models::{DBProjectId, DBUserId, user_limits::DBUserLimits}, - models::users::User, + models::pats::Scopes, }; fn utc_day_bounds(now: DateTime) -> (DateTime, DateTime) { @@ -23,8 +24,8 @@ pub struct UserLimits { } impl UserLimits { - fn adjust_for_user(self, user: &User) -> Self { - if user.role.is_admin() { + fn adjust_for_user(self, user: &UserWithScopes) -> Self { + if user.has_scopes(Scopes::ADMIN_WRITE) { Self { current: self.current, max: u64::MAX, @@ -35,12 +36,12 @@ impl UserLimits { } pub async fn get_for_projects( - user: &User, + user: &UserWithScopes, pool: &PgPool, ) -> Result { - let user_id = DBUserId::from(user.id); + let user_id = DBUserId::from(user.user.id); let db_limits = - DBUserLimits::get(DBUserId::from(user.id), pool).await?; + DBUserLimits::get(DBUserId::from(user.user.id), pool).await?; let current = sqlx::query_scalar!( "SELECT COUNT(*) FROM mods m JOIN teams t ON m.team_id = t.id @@ -60,11 +61,11 @@ impl UserLimits { } pub async fn get_for_projects_per_day( - user: &User, + user: &UserWithScopes, now: DateTime, pool: &PgPool, ) -> Result { - let user_id = DBUserId::from(user.id); + let user_id = DBUserId::from(user.user.id); let db_limits = DBUserLimits::get(user_id, pool).await?; let (day_start, day_end) = utc_day_bounds(now); let current = sqlx::query_scalar!( @@ -90,12 +91,12 @@ impl UserLimits { } pub async fn get_for_organizations( - user: &User, + user: &UserWithScopes, pool: &PgPool, ) -> Result { - let user_id = DBUserId::from(user.id); + let user_id = DBUserId::from(user.user.id); let db_limits = - DBUserLimits::get(DBUserId::from(user.id), pool).await?; + DBUserLimits::get(DBUserId::from(user.user.id), pool).await?; let current = sqlx::query_scalar!( "SELECT COUNT(*) FROM organizations o JOIN teams t ON o.team_id = t.id @@ -115,12 +116,12 @@ impl UserLimits { } pub async fn get_for_collections( - user: &User, + user: &UserWithScopes, pool: &PgPool, ) -> Result { - let user_id = DBUserId::from(user.id); + let user_id = DBUserId::from(user.user.id); let db_limits = - DBUserLimits::get(DBUserId::from(user.id), pool).await?; + DBUserLimits::get(DBUserId::from(user.user.id), pool).await?; let current = sqlx::query_scalar!( "SELECT COUNT(*) FROM collections WHERE user_id = $1", @@ -138,12 +139,12 @@ impl UserLimits { } pub async fn get_for_versions_per_project( - user: &User, + user: &UserWithScopes, project_id: DBProjectId, pool: &PgPool, ) -> Result { let db_limits = - DBUserLimits::get(DBUserId::from(user.id), pool).await?; + DBUserLimits::get(DBUserId::from(user.user.id), pool).await?; let current = sqlx::query_scalar!( "SELECT COUNT(*) FROM versions WHERE mod_id = $1", project_id as DBProjectId, @@ -160,11 +161,11 @@ impl UserLimits { } pub async fn get_for_versions_per_day( - user: &User, + user: &UserWithScopes, now: DateTime, pool: &PgPool, ) -> Result { - let user_id = DBUserId::from(user.id); + let user_id = DBUserId::from(user.user.id); let db_limits = DBUserLimits::get(user_id, pool).await?; let (day_start, day_end) = utc_day_bounds(now); let current = sqlx::query_scalar!( diff --git a/apps/labrinth/src/models/v3/users.rs b/apps/labrinth/src/models/v3/users.rs index 7aa2ac4f5b9..4438fa8c6cb 100644 --- a/apps/labrinth/src/models/v3/users.rs +++ b/apps/labrinth/src/models/v3/users.rs @@ -56,6 +56,7 @@ pub struct User { pub raw_avatar_url: Option, pub bio: Option, pub created: DateTime, + /// Should never be used for auth purposes, use scopes for checking admin/moderator permissions pub role: Role, pub badges: Badges, pub campaigns: UserCampaigns, @@ -301,20 +302,6 @@ impl Role { Role::Admin => "admin", } } - - pub fn is_mod(&self) -> bool { - match self { - Role::Developer => false, - Role::Moderator | Role::Admin => true, - } - } - - pub fn is_admin(&self) -> bool { - match self { - Role::Developer | Role::Moderator => false, - Role::Admin => true, - } - } } #[derive(Serialize, Deserialize, utoipa::ToSchema)] diff --git a/apps/labrinth/src/routes/internal/admin/user_credentials.rs b/apps/labrinth/src/routes/internal/admin/user_credentials.rs index 59890e2cc2e..21e65253dea 100644 --- a/apps/labrinth/src/routes/internal/admin/user_credentials.rs +++ b/apps/labrinth/src/routes/internal/admin/user_credentials.rs @@ -55,10 +55,9 @@ pub async fn force_password_reset( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "you must be an admin to reset a user's password" ))); @@ -69,7 +68,10 @@ pub async fn force_password_reset( .wrap_internal_err("fetching user from database")? .wrap_not_found_err("user not found")?; - if Role::from_string(&target.role).is_mod() { + if matches!( + Role::from_string(&target.role), + Role::Moderator | Role::Admin + ) { return Err(ApiError::Auth(eyre!( "cannot reset password of a staff account" ))); @@ -180,10 +182,9 @@ pub async fn reset_2fa( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "you must be an admin to reset a user's 2FA" ))); @@ -194,7 +195,10 @@ pub async fn reset_2fa( .wrap_internal_err("fetching user from database")? .wrap_not_found_err("user not found")?; - if Role::from_string(&target.role).is_mod() { + if matches!( + Role::from_string(&target.role), + Role::Moderator | Role::Admin + ) { return Err(ApiError::Auth(eyre!( "cannot reset 2FA of a staff account" ))); diff --git a/apps/labrinth/src/routes/internal/admin/user_lock.rs b/apps/labrinth/src/routes/internal/admin/user_lock.rs index 8ebd570ec0a..37e98572914 100644 --- a/apps/labrinth/src/routes/internal/admin/user_lock.rs +++ b/apps/labrinth/src/routes/internal/admin/user_lock.rs @@ -50,10 +50,9 @@ pub async fn lock_user( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!("only admins can lock users"))); } @@ -67,7 +66,10 @@ pub async fn lock_user( .wrap_internal_err("fetching user from database")? .wrap_not_found_err("user not found")?; - if Role::from_string(&target.role).is_mod() { + if matches!( + Role::from_string(&target.role), + Role::Moderator | Role::Admin + ) { return Err(ApiError::Auth(eyre!("cannot lock a staff account"))); } @@ -76,7 +78,7 @@ pub async fn lock_user( .await .wrap_internal_err("starting database transaction")?; - DBUserLock::upsert(target.id, user.id.into(), reason, &mut txn) + DBUserLock::upsert(target.id, user.user.id.into(), reason, &mut txn) .await .wrap_internal_err("locking user")?; @@ -121,10 +123,9 @@ pub async fn unlock_user( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!("only admins can unlock users"))); } diff --git a/apps/labrinth/src/routes/internal/admin/user_sessions.rs b/apps/labrinth/src/routes/internal/admin/user_sessions.rs index afcad564cf9..eabfc845e91 100644 --- a/apps/labrinth/src/routes/internal/admin/user_sessions.rs +++ b/apps/labrinth/src/routes/internal/admin/user_sessions.rs @@ -38,10 +38,9 @@ pub async fn revoke_user_sessions( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "only admins can revoke user sessions" ))); diff --git a/apps/labrinth/src/routes/internal/analytics_event.rs b/apps/labrinth/src/routes/internal/analytics_event.rs index c68fc7c0c24..cc84eeeca21 100644 --- a/apps/labrinth/src/routes/internal/analytics_event.rs +++ b/apps/labrinth/src/routes/internal/analytics_event.rs @@ -57,10 +57,9 @@ pub async fn analytics_event_create( Scopes::empty(), ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "you do not have permission to manage analytics events" ))); @@ -118,10 +117,9 @@ pub async fn analytics_event_edit( Scopes::empty(), ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "you do not have permission to manage analytics events" ))); @@ -169,10 +167,9 @@ pub async fn analytics_event_delete( Scopes::empty(), ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "you do not have permission to manage analytics events" ))); diff --git a/apps/labrinth/src/routes/internal/attribution.rs b/apps/labrinth/src/routes/internal/attribution.rs index 3817a573e25..1741c92df84 100644 --- a/apps/labrinth/src/routes/internal/attribution.rs +++ b/apps/labrinth/src/routes/internal/attribution.rs @@ -4,7 +4,7 @@ use chrono::{DateTime, Utc}; use eyre::eyre; use serde::{Deserialize, Serialize}; -use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; +use crate::auth::{UserWithScopes, get_user_from_headers}; use crate::database::PgPool; use crate::database::models::{ DBFileId, DBOrganization, DBProject, DBTeamMember, DBVersion, @@ -21,7 +21,6 @@ use crate::models::projects::{ AttributionResolutionKind, FlameProject, }; use crate::models::teams::ProjectPermissions; -use crate::models::users::User; use crate::queue::file_scan::{FileScanSummary, scan_file}; use crate::queue::moderation::ApprovalType; use crate::queue::session::AuthQueue; @@ -127,8 +126,7 @@ pub async fn scan( Scopes::VERSION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let mut version_ids: Vec = body .version_ids @@ -234,12 +232,12 @@ async fn force_scan_file( file_host: web::Data, path: web::Path, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("authenticating API request")?; @@ -317,7 +315,7 @@ pub async fn list( path: web::Path, ) -> Result>, ApiError> { let project_id: DBProjectId = path.into_inner().into(); - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -325,9 +323,8 @@ pub async fn list( Scopes::VERSION_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; - let requester_is_mod = user.role.is_mod(); + .wrap_auth_err("authenticating API request")?; + let requester_is_mod = scopes.contains(Scopes::MODERATION_READ); let project = DBProject::get_id(project_id, pool.as_ref(), redis.as_ref()) .await @@ -341,8 +338,8 @@ pub async fn list( ) .await .wrap_internal_err("failed to fetch project permissions")?; - if ProjectPermissions::get_permissions_by_role( - &user.role, + if ProjectPermissions::get_permissions_by_scopes( + scopes, &team_member, &organization_team_member, ) @@ -658,8 +655,7 @@ pub async fn update_group( Scopes::VERSION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; if !can_edit_attribution_group(pool.as_ref(), group_id, &user) .await @@ -673,24 +669,27 @@ pub async fn update_group( if matches!( body.attribution.kind, AttributionResolutionKind::GloballyAllowed { .. } - ) && !user.role.is_mod() + ) && !user.has_scopes(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "Only moderators can set globally allowed attributions", ))); } - if body.attribution.moderation_status.is_some() && !user.role.is_mod() { + if body.attribution.moderation_status.is_some() + && !user.has_scopes(Scopes::MODERATION_WRITE) + { return Err(ApiError::Auth(eyre::eyre!( "Only moderators can set attribution moderation status", ))); } let mut attribution = body.attribution; - attribution.updated_by_moderator = user.role.is_mod(); + attribution.updated_by_moderator = + user.has_scopes(Scopes::MODERATION_WRITE); if let Some(moderation_status) = &mut attribution.moderation_status { moderation_status.moderated_at = Some(Utc::now()); - moderation_status.moderated_by = Some(user.id); + moderation_status.moderated_by = Some(user.user.id); } let result = sqlx::query!( @@ -701,7 +700,7 @@ pub async fn update_group( ", &serde_json::to_value(&attribution).unwrap_or_default(), group_id, - user.id.0 as i64, + user.user.id.0 as i64, ) .execute(pool.as_ref()) .await @@ -738,12 +737,12 @@ pub async fn delete_groups( session_queue: web::Data, web::Json(body): web::Json, ) -> Result<(), ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("deleting database records for `delete_groups`")?; @@ -777,12 +776,12 @@ pub async fn delete_all_groups( session_queue: web::Data, web::Json(body): web::Json, ) -> Result<(), ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("deleting database records for `delete_all_groups`")?; @@ -905,8 +904,7 @@ pub async fn assign( Scopes::VERSION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let sha1 = body.sha1.trim().to_lowercase(); if hex_to_bytes(&sha1).is_none() { @@ -1068,8 +1066,7 @@ pub async fn split( Scopes::VERSION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let sha1 = body.sha1.trim().to_lowercase(); if hex_to_bytes(&sha1).is_none() { @@ -1220,7 +1217,7 @@ async fn clear_project_sha1_version_cache( async fn can_edit_attribution_group( pool: &PgPool, group_id: i64, - user: &User, + user: &UserWithScopes, ) -> Result { let group = sqlx::query!( r#" @@ -1259,16 +1256,16 @@ async fn can_edit_attribution_group( async fn ensure_can_upload_versions_to_project( pool: &PgPool, project_id: DBProjectId, - user: &User, + user: &UserWithScopes, permission_error: &'static str, ) -> Result<(), ApiError> { - if user.role.is_mod() { + if user.has_scopes(Scopes::MODERATION_WRITE) { return Ok(()); } let team_member = DBTeamMember::get_from_user_id_project( project_id, - user.id.into(), + user.user.id.into(), false, pool, ) @@ -1284,7 +1281,7 @@ async fn ensure_can_upload_versions_to_project( let organization_team_member = if let Some(organization) = organization { DBTeamMember::get_from_user_id( organization.team_id, - user.id.into(), + user.user.id.into(), pool, ) .await @@ -1293,8 +1290,8 @@ async fn ensure_can_upload_versions_to_project( None }; - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index 5162f0c8b55..6ca5bed758a 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -1,6 +1,6 @@ use self::payments::*; use self::update_subscriptions::*; -use crate::auth::get_user_from_headers; +use crate::auth::{UserWithScopes, get_user_from_headers}; use crate::database::models::charge_item::DBCharge; use crate::database::models::ids::DBUserSubscriptionId; use crate::database::models::notification_item::NotificationBuilder; @@ -130,7 +130,7 @@ pub async fn subscriptions( session_queue: web::Data, query: web::Query, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -138,13 +138,12 @@ pub async fn subscriptions( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let db_subscriptions = user_subscription_item::DBUserSubscription::get_all_user( if let Some(user_id) = query.user_id { - if user.role.is_admin() { + if scopes.contains(Scopes::ADMIN_READ) { user_id.into() } else { return Err(ApiError::Request(eyre::eyre!( @@ -210,7 +209,7 @@ pub async fn refund_charge( stripe_client: web::Data, anrok_client: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -218,12 +217,11 @@ pub async fn refund_charge( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let (id,) = info.into_inner(); - if !user.role.is_admin() { + if !scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to refund a subscription!", ))); @@ -501,7 +499,7 @@ pub async fn reprocess_charge_tax( anrok_client: web::Data, stripe_client: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -509,12 +507,11 @@ pub async fn reprocess_charge_tax( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let (id,) = info.into_inner(); - if !user.role.is_admin() { + if !scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to reprocess a tax transaction!", ))); @@ -734,7 +731,7 @@ pub async fn edit_subscription( stripe_client: web::Data, anrok_client: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -742,8 +739,7 @@ pub async fn edit_subscription( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; #[derive(Clone, Copy, PartialEq, Eq)] enum PaymentRequirement { @@ -875,7 +871,9 @@ pub async fn edit_subscription( .wrap_internal_err("fetching user subscription from database")? .wrap_not_found_err_with(|| "resource not found")?; - if subscription.user_id != user.id.into() && !user.role.is_admin() { + if subscription.user_id != user.id.into() + && !scopes.contains(Scopes::ADMIN_WRITE) + { return Err(ApiError::NotFound(eyre::eyre!("resource not found"))); } @@ -1250,7 +1248,7 @@ pub async fn user_customer( session_queue: web::Data, stripe_client: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, .. } = get_user_from_headers( &req, &**pool, &redis, @@ -1258,8 +1256,7 @@ pub async fn user_customer( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let customer_id = get_or_create_customer( user.id, @@ -1301,7 +1298,7 @@ pub async fn charges( session_queue: web::Data, query: web::Query, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -1309,13 +1306,12 @@ pub async fn charges( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let charges = crate::database::models::charge_item::DBCharge::get_from_user( if let Some(user_id) = query.user_id { - if user.role.is_admin() { + if scopes.contains(Scopes::ADMIN_READ) { user_id.into() } else { return Err(ApiError::Request(eyre::eyre!( @@ -1347,7 +1343,11 @@ pub async fn charges( subscription_interval: x.subscription_interval, platform: x.payment_platform, parent_charge_id: x.parent_charge_id.map(|x| x.into()), - net: if user.role.is_admin() { x.net } else { None }, + net: if scopes.contains(Scopes::ADMIN_READ) { + x.net + } else { + None + }, }) .collect::>(), )) @@ -1367,7 +1367,7 @@ pub async fn add_payment_method_flow( session_queue: web::Data, stripe_client: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, .. } = get_user_from_headers( &req, &**pool, &redis, @@ -1375,8 +1375,7 @@ pub async fn add_payment_method_flow( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let customer = get_or_create_customer( user.id, @@ -1429,7 +1428,7 @@ pub async fn edit_payment_method( session_queue: web::Data, stripe_client: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -1437,8 +1436,7 @@ pub async fn edit_payment_method( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let (id,) = info.into_inner(); @@ -1466,7 +1464,7 @@ pub async fn edit_payment_method( .wrap_failed_dependency_err("communicating with payment provider")?; if payment_method.customer.is_some_and(|x| x.id() == customer) - || user.role.is_admin() + || scopes.contains(Scopes::ADMIN_WRITE) { set_default_payment_method( &stripe_client, @@ -1496,7 +1494,7 @@ pub async fn remove_payment_method( session_queue: web::Data, stripe_client: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -1504,8 +1502,7 @@ pub async fn remove_payment_method( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let (id,) = info.into_inner(); @@ -1569,7 +1566,7 @@ pub async fn remove_payment_method( .customer .as_ref() .is_some_and(|x| x.id() == customer) - || user.role.is_admin() + || scopes.contains(Scopes::ADMIN_WRITE) { stripe::PaymentMethod::detach(&stripe_client, &payment_method_id) .await @@ -1616,7 +1613,7 @@ pub async fn payment_methods( session_queue: web::Data, stripe_client: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, .. } = get_user_from_headers( &req, &**pool, &redis, @@ -1624,8 +1621,7 @@ pub async fn payment_methods( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; if let Some(customer_id) = user .stripe_customer_id @@ -1792,7 +1788,7 @@ pub async fn initiate_payment( anrok_client: web::Data, payment_request: web::Json, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, .. } = get_user_from_headers( &req, &**pool, &redis, @@ -1800,8 +1796,7 @@ pub async fn initiate_payment( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let payment_request = payment_request.into_inner(); @@ -2881,7 +2876,7 @@ pub async fn credit( archon_client: web::Data, body: web::Json, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -2889,10 +2884,9 @@ pub async fn credit( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to credit subscriptions!", ))); diff --git a/apps/labrinth/src/routes/internal/delphi/mod.rs b/apps/labrinth/src/routes/internal/delphi/mod.rs index 78443deda77..1607157f28a 100644 --- a/apps/labrinth/src/routes/internal/delphi/mod.rs +++ b/apps/labrinth/src/routes/internal/delphi/mod.rs @@ -14,7 +14,7 @@ use tracing::info; use uuid::Uuid; use crate::{ - auth::check_is_moderator_from_headers, + auth::get_user_from_headers, database::{ advisory_lock::AdvisoryLock, models::{ @@ -361,12 +361,12 @@ pub async fn _run( run_parameters: web::Query, kafka_client: web::Data, ) -> Result { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("authenticating API request")?; @@ -450,12 +450,12 @@ pub async fn version( redis: web::Data, session_queue: web::Data, ) -> Result { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -482,12 +482,12 @@ pub async fn issue_type_schema( session_queue: web::Data, http: web::Data, ) -> Result { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("deserializing HTTP response")?; diff --git a/apps/labrinth/src/routes/internal/external_notifications.rs b/apps/labrinth/src/routes/internal/external_notifications.rs index e43eae6c543..54fa0c44c88 100644 --- a/apps/labrinth/src/routes/internal/external_notifications.rs +++ b/apps/labrinth/src/routes/internal/external_notifications.rs @@ -9,7 +9,6 @@ use crate::database::models::notification_item::DBNotification; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::user_item::DBUser; use crate::models::notifications::NotificationDeliveryStatus; -use crate::models::users::Role; use crate::models::v3::notifications::{Notification, NotificationBody}; use crate::models::v3::pats::Scopes; use crate::queue::email::EmailQueue; @@ -377,10 +376,9 @@ pub async fn send_custom_email( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if user.role != Role::Admin { + if !user.has_scopes(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to send custom emails!", ))); diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 50baf37c6fc..300ede8f447 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1855,7 +1855,7 @@ pub async fn delete_auth_provider( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); if user.auth_providers.is_none_or(|x| x.len() <= 1) && !user.has_password.unwrap_or(false) @@ -2614,7 +2614,7 @@ pub async fn begin_2fa_flow( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); if !user.has_totp.unwrap_or(false) { let string = totp_rs::Secret::generate(); @@ -2675,7 +2675,7 @@ pub async fn finish_2fa_flow( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); if user.id != user_id.into() { return Err(ApiError::Auth(eyre::eyre!( @@ -3270,7 +3270,7 @@ pub async fn set_email( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); if !crate::database::models::DBUser::get_by_case_insensitive_email( &email_address.email, @@ -3400,7 +3400,7 @@ pub async fn resend_verify_email( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); if let Some(email_address) = user.email { if user.email_verified.unwrap_or(false) { @@ -3564,7 +3564,7 @@ pub async fn subscribe_newsletter( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); sqlx::query!( " @@ -3670,7 +3670,7 @@ pub async fn register_passkey_start( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); // Get currently registered credentials, so an authenticator knows not to register // duplicate credentials @@ -3773,7 +3773,7 @@ pub async fn register_passkey_finish( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); response .validate() @@ -4049,7 +4049,7 @@ pub async fn list_passkeys( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let passkeys = DBPasskey::get_for_user(user.id.into(), &**pool) .await @@ -4103,7 +4103,7 @@ pub async fn rename_passkey( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); body.validate() .map_err(|err| eyre::eyre!(err)) @@ -4163,7 +4163,7 @@ pub async fn delete_passkey( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let id = DBPasskeyId( parse_base62(&info.into_inner().0) diff --git a/apps/labrinth/src/routes/internal/gdpr.rs b/apps/labrinth/src/routes/internal/gdpr.rs index 479209bd172..44bd5b9641c 100644 --- a/apps/labrinth/src/routes/internal/gdpr.rs +++ b/apps/labrinth/src/routes/internal/gdpr.rs @@ -32,10 +32,9 @@ pub async fn export( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - let user_id = user.id.into(); + let user_id = user.user.id.into(); let collection_ids = crate::database::models::DBUser::get_collections(user_id, &**pool) @@ -218,7 +217,7 @@ pub async fn export( .collect::>(); Ok(HttpResponse::Ok().json(serde_json::json!({ - "user": user, + "user": user.into_user(), "collections": collections, "follows": follows, "projects": projects, diff --git a/apps/labrinth/src/routes/internal/moderation/copyright_archives/mod.rs b/apps/labrinth/src/routes/internal/moderation/copyright_archives/mod.rs index 080faf9a7b2..00a0dab6306 100644 --- a/apps/labrinth/src/routes/internal/moderation/copyright_archives/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/copyright_archives/mod.rs @@ -5,7 +5,7 @@ use std::collections::{HashMap, HashSet}; use xredis::RedisPool; use crate::{ - auth::check_is_moderator_from_headers, + auth::get_user_from_headers, database::{ PgPool, PgTransaction, ReadOnlyPgPool, models::{ @@ -72,12 +72,12 @@ pub async fn list( redis: web::Data, session_queue: web::Data, ) -> Result>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::SESSION_ACCESS, + Scopes::SESSION_ACCESS | Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -128,12 +128,12 @@ pub async fn create( redis: web::Data, session_queue: web::Data, ) -> Result>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::SESSION_ACCESS, + Scopes::SESSION_ACCESS | Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("authenticating API request")?; @@ -183,12 +183,12 @@ pub async fn replace( redis: web::Data, session_queue: web::Data, ) -> Result<(), ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::SESSION_ACCESS, + Scopes::SESSION_ACCESS | Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("authenticating API request")?; @@ -271,12 +271,12 @@ pub async fn delete( redis: web::Data, session_queue: web::Data, ) -> Result<(), ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::SESSION_ACCESS, + Scopes::SESSION_ACCESS | Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("authenticating API request")?; diff --git a/apps/labrinth/src/routes/internal/moderation/external_license.rs b/apps/labrinth/src/routes/internal/moderation/external_license.rs index 84ae825b048..c6c50d4d8b4 100644 --- a/apps/labrinth/src/routes/internal/moderation/external_license.rs +++ b/apps/labrinth/src/routes/internal/moderation/external_license.rs @@ -12,7 +12,7 @@ use crate::database::models::moderation_external_item::ExternalLicense; use crate::models::pats::Scopes; use crate::queue::moderation::ApprovalType; use crate::routes::ApiError; -use crate::{auth::check_is_moderator_from_headers, queue::session::AuthQueue}; +use crate::{auth::get_user_from_headers, queue::session::AuthQueue}; use xredis::RedisPool; pub fn config(cfg: &mut actix_web::web::ServiceConfig) { @@ -351,12 +351,12 @@ pub async fn search( session_queue: web::Data, body: web::Json, ) -> Result>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating external license search")?; @@ -424,12 +424,12 @@ pub async fn lookup( session_queue: web::Data, body: web::Json, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -464,12 +464,12 @@ pub async fn get_by_sha1( session_queue: web::Data, path: web::Path<(String,)>, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -501,12 +501,12 @@ pub async fn get_by_sha1_bulk( session_queue: web::Data, body: web::Json, ) -> Result>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -561,15 +561,16 @@ async fn upsert_file_license( session_queue: web::Data, body: web::Json, ) -> Result, ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let body = body.into_inner(); let license_id = body @@ -674,15 +675,16 @@ pub async fn update_license( path: web::Path<(i64,)>, body: web::Json, ) -> Result, ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let id = path.into_inner().0; diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index a156ffbf8c6..55af3282981 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -14,10 +14,7 @@ use crate::routes::v3::organizations::OrganizationIds; use crate::routes::v3::users::UserIds; use crate::util::error::ApiContext as _; use crate::util::error::Context; -use crate::{ - auth::{check_is_moderator_from_headers, get_user_from_bearer_token}, - models::pats::Scopes, -}; +use crate::{auth::get_user_from_bearer_token, models::pats::Scopes}; use actix_web::{HttpRequest, delete, get, post, web}; use ariadne::ids::{UserId, random_base62}; use chrono::{DateTime, Utc}; @@ -264,12 +261,12 @@ pub async fn get_projects_internal( request_opts: web::Query, session_queue: web::Data, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -760,12 +757,12 @@ pub async fn get_project_ids( request_opts: web::Query, session_queue: web::Data, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -1111,12 +1108,12 @@ pub async fn get_project_meta( session_queue: web::Data, info: web::Path<(String,)>, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -1274,15 +1271,16 @@ pub async fn set_project_meta( session_queue: web::Data, judgements: web::Json>, ) -> Result<(), ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let mut transaction = pool .begin() @@ -1380,15 +1378,16 @@ pub async fn acquire_lock( session_queue: web::Data, path: web::Path<(String,)>, ) -> Result, ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_WRITE, + Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let project_id_str = path.into_inner().0; let project = @@ -1444,15 +1443,16 @@ pub async fn override_lock( session_queue: web::Data, path: web::Path<(String,)>, ) -> Result, ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_WRITE, + Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let project_id_str = path.into_inner().0; let project = @@ -1495,15 +1495,16 @@ pub async fn get_lock_status( session_queue: web::Data, path: web::Path<(String,)>, ) -> Result, ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let project_id_str = path.into_inner().0; let project = @@ -1562,15 +1563,16 @@ pub async fn release_lock( session_queue: web::Data, path: web::Path<(String,)>, ) -> Result, ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_WRITE, + Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let project_id_str = path.into_inner().0; let project = @@ -1626,7 +1628,7 @@ pub async fn release_lock_beacon( } let token = token.strip_prefix("Bearer ").unwrap_or(token).trim(); - let (scopes, user) = get_user_from_bearer_token( + let user = get_user_from_bearer_token( &req, Some(token), &**pool, @@ -1637,16 +1639,12 @@ pub async fn release_lock_beacon( .await .wrap_auth_err("authenticating API request")?; - if !scopes.contains(Scopes::PROJECT_WRITE) { - return Err(ApiError::Auth(eyre::eyre!( - "token is missing required scopes", - ))); - } - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "only moderators may release moderation locks", ))); } + let user = user.into_user(); let project_id_str = path.into_inner().0; let project = @@ -1691,10 +1689,9 @@ pub async fn delete_all_locks( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You must be an admin to delete all locks", ))); @@ -1724,12 +1721,12 @@ pub async fn get_user_project_by_status( redis: web::Data, session_queue: web::Data, ) -> Result>>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -1768,12 +1765,12 @@ pub async fn get_users_project_by_status( web::Json>>>, ApiError, > { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -1825,12 +1822,12 @@ pub async fn get_organization_project_by_status( redis: web::Data, session_queue: web::Data, ) -> Result>>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -1873,12 +1870,12 @@ pub async fn get_organizations_project_by_status( web::Json>>>, ApiError, > { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 2365d52cd13..46c1b634033 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -14,7 +14,7 @@ use crate::models::ids::OrganizationId; use crate::routes::v3::organizations::OrganizationIds; use crate::routes::v3::users::UserIds; use crate::{ - auth::check_is_moderator_from_headers, + auth::get_user_from_headers, database::{ DBProject, models::{ @@ -236,12 +236,12 @@ pub async fn get_issue( path: web::Path<(DelphiReportIssueId,)>, query: web::Query, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -303,12 +303,12 @@ pub async fn get_report( session_queue: web::Data, path: web::Path<(DelphiReportId,)>, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -755,12 +755,12 @@ pub async fn search_projects( session_queue: web::Data, search_req: web::Json, ) -> Result, ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -1003,12 +1003,12 @@ pub async fn get_project_report( session_queue: web::Data, path: web::Path<(ProjectId,)>, ) -> Result, ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -1107,12 +1107,12 @@ pub async fn submit_report( web::Json(submit_report): web::Json, path: web::Path<(ProjectId,)>, ) -> Result<(), ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_WRITE, + Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("authenticating API request")?; @@ -1192,7 +1192,7 @@ pub async fn submit_report( if let Some(body) = submit_report.message { ThreadMessageBuilder { - author_id: Some(user.id.into()), + author_id: Some(user.user.id.into()), body: MessageBody::Text { body, private: true, @@ -1200,7 +1200,7 @@ pub async fn submit_report( associated_images: Vec::new(), }, thread_id: record.thread_id, - hide_identity: user.role.is_mod(), + hide_identity: user.has_scopes(Scopes::MODERATION_WRITE), } .insert(&mut txn) .await @@ -1209,10 +1209,10 @@ pub async fn submit_report( let verdict = submit_report.verdict; ThreadMessageBuilder { - author_id: Some(user.id.into()), + author_id: Some(user.user.id.into()), body: MessageBody::TechReview { verdict }, thread_id: record.thread_id, - hide_identity: user.role.is_mod(), + hide_identity: user.has_scopes(Scopes::MODERATION_WRITE), } .insert(&mut txn) .await @@ -1249,13 +1249,13 @@ pub async fn submit_report( .wrap_internal_err("failed to mark project as rejected")?; ThreadMessageBuilder { - author_id: Some(user.id.into()), + author_id: Some(user.user.id.into()), body: MessageBody::StatusChange { new_status: ProjectStatus::Rejected, old_status: ProjectStatus::from_string(&record.old_status), }, thread_id: record.thread_id, - hide_identity: user.role.is_mod(), + hide_identity: user.has_scopes(Scopes::MODERATION_WRITE), } .insert(&mut txn) .await @@ -1353,12 +1353,12 @@ pub async fn update_issue_details( session_queue: web::Data, update_reqs: web::Json>, ) -> Result<(), ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_WRITE, + Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("updating database records for `update_issue_details`")?; @@ -1516,12 +1516,12 @@ pub async fn update_global_issue_details( session_queue: web::Data, update_reqs: web::Json>, ) -> Result<(), ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_WRITE, + Scopes::MODERATION_WRITE, ) .await .wrap_auth_err( @@ -1668,12 +1668,12 @@ pub async fn get_user_flagged_projects( redis: web::Data, session_queue: web::Data, ) -> Result>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -1708,12 +1708,12 @@ pub async fn get_users_flagged_projects( redis: web::Data, session_queue: web::Data, ) -> Result>>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -1763,12 +1763,12 @@ pub async fn get_organization_flagged_projects( redis: web::Data, session_queue: web::Data, ) -> Result>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -1804,12 +1804,12 @@ pub async fn get_organizations_flagged_projects( redis: web::Data, session_queue: web::Data, ) -> Result>>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs index 3ca452aea97..e1ad20c5a8e 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/global.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use xredis::RedisPool; use crate::{ - auth::check_is_moderator_from_headers, + auth::get_user_from_headers, database::{ PgPool, models::{ @@ -141,12 +141,12 @@ pub async fn search_global_issue_details( session_queue: web::Data, search_req: web::Json, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating global issue search")?; @@ -365,12 +365,12 @@ pub async fn get_global_issue_detail( session_queue: web::Data, get_req: web::Json, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating global issue detail request")?; diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs index e6fa468c8da..62693e6ba8e 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs @@ -7,7 +7,7 @@ use xredis::RedisPool; use super::rules_scan::RuleInput; use crate::{ - auth::check_is_moderator_from_headers, + auth::get_user_from_headers, database::{ PgPool, ReadOnlyPgPool, models::{ @@ -189,12 +189,12 @@ pub async fn test_rule( session_queue: web::Data, body: web::Json, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -245,12 +245,12 @@ pub async fn get_rules( redis: web::Data, session_queue: web::Data, ) -> Result>, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -435,12 +435,12 @@ pub async fn get_rule_affected_details( path: web::Path<(DelphiRuleId,)>, query: web::Query, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -567,15 +567,16 @@ pub async fn create_rule( session_queue: web::Data, body: web::Json, ) -> Result, ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_WRITE, + Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let rule = body.into_inner().validate().await?; let user_id = user.id.0 as i64; @@ -655,15 +656,16 @@ pub async fn update_rule( path: web::Path<(DelphiRuleId,)>, body: web::Json, ) -> Result, ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_WRITE, + Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let (id,) = path.into_inner(); let rule = body.into_inner().validate().await?; let user_id = user.id.0 as i64; @@ -738,15 +740,16 @@ pub async fn delete_rule( session_queue: web::Data, path: web::Path<(DelphiRuleId,)>, ) -> Result<(), ApiError> { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_WRITE, + Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let (id,) = path.into_inner(); let deleted = sqlx::query!( diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs index 2b1da2ab1e3..82846b3a982 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules_scan.rs @@ -21,7 +21,7 @@ use crate::routes::internal::delphi::tech_review_queue::{ self, TechReviewRemovalReason, }; use crate::{ - auth::check_is_moderator_from_headers, + auth::get_user_from_headers, database::{ PgPool, PgTransaction, ReadOnlyPgPool, models::{ @@ -160,12 +160,12 @@ pub async fn get_rule_schema( redis: web::Data, session_queue: web::Data, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -205,12 +205,12 @@ pub async fn get_detail_rule_input( session_queue: web::Data, path: web::Path<(DelphiReportIssueDetailsId,)>, ) -> Result, ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_READ, + Scopes::MODERATION_READ, ) .await .wrap_auth_err("authenticating API request")?; @@ -353,12 +353,12 @@ pub async fn scan_rules( redis: web::Data, session_queue: web::Data, ) -> Result { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::PROJECT_WRITE, + Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("authenticating API request")?; diff --git a/apps/labrinth/src/routes/internal/moderation/user_restrictions.rs b/apps/labrinth/src/routes/internal/moderation/user_restrictions.rs index 618a3d7ed20..3648cdc1dd5 100644 --- a/apps/labrinth/src/routes/internal/moderation/user_restrictions.rs +++ b/apps/labrinth/src/routes/internal/moderation/user_restrictions.rs @@ -40,16 +40,15 @@ async fn authenticate_moderator( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre!( "only moderators can manage user restrictions" ))); } - Ok(user) + Ok(user.into_user()) } async fn get_target( @@ -62,7 +61,10 @@ async fn get_target( .wrap_internal_err("fetching user from database")? .wrap_not_found_err("user not found")?; - if Role::from_string(&target.role).is_mod() { + if matches!( + Role::from_string(&target.role), + Role::Moderator | Role::Admin + ) { return Err(ApiError::Auth(eyre!("cannot restrict a staff account"))); } diff --git a/apps/labrinth/src/routes/internal/pats.rs b/apps/labrinth/src/routes/internal/pats.rs index cedc7c8237d..197134eb4ad 100644 --- a/apps/labrinth/src/routes/internal/pats.rs +++ b/apps/labrinth/src/routes/internal/pats.rs @@ -56,7 +56,7 @@ pub async fn get_pats( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let pat_ids = database::models::pat_item::DBPersonalAccessToken::get_user_pats( @@ -131,8 +131,14 @@ pub async fn create_pat( Scopes::PAT_CREATE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; + + if !user.has_scopes(info.scopes & Scopes::staff()) { + return Err(ApiError::Request(eyre::eyre!( + "cannot grant staff scopes which you do not hold" + ))); + } + let user = user.into_user(); let mut transaction = pool .begin() @@ -242,8 +248,16 @@ pub async fn edit_pat( Scopes::PAT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; + + if let Some(scopes) = info.scopes + && !user.has_scopes(scopes & Scopes::staff()) + { + return Err(ApiError::Request(eyre::eyre!( + "cannot grant staff scopes which you do not hold" + ))); + } + let user = user.into_user(); let id = id.into_inner().0; let pat = database::models::pat_item::DBPersonalAccessToken::get( @@ -361,7 +375,7 @@ pub async fn delete_pat( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let id = id.into_inner().0; let pat = database::models::pat_item::DBPersonalAccessToken::get( &id, &**pool, &redis, diff --git a/apps/labrinth/src/routes/internal/server_ping.rs b/apps/labrinth/src/routes/internal/server_ping.rs index 5847572ea54..e4ca56f835f 100644 --- a/apps/labrinth/src/routes/internal/server_ping.rs +++ b/apps/labrinth/src/routes/internal/server_ping.rs @@ -37,7 +37,7 @@ pub async fn ping_minecraft_java( redis: web::Data, session_queue: web::Data, ) -> Result<(), ApiError> { - let (_, _user) = get_user_from_headers( + get_user_from_headers( &req, &**pool, &redis, diff --git a/apps/labrinth/src/routes/internal/session.rs b/apps/labrinth/src/routes/internal/session.rs index 4158e558d8c..c177dd7639f 100644 --- a/apps/labrinth/src/routes/internal/session.rs +++ b/apps/labrinth/src/routes/internal/session.rs @@ -203,7 +203,7 @@ pub async fn list( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let session = extract_authorization_header(&req) .wrap_auth_err("authenticating API request")?; @@ -255,7 +255,7 @@ pub async fn delete( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let session = DBSession::get(info.into_inner().0, &**pool, &redis) .await @@ -332,7 +332,7 @@ pub async fn refresh( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let session = DBSession::get(session, &**pool, &redis) .await diff --git a/apps/labrinth/src/routes/maven.rs b/apps/labrinth/src/routes/maven.rs index 24e73607143..1b89336a2ae 100644 --- a/apps/labrinth/src/routes/maven.rs +++ b/apps/labrinth/src/routes/maven.rs @@ -188,7 +188,6 @@ pub async fn maven_metadata( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); if !is_visible_project(&project.inner, &user_option, &pool, false) @@ -432,7 +431,6 @@ pub async fn version_file( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); if !is_visible_project(&project.inner, &user_option, &pool, false) @@ -525,7 +523,6 @@ pub async fn version_file_sha1( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); if !is_visible_project(&project.inner, &user_option, &pool, false) @@ -597,7 +594,6 @@ pub async fn version_file_sha512( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); if !is_visible_project(&project.inner, &user_option, &pool, false) diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs index b0aeb43a515..cdab98ca305 100644 --- a/apps/labrinth/src/routes/mod.rs +++ b/apps/labrinth/src/routes/mod.rs @@ -145,7 +145,7 @@ pub async fn resolve_refs( /// Rewrites body references without redirecting the HTTP request or changing unresolved values. pub async fn resolve_body_refs( mut project_refs: Vec<&mut String>, - user: &Option, + user: &Option, pool: &PgPool, redis: &RedisPool, ) -> Result<(), ApiError> { @@ -201,14 +201,13 @@ async fn resolve_visible_refs( let user = get_user_from_headers(req, pool, redis, session_queue, required_scopes) .await - .map(|(_, user)| user) .ok(); resolve_visible_refs_for_user(project_refs, &user, pool, redis).await } async fn resolve_visible_refs_for_user( project_refs: &[String], - user: &Option, + user: &Option, pool: &PgPool, redis: &RedisPool, ) -> Result>, ApiError> { diff --git a/apps/labrinth/src/routes/updates.rs b/apps/labrinth/src/routes/updates.rs index 5ef0222c9ae..0329e3d3fe3 100644 --- a/apps/labrinth/src/routes/updates.rs +++ b/apps/labrinth/src/routes/updates.rs @@ -69,7 +69,6 @@ pub async fn forge_updates( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); if !is_visible_project(&project.inner, &user_option, &pool, false) diff --git a/apps/labrinth/src/routes/v2/versions.rs b/apps/labrinth/src/routes/v2/versions.rs index c97a0ed7c7e..36c46a485db 100644 --- a/apps/labrinth/src/routes/v2/versions.rs +++ b/apps/labrinth/src/routes/v2/versions.rs @@ -361,7 +361,6 @@ pub async fn versions_get( Scopes::PROJECT_READ | Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); let mut projects = crate::database::models::DBProject::get_many( &project_ids, diff --git a/apps/labrinth/src/routes/v3/analytics_get/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/mod.rs index 6dbf0f7f452..4532fa6ea22 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/mod.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/mod.rs @@ -185,7 +185,7 @@ pub async fn fetch_analytics( session_queue: web::Data, clickhouse: web::Data, ) -> Result, ApiError> { - let (scopes, user) = get_user_from_headers( + let user = get_user_from_headers( &http_req, &**pool, &redis, @@ -239,7 +239,7 @@ pub async fn fetch_analytics( let project_ids = { if req.project_ids.is_empty() { - DBUser::get_projects(user.id.into(), &**pool, &redis) + DBUser::get_projects(user.user.id.into(), &**pool, &redis) .await .wrap_internal_err("fetching users from database")? } else { @@ -354,7 +354,7 @@ pub async fn fetch_analytics( } if let Some(metrics) = &req.return_metrics.project_revenue { - if !scopes.contains(Scopes::PAYOUTS_READ) { + if !user.has_scopes(Scopes::PAYOUTS_READ) { return Err(ApiError::Auth(eyre::eyre!( AuthenticationError::InvalidCredentials ))); @@ -371,7 +371,7 @@ pub async fn fetch_analytics( AND tm.accepted ", &project_id_values, - DBUserId::from(user.id).0, + DBUserId::from(user.user.id).0, ) .fetch_all(&**pool) .await @@ -387,7 +387,7 @@ pub async fn fetch_analytics( num_time_slices, &project_id_values, &user_id_bucket_project_ids, - user.role.is_mod(), + user.has_scopes(Scopes::MODERATION_READ), metrics, ) .await @@ -476,7 +476,7 @@ pub(crate) fn normalize_loader_for_project( async fn fetch_response_projects( time_slices: &mut [TimeSlice], - user: &crate::models::users::User, + user: &crate::auth::UserWithScopes, pool: &PgPool, redis: &RedisPool, ) -> Result, ApiError> { @@ -719,7 +719,7 @@ pub(crate) fn add_to_time_slice( async fn filter_allowed_project_ids( project_ids: &[DBProjectId], - user: &crate::models::users::User, + user: &crate::auth::UserWithScopes, pool: &PgPool, redis: &RedisPool, ) -> Result, ApiError> { @@ -767,7 +767,7 @@ async fn filter_allowed_project_ids( .filter(|project| { let team_member = team_members.iter().find(|x| { x.team_id == project.inner.team_id - && x.user_id == user.id.into() + && x.user_id == user.user.id.into() }); let organization = project @@ -779,14 +779,14 @@ async fn filter_allowed_project_ids( if let Some(organization) = organization { organization_team_members.iter().find(|x| { x.team_id == organization.team_id - && x.user_id == user.id.into() + && x.user_id == user.user.id.into() }) } else { None }; - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member.cloned(), &organization_team_member.cloned(), ) diff --git a/apps/labrinth/src/routes/v3/blocked_users.rs b/apps/labrinth/src/routes/v3/blocked_users.rs index 2cdd00bbc92..d93c2b63e42 100644 --- a/apps/labrinth/src/routes/v3/blocked_users.rs +++ b/apps/labrinth/src/routes/v3/blocked_users.rs @@ -37,7 +37,7 @@ pub async fn block_user( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let user_id = info.into_inner().0; let Some(blocked) = DBUser::get(&user_id, &**pool, &redis) @@ -95,7 +95,7 @@ pub async fn unblock_user( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let user_id = info.into_inner().0; let Some(blocked) = DBUser::get(&user_id, &**pool, &redis) @@ -130,7 +130,7 @@ pub async fn get_blocked_users( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let blocked = DBBlockedUser::get_blocked_for_user(user.id.into(), &**pool) .await diff --git a/apps/labrinth/src/routes/v3/collections.rs b/apps/labrinth/src/routes/v3/collections.rs index 7bacbfe4572..a1730101fe7 100644 --- a/apps/labrinth/src/routes/v3/collections.rs +++ b/apps/labrinth/src/routes/v3/collections.rs @@ -2,6 +2,7 @@ use crate::auth::checks::is_visible_collection; use crate::auth::{ filter_visible_collections, get_user_from_headers, require_verified_email, }; +use crate::database; use crate::database::PgPool; use crate::database::models::{ collection_item, generate_collection_id, project_item, @@ -20,7 +21,6 @@ use crate::util::error::Context; use crate::util::img::delete_old_images; use crate::util::routes::read_limited_from_payload; use crate::util::validate::validation_errors_to_string; -use crate::{database, models}; use actix_web::web::Data; use actix_web::{HttpRequest, HttpResponse, delete, get, patch, post, web}; use ariadne::ids::base62_impl::parse_base62; @@ -77,10 +77,9 @@ pub async fn collection_create( &session_queue, Scopes::COLLECTION_CREATE, ) - .await? - .1; + .await?; - require_verified_email(¤t_user)?; + require_verified_email(¤t_user.user)?; let limits = UserLimits::get_for_collections(¤t_user, &client).await?; @@ -118,7 +117,7 @@ pub async fn collection_create( let collection_builder_actual = collection_item::CollectionBuilder { collection_id: collection_id.into(), - user_id: current_user.id.into(), + user_id: current_user.user.id.into(), name: collection_create_data.name, description: collection_create_data.description, status: CollectionStatus::Listed, @@ -191,7 +190,6 @@ pub async fn collections_get( Scopes::COLLECTION_READ, ) .await - .map(|x| x.1) .ok(); let collections = @@ -228,7 +226,6 @@ pub async fn collection_get( Scopes::COLLECTION_READ, ) .await - .map(|x| x.1) .ok(); if let Some(data) = collection_data @@ -280,8 +277,7 @@ pub async fn collection_edit( Scopes::COLLECTION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; new_collection .validate() @@ -349,7 +345,7 @@ pub async fn collection_edit( } if let Some(status) = &new_collection.status { - if !(user.role.is_mod() + if !(user.has_scopes(Scopes::MODERATION_WRITE) || collection_item.status.is_approved() && status.can_be_requested()) { @@ -473,8 +469,7 @@ pub async fn collection_icon_edit( Scopes::COLLECTION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let id = database::models::DBCollectionId( @@ -576,8 +571,7 @@ pub async fn delete_collection_icon( Scopes::COLLECTION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let id = database::models::DBCollectionId( @@ -647,8 +641,7 @@ pub async fn collection_delete( Scopes::COLLECTION_DELETE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let id = database::models::DBCollectionId( @@ -693,7 +686,8 @@ pub async fn collection_delete( fn can_modify_collection( collection: &database::models::DBCollection, - user: &models::users::User, + user: &crate::auth::UserWithScopes, ) -> bool { - collection.user_id == user.id.into() || user.role.is_mod() + collection.user_id == user.user.id.into() + || user.has_scopes(Scopes::MODERATION_WRITE) } diff --git a/apps/labrinth/src/routes/v3/content/mod.rs b/apps/labrinth/src/routes/v3/content/mod.rs index 0a5b8a02779..f99ca47476b 100644 --- a/apps/labrinth/src/routes/v3/content/mod.rs +++ b/apps/labrinth/src/routes/v3/content/mod.rs @@ -1,4 +1,5 @@ use super::ApiError; +use crate::auth::UserWithScopes; use crate::auth::checks::{ filter_visible_versions, is_visible_project, is_visible_version, }; @@ -9,7 +10,6 @@ use crate::database::models::{DBProject, DBVersion}; use crate::database::{PgPool, ReadOnlyPgPool}; use crate::models::pats::Scopes; use crate::models::projects::{DependencyType, Version, VersionType}; -use crate::models::users::User; use crate::queue::session::AuthQueue; use crate::util::error::ApiContext as _; use actix_web::{HttpRequest, post, web}; @@ -56,7 +56,6 @@ pub async fn resolve_content( Scopes::PROJECT_READ | Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); let cache_public_result = user_option.is_none(); let mut provider = LabrinthContentProvider { @@ -90,7 +89,7 @@ struct LabrinthContentProvider<'a> { pool: &'a PgPool, ro_pool: &'a ReadOnlyPgPool, redis: &'a RedisPool, - user_option: &'a Option, + user_option: &'a Option, trace: ResolveContentTrace, } @@ -576,7 +575,7 @@ fn parse_version_id(version_id: &str) -> Option { async fn visible_versions( versions: Vec, - user_option: &Option, + user_option: &Option, ro_pool: &ReadOnlyPgPool, redis: &RedisPool, ) -> Result, ApiError> { diff --git a/apps/labrinth/src/routes/v3/disclosures.rs b/apps/labrinth/src/routes/v3/disclosures.rs index c46d5d4f272..bfcc97156bc 100644 --- a/apps/labrinth/src/routes/v3/disclosures.rs +++ b/apps/labrinth/src/routes/v3/disclosures.rs @@ -74,7 +74,6 @@ pub async fn get_project_disclosures( Scopes::PROJECT_READ, ) .await - .map(|(_, user)| user) .ok(); if !is_visible_project(&project.inner, &user_option, &pool, false) @@ -84,8 +83,9 @@ pub async fn get_project_disclosures( return Err(ApiError::NotFound(eyre!("resource not found"))); } - let viewer_is_moderator = - user_option.as_ref().is_some_and(|user| user.role.is_mod()); + let viewer_is_moderator = user_option + .as_ref() + .is_some_and(|user| user.has_scopes(Scopes::MODERATION_READ)); // Moderators can see regardless of membership, short circuit to avoid extra db call let viewer_is_member = viewer_is_moderator @@ -169,8 +169,7 @@ pub async fn modify_project_disclosures( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let project = db_models::DBProject::get(&string, &**pool, &redis) .await @@ -180,14 +179,14 @@ pub async fn modify_project_disclosures( let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project.inner, - user.id.into(), + user.user.id.into(), &**pool, ) .await .wrap_internal_err("failed to fetch project permissions")?; - let can_edit_details = ProjectPermissions::get_permissions_by_role( - &user.role, + let can_edit_details = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) @@ -199,7 +198,7 @@ pub async fn modify_project_disclosures( ))); } - let is_moderator = user.role.is_mod(); + let is_moderator = user.has_scopes(Scopes::MODERATION_WRITE); let is_project_member = team_member.is_some() || organization_team_member.is_some(); @@ -274,7 +273,7 @@ pub async fn modify_project_disclosures( project_id: project.inner.id, disclosure, updated_at: Utc::now(), - updated_by: user.id.into(), + updated_by: user.user.id.into(), set_by_moderator, deleted_at: None, lock_status, @@ -298,7 +297,7 @@ pub async fn modify_project_disclosures( db_models::DBProjectDisclosure::remove( project.inner.id, disclosure_type, - user.id.into(), + user.user.id.into(), set_by_moderator, &mut transaction, ) diff --git a/apps/labrinth/src/routes/v3/friends.rs b/apps/labrinth/src/routes/v3/friends.rs index 4dd86f41188..3c3061bfaae 100644 --- a/apps/labrinth/src/routes/v3/friends.rs +++ b/apps/labrinth/src/routes/v3/friends.rs @@ -46,7 +46,7 @@ pub async fn add_friend( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let string = info.into_inner().0; let Some(friend) = DBUser::get(&string, &**pool, &redis) @@ -196,7 +196,7 @@ pub async fn remove_friend( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let string = info.into_inner().0; let friend = DBUser::get(&string, &**pool, &redis) @@ -250,7 +250,7 @@ pub async fn friends( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let friends = DBFriend::get_user_friends(user.id.into(), None, &***pool) .await diff --git a/apps/labrinth/src/routes/v3/images.rs b/apps/labrinth/src/routes/v3/images.rs index 797a457849f..6ef5ce213f6 100644 --- a/apps/labrinth/src/routes/v3/images.rs +++ b/apps/labrinth/src/routes/v3/images.rs @@ -9,6 +9,7 @@ use crate::database::models::{ use crate::file_hosting::{FileHost, FileHostPublicity}; use crate::models::ids::{ReportId, ThreadMessageId, VersionId}; use crate::models::images::{Image, ImageContext}; +use crate::models::pats::Scopes; use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::util::error::ApiContext as _; @@ -87,8 +88,7 @@ pub async fn images_add( context.relevant_scope(), ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; // Attempt to associated a supplied id with the context // If the context cannot be found, or the user is not authorized to upload images for the context, return an error @@ -165,9 +165,14 @@ pub async fn images_add( .await.wrap_internal_err("fetching thread from database")? .wrap_request_err_with(|| "the thread associated with the thread message could not be found" .to_string())?; - if is_authorized_thread(&thread, &user, &pool) - .await - .wrap_api_err("checking thread authorization")? + if is_authorized_thread( + &thread, + &user, + Scopes::MODERATION_WRITE, + &pool, + ) + .await + .wrap_api_err("checking thread authorization")? { *thread_message_id = Some(thread_message.id.into()); } else { @@ -195,9 +200,14 @@ pub async fn images_add( "the thread associated with the report could not be found." .to_string() })?; - if is_authorized_thread(&thread, &user, &pool) - .await - .wrap_api_err("checking thread authorization")? + if is_authorized_thread( + &thread, + &user, + Scopes::MODERATION_WRITE, + &pool, + ) + .await + .wrap_api_err("checking thread authorization")? { *report_id = Some(report.id.into()); } else { @@ -248,7 +258,7 @@ pub async fn images_add( raw_url: upload_result.raw_url, size: content_length as u64, created: chrono::Utc::now(), - owner_id: database::models::DBUserId::from(user.id), + owner_id: database::models::DBUserId::from(user.user.id), context: context.context_as_str().to_string(), project_id: if let ImageContext::Project { project_id: Some(id), diff --git a/apps/labrinth/src/routes/v3/limits.rs b/apps/labrinth/src/routes/v3/limits.rs index ab11d37a8eb..74b7222ae1a 100644 --- a/apps/labrinth/src/routes/v3/limits.rs +++ b/apps/labrinth/src/routes/v3/limits.rs @@ -23,7 +23,7 @@ pub async fn get_project_limits( redis: web::Data, session_queue: web::Data, ) -> Result, ApiError> { - let (_, user) = get_user_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, @@ -47,7 +47,7 @@ pub async fn get_organization_limits( redis: web::Data, session_queue: web::Data, ) -> Result, ApiError> { - let (_, user) = get_user_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, @@ -71,7 +71,7 @@ pub async fn get_collection_limits( redis: web::Data, session_queue: web::Data, ) -> Result, ApiError> { - let (_, user) = get_user_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, diff --git a/apps/labrinth/src/routes/v3/notifications.rs b/apps/labrinth/src/routes/v3/notifications.rs index 90ca9829b47..e532f38a2f1 100644 --- a/apps/labrinth/src/routes/v3/notifications.rs +++ b/apps/labrinth/src/routes/v3/notifications.rs @@ -1,4 +1,4 @@ -use crate::auth::get_user_from_headers; +use crate::auth::{UserWithScopes, get_user_from_headers}; use crate::database; use crate::database::PgPool; use crate::models::ids::NotificationId; @@ -48,7 +48,7 @@ pub async fn notifications_get( redis: web::Data, session_queue: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -56,8 +56,7 @@ pub async fn notifications_get( Scopes::NOTIFICATION_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; use database::models::DBNotificationId; use database::models::notification_item::DBNotification; @@ -79,7 +78,9 @@ pub async fn notifications_get( let notifications: Vec = notifications_data .into_iter() - .filter(|n| n.user_id == user.id.into() || user.role.is_admin()) + .filter(|n| { + n.user_id == user.id.into() || scopes.contains(Scopes::ADMIN_READ) + }) .map(Notification::from) .collect(); @@ -105,7 +106,7 @@ pub async fn notification_get( redis: web::Data, session_queue: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -113,8 +114,7 @@ pub async fn notification_get( Scopes::NOTIFICATION_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id = info.into_inner().0; @@ -127,7 +127,8 @@ pub async fn notification_get( .wrap_internal_err("fetching notification from database")?; if let Some(data) = notification_data { - if user.id == data.user_id.into() || user.role.is_admin() { + if user.id == data.user_id.into() || scopes.contains(Scopes::ADMIN_READ) + { Ok(HttpResponse::Ok().json(Notification::from(data))) } else { Err(ApiError::NotFound(eyre::eyre!("resource not found"))) @@ -156,7 +157,7 @@ pub async fn notification_read( redis: web::Data, session_queue: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -164,8 +165,7 @@ pub async fn notification_read( Scopes::NOTIFICATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id = info.into_inner().0; @@ -178,7 +178,9 @@ pub async fn notification_read( .wrap_internal_err("fetching notification from database")?; if let Some(data) = notification_data { - if data.user_id == user.id.into() || user.role.is_admin() { + if data.user_id == user.id.into() + || scopes.contains(Scopes::ADMIN_WRITE) + { let mut transaction = pool .begin() .await @@ -227,7 +229,7 @@ pub async fn notification_delete( redis: web::Data, session_queue: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -235,8 +237,7 @@ pub async fn notification_delete( Scopes::NOTIFICATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id = info.into_inner().0; @@ -249,7 +250,9 @@ pub async fn notification_delete( .wrap_internal_err("fetching notification from database")?; if let Some(data) = notification_data { - if data.user_id == user.id.into() || user.role.is_admin() { + if data.user_id == user.id.into() + || scopes.contains(Scopes::ADMIN_WRITE) + { let mut transaction = pool .begin() .await @@ -302,7 +305,7 @@ pub async fn notifications_read( redis: web::Data, session_queue: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -310,8 +313,7 @@ pub async fn notifications_read( Scopes::NOTIFICATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let notification_ids = serde_json::from_str::>(&ids.ids) @@ -337,7 +339,9 @@ pub async fn notifications_read( Vec::new(); for notification in notifications_data { - if notification.user_id == user.id.into() || user.role.is_admin() { + if notification.user_id == user.id.into() + || scopes.contains(Scopes::ADMIN_WRITE) + { notifications.push(notification.id); } } @@ -381,7 +385,7 @@ pub async fn notifications_delete( redis: web::Data, session_queue: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -389,8 +393,7 @@ pub async fn notifications_delete( Scopes::NOTIFICATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let notification_ids = serde_json::from_str::>(&ids.ids) @@ -416,7 +419,9 @@ pub async fn notifications_delete( Vec::new(); for notification in notifications_data { - if notification.user_id == user.id.into() || user.role.is_admin() { + if notification.user_id == user.id.into() + || scopes.contains(Scopes::ADMIN_WRITE) + { notifications.push(notification.id); } } diff --git a/apps/labrinth/src/routes/v3/oauth_clients.rs b/apps/labrinth/src/routes/v3/oauth_clients.rs index 87119de7d57..eb8421df32e 100644 --- a/apps/labrinth/src/routes/v3/oauth_clients.rs +++ b/apps/labrinth/src/routes/v3/oauth_clients.rs @@ -76,16 +76,15 @@ pub async fn get_user_clients( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let target_user = DBUser::get(&info.into_inner(), &**pool, &redis) .await .wrap_internal_err("fetching user from database")?; if let Some(target_user) = target_user { - if target_user.id != current_user.id.into() - && !current_user.role.is_admin() + if target_user.id != current_user.user.id.into() + && !current_user.has_scopes(Scopes::ADMIN_READ) { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to see the OAuth clients of this user!", @@ -202,8 +201,7 @@ pub async fn oauth_client_create( &session_queue, Scopes::SESSION_ACCESS, ) - .await? - .1; + .await?; new_oauth_app.validate().map_err(|e| { CreateError::ValidationError(validation_errors_to_string(e, None)) @@ -231,7 +229,7 @@ pub async fn oauth_client_create( name: new_oauth_app.name.clone(), redirect_uris, created: Utc::now(), - created_by: current_user.id.into(), + created_by: current_user.user.id.into(), url: new_oauth_app.url.clone(), description: new_oauth_app.description.clone(), secret_hash: client_secret_hash, @@ -271,8 +269,7 @@ pub async fn oauth_client_delete( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let client = DBOAuthClient::get(client_id.into_inner().into(), &**pool) .await @@ -341,8 +338,7 @@ pub async fn oauth_client_edit( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; client_updates .validate() @@ -446,8 +442,7 @@ pub async fn oauth_client_icon_edit( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let client = DBOAuthClient::get((*client_id).into(), &**pool) .await @@ -541,8 +536,7 @@ pub async fn oauth_client_icon_delete( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let client = DBOAuthClient::get((*client_id).into(), &**pool) .await @@ -607,11 +601,10 @@ pub async fn get_user_oauth_authorizations( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let authorizations = DBOAuthClientAuthorization::get_all_for_user( - current_user.id.into(), + current_user.user.id.into(), &**pool, ) .await @@ -646,12 +639,11 @@ pub async fn revoke_oauth_authorization( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; DBOAuthClientAuthorization::remove( info.client_id.into(), - current_user.id.into(), + current_user.user.id.into(), &**pool, ) .await diff --git a/apps/labrinth/src/routes/v3/organizations.rs b/apps/labrinth/src/routes/v3/organizations.rs index fbe5bb2f644..9b90ceb4b6b 100644 --- a/apps/labrinth/src/routes/v3/organizations.rs +++ b/apps/labrinth/src/routes/v3/organizations.rs @@ -65,7 +65,6 @@ pub async fn organization_projects_get( Scopes::ORGANIZATION_READ | Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); let organization_data = DBOrganization::get(&id, &**pool, &redis) @@ -139,10 +138,9 @@ pub async fn organization_create( &session_queue, Scopes::ORGANIZATION_CREATE, ) - .await? - .1; + .await?; - require_verified_email(¤t_user)?; + require_verified_email(¤t_user.user)?; let limits = UserLimits::get_for_organizations(¤t_user, &pool).await?; @@ -179,7 +177,7 @@ pub async fn organization_create( // Create organization managerial team let team = team_item::TeamBuilder { members: vec![team_item::TeamMemberBuilder { - user_id: current_user.id.into(), + user_id: current_user.user.id.into(), role: crate::models::teams::DEFAULT_ROLE.to_owned(), is_owner: true, permissions: ProjectPermissions::all(), @@ -214,7 +212,7 @@ pub async fn organization_create( let members_data = if let Some(member_data) = member_data { vec![crate::models::teams::TeamMember::from_model( member_data, - current_user.clone(), + current_user.to_user(), false, )] } else { @@ -247,9 +245,8 @@ pub async fn organization_get( Scopes::ORGANIZATION_READ, ) .await - .map(|x| x.1) .ok(); - let user_id = current_user.as_ref().map(|x| x.id.into()); + let user_id = current_user.as_ref().map(|x| x.user.id.into()); let organization_data = DBOrganization::get(&id, &**pool, &redis) .await @@ -276,7 +273,7 @@ pub async fn organization_get( .and_then(|user| { members_data .iter() - .find(|x| x.user_id == user.id.into() && x.accepted) + .find(|x| x.user_id == user.user.id.into() && x.accepted) }) .is_some(); let team_members: Vec<_> = members_data @@ -301,7 +298,10 @@ pub async fn organization_get( let mut organization = models::organizations::Organization::from(data, team_members); - if current_user.as_ref().is_some_and(|x| x.role.is_mod()) { + if current_user + .as_ref() + .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) + { let note = DBModerationNote::get_organization( organization.id.into(), &**pool, @@ -334,10 +334,9 @@ pub async fn organization_notes_edit( Scopes::SESSION_ACCESS, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "you do not have permission to edit moderation notes", ))); @@ -366,7 +365,7 @@ pub async fn organization_notes_edit( let updated = DBModerationNote::update( None, Some(organization.id), - user.id.into(), + user.user.id.into(), expected, new_note.notes.as_deref(), new_note.user_rating, @@ -384,7 +383,7 @@ pub async fn organization_notes_edit( let updated = DBModerationNote::insert( None, Some(organization.id), - user.id.into(), + user.user.id.into(), new_note.notes.as_deref(), new_note.user_rating, &mut transaction, @@ -458,10 +457,11 @@ pub async fn organizations_get( Scopes::ORGANIZATION_READ, ) .await - .map(|x| x.1) .ok(); - let user_id = current_user.as_ref().map(|x| x.id.into()); - let include_notes = current_user.as_ref().is_some_and(|x| x.role.is_mod()); + let user_id = current_user.as_ref().map(|x| x.user.id.into()); + let include_notes = current_user + .as_ref() + .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)); let notes = if include_notes { DBModerationNote::get_many_organizations( &organizations_data.iter().map(|x| x.id).collect::>(), @@ -495,7 +495,7 @@ pub async fn organizations_get( .and_then(|user| { members_data .iter() - .find(|x| x.user_id == user.id.into() && x.accepted) + .find(|x| x.user_id == user.user.id.into() && x.accepted) }) .is_some(); @@ -563,8 +563,7 @@ pub async fn organizations_edit( Scopes::ORGANIZATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; new_organization .validate() @@ -581,14 +580,14 @@ pub async fn organizations_edit( let team_member = database::models::DBTeamMember::get_from_user_id( organization_item.team_id, - user.id.into(), + user.user.id.into(), &**pool, ) .await .wrap_internal_err("fetching team member from database")?; - let permissions = OrganizationPermissions::get_permissions_by_role( - &user.role, + let permissions = OrganizationPermissions::get_permissions_by_scopes( + user.scopes, &team_member, ); @@ -745,8 +744,7 @@ pub async fn organization_delete( Scopes::ORGANIZATION_DELETE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let organization = @@ -757,11 +755,11 @@ pub async fn organization_delete( "the specified organization does not exist!".to_string() })?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { let team_member = database::models::DBTeamMember::get_from_user_id_organization( organization.id, - user.id.into(), + user.user.id.into(), false, &**pool, ) @@ -771,8 +769,8 @@ pub async fn organization_delete( "the specified organization does not exist!".to_string() })?; - let permissions = OrganizationPermissions::get_permissions_by_role( - &user.role, + let permissions = OrganizationPermissions::get_permissions_by_scopes( + user.scopes, &Some(team_member), ) .unwrap_or_default(); @@ -931,8 +929,7 @@ pub async fn organization_projects_add( Scopes::PROJECT_WRITE | Scopes::ORGANIZATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let organization = database::models::DBOrganization::get(&info, &**pool, &redis) @@ -969,7 +966,7 @@ pub async fn organization_projects_add( let project_team_member = database::models::DBTeamMember::get_from_user_id_project( project_item.inner.id, - current_user.id.into(), + current_user.user.id.into(), false, &**pool, ) @@ -981,7 +978,7 @@ pub async fn organization_projects_add( let organization_team_member = database::models::DBTeamMember::get_from_user_id_organization( organization.id, - current_user.id.into(), + current_user.user.id.into(), false, &**pool, ) @@ -992,14 +989,16 @@ pub async fn organization_projects_add( })?; // Require ownership of a project to add it to an organization - if !current_user.role.is_admin() && !project_team_member.is_owner { + if !current_user.has_scopes(Scopes::ADMIN_WRITE) + && !project_team_member.is_owner + { return Err(ApiError::Auth(eyre::eyre!( "You need to be an owner of a project to add it to an organization!", ))); } - let permissions = OrganizationPermissions::get_permissions_by_role( - ¤t_user.role, + let permissions = OrganizationPermissions::get_permissions_by_scopes( + current_user.scopes, &Some(organization_team_member), ) .unwrap_or_default(); @@ -1068,7 +1067,7 @@ pub async fn organization_projects_add( .await?; database::models::DBUser::clear_project_cache( - &[current_user.id.into()], + &[current_user.user.id.into()], &redis, ) .await @@ -1131,8 +1130,7 @@ pub async fn organization_projects_remove( Scopes::PROJECT_WRITE | Scopes::ORGANIZATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let organization = database::models::DBOrganization::get( &organization_id, @@ -1166,7 +1164,7 @@ pub async fn organization_projects_remove( let organization_team_member = database::models::DBTeamMember::get_from_user_id_organization( organization.id, - current_user.id.into(), + current_user.user.id.into(), false, &**pool, ) @@ -1176,8 +1174,8 @@ pub async fn organization_projects_remove( "you are not a member of this organization!".to_string() })?; - let permissions = OrganizationPermissions::get_permissions_by_role( - ¤t_user.role, + let permissions = OrganizationPermissions::get_permissions_by_scopes( + current_user.scopes, &Some(organization_team_member), ) .unwrap_or_default(); @@ -1287,7 +1285,7 @@ pub async fn organization_projects_remove( ) .await?; database::models::DBUser::clear_project_cache( - &[current_user.id.into()], + &[current_user.user.id.into()], &redis, ) .await @@ -1341,8 +1339,7 @@ pub async fn organization_icon_edit( Scopes::ORGANIZATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let organization_item = @@ -1353,17 +1350,17 @@ pub async fn organization_icon_edit( "the specified organization does not exist!".to_string() })?; - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { let team_member = database::models::DBTeamMember::get_from_user_id( organization_item.team_id, - user.id.into(), + user.user.id.into(), &**pool, ) .await .wrap_internal_err("fetching organization team member")?; - let permissions = OrganizationPermissions::get_permissions_by_role( - &user.role, + let permissions = OrganizationPermissions::get_permissions_by_scopes( + user.scopes, &team_member, ) .unwrap_or_default(); @@ -1463,8 +1460,7 @@ pub async fn delete_organization_icon( Scopes::ORGANIZATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let organization_item = @@ -1475,17 +1471,17 @@ pub async fn delete_organization_icon( "the specified organization does not exist!".to_string() })?; - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { let team_member = database::models::DBTeamMember::get_from_user_id( organization_item.team_id, - user.id.into(), + user.user.id.into(), &**pool, ) .await .wrap_internal_err("fetching organization team member")?; - let permissions = OrganizationPermissions::get_permissions_by_role( - &user.role, + let permissions = OrganizationPermissions::get_permissions_by_scopes( + user.scopes, &team_member, ) .unwrap_or_default(); diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index f8bf9529643..f199bbc4b75 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -73,7 +73,7 @@ pub async fn post_compliance_form( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let user_id = DBUserId(user.id.0 as i64); @@ -781,7 +781,7 @@ pub async fn transaction_history( redis: web::Data, session_queue: web::Data, ) -> Result>, ApiError> { - let (_, user) = get_user_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, @@ -789,7 +789,8 @@ pub async fn transaction_history( Scopes::PAYOUTS_READ, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); let payout_ids = crate::database::models::payout_item::DBPayout::get_all_for_user( @@ -880,8 +881,7 @@ pub async fn cancel_payout( Scopes::PAYOUTS_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id = info.into_inner().0; let payout = @@ -890,7 +890,9 @@ pub async fn cancel_payout( .wrap_internal_err("fetching payout from database")?; if let Some(payout) = payout { - if payout.user_id != user.id.into() && !user.role.is_admin() { + if payout.user_id != user.user.id.into() + && !user.has_scopes(Scopes::ADMIN_WRITE) + { return Ok(HttpResponse::NotFound().finish()); } @@ -1064,7 +1066,7 @@ pub async fn get_balance( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let balance = get_user_balance(user.id.into(), &pool) .await diff --git a/apps/labrinth/src/routes/v3/project_creation.rs b/apps/labrinth/src/routes/v3/project_creation.rs index 1a7b0bda53b..e880286f595 100644 --- a/apps/labrinth/src/routes/v3/project_creation.rs +++ b/apps/labrinth/src/routes/v3/project_creation.rs @@ -1,4 +1,5 @@ use super::version_creation::{InitialVersionData, create_minecraft_component}; +use crate::auth::UserWithScopes; use crate::auth::{ AuthenticationError, get_user_from_headers, require_verified_email, }; @@ -20,7 +21,6 @@ use crate::models::projects::{ }; use crate::models::teams::{OrganizationPermissions, ProjectPermissions}; use crate::models::threads::ThreadType; -use crate::models::users::User; use crate::models::v3::user_limits::UserLimits; use crate::queue::session::AuthQueue; use crate::routes::internal::globals::GLOBALS; @@ -364,8 +364,7 @@ pub async fn project_create_internal( &session_queue, Scopes::PROJECT_CREATE, ) - .await? - .1; + .await?; let result = async { let response = project_create_inner( @@ -452,8 +451,7 @@ pub async fn project_create_with_id( &session_queue, Scopes::PROJECT_CREATE, ) - .await? - .1; + .await?; let result = async { let response = project_create_inner( @@ -546,9 +544,9 @@ async fn project_create_inner( redis: &RedisPool, delphi_scan_files: &mut Vec, project_id: ProjectId, - current_user: &User, + current_user: &UserWithScopes, ) -> Result { - require_verified_email(current_user)?; + require_verified_email(¤t_user.user)?; let limits = UserLimits::get_for_projects(current_user, pool).await?; if limits.current >= limits.max { @@ -712,7 +710,7 @@ async fn project_create_inner( minecraft_meta.clone(), data, project_id, - current_user.id, + current_user.user.id, &all_loaders, transaction, redis, @@ -931,13 +929,13 @@ async fn project_create_inner( let team_member = models::DBTeamMember::get_from_user_id( org.team_id, - current_user.id.into(), + current_user.user.id.into(), pool, ) .await?; - let perms = OrganizationPermissions::get_permissions_by_role( - ¤t_user.role, + let perms = OrganizationPermissions::get_permissions_by_scopes( + current_user.scopes, &team_member, ); @@ -951,7 +949,7 @@ async fn project_create_inner( } } else { members.push(models::team_item::TeamMemberBuilder { - user_id: current_user.id.into(), + user_id: current_user.user.id.into(), role: crate::models::teams::DEFAULT_ROLE.to_owned(), is_owner: true, permissions: ProjectPermissions::all(), @@ -1035,7 +1033,8 @@ async fn project_create_inner( let id = project_builder_actual .insert(&mut *transaction, redis, file_host, delphi_scan_files) .await?; - DBUser::clear_project_cache(&[current_user.id.into()], redis).await?; + DBUser::clear_project_cache(&[current_user.user.id.into()], redis) + .await?; for image_id in project_create_data.uploaded_images { if let Some(db_image) = image_item::DBImage::get( diff --git a/apps/labrinth/src/routes/v3/project_creation/new.rs b/apps/labrinth/src/routes/v3/project_creation/new.rs index a8d570bef9f..9bedeed806f 100644 --- a/apps/labrinth/src/routes/v3/project_creation/new.rs +++ b/apps/labrinth/src/routes/v3/project_creation/new.rs @@ -142,7 +142,7 @@ pub async fn create( web::Json(create): web::Json, ) -> Result, CreateError> { // check that the user can make a project - let (_, user) = get_user_from_headers( + let user = get_user_from_headers( &req, &**db, &redis, @@ -152,7 +152,7 @@ pub async fn create( .await .wrap_auth_err("authenticating project creator")?; - require_verified_email(&user)?; + require_verified_email(&user.user)?; let limits = UserLimits::get_for_projects(&user, &db) .await @@ -224,15 +224,18 @@ pub async fn create( .wrap_internal_err("failed to get organization")? .wrap_request_err("invalid organization ID")?; - let team_member = - DBTeamMember::get_from_user_id(org.team_id, user.id.into(), &**db) - .await - .wrap_internal_err( - "failed to get team member of user for organization", - )?; + let team_member = DBTeamMember::get_from_user_id( + org.team_id, + user.user.id.into(), + &**db, + ) + .await + .wrap_internal_err( + "failed to get team member of user for organization", + )?; - let perms = OrganizationPermissions::get_permissions_by_role( - &user.role, + let perms = OrganizationPermissions::get_permissions_by_scopes( + user.scopes, &team_member, ); @@ -250,7 +253,7 @@ pub async fn create( } } else { let members = vec![models::team_item::TeamMemberBuilder { - user_id: user.id.into(), + user_id: user.user.id.into(), role: crate::models::teams::DEFAULT_ROLE.to_owned(), is_owner: true, permissions: ProjectPermissions::all(), @@ -302,7 +305,7 @@ pub async fn create( version_builder = Some(VersionBuilder { version_id, project_id: project_id.into(), - author_id: user.id.into(), + author_id: user.user.id.into(), name: "__synthetic".into(), version_number: String::new(), changelog: String::new(), @@ -357,7 +360,7 @@ pub async fn create( .wrap_internal_err("failed to insert initial version")?; } - DBUser::clear_project_cache(&[user.id.into()], &redis) + DBUser::clear_project_cache(&[user.user.id.into()], &redis) .await .wrap_internal_err("failed to clear user project cache")?; diff --git a/apps/labrinth/src/routes/v3/projects/mod.rs b/apps/labrinth/src/routes/v3/projects/mod.rs index 06aa34a0613..a1e7b3cb6bd 100644 --- a/apps/labrinth/src/routes/v3/projects/mod.rs +++ b/apps/labrinth/src/routes/v3/projects/mod.rs @@ -31,7 +31,7 @@ use crate::models::projects::{ }; use crate::models::teams::{DEFAULT_ROLE, ProjectPermissions}; use crate::models::threads::MessageBody; -use crate::models::users::{DELETED_USER, User}; +use crate::models::users::DELETED_USER; use crate::models::{self, exp}; use crate::queue::session::AuthQueue; use crate::routes::ApiError; @@ -294,7 +294,6 @@ pub async fn projects_get( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); let projects = @@ -356,7 +355,6 @@ pub async fn project_get_internal( Scopes::PROJECT_READ, ) .await - .map(|(_, user)| user) .ok(); if let Some(data) = project_data @@ -517,8 +515,7 @@ pub async fn project_edit_internal( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; new_project.validate().map_err(|err| { let message = @@ -540,14 +537,14 @@ pub async fn project_edit_internal( let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, - user.id.into(), + user.user.id.into(), &**pool, ) .await .wrap_internal_err("fetching team member from database")?; - let Some(perms) = ProjectPermissions::get_permissions_by_role( - &user.role, + let Some(perms) = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) else { @@ -558,7 +555,7 @@ pub async fn project_edit_internal( let submit_for_review = new_project.status == Some(ProjectStatus::Processing) - && !user.role.is_mod(); + && !user.has_scopes(Scopes::MODERATION_WRITE); if submit_for_review && !perms.contains(ProjectPermissions::EDIT_DETAILS) { return Err(ApiError::Auth(eyre!( @@ -668,8 +665,8 @@ pub async fn project_edit_internal( project_id: project_item.inner.id, disclosure: ProjectDisclosure::Archived { note: None }, updated_at: Utc::now(), - updated_by: user.id.into(), - set_by_moderator: user.role.is_mod(), + updated_by: user.user.id.into(), + set_by_moderator: user.has_scopes(Scopes::MODERATION_WRITE), deleted_at: None, lock_status: DisclosureLockStatus::Unlocked, } @@ -678,7 +675,7 @@ pub async fn project_edit_internal( .wrap_internal_err("failed to upsert archival disclosure")?; } } else { - if !(user.role.is_mod() + if !(user.has_scopes(Scopes::MODERATION_WRITE) || !project_item.inner.status.is_approved() && status == &ProjectStatus::Processing || project_item.inner.status.is_approved() @@ -691,8 +688,8 @@ pub async fn project_edit_internal( // If a moderator (non-admin) is completing a review while another moderator holds an // active checklist lock, block them from changing the project status. - if user.role.is_mod() - && !user.role.is_admin() + if user.has_scopes(Scopes::MODERATION_WRITE) + && !user.has_scopes(Scopes::ADMIN_WRITE) && project_item.inner.status == ProjectStatus::Processing && status != &ProjectStatus::Processing && let Some(lock) = DBModerationLock::get_with_user( @@ -701,7 +698,7 @@ pub async fn project_edit_internal( ) .await .wrap_internal_err("fetching moderation lock from database")? - && lock.moderator_id != db_ids::DBUserId::from(user.id) + && lock.moderator_id != db_ids::DBUserId::from(user.user.id) && !lock.expired { return Err(ApiError::Auth(eyre::eyre!(format!( @@ -778,7 +775,9 @@ pub async fn project_edit_internal( )?; } - if user.role.is_mod() && !ENV.MODERATION_SLACK_WEBHOOK.is_empty() { + if user.has_scopes(Scopes::MODERATION_WRITE) + && !ENV.MODERATION_SLACK_WEBHOOK.is_empty() + { crate::util::webhook::send_slack_project_webhook( &minecraft_meta, project_item.inner.id.into(), @@ -789,8 +788,8 @@ pub async fn project_edit_internal( format!( "*<{}/user/{}|{}>* changed project status from *{}* to *{}*", ENV.SITE_URL, - user.username, - user.username, + user.user.username, + user.user.username, &project_item.inner.status.as_friendly_str(), status.as_friendly_str(), ) @@ -850,13 +849,13 @@ pub async fn project_edit_internal( } ThreadMessageBuilder { - author_id: Some(user.id.into()), + author_id: Some(user.user.id.into()), body: MessageBody::StatusChange { new_status: *status, old_status: project_item.inner.status, }, thread_id: project_item.thread_id, - hide_identity: user.role.is_mod(), + hide_identity: user.has_scopes(Scopes::MODERATION_WRITE), } .insert(&mut transaction) .await @@ -881,15 +880,15 @@ pub async fn project_edit_internal( if sync_archival_disclosure && archival_disclosure.is_some_and(|disclosure| { - user.role.is_mod() + user.has_scopes(Scopes::MODERATION_WRITE) || disclosure.lock_status.allows_removal() }) { db_models::DBProjectDisclosure::remove( project_item.inner.id, ProjectDisclosureType::Archived, - user.id.into(), - user.role.is_mod(), + user.user.id.into(), + user.has_scopes(Scopes::MODERATION_WRITE), &mut transaction, ) .await @@ -1156,7 +1155,7 @@ pub async fn project_edit_internal( } } if let Some(moderation_message) = &new_project.moderation_message { - if !user.role.is_mod() + if !user.has_scopes(Scopes::MODERATION_WRITE) && (!project_item.inner.status.is_approved() || moderation_message.is_some()) { @@ -1181,7 +1180,7 @@ pub async fn project_edit_internal( if let Some(moderation_message_body) = &new_project.moderation_message_body { - if !user.role.is_mod() + if !user.has_scopes(Scopes::MODERATION_WRITE) && (!project_item.inner.status.is_approved() || moderation_message_body.is_some()) { @@ -1235,7 +1234,7 @@ pub async fn project_edit_internal( if (*monetization_status == MonetizationStatus::ForceDemonetized || project_item.inner.monetization_status == MonetizationStatus::ForceDemonetized) - && !user.role.is_mod() + && !user.has_scopes(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You do not have the permissions to edit the monetization status of this project!", @@ -1548,7 +1547,7 @@ pub async fn project_edit_internal( async fn submit_project_for_review( project: &ProjectQueryResult, - user: &User, + user: &crate::auth::UserWithScopes, notify_team_members: bool, sync_archival_disclosure: bool, transaction: &mut PgTransaction<'_>, @@ -1575,7 +1574,7 @@ async fn submit_project_for_review( db_models::DBProjectDisclosure::remove( project.inner.id, ProjectDisclosureType::Archived, - user.id.into(), + user.user.id.into(), false, &mut *transaction, ) @@ -1608,7 +1607,7 @@ async fn submit_project_for_review( .wrap_internal_err("querying database for `project_edit_internal`")?; if state.status().is_approved() - || !user.role.is_mod() + || !user.has_scopes(Scopes::MODERATION_WRITE) && matches!( state.status(), ProjectStatus::Rejected | ProjectStatus::Withheld @@ -1662,7 +1661,7 @@ async fn submit_project_for_review( } ThreadMessageBuilder { - author_id: Some(user.id.into()), + author_id: Some(user.user.id.into()), body: MessageBody::StatusChange { new_status, old_status: project.inner.status, @@ -1918,7 +1917,6 @@ pub async fn dependency_list_internal( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); if let Some(project) = result { @@ -2091,8 +2089,7 @@ pub async fn projects_edit( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; bulk_edit_project .validate() @@ -2167,10 +2164,10 @@ pub async fn projects_edit( let mut changed_projects = Vec::new(); for project in projects_data { - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { let team_member = team_members.iter().find(|x| { x.team_id == project.inner.team_id - && x.user_id == user.id.into() + && x.user_id == user.user.id.into() }); let organization = project @@ -2182,14 +2179,14 @@ pub async fn projects_edit( if let Some(organization) = organization { organization_team_members.iter().find(|x| { x.team_id == organization.team_id - && x.user_id == user.id.into() + && x.user_id == user.user.id.into() }) } else { None }; - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member.cloned(), &organization_team_member.cloned(), ) @@ -2311,7 +2308,7 @@ pub async fn projects_edit( } } - if !user.role.is_mod() + if !user.has_scopes(Scopes::MODERATION_WRITE) && project.inner.status == ProjectStatus::Processing { validate::ensure_project_is_valid_for_review( @@ -2508,8 +2505,7 @@ pub async fn project_icon_edit_internal( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let project_item = db_models::DBProject::get(&string, &**pool, &redis) @@ -2519,11 +2515,11 @@ pub async fn project_icon_edit_internal( "the specified project does not exist!".to_string() })?; - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, - user.id.into(), + user.user.id.into(), &**pool, ) .await @@ -2536,8 +2532,8 @@ pub async fn project_icon_edit_internal( ))); } - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) @@ -2687,8 +2683,7 @@ pub async fn delete_project_icon_internal( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let project_item = db_models::DBProject::get(&string, &**pool, &redis) @@ -2698,11 +2693,11 @@ pub async fn delete_project_icon_internal( "the specified project does not exist!".to_string() })?; - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, - user.id.into(), + user.user.id.into(), &**pool, ) .await @@ -2714,8 +2709,8 @@ pub async fn delete_project_icon_internal( "The specified project does not exist!", ))); } - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) @@ -2863,8 +2858,7 @@ pub async fn add_gallery_item_internal( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let project_item = db_models::DBProject::get(&string, &**pool, &redis) @@ -2880,11 +2874,11 @@ pub async fn add_gallery_item_internal( ))); } - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, - user.id.into(), + user.user.id.into(), &**pool, ) .await @@ -2897,8 +2891,8 @@ pub async fn add_gallery_item_internal( ))); } - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) @@ -2987,7 +2981,7 @@ pub async fn add_gallery_item_internal( .await .wrap_internal_err("inserting galleries into database")?; - let validation_error = if !user.role.is_mod() + let validation_error = if !user.has_scopes(Scopes::MODERATION_WRITE) && project_item.inner.status == ProjectStatus::Processing { validate::ensure_project_is_valid_for_review( @@ -3099,8 +3093,7 @@ pub async fn edit_gallery_item_internal( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; item.validate() .map_err(|err| eyre::eyre!(err)) @@ -3134,11 +3127,11 @@ pub async fn edit_gallery_item_internal( "the specified project does not exist!".to_string() })?; - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, - user.id.into(), + user.user.id.into(), &**pool, ) .await @@ -3150,8 +3143,8 @@ pub async fn edit_gallery_item_internal( "The specified project does not exist!", ))); } - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) @@ -3317,8 +3310,7 @@ pub async fn delete_gallery_item_internal( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let item = sqlx::query!( " @@ -3348,11 +3340,11 @@ pub async fn delete_gallery_item_internal( "the specified project does not exist!".to_string() })?; - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, - user.id.into(), + user.user.id.into(), &**pool, ) .await @@ -3365,8 +3357,8 @@ pub async fn delete_gallery_item_internal( ))); } - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) @@ -3470,7 +3462,7 @@ pub async fn project_delete_internal( session_queue: web::Data, search_state: web::Data, ) -> Result<(), ApiError> { - let (_, user) = get_user_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, @@ -3494,11 +3486,11 @@ pub async fn project_delete_internal( .wrap_internal_err("failed to get project")? .wrap_auth_err("the specified project does not exist")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project.inner, - user.id.into(), + user.user.id.into(), &**pool, ) .await @@ -3511,8 +3503,8 @@ pub async fn project_delete_internal( ))); } - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) @@ -3647,8 +3639,8 @@ pub async fn project_delete_internal( body: MessageBody::Text { body: format!( "Project transferred to Ghost when user account `{}` (`{}`) deleted this project", - user.username, - user.id + user.user.username, + user.user.id ), private: true, replying_to: None, @@ -3815,8 +3807,7 @@ pub async fn project_follow_internal( Scopes::USER_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let project = db_models::DBProject::get(&string, &**pool, &redis) @@ -3826,7 +3817,7 @@ pub async fn project_follow_internal( "the specified project does not exist!".to_string() })?; - let user_id: db_ids::DBUserId = user.id.into(); + let user_id: db_ids::DBUserId = user.user.id.into(); let project_id: db_ids::DBProjectId = project.inner.id; if !is_visible_project(&project.inner, &Some(user), &pool, false) @@ -3933,8 +3924,7 @@ pub async fn project_unfollow_internal( Scopes::USER_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let string = info.into_inner().0; let project = db_models::DBProject::get(&string, &**pool, &redis) @@ -3944,7 +3934,7 @@ pub async fn project_unfollow_internal( "the specified project does not exist!".to_string() })?; - let user_id: db_ids::DBUserId = user.id.into(); + let user_id: db_ids::DBUserId = user.user.id.into(); let project_id = project.inner.id; let mut transaction = pool @@ -4033,9 +4023,8 @@ pub async fn project_get_organization( Scopes::PROJECT_READ | Scopes::ORGANIZATION_READ, ) .await - .map(|x| x.1) .ok(); - let user_id = current_user.as_ref().map(|x| x.id.into()); + let user_id = current_user.as_ref().map(|x| x.user.id.into()); let string = info.into_inner().0; let result = db_models::DBProject::get(&string, &**pool, &redis) @@ -4081,7 +4070,7 @@ pub async fn project_get_organization( .and_then(|user| { members_data .iter() - .find(|x| x.user_id == user.id.into() && x.accepted) + .find(|x| x.user_id == user.user.id.into() && x.accepted) }) .is_some(); let team_members: Vec<_> = members_data diff --git a/apps/labrinth/src/routes/v3/projects/mutation.rs b/apps/labrinth/src/routes/v3/projects/mutation.rs index 98022895d10..84034ae1231 100644 --- a/apps/labrinth/src/routes/v3/projects/mutation.rs +++ b/apps/labrinth/src/routes/v3/projects/mutation.rs @@ -1,6 +1,7 @@ use eyre::eyre; use xredis::RedisPool; +use crate::auth::UserWithScopes; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::project_item::ProjectQueryResult; use crate::database::models::thread_item::ThreadMessageBuilder; @@ -11,13 +12,13 @@ use crate::database::models::{ use crate::database::{PgPool, PgTransaction, models as db_models}; use crate::models::ids::ProjectId; use crate::models::notifications::NotificationBody; +use crate::models::pats::Scopes; use crate::models::projects::{Project, ProjectStatus, Version}; use crate::models::thread_issues::{ ThreadIssueContext, ThreadIssueTarget, ThreadIssueTeamMember, ThreadIssueValueState, ThreadIssueVerdict, }; use crate::models::threads::MessageBody; -use crate::models::users::User; use crate::routes::ApiError; use crate::routes::internal::delphi; use crate::util::error::{ApiContext as _, Context as _}; @@ -385,7 +386,7 @@ pub(crate) async fn finalize_mutation( transaction: PgTransaction<'_>, pool: &PgPool, redis: &RedisPool, - project_editor: Option<&User>, + project_editor: Option<&UserWithScopes>, ) -> Result<(), ApiError> { finalize_mutations(&[project_id], transaction, pool, redis, project_editor) .await @@ -396,7 +397,7 @@ pub(crate) async fn finalize_mutations( mut transaction: PgTransaction<'_>, pool: &PgPool, redis: &RedisPool, - project_editor: Option<&User>, + project_editor: Option<&UserWithScopes>, ) -> Result<(), ApiError> { let prepared = prepare_mutations( project_ids, @@ -441,7 +442,7 @@ pub(crate) async fn prepare_mutations( transaction: &mut PgTransaction<'_>, pool: &PgPool, redis: &RedisPool, - project_editor: Option<&User>, + project_editor: Option<&UserWithScopes>, ) -> Result { let mut project_ids = project_ids.to_vec(); project_ids.sort_by_key(|project_id| project_id.0); @@ -470,7 +471,9 @@ pub(crate) async fn prepare_mutations( states.push(state); } - if project_editor.is_some_and(|user| !user.role.is_mod()) { + if project_editor + .is_some_and(|user| !user.has_scopes(Scopes::MODERATION_WRITE)) + { for state in &states { if state.project.status == ProjectStatus::Processing && state.has_required_nags @@ -519,7 +522,7 @@ pub(crate) async fn finalize_project_edit( mut transaction: PgTransaction<'_>, pool: &PgPool, redis: &RedisPool, - project_editor: Option<&User>, + project_editor: Option<&UserWithScopes>, ) -> Result<(), ApiError> { let state = sync_project_state(project_id, &mut transaction, pool, redis).await?; @@ -534,7 +537,8 @@ pub(crate) async fn finalize_project_edit( if (state.project.status == ProjectStatus::Processing || validate_review_state || entered_approved_state) - && !project_editor.is_some_and(|user| user.role.is_mod()) + && !project_editor + .is_some_and(|user| user.has_scopes(Scopes::MODERATION_WRITE)) && state.has_required_nags { return Err(ApiError::Request(eyre!( @@ -543,7 +547,8 @@ pub(crate) async fn finalize_project_edit( } if validate_review_state - && !project_editor.is_some_and(|user| user.role.is_mod()) + && !project_editor + .is_some_and(|user| user.has_scopes(Scopes::MODERATION_WRITE)) && state .thread_issues .iter() diff --git a/apps/labrinth/src/routes/v3/projects/validate.rs b/apps/labrinth/src/routes/v3/projects/validate.rs index cb2b59a3dea..79b81881592 100644 --- a/apps/labrinth/src/routes/v3/projects/validate.rs +++ b/apps/labrinth/src/routes/v3/projects/validate.rs @@ -147,7 +147,7 @@ pub async fn validate( return Ok(response); } - let user = get_user_from_headers( + let crate::auth::UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -155,8 +155,7 @@ pub async fn validate( Scopes::PROJECT_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let project = db_models::DBProject::get(&info.into_inner().0, &***ro_pool, &redis) @@ -173,8 +172,8 @@ pub async fn validate( .await .wrap_internal_err("fetching project permissions")?; - if ProjectPermissions::get_permissions_by_role( - &user.role, + if ProjectPermissions::get_permissions_by_scopes( + scopes, &team_member, &organization_team_member, ) diff --git a/apps/labrinth/src/routes/v3/reports.rs b/apps/labrinth/src/routes/v3/reports.rs index d3ad4136e00..1ee9db39662 100644 --- a/apps/labrinth/src/routes/v3/reports.rs +++ b/apps/labrinth/src/routes/v3/reports.rs @@ -1,4 +1,4 @@ -use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; +use crate::auth::{UserWithScopes, get_user_from_headers}; use crate::database; use crate::database::PgPool; use crate::database::models::SharedInstanceId; @@ -88,7 +88,7 @@ pub async fn report_create( ) .await .wrap_auth_err("authenticating API request")? - .1; + .into_user(); let new_report: CreateReport = read_typed_from_payload(&mut body) .await @@ -404,7 +404,7 @@ pub async fn reports( request_opts: web::Query, session_queue: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -412,47 +412,47 @@ pub async fn reports( Scopes::REPORT_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; use futures::stream::TryStreamExt; - let report_ids = if user.role.is_mod() && request_opts.all { - sqlx::query!( - " + let report_ids = + if scopes.contains(Scopes::MODERATION_READ) && request_opts.all { + sqlx::query!( + " SELECT id FROM reports WHERE closed = FALSE ORDER BY created ASC OFFSET $2 LIMIT $1 ", - request_opts.count as i64, - request_opts.offset as i64 - ) - .fetch(&**pool) - .map_ok(|m| crate::database::models::ids::DBReportId(m.id)) - .try_collect::>() - .await - .wrap_internal_err("fetching report IDs from database")? - } else { - sqlx::query!( - " + request_opts.count as i64, + request_opts.offset as i64 + ) + .fetch(&**pool) + .map_ok(|m| crate::database::models::ids::DBReportId(m.id)) + .try_collect::>() + .await + .wrap_internal_err("fetching report IDs from database")? + } else { + sqlx::query!( + " SELECT id FROM reports WHERE closed = FALSE AND reporter = $1 ORDER BY created ASC OFFSET $3 LIMIT $2 ", - user.id.0 as i64, - request_opts.count as i64, - request_opts.offset as i64 - ) - .fetch(&**pool) - .map_ok(|m| crate::database::models::ids::DBReportId(m.id)) - .try_collect::>() - .await - .wrap_internal_err("querying database for `reports`")? - }; + user.id.0 as i64, + request_opts.count as i64, + request_opts.offset as i64 + ) + .fetch(&**pool) + .map_ok(|m| crate::database::models::ids::DBReportId(m.id)) + .try_collect::>() + .await + .wrap_internal_err("querying database for `reports`")? + }; let query_reports = crate::database::models::report_item::DBReport::get_many( @@ -514,7 +514,7 @@ pub async fn reports_get( .await .wrap_internal_err("fetching reports from database")?; - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -522,12 +522,14 @@ pub async fn reports_get( Scopes::REPORT_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let all_reports = reports_data .into_iter() - .filter(|x| user.role.is_mod() || x.reporter == user.id.into()) + .filter(|x| { + scopes.contains(Scopes::MODERATION_READ) + || x.reporter == user.id.into() + }) .map(|x| x.into()) .collect::>(); @@ -553,7 +555,7 @@ pub async fn report_get( info: web::Path<(crate::models::ids::ReportId,)>, session_queue: web::Data, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -561,8 +563,7 @@ pub async fn report_get( Scopes::REPORT_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id = info.into_inner().0.into(); let report = @@ -571,7 +572,9 @@ pub async fn report_get( .wrap_internal_err("fetching report from database")?; if let Some(report) = report { - if !user.role.is_mod() && report.reporter != user.id.into() { + if !scopes.contains(Scopes::MODERATION_READ) + && report.reporter != user.id.into() + { return Err(ApiError::NotFound(eyre::eyre!("resource not found"))); } @@ -610,7 +613,7 @@ pub async fn report_edit( session_queue: web::Data, edit_report: web::Json, ) -> Result { - let user = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -618,8 +621,7 @@ pub async fn report_edit( Scopes::REPORT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id = info.into_inner().0.into(); let report = @@ -628,7 +630,9 @@ pub async fn report_edit( .wrap_internal_err("fetching report from database")?; if let Some(report) = report { - if !user.role.is_mod() && report.reporter != user.id.into() { + if !scopes.contains(Scopes::MODERATION_WRITE) + && report.reporter != user.id.into() + { return Err(ApiError::NotFound(eyre::eyre!("resource not found"))); } @@ -653,7 +657,7 @@ pub async fn report_edit( } if let Some(edit_closed) = edit_report.closed { - if !user.role.is_mod() { + if !scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Request(eyre::eyre!( "You cannot reopen a report!", ))); @@ -667,7 +671,7 @@ pub async fn report_edit( MessageBody::ThreadClosure }, thread_id: report.thread_id, - hide_identity: user.role.is_mod(), + hide_identity: scopes.contains(Scopes::MODERATION_WRITE), } .insert(&mut transaction) .await @@ -747,12 +751,12 @@ pub async fn report_delete( redis: web::Data, session_queue: web::Data, ) -> Result { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::REPORT_DELETE, + Scopes::REPORT_DELETE | Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("authenticating API request")?; diff --git a/apps/labrinth/src/routes/v3/teams.rs b/apps/labrinth/src/routes/v3/teams.rs index a6c44e87bb5..36d86ddc2d4 100644 --- a/apps/labrinth/src/routes/v3/teams.rs +++ b/apps/labrinth/src/routes/v3/teams.rs @@ -114,7 +114,6 @@ pub async fn team_members_get_project_internal( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); if !is_visible_project(&project.inner, ¤t_user, &pool, false) @@ -138,7 +137,7 @@ pub async fn team_members_get_project_internal( .await .wrap_internal_err("fetching users from database")?; - let user_id = current_user.as_ref().map(|x| x.id.into()); + let user_id = current_user.as_ref().map(|x| x.user.id.into()); let logged_in = if let Some(user_id) = user_id { let (team_member, organization_team_member) = DBTeamMember::get_for_project_permissions( @@ -203,7 +202,6 @@ pub async fn team_members_get_organization( Scopes::ORGANIZATION_READ, ) .await - .map(|x| x.1) .ok(); if let Some(organization) = organization_data @@ -226,13 +224,13 @@ pub async fn team_members_get_organization( .await .wrap_internal_err("fetching users from database")?; - let user_id = current_user.as_ref().map(|x| x.id.into()); + let user_id = current_user.as_ref().map(|x| x.user.id.into()); let logged_in = current_user .and_then(|user| { members_data .iter() - .find(|x| x.user_id == user.id.into() && x.accepted) + .find(|x| x.user_id == user.user.id.into() && x.accepted) }) .is_some(); @@ -303,15 +301,14 @@ pub async fn team_members_get( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); - let user_id = current_user.as_ref().map(|x| x.id.into()); + let user_id = current_user.as_ref().map(|x| x.user.id.into()); let logged_in = current_user .and_then(|user| { members_data .iter() - .find(|x| x.user_id == user.id.into() && x.accepted) + .find(|x| x.user_id == user.user.id.into() && x.accepted) }) .is_some(); @@ -394,7 +391,6 @@ pub async fn teams_get( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); let teams_groups = teams_data.into_iter().chunk_by(|data| data.team_id.0); @@ -409,7 +405,7 @@ pub async fn teams_get( .and_then(|user| { members .iter() - .find(|x| x.user_id == user.id.into() && x.accepted) + .find(|x| x.user_id == user.user.id.into() && x.accepted) }) .is_some(); @@ -460,12 +456,11 @@ pub async fn join_team( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let member = DBTeamMember::get_from_user_id_pending( team_id, - current_user.id.into(), + current_user.user.id.into(), &**pool, ) .await @@ -485,7 +480,7 @@ pub async fn join_team( // Edit Team Member to set Accepted to True DBTeamMember::edit_team_member( team_id, - current_user.id.into(), + current_user.user.id.into(), None, None, None, @@ -503,7 +498,7 @@ pub async fn join_team( .await .wrap_internal_err("committing database transaction")?; - DBUser::clear_project_cache(&[current_user.id.into()], &redis) + DBUser::clear_project_cache(&[current_user.user.id.into()], &redis) .await .wrap_internal_err("clearing cached user in Redis")?; DBTeamMember::clear_cache(team_id, &redis) @@ -578,8 +573,7 @@ pub async fn add_team_member( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let team_association = DBTeam::get_association(team_id, &**pool) .await .wrap_internal_err("fetching team from database")? @@ -588,7 +582,7 @@ pub async fn add_team_member( })?; let member = DBTeamMember::get_from_user_id( team_id, - current_user.id.into(), + current_user.user.id.into(), &**pool, ) .await @@ -606,7 +600,7 @@ pub async fn add_team_member( if let Some(organization) = &organization { DBTeamMember::get_from_user_id( organization.team_id, - current_user.id.into(), + current_user.user.id.into(), &**pool, ) .await @@ -614,8 +608,8 @@ pub async fn add_team_member( } else { None }; - let permissions = ProjectPermissions::get_permissions_by_role( - ¤t_user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + current_user.scopes, &member, &organization_team_member, ) @@ -641,8 +635,8 @@ pub async fn add_team_member( // If team is associated with an organization, check if they have permissions to invite users to that organization TeamAssociationId::Organization(_) => { let organization_permissions = - OrganizationPermissions::get_permissions_by_role( - ¤t_user.role, + OrganizationPermissions::get_permissions_by_scopes( + current_user.scopes, &member, ) .unwrap_or_default(); @@ -775,7 +769,7 @@ pub async fn add_team_member( body: NotificationBody::TeamInvite { project_id: pid.into(), team_id: team_id.into(), - invited_by: current_user.id, + invited_by: current_user.user.id, role: new_member.role.clone(), }, } @@ -790,7 +784,7 @@ pub async fn add_team_member( body: NotificationBody::OrganizationInvite { organization_id: oid.into(), team_id: team_id.into(), - invited_by: current_user.id, + invited_by: current_user.user.id, role: new_member.role.clone(), }, } @@ -870,17 +864,19 @@ pub async fn edit_team_member( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let team_association = DBTeam::get_association(id, &**pool) .await .wrap_internal_err("failed to fetch the specified team")? .wrap_request_err("the specified team does not exist")?; - let member = - DBTeamMember::get_from_user_id(id, current_user.id.into(), &**pool) - .await - .wrap_internal_err("fetching team member from database")?; + let member = DBTeamMember::get_from_user_id( + id, + current_user.user.id.into(), + &**pool, + ) + .await + .wrap_internal_err("fetching team member from database")?; let edit_member_db = DBTeamMember::get_from_user_id_pending(id, user_id, &**pool) .await @@ -916,7 +912,7 @@ pub async fn edit_team_member( if let Some(organization) = &organization { DBTeamMember::get_from_user_id( organization.team_id, - current_user.id.into(), + current_user.user.id.into(), &**pool, ) .await @@ -950,8 +946,8 @@ pub async fn edit_team_member( ))); } - let permissions = ProjectPermissions::get_permissions_by_role( - ¤t_user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + current_user.scopes, &member.clone(), &organization_team_member, ) @@ -978,8 +974,8 @@ pub async fn edit_team_member( } TeamAssociationId::Organization(_) => { let organization_permissions = - OrganizationPermissions::get_permissions_by_role( - ¤t_user.role, + OrganizationPermissions::get_permissions_by_scopes( + current_user.scopes, &member, ) .unwrap_or_default(); @@ -1089,8 +1085,7 @@ pub async fn transfer_ownership( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; // Forbid transferring ownership of a project team that is owned by an organization // These are owned by the organization owner, and must be removed from the organization first @@ -1111,10 +1106,10 @@ pub async fn transfer_ownership( } } - if !current_user.role.is_admin() { + if !current_user.has_scopes(Scopes::ADMIN_WRITE) { let member = DBTeamMember::get_from_user_id( id.into(), - current_user.id.into(), + current_user.user.id.into(), &**pool, ) .await @@ -1317,8 +1312,7 @@ pub async fn remove_team_member( Scopes::PROJECT_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let team_association = DBTeam::get_association(id, &**pool) .await @@ -1326,10 +1320,13 @@ pub async fn remove_team_member( .wrap_request_err_with(|| { "the team specified does not exist".to_string() })?; - let member = - DBTeamMember::get_from_user_id(id, current_user.id.into(), &**pool) - .await - .wrap_internal_err("fetching team member from database")?; + let member = DBTeamMember::get_from_user_id( + id, + current_user.user.id.into(), + &**pool, + ) + .await + .wrap_internal_err("fetching team member from database")?; let delete_member = DBTeamMember::get_from_user_id_pending(id, user_id, &**pool) @@ -1363,7 +1360,7 @@ pub async fn remove_team_member( { DBTeamMember::get_from_user_id( organization.team_id, - current_user.id.into(), + current_user.user.id.into(), &**pool, ) .await @@ -1371,12 +1368,13 @@ pub async fn remove_team_member( } else { None }; - let permissions = ProjectPermissions::get_permissions_by_role( - ¤t_user.role, - &member, - &organization_team_member, - ) - .unwrap_or_default(); + let permissions = + ProjectPermissions::get_permissions_by_scopes( + current_user.scopes, + &member, + &organization_team_member, + ) + .unwrap_or_default(); if delete_member.accepted { // Members other than the owner can either leave the team, or be @@ -1416,8 +1414,8 @@ pub async fn remove_team_member( } TeamAssociationId::Organization(_) => { let organization_permissions = - OrganizationPermissions::get_permissions_by_role( - ¤t_user.role, + OrganizationPermissions::get_permissions_by_scopes( + current_user.scopes, &member, ) .unwrap_or_default(); diff --git a/apps/labrinth/src/routes/v3/threads.rs b/apps/labrinth/src/routes/v3/threads.rs index 7bd266022dd..8fc2b32b67b 100644 --- a/apps/labrinth/src/routes/v3/threads.rs +++ b/apps/labrinth/src/routes/v3/threads.rs @@ -1,4 +1,4 @@ -use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; +use crate::auth::{UserWithScopes, get_user_from_headers}; use crate::database; use crate::database::PgPool; use crate::database::models::image_item; @@ -40,16 +40,19 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { .service(threads_get_route); } +/// Whether `user` may access `thread`, either as a participant or because +/// their credentials grant `moderation_scope`. pub async fn is_authorized_thread( thread: &database::models::DBThread, - user: &User, + user: &UserWithScopes, + moderation_scope: Scopes, pool: &PgPool, ) -> Result { - if user.role.is_mod() { + if user.has_scopes(moderation_scope) { return Ok(true); } - let user_id: database::models::DBUserId = user.id.into(); + let user_id: database::models::DBUserId = user.user.id.into(); Ok(match thread.type_ { ThreadType::Report => { if let Some(report_id) = thread.report_id { @@ -102,17 +105,18 @@ pub async fn is_authorized_thread( pub async fn filter_authorized_threads( threads: Vec, - user: &User, + user: &UserWithScopes, pool: &web::Data, redis: &RedisPool, ) -> Result, ApiError> { - let user_id: database::models::DBUserId = user.id.into(); + let user_id: database::models::DBUserId = user.user.id.into(); + let is_moderator = user.has_scopes(Scopes::MODERATION_READ); let mut return_threads = Vec::new(); let mut check_threads = Vec::new(); for thread in threads { - if user.role.is_mod() + if is_moderator || (thread.type_ == ThreadType::DirectMessage && thread.members.contains(&user_id)) { @@ -230,7 +234,7 @@ pub async fn filter_authorized_threads( .flat_map(|x| x.messages.iter().filter_map(|x| x.author_id)) .collect::>(), ); - if user.role.is_mod() { + if is_moderator { user_ids.extend(return_threads.iter().flat_map(|thread| { thread.issues.iter().map(|issue| issue.created_by) })); @@ -254,7 +258,7 @@ pub async fn filter_authorized_threads( .messages .iter() .filter_map(|x| { - if x.hide_identity && !user.role.is_mod() { + if x.hide_identity && !is_moderator { None } else { x.author_id @@ -262,7 +266,7 @@ pub async fn filter_authorized_threads( }) .collect::>(), ); - if user.role.is_mod() { + if is_moderator { authors.extend(thread.issues.iter().map(|issue| issue.created_by)); } @@ -313,14 +317,14 @@ pub async fn thread_get( Scopes::THREAD_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; if let Some(mut data) = thread_data - && is_authorized_thread(&data, &user, &pool) + && is_authorized_thread(&data, &user, Scopes::MODERATION_READ, &pool) .await .wrap_api_err("checking thread authorization")? { + let is_moderator = user.has_scopes(Scopes::MODERATION_READ); let authors = &mut data.members; authors.append( @@ -328,7 +332,7 @@ pub async fn thread_get( .messages .iter() .filter_map(|x| { - if x.hide_identity && !user.role.is_mod() { + if x.hide_identity && !is_moderator { None } else { x.author_id @@ -336,7 +340,7 @@ pub async fn thread_get( }) .collect::>(), ); - if user.role.is_mod() { + if is_moderator { authors.extend(data.issues.iter().map(|issue| issue.created_by)); } @@ -389,8 +393,7 @@ pub async fn threads_get( Scopes::THREAD_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let thread_ids: Vec = serde_json::from_str::>(&ids.ids) @@ -585,15 +588,16 @@ pub async fn thread_issues_create( session_queue: web::Data, web::Json(new_issues): web::Json, ) -> Result<(), ApiError> { - let moderator = check_is_moderator_from_headers( + let moderator = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::THREAD_WRITE, + Scopes::THREAD_WRITE | Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); if new_issues.issues.is_empty() { return Err(ApiError::Request(eyre::eyre!( @@ -686,8 +690,7 @@ pub async fn thread_issue_facet_edit( Scopes::THREAD_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let facet_id: database::models::DBThreadIssueFacetId = info.into_inner().0.into(); let facet_context = thread_issue_facet_context(facet_id, &pool) @@ -696,7 +699,9 @@ pub async fn thread_issue_facet_edit( let project_id = facet_context.project_id; let project_exists = project_exists(project_id, &pool).await?; - if edit.moderator_verified.is_some() && !user.role.is_mod() { + if edit.moderator_verified.is_some() + && !user.has_scopes(Scopes::MODERATION_WRITE) + { return Err(ApiError::Auth(eyre::eyre!( "only moderators can verify thread issues" ))); @@ -709,9 +714,13 @@ pub async fn thread_issue_facet_edit( prepare_thread_issue_mutation(project_id, &mut transaction).await?; } if edit.user_addressed.is_some() - && !user.role.is_mod() - && !is_project_team_member(project_id, user.id.into(), &mut transaction) - .await? + && !user.has_scopes(Scopes::MODERATION_WRITE) + && !is_project_team_member( + project_id, + user.user.id.into(), + &mut transaction, + ) + .await? { return Err(ApiError::Auth(eyre::eyre!( "only project team members or moderators can address thread issues" @@ -794,12 +803,12 @@ pub async fn thread_issue_delete( redis: web::Data, session_queue: web::Data, ) -> Result<(), ApiError> { - check_is_moderator_from_headers( + get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::THREAD_WRITE, + Scopes::THREAD_WRITE | Scopes::MODERATION_WRITE, ) .await .wrap_auth_err("authenticating API request")?; @@ -876,8 +885,7 @@ pub async fn thread_send_message( Scopes::THREAD_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; thread_send_message_internal( &user, @@ -892,7 +900,7 @@ pub async fn thread_send_message( } pub async fn thread_send_message_internal( - user: &User, + user: &UserWithScopes, thread_id: ThreadId, pool: &PgPool, new_message: NewThreadMessage, @@ -915,7 +923,7 @@ pub async fn thread_send_message_internal( ))); } - if *private && !user.role.is_mod() { + if *private && !user.has_scopes(Scopes::MODERATION_WRITE) { return Err(ApiError::Request(eyre::eyre!( "You are not allowed to send private messages!", ))); @@ -954,7 +962,7 @@ pub async fn thread_send_message_internal( .wrap_internal_err("fetching thread from database")?; if let Some(thread) = result { - if !is_authorized_thread(&thread, user, pool) + if !is_authorized_thread(&thread, user, Scopes::MODERATION_WRITE, pool) .await .wrap_api_err("checking thread authorization")? { @@ -967,10 +975,10 @@ pub async fn thread_send_message_internal( .wrap_internal_err("starting database transaction")?; let id = ThreadMessageBuilder { - author_id: Some(user.id.into()), + author_id: Some(user.user.id.into()), body: new_message.body.clone(), thread_id: thread.id, - hide_identity: user.role.is_mod(), + hide_identity: user.has_scopes(Scopes::MODERATION_WRITE), } .insert(&mut transaction) .await @@ -986,7 +994,7 @@ pub async fn thread_send_message_internal( if let Some(project) = project && project.inner.status != ProjectStatus::Processing - && user.role.is_mod() + && user.has_scopes(Scopes::MODERATION_WRITE) && !is_private { let members = @@ -1034,13 +1042,13 @@ pub async fn thread_send_message_internal( .wrap_internal_err("fetching report from database")?; if let Some(report) = report { - if report.closed && !user.role.is_mod() { + if report.closed && !user.has_scopes(Scopes::MODERATION_WRITE) { return Err(ApiError::Request(eyre::eyre!( "You may not reply to a closed report", ))); } - if user.id != report.reporter.into() && !is_private { + if user.user.id != report.reporter.into() && !is_private { NotificationBuilder { body: NotificationBody::ModeratorMessage { thread_id: thread.id.into(), @@ -1146,8 +1154,7 @@ pub async fn message_delete( Scopes::THREAD_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let result = database::models::DBThreadMessage::get( info.into_inner().0.into(), @@ -1157,7 +1164,9 @@ pub async fn message_delete( .wrap_internal_err("fetching thread message from database")?; if let Some(thread) = result { - if !user.role.is_mod() && thread.author_id != Some(user.id.into()) { + if !user.has_scopes(Scopes::MODERATION_WRITE) + && thread.author_id != Some(user.user.id.into()) + { return Err(ApiError::Auth(eyre::eyre!( "You cannot delete this message!", ))); diff --git a/apps/labrinth/src/routes/v3/users.rs b/apps/labrinth/src/routes/v3/users.rs index 9f7a0320823..e8b446ef5d8 100644 --- a/apps/labrinth/src/routes/v3/users.rs +++ b/apps/labrinth/src/routes/v3/users.rs @@ -11,7 +11,7 @@ use crate::routes::internal::globals::GLOBALS; use crate::util::error::Context; use crate::{ auth::{ - check_is_moderator_from_headers, checks::is_visible_organization, + UserWithScopes, checks::is_visible_organization, filter_visible_collections, filter_visible_projects, get_user_from_headers, }, @@ -93,7 +93,6 @@ pub async fn all_projects( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); let target_user = DBUser::get(&info.into_inner().0, &**pool, &redis) .await @@ -158,7 +157,7 @@ pub async fn all_projects( .and_then(|user| { members_data .iter() - .find(|x| x.user_id == user.id.into() && x.accepted) + .find(|x| x.user_id == user.user.id.into() && x.accepted) }) .is_some(); let team_members = members_data @@ -241,10 +240,9 @@ pub async fn admin_user_email( Scopes::SESSION_ACCESS, ) .await - .map(|x| x.1) .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_READ) { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to get a user from their email!", ))); @@ -301,10 +299,9 @@ pub async fn admin_user_discord( Scopes::SESSION_ACCESS, ) .await - .map(|x| x.1) .wrap_auth_err("authenticating API request")?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_READ) { return Err(ApiError::Auth(eyre!( "you must be an admin to look up users by discord ID" ))); @@ -352,7 +349,6 @@ pub async fn projects_list( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis) @@ -397,7 +393,7 @@ pub async fn user_auth_get( redis: web::Data, session_queue: web::Data, ) -> Result { - let (scopes, mut user) = get_user_from_headers( + let UserWithScopes { mut user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -415,7 +411,7 @@ pub async fn user_auth_get( user.payout_data = None; } - if user.role.is_mod() { + if scopes.contains(Scopes::MODERATION_READ) { let note = DBModerationNote::get_user(user.id.into(), &**pool, &redis) .await .wrap_internal_err("fetching moderation note from database")?; @@ -434,7 +430,10 @@ pub async fn get_user_preferences( redis: web::Data, session_queue: web::Data, ) -> Result, ApiError> { - let (_, requester) = get_user_from_headers( + let UserWithScopes { + user: requester, + scopes, + } = get_user_from_headers( &req, &**pool, &redis, @@ -449,8 +448,8 @@ pub async fn get_user_preferences( .wrap_internal_err("fetching user from database")? .wrap_not_found_err("resource not found")?; - let can_access = - requester.id == target.id.into() || requester.role.is_mod(); + let can_access = requester.id == target.id.into() + || scopes.contains(Scopes::MODERATION_READ); if !can_access { return Err(ApiError::Auth(eyre!( "you do not have permission to access this user's preferences" @@ -478,7 +477,10 @@ pub async fn edit_user_preferences( session_queue: web::Data, body: web::Json, ) -> Result, ApiError> { - let (_, requester) = get_user_from_headers( + let UserWithScopes { + user: requester, + scopes, + } = get_user_from_headers( &req, &**pool, &redis, @@ -493,8 +495,8 @@ pub async fn edit_user_preferences( .wrap_internal_err("fetching user from database")? .wrap_not_found_err("resource not found")?; - let can_access = - requester.id == target.id.into() || requester.role.is_mod(); + let can_access = requester.id == target.id.into() + || scopes.contains(Scopes::MODERATION_WRITE); if !can_access { return Err(ApiError::Auth(eyre!( "you do not have permission to access this user's preferences" @@ -596,10 +598,12 @@ pub async fn users_get( Scopes::SESSION_ACCESS, ) .await - .map(|x| x.1) .ok(); - let notes = if auth_user.as_ref().is_some_and(|x| x.role.is_mod()) { + let notes = if auth_user + .as_ref() + .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) + { DBModerationNote::get_many_users( &users_data.iter().map(|x| x.id).collect::>(), &**pool, @@ -611,7 +615,9 @@ pub async fn users_get( HashMap::new() }; - let is_mod = auth_user.as_ref().is_some_and(|x| x.role.is_mod()); + let is_mod = auth_user + .as_ref() + .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)); let users: Vec = users_data .into_iter() @@ -664,14 +670,18 @@ pub async fn user_get( Scopes::SESSION_ACCESS, ) .await - .map(|x| x.1) .ok(); - let is_admin = auth_user.as_ref().is_some_and(|x| x.role.is_admin()); - let is_mod = auth_user.as_ref().is_some_and(|x| x.role.is_mod()); + let is_admin = auth_user + .as_ref() + .is_some_and(|x| x.has_scopes(Scopes::ADMIN_READ)); + let is_mod = auth_user + .as_ref() + .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)); let user_id = data.id; - let is_self = - auth_user.as_ref().is_some_and(|x| x.id == user_id.into()); + let is_self = auth_user + .as_ref() + .is_some_and(|x| x.user.id == user_id.into()); let lock = data.lock.clone(); let restriction = data.restriction.clone(); @@ -719,15 +729,16 @@ pub async fn user_notes_edit( redis: web::Data, session_queue: web::Data, ) -> Result { - let user = check_is_moderator_from_headers( + let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, - Scopes::SESSION_ACCESS, + Scopes::SESSION_ACCESS | Scopes::MODERATION_WRITE, ) .await - .wrap_auth_err("authenticating API request")?; + .wrap_auth_err("authenticating API request")? + .into_user(); new_note .validate_not_empty() @@ -812,7 +823,6 @@ pub async fn collections_list( Scopes::COLLECTION_READ, ) .await - .map(|x| x.1) .ok(); let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis) @@ -859,7 +869,6 @@ pub async fn orgs_list( Scopes::PROJECT_READ, ) .await - .map(|x| x.1) .ok(); let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis) @@ -915,9 +924,9 @@ pub async fn orgs_list( let logged_in = user .as_ref() .and_then(|user| { - members_data - .iter() - .find(|x| x.user_id == user.id.into() && x.accepted) + members_data.iter().find(|x| { + x.user_id == user.user.id.into() && x.accepted + }) }) .is_some(); @@ -987,7 +996,7 @@ pub async fn user_edit( redis: web::Data, session_queue: web::Data, ) -> Result { - let (scopes, user) = get_user_from_headers( + let UserWithScopes { user, scopes } = get_user_from_headers( &req, &**pool, &redis, @@ -1010,7 +1019,7 @@ pub async fn user_edit( let id = actual_user.id; let user_id: UserId = id.into(); - if user.id == user_id || user.role.is_mod() { + if user.id == user_id || scopes.contains(Scopes::MODERATION_WRITE) { let mut transaction = pool .begin() .await @@ -1061,7 +1070,7 @@ pub async fn user_edit( } if let Some(role) = &new_user.role { - if !user.role.is_admin() { + if !scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You do not have the permissions to edit the role of this user!", ))); @@ -1084,7 +1093,7 @@ pub async fn user_edit( } if let Some(badges) = &new_user.badges { - if !user.role.is_admin() { + if !scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You do not have the permissions to edit the badges of this user!", ))); @@ -1214,14 +1223,15 @@ pub async fn user_icon_edit( Scopes::USER_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis) .await .wrap_internal_err("fetching user from database")?; if let Some(actual_user) = id_option { - if user.id != actual_user.id.into() && !user.role.is_mod() { + if user.user.id != actual_user.id.into() + && !user.has_scopes(Scopes::MODERATION_WRITE) + { return Err(ApiError::Auth(eyre::eyre!( "You don't have permission to edit this user's icon.", ))); @@ -1314,14 +1324,15 @@ pub async fn user_icon_delete( Scopes::USER_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis) .await .wrap_internal_err("fetching user from database")?; if let Some(actual_user) = id_option { - if user.id != actual_user.id.into() && !user.role.is_mod() { + if user.user.id != actual_user.id.into() + && !user.has_scopes(Scopes::MODERATION_WRITE) + { return Err(ApiError::Auth(eyre::eyre!( "You don't have permission to edit this user's icon.", ))); @@ -1385,8 +1396,7 @@ pub async fn user_delete( Scopes::USER_DELETE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis) .await .wrap_internal_err("failed to get user")?; @@ -1394,7 +1404,7 @@ pub async fn user_delete( let id = id_option .map(|x| x.id) .wrap_not_found_err("resource not found")?; - if !user.role.is_admin() && user.id != id.into() { + if !user.has_scopes(Scopes::ADMIN_WRITE) && user.user.id != id.into() { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to delete this user!", ))); @@ -1448,14 +1458,13 @@ pub async fn user_follows( Scopes::USER_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis) .await .wrap_internal_err("fetching user from database")?; if let Some(id) = id_option.map(|x| x.id) { - if !user.role.is_admin() && user.id != id.into() { + if !user.has_scopes(Scopes::ADMIN_READ) && user.user.id != id.into() { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to see the projects this user follows!", ))); @@ -1508,14 +1517,13 @@ pub async fn user_notifications( Scopes::NOTIFICATION_READ, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis) .await .wrap_internal_err("fetching user from database")?; if let Some(id) = id_option.map(|x| x.id) { - if !user.role.is_admin() && user.id != id.into() { + if !user.has_scopes(Scopes::ADMIN_READ) && user.user.id != id.into() { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to see the notifications of this user!", ))); diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index f3a0b3f0921..2f13a48293a 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -1,4 +1,5 @@ use super::project_creation::{CreateError, UploadedFile}; +use crate::auth::UserWithScopes; use crate::auth::get_user_from_headers; use crate::database::PgPool; use crate::database::PgTransaction; @@ -22,7 +23,6 @@ use crate::models::projects::{ }; use crate::models::projects::{DependencyType, skip_nulls}; use crate::models::teams::ProjectPermissions; -use crate::models::users::User; use crate::models::v3::user_limits::UserLimits; use crate::queue::session::AuthQueue; use crate::search::SearchState; @@ -176,8 +176,7 @@ pub async fn version_create( &session_queue, Scopes::VERSION_CREATE, ) - .await? - .1; + .await?; let result = async { let (response, project_id, version_id) = version_create_inner( @@ -241,7 +240,7 @@ async fn version_create_inner( redis: &RedisPool, file_host: &dyn FileHost, uploaded_files: &mut Vec, - user: &User, + user: &UserWithScopes, pool: &PgPool, delphi_scan_files: &mut Vec, ) -> Result<(HttpResponse, models::DBProjectId, models::DBVersionId), CreateError> @@ -305,7 +304,7 @@ async fn version_create_inner( // of the project the version is being added to. let team_member = models::DBTeamMember::get_from_user_id_project( project_id, - user.id.into(), + user.user.id.into(), false, &mut *transaction, ) @@ -321,7 +320,7 @@ async fn version_create_inner( let organization_team_member = if let Some(organization) = &organization { models::DBTeamMember::get_from_user_id( organization.team_id, - user.id.into(), + user.user.id.into(), &mut *transaction, ) .await? @@ -329,8 +328,8 @@ async fn version_create_inner( None }; - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) @@ -403,7 +402,7 @@ async fn version_create_inner( version_builder = Some(VersionBuilder { version_id: version_id.into(), project_id, - author_id: user.id.into(), + author_id: user.user.id.into(), name: version_create_data.version_title.clone(), version_number: version_create_data.version_number.clone(), changelog: version_create_data.version_body.clone().unwrap_or_default(), @@ -533,7 +532,7 @@ async fn version_create_inner( let response = Version { id: builder.version_id.into(), project_id: builder.project_id.into(), - author_id: user.id, + author_id: user.user.id, featured: builder.featured, name: builder.name.clone(), version_number: builder.version_number.clone(), @@ -706,8 +705,7 @@ pub async fn upload_file_to_version( &session_queue, Scopes::VERSION_WRITE, ) - .await? - .1; + .await?; let result = async { let (response, project_id) = upload_file_to_version_inner( @@ -774,7 +772,7 @@ async fn upload_file_to_version_inner( file_host: &dyn FileHost, uploaded_files: &mut Vec, version_id: models::DBVersionId, - user: &User, + user: &UserWithScopes, delphi_scan_files: &mut Vec, ) -> Result<(HttpResponse, models::DBProjectId), CreateError> { let mut initial_file_data: Option = None; @@ -815,10 +813,10 @@ async fn upload_file_to_version_inner( )); } - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { let team_member = models::DBTeamMember::get_from_user_id_project( version.inner.project_id, - user.id.into(), + user.user.id.into(), false, &mut *transaction, ) @@ -835,7 +833,7 @@ async fn upload_file_to_version_inner( { models::DBTeamMember::get_from_user_id( organization.team_id, - user.id.into(), + user.user.id.into(), &mut *transaction, ) .await? @@ -843,8 +841,8 @@ async fn upload_file_to_version_inner( None }; - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) diff --git a/apps/labrinth/src/routes/v3/version_file.rs b/apps/labrinth/src/routes/v3/version_file.rs index ef0dabf0b40..dbf16f4d5f9 100644 --- a/apps/labrinth/src/routes/v3/version_file.rs +++ b/apps/labrinth/src/routes/v3/version_file.rs @@ -80,7 +80,6 @@ pub async fn get_version_from_hash( Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); let hash = info.into_inner().0.to_lowercase(); let algorithm = hash_query.algorithm.clone().unwrap_or_else(|| { @@ -212,7 +211,6 @@ pub async fn get_update_from_hash( Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); let hash = info.into_inner().0.to_lowercase(); if let Some(file) = database::models::DBVersion::get_file_from_hash( @@ -313,7 +311,6 @@ pub async fn get_versions_from_hashes( Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); let algorithm = file_data @@ -395,7 +392,6 @@ pub async fn get_projects_from_hashes( Scopes::PROJECT_READ | Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); let algorithm = file_data @@ -570,7 +566,6 @@ async fn update_files_internal( Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); let algorithm = update_data @@ -676,10 +671,12 @@ async fn update_files_internal( .filter(|x| !x.is_hidden()) .map(|x| x.to_string()) .collect::>(), - user_option.as_ref().is_some_and(|x| x.role.is_mod()), user_option .as_ref() - .map(|x| database::models::DBUserId::from(x.id).0), + .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)), + user_option + .as_ref() + .map(|x| database::models::DBUserId::from(x.user.id).0), ) .fetch_all(&mut transaction) .await @@ -772,7 +769,6 @@ pub async fn update_individual_files( Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); let algorithm = update_data.algorithm.clone().unwrap_or_else(|| { @@ -986,8 +982,7 @@ pub async fn delete_file( Scopes::VERSION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let hash = info.into_inner().0.to_lowercase(); let algorithm = hash_query.algorithm.clone().unwrap_or_else(|| { @@ -1004,11 +999,11 @@ pub async fn delete_file( .wrap_internal_err("fetching version from database")?; if let Some(row) = file { - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { let team_member = database::models::DBTeamMember::get_from_user_id_version( row.version_id, - user.id.into(), + user.user.id.into(), &**pool, ) .await @@ -1027,7 +1022,7 @@ pub async fn delete_file( { database::models::DBTeamMember::get_from_user_id_organization( organization.id, - user.id.into(), + user.user.id.into(), false, &**pool, ) @@ -1037,8 +1032,8 @@ pub async fn delete_file( None }; - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) @@ -1164,7 +1159,6 @@ pub async fn download_version( Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); let hash = info.into_inner().0.to_lowercase(); diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index 05250806810..df7e2114537 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -102,7 +102,6 @@ pub async fn version_project_get_helper( Scopes::PROJECT_READ | Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); if let Some(project) = result { @@ -237,7 +236,6 @@ pub async fn versions_get( Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); let mut versions = @@ -314,7 +312,6 @@ pub async fn version_get_helper( Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); if let Some(data) = version_data @@ -540,7 +537,7 @@ pub(crate) async fn version_edit_in_transaction( ) -> Result< ( database::models::version_item::VersionQueryResult, - crate::models::users::User, + crate::auth::UserWithScopes, ), ApiError, > { @@ -552,8 +549,7 @@ pub(crate) async fn version_edit_in_transaction( Scopes::VERSION_WRITE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; new_version .validate() @@ -570,7 +566,7 @@ pub(crate) async fn version_edit_in_transaction( let team_member = database::models::DBTeamMember::get_from_user_id_project( version_item.inner.project_id, - user.id.into(), + user.user.id.into(), false, &**pool, ) @@ -589,7 +585,7 @@ pub(crate) async fn version_edit_in_transaction( { database::models::DBTeamMember::get_from_user_id( organization.team_id, - user.id.into(), + user.user.id.into(), &**pool, ) .await @@ -598,8 +594,8 @@ pub(crate) async fn version_edit_in_transaction( None }; - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ); @@ -867,7 +863,7 @@ pub(crate) async fn version_edit_in_transaction( } if let Some(downloads) = &new_version.downloads { - if !user.role.is_mod() { + if !user.has_scopes(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You don't have permission to set the downloads of this mod", ))); @@ -1275,7 +1271,6 @@ pub async fn version_list_internal( Scopes::PROJECT_READ | Scopes::VERSION_READ, ) .await - .map(|x| x.1) .ok(); if let Some(project) = result { @@ -1368,8 +1363,7 @@ pub async fn version_delete( Scopes::VERSION_DELETE, ) .await - .wrap_auth_err("authenticating API request")? - .1; + .wrap_auth_err("authenticating API request")?; let id = info.into_inner().0; let version = database::models::DBVersion::get(id.into(), &**pool, &redis) @@ -1379,11 +1373,11 @@ pub async fn version_delete( "the specified version does not exist!".to_string() })?; - if !user.role.is_admin() { + if !user.has_scopes(Scopes::ADMIN_WRITE) { let team_member = database::models::DBTeamMember::get_from_user_id_project( version.inner.project_id, - user.id.into(), + user.user.id.into(), false, &**pool, ) @@ -1402,7 +1396,7 @@ pub async fn version_delete( { database::models::DBTeamMember::get_from_user_id( organization.team_id, - user.id.into(), + user.user.id.into(), &**pool, ) .await @@ -1410,8 +1404,8 @@ pub async fn version_delete( } else { None }; - let permissions = ProjectPermissions::get_permissions_by_role( - &user.role, + let permissions = ProjectPermissions::get_permissions_by_scopes( + user.scopes, &team_member, &organization_team_member, ) diff --git a/apps/labrinth/src/util/validate.rs b/apps/labrinth/src/util/validate.rs index 0d4c796bd16..17548f2fd94 100644 --- a/apps/labrinth/src/util/validate.rs +++ b/apps/labrinth/src/util/validate.rs @@ -128,7 +128,7 @@ pub fn validate_url_hashmap_values( pub fn validate_no_restricted_scopes( value: &Scopes, ) -> Result<(), validator::ValidationError> { - if value.is_restricted() { + if value.is_restricted() || value.intersects(Scopes::staff()) { return Err(validator::ValidationError::new( "Restricted scopes not allowed", )); diff --git a/apps/labrinth/tests/limits.rs b/apps/labrinth/tests/limits.rs index 5ec590d4382..fe4fe1527bc 100644 --- a/apps/labrinth/tests/limits.rs +++ b/apps/labrinth/tests/limits.rs @@ -5,11 +5,12 @@ use common::api_v3::ApiV3; use common::database::{USER_USER_ID_PARSED, USER_USER_PAT}; use common::dummy_data::TestFile; use common::environment::{TestEnvironment, with_test_environment}; +use labrinth::auth::UserWithScopes; use labrinth::database::PgPool; use labrinth::database::models::{ DBProjectId, DBUserId, user_limits::DBUserLimits, }; -use labrinth::models::{users::User, v3::user_limits::UserLimits}; +use labrinth::models::{pats::Scopes, v3::user_limits::UserLimits}; use serde_json::Value; use crate::common::api_common::{ApiProject, ApiUser, ApiVersion}; @@ -79,7 +80,10 @@ pub async fn max_projects_created_per_day() { let api = &test_env.api; let user_response = api.get_current_user(USER_USER_PAT).await; assert_status!(&user_response, StatusCode::OK); - let user: User = test::read_body_json(user_response).await; + let user = UserWithScopes { + user: test::read_body_json(user_response).await, + scopes: Scopes::empty(), + }; let now = Utc::now(); let current_daily_projects = UserLimits::get_for_projects_per_day( &user, @@ -256,7 +260,10 @@ pub async fn max_versions_uploaded_per_day() { let project_id = test_env.dummy.project_alpha.project_id_parsed; let user_response = api.get_current_user(USER_USER_PAT).await; assert_status!(&user_response, StatusCode::OK); - let user: User = test::read_body_json(user_response).await; + let user = UserWithScopes { + user: test::read_body_json(user_response).await, + scopes: Scopes::empty(), + }; let now = Utc::now(); let current_daily_versions = UserLimits::get_for_versions_per_day( &user, diff --git a/apps/labrinth/tests/oauth_clients.rs b/apps/labrinth/tests/oauth_clients.rs index 8dd8604c5a0..20187f1556b 100644 --- a/apps/labrinth/tests/oauth_clients.rs +++ b/apps/labrinth/tests/oauth_clients.rs @@ -31,7 +31,7 @@ async fn can_create_edit_get_oauth_client() { .api .add_oauth_client( client_name.clone(), - Scopes::all() - Scopes::restricted(), + Scopes::all() - Scopes::restricted() - Scopes::staff(), redirect_uris.clone(), FRIEND_USER_PAT, ) diff --git a/apps/labrinth/tests/pats.rs b/apps/labrinth/tests/pats.rs index 8ea02592207..e1852abeb18 100644 --- a/apps/labrinth/tests/pats.rs +++ b/apps/labrinth/tests/pats.rs @@ -149,7 +149,11 @@ pub async fn pat_full_test() { let resp = test_env.call(req).await; assert_eq!( resp.status().as_u16(), - if scope.is_restricted() { 400 } else { 204 } + if scope.is_restricted() || scope.intersects(Scopes::staff()) { + 400 + } else { + 204 + } ); } @@ -226,7 +230,11 @@ pub async fn bad_pats() { let resp = test_env.call(req).await; assert_eq!( resp.status().as_u16(), - if scope.is_restricted() { 400 } else { 200 } + if scope.is_restricted() || scope.intersects(Scopes::staff()) { + 400 + } else { + 200 + } ); } @@ -286,7 +294,11 @@ pub async fn bad_pats() { let resp = test_env.call(req).await; assert_eq!( resp.status().as_u16(), - if scope.is_restricted() { 400 } else { 204 } + if scope.is_restricted() || scope.intersects(Scopes::staff()) { + 400 + } else { + 204 + } ); } }) diff --git a/apps/labrinth/tests/project.rs b/apps/labrinth/tests/project.rs index 81aa237e442..6c5fbba490d 100644 --- a/apps/labrinth/tests/project.rs +++ b/apps/labrinth/tests/project.rs @@ -146,7 +146,7 @@ async fn project_redirect_cache_respects_visibility() { Some(ADMIN_USER_ID_PARSED), ] { let user = if let Some(user_id) = user_id { - Some(labrinth::models::users::User::from( + let user = labrinth::models::users::User::from( labrinth::database::models::DBUser::get_id( labrinth::database::models::DBUserId(user_id), &*env.db.pool, @@ -155,7 +155,9 @@ async fn project_redirect_cache_respects_visibility() { .await .unwrap() .unwrap(), - )) + ); + let scopes = Scopes::staff_for_role(&user.role); + Some(labrinth::auth::UserWithScopes { user, scopes }) } else { None }; diff --git a/apps/labrinth/tests/scopes.rs b/apps/labrinth/tests/scopes.rs index c8a42b87d83..ac02fb08c34 100644 --- a/apps/labrinth/tests/scopes.rs +++ b/apps/labrinth/tests/scopes.rs @@ -93,7 +93,7 @@ async fn user_scopes() { }; ScopeTest::new(&test_env) .with_user_id(ADMIN_USER_ID_PARSED) - .test(req_gen, write_user) + .test(req_gen, write_user | Scopes::ADMIN_WRITE) .await .unwrap(); @@ -841,7 +841,7 @@ pub async fn report_scopes() { // Delete report // We use a moderator PAT here, as only moderators can delete reports - let report_delete = Scopes::REPORT_DELETE; + let report_delete = Scopes::REPORT_DELETE | Scopes::MODERATION_WRITE; let req_gen = |pat: Option| async move { api.delete_report(report_id, pat.as_deref()).await }; From 0c96d8e31108bc012eca4c3f16686828067301ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?= <108630700+fetchfern@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:09:21 -0500 Subject: [PATCH 2/8] Update apps/labrinth/src/routes/v3/users.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Signed-off-by: François-Xavier Talbot <108630700+fetchfern@users.noreply.github.com> --- apps/labrinth/src/routes/v3/users.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/labrinth/src/routes/v3/users.rs b/apps/labrinth/src/routes/v3/users.rs index e8b446ef5d8..0a1dd4b0d40 100644 --- a/apps/labrinth/src/routes/v3/users.rs +++ b/apps/labrinth/src/routes/v3/users.rs @@ -1019,7 +1019,9 @@ pub async fn user_edit( let id = actual_user.id; let user_id: UserId = id.into(); - if user.id == user_id || scopes.contains(Scopes::MODERATION_WRITE) { + if user.id == user_id + || scopes.intersects(Scopes::MODERATION_WRITE | Scopes::ADMIN_WRITE) + { let mut transaction = pool .begin() .await From d8fdd0698fb256cc6b7eb13824b00eabeb63d439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?= <108630700+fetchfern@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:15:47 -0500 Subject: [PATCH 3/8] Revert "Update apps/labrinth/src/routes/v3/users.rs" This reverts commit 0c96d8e31108bc012eca4c3f16686828067301ff. --- apps/labrinth/src/routes/v3/users.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/labrinth/src/routes/v3/users.rs b/apps/labrinth/src/routes/v3/users.rs index 0a1dd4b0d40..e8b446ef5d8 100644 --- a/apps/labrinth/src/routes/v3/users.rs +++ b/apps/labrinth/src/routes/v3/users.rs @@ -1019,9 +1019,7 @@ pub async fn user_edit( let id = actual_user.id; let user_id: UserId = id.into(); - if user.id == user_id - || scopes.intersects(Scopes::MODERATION_WRITE | Scopes::ADMIN_WRITE) - { + if user.id == user_id || scopes.contains(Scopes::MODERATION_WRITE) { let mut transaction = pool .begin() .await From a408ad7522cfeb1c7d78ec312b051dd2e0f6e828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?= <108630700+fetchfern@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:46:02 -0500 Subject: [PATCH 4/8] chore: add moderator scopes for admins --- apps/labrinth/src/auth/validate.rs | 5 +++-- apps/labrinth/src/models/v3/pats.rs | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/labrinth/src/auth/validate.rs b/apps/labrinth/src/auth/validate.rs index 9132ee9b009..fa904d60b97 100644 --- a/apps/labrinth/src/auth/validate.rs +++ b/apps/labrinth/src/auth/validate.rs @@ -278,8 +278,9 @@ where Ok(possible_user.map(|(scopes, user)| { let allowed_staff = Scopes::staff_for_role(&Role::from_string(&user.role)); - let scopes = - scopes - (Scopes::staff() - allowed_staff) - user.removed_perms(); + let scopes = (scopes - (Scopes::staff() - allowed_staff)) + .with_implied_staff() + - user.removed_perms(); (scopes, user) })) } diff --git a/apps/labrinth/src/models/v3/pats.rs b/apps/labrinth/src/models/v3/pats.rs index 7c37252c5af..a3b53ff40ec 100644 --- a/apps/labrinth/src/models/v3/pats.rs +++ b/apps/labrinth/src/models/v3/pats.rs @@ -206,6 +206,19 @@ impl Scopes { } } + /// Adds the moderation scopes implied by admin scopes: `ADMIN_READ` + /// implies `MODERATION_READ`, and `ADMIN_WRITE` implies `MODERATION_WRITE`. + pub fn with_implied_staff(self) -> Scopes { + let mut scopes = self; + if self.contains(Scopes::ADMIN_READ) { + scopes |= Scopes::MODERATION_READ; + } + if self.contains(Scopes::ADMIN_WRITE) { + scopes |= Scopes::MODERATION_WRITE; + } + scopes + } + pub fn parse_from_oauth_scopes( scopes: &str, ) -> Result { From 2a4f7c8878eb91ee1a545ff5750ee301b961e35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?= <108630700+fetchfern@users.noreply.github.com> Date: Thu, 5 Nov 2026 20:02:54 -0500 Subject: [PATCH 5/8] review --- apps/labrinth/src/auth/checks.rs | 32 ++++++++--- apps/labrinth/src/models/v3/teams.rs | 27 ++++----- apps/labrinth/src/routes/v3/images.rs | 1 + apps/labrinth/tests/pats.rs | 80 ++++++++++++++++++++++++++- 4 files changed, 115 insertions(+), 25 deletions(-) diff --git a/apps/labrinth/src/auth/checks.rs b/apps/labrinth/src/auth/checks.rs index 15ce7d49100..54ac4e0293b 100644 --- a/apps/labrinth/src/auth/checks.rs +++ b/apps/labrinth/src/auth/checks.rs @@ -203,15 +203,23 @@ pub async fn is_visible_version( .map(|x| !x.is_empty()) } +/// Whether the user is on the version's team, or holds `moderation_scope`. pub async fn is_team_member_version( version_data: &DBVersion, user_option: &Option, + moderation_scope: Scopes, pool: &PgPool, redis: &RedisPool, ) -> Result { - filter_enlisted_version_ids(vec![version_data], user_option, pool, redis) - .await - .map(|x| !x.is_empty()) + filter_enlisted_version_ids( + vec![version_data], + user_option, + moderation_scope, + pool, + redis, + ) + .await + .map(|x| !x.is_empty()) } pub async fn filter_visible_versions( @@ -323,10 +331,15 @@ pub async fn filter_visible_version_ids( .wrap_api_err("filtering visible project IDs")?; // 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 - .wrap_api_err("filtering enlisted version ids")?; + let enlisted_version_ids = filter_enlisted_version_ids( + versions.clone(), + user_option, + Scopes::MODERATION_READ, + pool, + redis, + ) + .await + .wrap_api_err("filtering enlisted version ids")?; let withheld_versions = get_files_missing_attribution( pool, @@ -357,9 +370,12 @@ pub async fn filter_visible_version_ids( Ok(return_versions) } +/// Filters out versions whose team the user is on, or all versions if the +/// user holds `moderation_scope`. pub async fn filter_enlisted_version_ids( versions: Vec<&DBVersion>, user_option: &Option, + moderation_scope: Scopes, pool: &PgPool, redis: &RedisPool, ) -> Result, ApiError> { @@ -385,7 +401,7 @@ pub async fn filter_enlisted_version_ids( for version in versions { if user_option .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) + .is_some_and(|x| x.has_scopes(moderation_scope)) || (user_option.is_some() && authorized_project_ids.contains(&version.project_id)) { diff --git a/apps/labrinth/src/models/v3/teams.rs b/apps/labrinth/src/models/v3/teams.rs index cc921a2c458..fa169da1d83 100644 --- a/apps/labrinth/src/models/v3/teams.rs +++ b/apps/labrinth/src/models/v3/teams.rs @@ -59,6 +59,9 @@ impl Default for ProjectPermissions { impl ProjectPermissions { /// Permissions on a project for a user whose credentials grant `scopes`. + /// + /// Callers treat `Some` as access to edit the project, so only staff write + /// scopes grant permissions to users outside the project's team. pub fn get_permissions_by_scopes( scopes: Scopes, project_team_member: &Option, // team member of the user in the project @@ -70,33 +73,27 @@ impl ProjectPermissions { return Some(ProjectPermissions::all()); } - let admin_read = if scopes.contains(Scopes::ADMIN_READ) { - ProjectPermissions::VIEW_ANALYTICS - | ProjectPermissions::VIEW_PAYOUTS - } else { - ProjectPermissions::empty() - }; - if let Some(member) = project_team_member && member.accepted { - return Some(member.permissions | admin_read); + return Some(member.permissions); } if let Some(member) = organization_team_member && member.accepted { - return Some(member.permissions | admin_read); + return Some(member.permissions); } - let mut permissions = admin_read; if scopes.contains(Scopes::MODERATION_WRITE) { - permissions |= ProjectPermissions::EDIT_DETAILS - | ProjectPermissions::EDIT_BODY - | ProjectPermissions::UPLOAD_VERSION; + Some( + ProjectPermissions::EDIT_DETAILS + | ProjectPermissions::EDIT_BODY + | ProjectPermissions::UPLOAD_VERSION, + ) + } else { + None } - - (!permissions.is_empty()).then_some(permissions) } } diff --git a/apps/labrinth/src/routes/v3/images.rs b/apps/labrinth/src/routes/v3/images.rs index 6ef5ce213f6..8b5fb65f712 100644 --- a/apps/labrinth/src/routes/v3/images.rs +++ b/apps/labrinth/src/routes/v3/images.rs @@ -131,6 +131,7 @@ pub async fn images_add( if is_team_member_version( &version.inner, &Some(user.clone()), + Scopes::MODERATION_WRITE, &pool, &redis, ) diff --git a/apps/labrinth/tests/pats.rs b/apps/labrinth/tests/pats.rs index e1852abeb18..a11305d2e16 100644 --- a/apps/labrinth/tests/pats.rs +++ b/apps/labrinth/tests/pats.rs @@ -1,15 +1,91 @@ use actix_http::StatusCode; use actix_web::test; use chrono::{Duration, Utc}; -use common::{database::*, environment::with_test_environment_all}; +use common::api_v3::ApiV3; +use common::{ + database::*, + environment::{ + TestEnvironment, with_test_environment, with_test_environment_all, + }, +}; use labrinth::models::pats::Scopes; +use labrinth::test::pats::create_test_pat; use serde_json::json; -use crate::common::api_common::AppendsOptionalPat; +use crate::common::api_common::{ApiUser, AppendsOptionalPat}; pub mod common; +// Staff can grant the staff scopes their role allows, and no others +#[actix_rt::test] +pub async fn staff_scope_grants_follow_role() { + with_test_environment_all(None, |test_env| async move { + for (pat, scopes, expected) in [ + ( + MOD_USER_PAT, + Scopes::MODERATION_READ | Scopes::MODERATION_WRITE, + StatusCode::OK, + ), + (MOD_USER_PAT, Scopes::ADMIN_READ, StatusCode::BAD_REQUEST), + (ADMIN_USER_PAT, Scopes::staff(), StatusCode::OK), + ] { + let req = test::TestRequest::post() + .uri("/_internal/pat") + .append_pat(pat) + .set_json(json!({ + "scopes": scopes, + "name": "staff_scope_grants_follow_role Test", + "expires": Utc::now() + Duration::days(1), + })) + .to_request(); + let resp = test_env.call(req).await; + assert_status!(&resp, expected); + } + }) + .await; +} + +// A staff PAT loses its staff access once its user is no longer staff +#[actix_rt::test] +pub async fn staff_scopes_revoked_on_demotion() { + with_test_environment( + None, + |test_env: TestEnvironment| async move { + let pat = create_test_pat( + Scopes::MODERATION_READ, + MOD_USER_ID_PARSED, + &test_env.db, + ) + .await; + let pid = test_env.dummy.project_alpha.project_id.clone(); + let lock_status = || { + test::TestRequest::get() + .uri(&format!("/_internal/moderation/lock/{pid}")) + .append_pat(Some(&pat)) + .to_request() + }; + + let resp = test_env.call(lock_status()).await; + assert_status!(&resp, StatusCode::OK); + + let resp = test_env + .api + .edit_user( + MOD_USER_ID, + json!({ "role": "developer" }), + ADMIN_USER_PAT, + ) + .await; + assert_status!(&resp, StatusCode::NO_CONTENT); + + let resp = test_env.call(lock_status()).await; + assert_status!(&resp, StatusCode::UNAUTHORIZED); + }, + ) + .await; +} + // Full pat test: // - create a PAT and ensure it can be used for the scope // - ensure access token is not returned for any PAT in GET From 2ee2dd04a5c952ef8c6fb15157b0b089015b0da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?= <108630700+fetchfern@users.noreply.github.com> Date: Mon, 9 Nov 2026 16:46:51 -0500 Subject: [PATCH 6/8] fix: let ADMIN_READ view analytics for any project --- apps/labrinth/src/routes/v3/analytics_get/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/labrinth/src/routes/v3/analytics_get/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/mod.rs index 4532fa6ea22..aa09d48d7aa 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/mod.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/mod.rs @@ -727,6 +727,10 @@ async fn filter_allowed_project_ids( .await .wrap_internal_err("fetching projects for analytics authorization")?; + if user.has_scopes(Scopes::ADMIN_READ) { + return Ok(projects.into_iter().map(|x| x.inner.id).collect()); + } + let team_ids = projects .iter() .map(|x| x.inner.team_id) From 5e2ec9ae82fbc36bc42da975b7f7032d27b54592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?= <108630700+fetchfern@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:08:13 -0500 Subject: [PATCH 7/8] review --- apps/labrinth/src/routes/internal/attribution.rs | 13 +++++++------ apps/labrinth/src/routes/v3/projects/validate.rs | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/labrinth/src/routes/internal/attribution.rs b/apps/labrinth/src/routes/internal/attribution.rs index 1741c92df84..fb74805c580 100644 --- a/apps/labrinth/src/routes/internal/attribution.rs +++ b/apps/labrinth/src/routes/internal/attribution.rs @@ -338,12 +338,13 @@ pub async fn list( ) .await .wrap_internal_err("failed to fetch project permissions")?; - if ProjectPermissions::get_permissions_by_scopes( - scopes, - &team_member, - &organization_team_member, - ) - .is_none() + if !requester_is_mod + && ProjectPermissions::get_permissions_by_scopes( + scopes, + &team_member, + &organization_team_member, + ) + .is_none() { return Err(ApiError::Auth(eyre!( "you do not have permission to read versions for this project" diff --git a/apps/labrinth/src/routes/v3/projects/validate.rs b/apps/labrinth/src/routes/v3/projects/validate.rs index 79b81881592..1ca6f3bc17e 100644 --- a/apps/labrinth/src/routes/v3/projects/validate.rs +++ b/apps/labrinth/src/routes/v3/projects/validate.rs @@ -172,12 +172,13 @@ pub async fn validate( .await .wrap_internal_err("fetching project permissions")?; - if ProjectPermissions::get_permissions_by_scopes( - scopes, - &team_member, - &organization_team_member, - ) - .is_none() + if !scopes.contains(Scopes::MODERATION_READ) + && ProjectPermissions::get_permissions_by_scopes( + scopes, + &team_member, + &organization_team_member, + ) + .is_none() { return Err(ApiError::Auth(eyre!( "you do not have permission to validate this project" From 31fbe9b6b9dadf8e81c15c062f84cfe183f89cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?= <108630700+fetchfern@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:13:24 -0500 Subject: [PATCH 8/8] chore: remove into_user/to_user --- apps/labrinth/src/auth/checks.rs | 16 +++---- apps/labrinth/src/auth/oauth/mod.rs | 4 +- apps/labrinth/src/auth/validate.rs | 15 ------ apps/labrinth/src/models/v3/thread_issues.rs | 3 +- apps/labrinth/src/models/v3/threads.rs | 4 +- apps/labrinth/src/models/v3/user_limits.rs | 2 +- .../routes/internal/admin/user_credentials.rs | 4 +- .../src/routes/internal/admin/user_lock.rs | 4 +- .../routes/internal/admin/user_sessions.rs | 2 +- .../src/routes/internal/analytics_event.rs | 6 +-- .../src/routes/internal/attribution.rs | 8 ++-- .../routes/internal/external_notifications.rs | 2 +- apps/labrinth/src/routes/internal/flows.rs | 22 ++++----- apps/labrinth/src/routes/internal/gdpr.rs | 2 +- .../internal/moderation/external_license.rs | 4 +- .../src/routes/internal/moderation/mod.rs | 18 ++++---- .../routes/internal/moderation/tech_review.rs | 6 +-- .../internal/moderation/tech_review/rules.rs | 6 +-- .../internal/moderation/user_restrictions.rs | 4 +- apps/labrinth/src/routes/internal/pats.rs | 12 ++--- apps/labrinth/src/routes/internal/session.rs | 6 +-- .../src/routes/v3/analytics_get/mod.rs | 6 +-- apps/labrinth/src/routes/v3/blocked_users.rs | 6 +-- apps/labrinth/src/routes/v3/collections.rs | 4 +- apps/labrinth/src/routes/v3/disclosures.rs | 4 +- apps/labrinth/src/routes/v3/friends.rs | 6 +-- apps/labrinth/src/routes/v3/oauth_clients.rs | 2 +- apps/labrinth/src/routes/v3/organizations.rs | 16 +++---- apps/labrinth/src/routes/v3/payouts.rs | 8 ++-- apps/labrinth/src/routes/v3/projects/mod.rs | 46 ++++++++++--------- .../src/routes/v3/projects/mutation.rs | 6 +-- apps/labrinth/src/routes/v3/reports.rs | 2 +- apps/labrinth/src/routes/v3/teams.rs | 2 +- apps/labrinth/src/routes/v3/threads.rs | 24 +++++----- apps/labrinth/src/routes/v3/users.rs | 28 ++++++----- .../src/routes/v3/version_creation.rs | 2 +- apps/labrinth/src/routes/v3/version_file.rs | 4 +- apps/labrinth/src/routes/v3/versions.rs | 4 +- 38 files changed, 157 insertions(+), 163 deletions(-) diff --git a/apps/labrinth/src/auth/checks.rs b/apps/labrinth/src/auth/checks.rs index 54ac4e0293b..c2c095f5afb 100644 --- a/apps/labrinth/src/auth/checks.rs +++ b/apps/labrinth/src/auth/checks.rs @@ -123,7 +123,7 @@ pub async fn filter_visible_project_ids( !project.status.is_hidden() }) || user_option .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) + .is_some_and(|x| x.scopes.contains(Scopes::MODERATION_READ)) { return_projects.push(project.id); } else if user_option.is_some() { @@ -287,7 +287,7 @@ impl ValidateAuthorized for models::DBOAuthClient { user_option: Option<&UserWithScopes>, ) -> Result<(), ApiError> { if let Some(user) = user_option { - return if user.has_scopes(Scopes::MODERATION_WRITE) + return if user.scopes.contains(Scopes::MODERATION_WRITE) || user.user.id == self.created_by.into() { Ok(()) @@ -360,7 +360,7 @@ pub async fn filter_visible_version_ids( && visible_project_ids.contains(&version.project_id)) || user_option .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) + .is_some_and(|x| x.scopes.contains(Scopes::MODERATION_READ)) || enlisted_version_ids.contains(&version.id) { return_versions.push(version.id); @@ -401,7 +401,7 @@ pub async fn filter_enlisted_version_ids( for version in versions { if user_option .as_ref() - .is_some_and(|x| x.has_scopes(moderation_scope)) + .is_some_and(|x| x.scopes.contains(moderation_scope)) || (user_option.is_some() && authorized_project_ids.contains(&version.project_id)) { @@ -424,7 +424,7 @@ pub async fn is_visible_collection( }) && !collection_data.projects.is_empty(); if let Some(user) = &user_option && !authorized - && (user.has_scopes(Scopes::MODERATION_READ) + && (user.scopes.contains(Scopes::MODERATION_READ) || user.user.id == collection_data.user_id.into()) { authorized = true; @@ -448,7 +448,7 @@ pub async fn filter_visible_collections( }) && !collection.projects.is_empty()) || user_option .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) + .is_some_and(|x| x.scopes.contains(Scopes::MODERATION_READ)) { return_collections.push(collection.into()); } else if user_option.is_some() { @@ -459,7 +459,7 @@ pub async fn filter_visible_collections( 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.has_scopes(Scopes::MODERATION_READ) + && (user.scopes.contains(Scopes::MODERATION_READ) || user.user.id == collection.user_id.into()) { return_collections.push(collection.into()); @@ -494,7 +494,7 @@ pub async fn is_visible_organization( let visible = has_searchable_projects || members.iter().filter(|member| member.accepted).count() > 1 || viewing_user.as_ref().is_some_and(|viewing_user| { - viewing_user.has_scopes(Scopes::MODERATION_READ) + viewing_user.scopes.contains(Scopes::MODERATION_READ) || members .iter() .any(|member| member.user_id == viewing_user.user.id.into()) diff --git a/apps/labrinth/src/auth/oauth/mod.rs b/apps/labrinth/src/auth/oauth/mod.rs index efe8e159441..1663023649f 100644 --- a/apps/labrinth/src/auth/oauth/mod.rs +++ b/apps/labrinth/src/auth/oauth/mod.rs @@ -85,7 +85,7 @@ pub async fn init_oauth( Scopes::USER_AUTH_WRITE, ) .await? - .into_user(); + .user; let client_id = oauth_info.client_id.into(); let client = DBOAuthClient::get(client_id, &**pool).await?; @@ -352,7 +352,7 @@ pub async fn accept_or_reject_client_scopes( Scopes::SESSION_ACCESS | Scopes::USER_AUTH_WRITE, ) .await? - .into_user(); + .user; let flow = DBFlow::take_if( &body.flow, diff --git a/apps/labrinth/src/auth/validate.rs b/apps/labrinth/src/auth/validate.rs index fa904d60b97..5610c5b2d40 100644 --- a/apps/labrinth/src/auth/validate.rs +++ b/apps/labrinth/src/auth/validate.rs @@ -21,21 +21,6 @@ pub struct UserWithScopes { pub scopes: Scopes, } -impl UserWithScopes { - pub fn into_user(self) -> User { - self.user - } - - pub fn to_user(&self) -> User { - self.user.clone() - } - - /// Whether the credentials grant all of `scopes`. - pub fn has_scopes(&self, scopes: Scopes) -> bool { - self.scopes.contains(scopes) - } -} - pub async fn get_maybe_user_from_headers<'a, E>( req: &HttpRequest, executor: E, diff --git a/apps/labrinth/src/models/v3/thread_issues.rs b/apps/labrinth/src/models/v3/thread_issues.rs index b611653b637..e44ee51414a 100644 --- a/apps/labrinth/src/models/v3/thread_issues.rs +++ b/apps/labrinth/src/models/v3/thread_issues.rs @@ -806,7 +806,8 @@ impl ThreadIssue { Self { id: data.id.into(), created_by: user - .has_scopes(Scopes::MODERATION_READ) + .scopes + .contains(Scopes::MODERATION_READ) .then(|| data.created_by.into()), why: data.why, facets: data.facets, diff --git a/apps/labrinth/src/models/v3/threads.rs b/apps/labrinth/src/models/v3/threads.rs index 39a7046d7ea..b07168043a0 100644 --- a/apps/labrinth/src/models/v3/threads.rs +++ b/apps/labrinth/src/models/v3/threads.rs @@ -137,7 +137,7 @@ impl Thread { .messages .into_iter() .filter(|x| { - user.has_scopes(Scopes::MODERATION_READ) + user.scopes.contains(Scopes::MODERATION_READ) || !x.body.is_private() }) .map(|x| ThreadMessage::from(x, user)) @@ -160,7 +160,7 @@ impl ThreadMessage { Self { id: data.id.into(), author_id: if data.hide_identity - && !user.has_scopes(Scopes::MODERATION_READ) + && !user.scopes.contains(Scopes::MODERATION_READ) { None } else { diff --git a/apps/labrinth/src/models/v3/user_limits.rs b/apps/labrinth/src/models/v3/user_limits.rs index e45ed75b57a..38da696a08a 100644 --- a/apps/labrinth/src/models/v3/user_limits.rs +++ b/apps/labrinth/src/models/v3/user_limits.rs @@ -25,7 +25,7 @@ pub struct UserLimits { impl UserLimits { fn adjust_for_user(self, user: &UserWithScopes) -> Self { - if user.has_scopes(Scopes::ADMIN_WRITE) { + if user.scopes.contains(Scopes::ADMIN_WRITE) { Self { current: self.current, max: u64::MAX, diff --git a/apps/labrinth/src/routes/internal/admin/user_credentials.rs b/apps/labrinth/src/routes/internal/admin/user_credentials.rs index 21e65253dea..f0f0298c96d 100644 --- a/apps/labrinth/src/routes/internal/admin/user_credentials.rs +++ b/apps/labrinth/src/routes/internal/admin/user_credentials.rs @@ -57,7 +57,7 @@ pub async fn force_password_reset( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "you must be an admin to reset a user's password" ))); @@ -184,7 +184,7 @@ pub async fn reset_2fa( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "you must be an admin to reset a user's 2FA" ))); diff --git a/apps/labrinth/src/routes/internal/admin/user_lock.rs b/apps/labrinth/src/routes/internal/admin/user_lock.rs index 37e98572914..cac9d5553d3 100644 --- a/apps/labrinth/src/routes/internal/admin/user_lock.rs +++ b/apps/labrinth/src/routes/internal/admin/user_lock.rs @@ -52,7 +52,7 @@ pub async fn lock_user( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!("only admins can lock users"))); } @@ -125,7 +125,7 @@ pub async fn unlock_user( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!("only admins can unlock users"))); } diff --git a/apps/labrinth/src/routes/internal/admin/user_sessions.rs b/apps/labrinth/src/routes/internal/admin/user_sessions.rs index eabfc845e91..9e1b273d0cb 100644 --- a/apps/labrinth/src/routes/internal/admin/user_sessions.rs +++ b/apps/labrinth/src/routes/internal/admin/user_sessions.rs @@ -40,7 +40,7 @@ pub async fn revoke_user_sessions( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "only admins can revoke user sessions" ))); diff --git a/apps/labrinth/src/routes/internal/analytics_event.rs b/apps/labrinth/src/routes/internal/analytics_event.rs index cc84eeeca21..bb4787f4586 100644 --- a/apps/labrinth/src/routes/internal/analytics_event.rs +++ b/apps/labrinth/src/routes/internal/analytics_event.rs @@ -59,7 +59,7 @@ pub async fn analytics_event_create( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "you do not have permission to manage analytics events" ))); @@ -119,7 +119,7 @@ pub async fn analytics_event_edit( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "you do not have permission to manage analytics events" ))); @@ -169,7 +169,7 @@ pub async fn analytics_event_delete( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre!( "you do not have permission to manage analytics events" ))); diff --git a/apps/labrinth/src/routes/internal/attribution.rs b/apps/labrinth/src/routes/internal/attribution.rs index fb74805c580..2ad386822e2 100644 --- a/apps/labrinth/src/routes/internal/attribution.rs +++ b/apps/labrinth/src/routes/internal/attribution.rs @@ -670,7 +670,7 @@ pub async fn update_group( if matches!( body.attribution.kind, AttributionResolutionKind::GloballyAllowed { .. } - ) && !user.has_scopes(Scopes::MODERATION_WRITE) + ) && !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "Only moderators can set globally allowed attributions", @@ -678,7 +678,7 @@ pub async fn update_group( } if body.attribution.moderation_status.is_some() - && !user.has_scopes(Scopes::MODERATION_WRITE) + && !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "Only moderators can set attribution moderation status", @@ -687,7 +687,7 @@ pub async fn update_group( let mut attribution = body.attribution; attribution.updated_by_moderator = - user.has_scopes(Scopes::MODERATION_WRITE); + user.scopes.contains(Scopes::MODERATION_WRITE); if let Some(moderation_status) = &mut attribution.moderation_status { moderation_status.moderated_at = Some(Utc::now()); moderation_status.moderated_by = Some(user.user.id); @@ -1260,7 +1260,7 @@ async fn ensure_can_upload_versions_to_project( user: &UserWithScopes, permission_error: &'static str, ) -> Result<(), ApiError> { - if user.has_scopes(Scopes::MODERATION_WRITE) { + if user.scopes.contains(Scopes::MODERATION_WRITE) { return Ok(()); } diff --git a/apps/labrinth/src/routes/internal/external_notifications.rs b/apps/labrinth/src/routes/internal/external_notifications.rs index 54fa0c44c88..f71bdf1dfaf 100644 --- a/apps/labrinth/src/routes/internal/external_notifications.rs +++ b/apps/labrinth/src/routes/internal/external_notifications.rs @@ -378,7 +378,7 @@ pub async fn send_custom_email( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to send custom emails!", ))); diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 300ede8f447..e5b449b6852 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1855,7 +1855,7 @@ pub async fn delete_auth_provider( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; if user.auth_providers.is_none_or(|x| x.len() <= 1) && !user.has_password.unwrap_or(false) @@ -2614,7 +2614,7 @@ pub async fn begin_2fa_flow( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; if !user.has_totp.unwrap_or(false) { let string = totp_rs::Secret::generate(); @@ -2675,7 +2675,7 @@ pub async fn finish_2fa_flow( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; if user.id != user_id.into() { return Err(ApiError::Auth(eyre::eyre!( @@ -3270,7 +3270,7 @@ pub async fn set_email( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; if !crate::database::models::DBUser::get_by_case_insensitive_email( &email_address.email, @@ -3400,7 +3400,7 @@ pub async fn resend_verify_email( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; if let Some(email_address) = user.email { if user.email_verified.unwrap_or(false) { @@ -3564,7 +3564,7 @@ pub async fn subscribe_newsletter( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; sqlx::query!( " @@ -3670,7 +3670,7 @@ pub async fn register_passkey_start( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; // Get currently registered credentials, so an authenticator knows not to register // duplicate credentials @@ -3773,7 +3773,7 @@ pub async fn register_passkey_finish( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; response .validate() @@ -4049,7 +4049,7 @@ pub async fn list_passkeys( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let passkeys = DBPasskey::get_for_user(user.id.into(), &**pool) .await @@ -4103,7 +4103,7 @@ pub async fn rename_passkey( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; body.validate() .map_err(|err| eyre::eyre!(err)) @@ -4163,7 +4163,7 @@ pub async fn delete_passkey( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let id = DBPasskeyId( parse_base62(&info.into_inner().0) diff --git a/apps/labrinth/src/routes/internal/gdpr.rs b/apps/labrinth/src/routes/internal/gdpr.rs index 44bd5b9641c..39a230a2532 100644 --- a/apps/labrinth/src/routes/internal/gdpr.rs +++ b/apps/labrinth/src/routes/internal/gdpr.rs @@ -217,7 +217,7 @@ pub async fn export( .collect::>(); Ok(HttpResponse::Ok().json(serde_json::json!({ - "user": user.into_user(), + "user": user.user, "collections": collections, "follows": follows, "projects": projects, diff --git a/apps/labrinth/src/routes/internal/moderation/external_license.rs b/apps/labrinth/src/routes/internal/moderation/external_license.rs index c6c50d4d8b4..5da4d85e18e 100644 --- a/apps/labrinth/src/routes/internal/moderation/external_license.rs +++ b/apps/labrinth/src/routes/internal/moderation/external_license.rs @@ -570,7 +570,7 @@ async fn upsert_file_license( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let body = body.into_inner(); let license_id = body @@ -684,7 +684,7 @@ pub async fn update_license( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let id = path.into_inner().0; diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs index 58837eb4b91..33e81c4b51d 100644 --- a/apps/labrinth/src/routes/internal/moderation/mod.rs +++ b/apps/labrinth/src/routes/internal/moderation/mod.rs @@ -1283,7 +1283,7 @@ pub async fn set_project_meta( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let mut transaction = pool .begin() @@ -1390,7 +1390,7 @@ pub async fn acquire_lock( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let project_id_str = path.into_inner().0; let project = @@ -1455,7 +1455,7 @@ pub async fn override_lock( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let project_id_str = path.into_inner().0; let project = @@ -1504,7 +1504,7 @@ pub async fn get_lock_statuses( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let locks = DBModerationLock::get_all_with_user(&ro_pool) .await .wrap_internal_err("fetching moderation locks from database")?; @@ -1576,7 +1576,7 @@ pub async fn get_lock_status( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let project_id_str = path.into_inner().0; let project = @@ -1620,7 +1620,7 @@ pub async fn release_lock( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let project_id_str = path.into_inner().0; let project = @@ -1687,12 +1687,12 @@ pub async fn release_lock_beacon( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "only moderators may release moderation locks", ))); } - let user = user.into_user(); + let user = user.user; let project_id_str = path.into_inner().0; let project = @@ -1739,7 +1739,7 @@ pub async fn delete_all_locks( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You must be an admin to delete all locks", ))); diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review.rs b/apps/labrinth/src/routes/internal/moderation/tech_review.rs index 46c1b634033..065f39cee7a 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review.rs @@ -1200,7 +1200,7 @@ pub async fn submit_report( associated_images: Vec::new(), }, thread_id: record.thread_id, - hide_identity: user.has_scopes(Scopes::MODERATION_WRITE), + hide_identity: user.scopes.contains(Scopes::MODERATION_WRITE), } .insert(&mut txn) .await @@ -1212,7 +1212,7 @@ pub async fn submit_report( author_id: Some(user.user.id.into()), body: MessageBody::TechReview { verdict }, thread_id: record.thread_id, - hide_identity: user.has_scopes(Scopes::MODERATION_WRITE), + hide_identity: user.scopes.contains(Scopes::MODERATION_WRITE), } .insert(&mut txn) .await @@ -1255,7 +1255,7 @@ pub async fn submit_report( old_status: ProjectStatus::from_string(&record.old_status), }, thread_id: record.thread_id, - hide_identity: user.has_scopes(Scopes::MODERATION_WRITE), + hide_identity: user.scopes.contains(Scopes::MODERATION_WRITE), } .insert(&mut txn) .await diff --git a/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs index 62693e6ba8e..07cbcf5c9a6 100644 --- a/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs +++ b/apps/labrinth/src/routes/internal/moderation/tech_review/rules.rs @@ -576,7 +576,7 @@ pub async fn create_rule( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let rule = body.into_inner().validate().await?; let user_id = user.id.0 as i64; @@ -665,7 +665,7 @@ pub async fn update_rule( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let (id,) = path.into_inner(); let rule = body.into_inner().validate().await?; let user_id = user.id.0 as i64; @@ -749,7 +749,7 @@ pub async fn delete_rule( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let (id,) = path.into_inner(); let deleted = sqlx::query!( diff --git a/apps/labrinth/src/routes/internal/moderation/user_restrictions.rs b/apps/labrinth/src/routes/internal/moderation/user_restrictions.rs index 3648cdc1dd5..ca68cc62db7 100644 --- a/apps/labrinth/src/routes/internal/moderation/user_restrictions.rs +++ b/apps/labrinth/src/routes/internal/moderation/user_restrictions.rs @@ -42,13 +42,13 @@ async fn authenticate_moderator( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre!( "only moderators can manage user restrictions" ))); } - Ok(user.into_user()) + Ok(user.user) } async fn get_target( diff --git a/apps/labrinth/src/routes/internal/pats.rs b/apps/labrinth/src/routes/internal/pats.rs index 197134eb4ad..c2876be93ea 100644 --- a/apps/labrinth/src/routes/internal/pats.rs +++ b/apps/labrinth/src/routes/internal/pats.rs @@ -56,7 +56,7 @@ pub async fn get_pats( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let pat_ids = database::models::pat_item::DBPersonalAccessToken::get_user_pats( @@ -133,12 +133,12 @@ pub async fn create_pat( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(info.scopes & Scopes::staff()) { + if !user.scopes.contains(info.scopes & Scopes::staff()) { return Err(ApiError::Request(eyre::eyre!( "cannot grant staff scopes which you do not hold" ))); } - let user = user.into_user(); + let user = user.user; let mut transaction = pool .begin() @@ -251,13 +251,13 @@ pub async fn edit_pat( .wrap_auth_err("authenticating API request")?; if let Some(scopes) = info.scopes - && !user.has_scopes(scopes & Scopes::staff()) + && !user.scopes.contains(scopes & Scopes::staff()) { return Err(ApiError::Request(eyre::eyre!( "cannot grant staff scopes which you do not hold" ))); } - let user = user.into_user(); + let user = user.user; let id = id.into_inner().0; let pat = database::models::pat_item::DBPersonalAccessToken::get( @@ -375,7 +375,7 @@ pub async fn delete_pat( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let id = id.into_inner().0; let pat = database::models::pat_item::DBPersonalAccessToken::get( &id, &**pool, &redis, diff --git a/apps/labrinth/src/routes/internal/session.rs b/apps/labrinth/src/routes/internal/session.rs index c177dd7639f..5e1f7e81c08 100644 --- a/apps/labrinth/src/routes/internal/session.rs +++ b/apps/labrinth/src/routes/internal/session.rs @@ -203,7 +203,7 @@ pub async fn list( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let session = extract_authorization_header(&req) .wrap_auth_err("authenticating API request")?; @@ -255,7 +255,7 @@ pub async fn delete( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let session = DBSession::get(info.into_inner().0, &**pool, &redis) .await @@ -332,7 +332,7 @@ pub async fn refresh( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let session = DBSession::get(session, &**pool, &redis) .await diff --git a/apps/labrinth/src/routes/v3/analytics_get/mod.rs b/apps/labrinth/src/routes/v3/analytics_get/mod.rs index aa09d48d7aa..69eb0640db2 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/mod.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/mod.rs @@ -354,7 +354,7 @@ pub async fn fetch_analytics( } if let Some(metrics) = &req.return_metrics.project_revenue { - if !user.has_scopes(Scopes::PAYOUTS_READ) { + if !user.scopes.contains(Scopes::PAYOUTS_READ) { return Err(ApiError::Auth(eyre::eyre!( AuthenticationError::InvalidCredentials ))); @@ -387,7 +387,7 @@ pub async fn fetch_analytics( num_time_slices, &project_id_values, &user_id_bucket_project_ids, - user.has_scopes(Scopes::MODERATION_READ), + user.scopes.contains(Scopes::MODERATION_READ), metrics, ) .await @@ -727,7 +727,7 @@ async fn filter_allowed_project_ids( .await .wrap_internal_err("fetching projects for analytics authorization")?; - if user.has_scopes(Scopes::ADMIN_READ) { + if user.scopes.contains(Scopes::ADMIN_READ) { return Ok(projects.into_iter().map(|x| x.inner.id).collect()); } diff --git a/apps/labrinth/src/routes/v3/blocked_users.rs b/apps/labrinth/src/routes/v3/blocked_users.rs index d93c2b63e42..093517207f8 100644 --- a/apps/labrinth/src/routes/v3/blocked_users.rs +++ b/apps/labrinth/src/routes/v3/blocked_users.rs @@ -37,7 +37,7 @@ pub async fn block_user( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let user_id = info.into_inner().0; let Some(blocked) = DBUser::get(&user_id, &**pool, &redis) @@ -95,7 +95,7 @@ pub async fn unblock_user( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let user_id = info.into_inner().0; let Some(blocked) = DBUser::get(&user_id, &**pool, &redis) @@ -130,7 +130,7 @@ pub async fn get_blocked_users( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let blocked = DBBlockedUser::get_blocked_for_user(user.id.into(), &**pool) .await diff --git a/apps/labrinth/src/routes/v3/collections.rs b/apps/labrinth/src/routes/v3/collections.rs index a1730101fe7..6a22b330cee 100644 --- a/apps/labrinth/src/routes/v3/collections.rs +++ b/apps/labrinth/src/routes/v3/collections.rs @@ -345,7 +345,7 @@ pub async fn collection_edit( } if let Some(status) = &new_collection.status { - if !(user.has_scopes(Scopes::MODERATION_WRITE) + if !(user.scopes.contains(Scopes::MODERATION_WRITE) || collection_item.status.is_approved() && status.can_be_requested()) { @@ -689,5 +689,5 @@ fn can_modify_collection( user: &crate::auth::UserWithScopes, ) -> bool { collection.user_id == user.user.id.into() - || user.has_scopes(Scopes::MODERATION_WRITE) + || user.scopes.contains(Scopes::MODERATION_WRITE) } diff --git a/apps/labrinth/src/routes/v3/disclosures.rs b/apps/labrinth/src/routes/v3/disclosures.rs index bfcc97156bc..dac969108f5 100644 --- a/apps/labrinth/src/routes/v3/disclosures.rs +++ b/apps/labrinth/src/routes/v3/disclosures.rs @@ -85,7 +85,7 @@ pub async fn get_project_disclosures( let viewer_is_moderator = user_option .as_ref() - .is_some_and(|user| user.has_scopes(Scopes::MODERATION_READ)); + .is_some_and(|user| user.scopes.contains(Scopes::MODERATION_READ)); // Moderators can see regardless of membership, short circuit to avoid extra db call let viewer_is_member = viewer_is_moderator @@ -198,7 +198,7 @@ pub async fn modify_project_disclosures( ))); } - let is_moderator = user.has_scopes(Scopes::MODERATION_WRITE); + let is_moderator = user.scopes.contains(Scopes::MODERATION_WRITE); let is_project_member = team_member.is_some() || organization_team_member.is_some(); diff --git a/apps/labrinth/src/routes/v3/friends.rs b/apps/labrinth/src/routes/v3/friends.rs index 3c3061bfaae..526bcddee9d 100644 --- a/apps/labrinth/src/routes/v3/friends.rs +++ b/apps/labrinth/src/routes/v3/friends.rs @@ -46,7 +46,7 @@ pub async fn add_friend( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let string = info.into_inner().0; let Some(friend) = DBUser::get(&string, &**pool, &redis) @@ -196,7 +196,7 @@ pub async fn remove_friend( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let string = info.into_inner().0; let friend = DBUser::get(&string, &**pool, &redis) @@ -250,7 +250,7 @@ pub async fn friends( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let friends = DBFriend::get_user_friends(user.id.into(), None, &***pool) .await diff --git a/apps/labrinth/src/routes/v3/oauth_clients.rs b/apps/labrinth/src/routes/v3/oauth_clients.rs index eb8421df32e..c5816eb8c31 100644 --- a/apps/labrinth/src/routes/v3/oauth_clients.rs +++ b/apps/labrinth/src/routes/v3/oauth_clients.rs @@ -84,7 +84,7 @@ pub async fn get_user_clients( if let Some(target_user) = target_user { if target_user.id != current_user.user.id.into() - && !current_user.has_scopes(Scopes::ADMIN_READ) + && !current_user.scopes.contains(Scopes::ADMIN_READ) { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to see the OAuth clients of this user!", diff --git a/apps/labrinth/src/routes/v3/organizations.rs b/apps/labrinth/src/routes/v3/organizations.rs index 9b90ceb4b6b..31571079812 100644 --- a/apps/labrinth/src/routes/v3/organizations.rs +++ b/apps/labrinth/src/routes/v3/organizations.rs @@ -212,7 +212,7 @@ pub async fn organization_create( let members_data = if let Some(member_data) = member_data { vec![crate::models::teams::TeamMember::from_model( member_data, - current_user.to_user(), + current_user.user.clone(), false, )] } else { @@ -300,7 +300,7 @@ pub async fn organization_get( models::organizations::Organization::from(data, team_members); if current_user .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) + .is_some_and(|x| x.scopes.contains(Scopes::MODERATION_READ)) { let note = DBModerationNote::get_organization( organization.id.into(), @@ -336,7 +336,7 @@ pub async fn organization_notes_edit( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "you do not have permission to edit moderation notes", ))); @@ -461,7 +461,7 @@ pub async fn organizations_get( let user_id = current_user.as_ref().map(|x| x.user.id.into()); let include_notes = current_user .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)); + .is_some_and(|x| x.scopes.contains(Scopes::MODERATION_READ)); let notes = if include_notes { DBModerationNote::get_many_organizations( &organizations_data.iter().map(|x| x.id).collect::>(), @@ -755,7 +755,7 @@ pub async fn organization_delete( "the specified organization does not exist!".to_string() })?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { let team_member = database::models::DBTeamMember::get_from_user_id_organization( organization.id, @@ -989,7 +989,7 @@ pub async fn organization_projects_add( })?; // Require ownership of a project to add it to an organization - if !current_user.has_scopes(Scopes::ADMIN_WRITE) + if !current_user.scopes.contains(Scopes::ADMIN_WRITE) && !project_team_member.is_owner { return Err(ApiError::Auth(eyre::eyre!( @@ -1350,7 +1350,7 @@ pub async fn organization_icon_edit( "the specified organization does not exist!".to_string() })?; - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { let team_member = database::models::DBTeamMember::get_from_user_id( organization_item.team_id, user.user.id.into(), @@ -1471,7 +1471,7 @@ pub async fn delete_organization_icon( "the specified organization does not exist!".to_string() })?; - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { let team_member = database::models::DBTeamMember::get_from_user_id( organization_item.team_id, user.user.id.into(), diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index f199bbc4b75..57ab1b93028 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -73,7 +73,7 @@ pub async fn post_compliance_form( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let user_id = DBUserId(user.id.0 as i64); @@ -790,7 +790,7 @@ pub async fn transaction_history( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let payout_ids = crate::database::models::payout_item::DBPayout::get_all_for_user( @@ -891,7 +891,7 @@ pub async fn cancel_payout( if let Some(payout) = payout { if payout.user_id != user.user.id.into() - && !user.has_scopes(Scopes::ADMIN_WRITE) + && !user.scopes.contains(Scopes::ADMIN_WRITE) { return Ok(HttpResponse::NotFound().finish()); } @@ -1066,7 +1066,7 @@ pub async fn get_balance( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let balance = get_user_balance(user.id.into(), &pool) .await diff --git a/apps/labrinth/src/routes/v3/projects/mod.rs b/apps/labrinth/src/routes/v3/projects/mod.rs index a1e7b3cb6bd..99c0a58638d 100644 --- a/apps/labrinth/src/routes/v3/projects/mod.rs +++ b/apps/labrinth/src/routes/v3/projects/mod.rs @@ -555,7 +555,7 @@ pub async fn project_edit_internal( let submit_for_review = new_project.status == Some(ProjectStatus::Processing) - && !user.has_scopes(Scopes::MODERATION_WRITE); + && !user.scopes.contains(Scopes::MODERATION_WRITE); if submit_for_review && !perms.contains(ProjectPermissions::EDIT_DETAILS) { return Err(ApiError::Auth(eyre!( @@ -666,7 +666,9 @@ pub async fn project_edit_internal( disclosure: ProjectDisclosure::Archived { note: None }, updated_at: Utc::now(), updated_by: user.user.id.into(), - set_by_moderator: user.has_scopes(Scopes::MODERATION_WRITE), + set_by_moderator: user + .scopes + .contains(Scopes::MODERATION_WRITE), deleted_at: None, lock_status: DisclosureLockStatus::Unlocked, } @@ -675,7 +677,7 @@ pub async fn project_edit_internal( .wrap_internal_err("failed to upsert archival disclosure")?; } } else { - if !(user.has_scopes(Scopes::MODERATION_WRITE) + if !(user.scopes.contains(Scopes::MODERATION_WRITE) || !project_item.inner.status.is_approved() && status == &ProjectStatus::Processing || project_item.inner.status.is_approved() @@ -688,8 +690,8 @@ pub async fn project_edit_internal( // If a moderator (non-admin) is completing a review while another moderator holds an // active checklist lock, block them from changing the project status. - if user.has_scopes(Scopes::MODERATION_WRITE) - && !user.has_scopes(Scopes::ADMIN_WRITE) + if user.scopes.contains(Scopes::MODERATION_WRITE) + && !user.scopes.contains(Scopes::ADMIN_WRITE) && project_item.inner.status == ProjectStatus::Processing && status != &ProjectStatus::Processing && let Some(lock) = DBModerationLock::get_with_user( @@ -775,7 +777,7 @@ pub async fn project_edit_internal( )?; } - if user.has_scopes(Scopes::MODERATION_WRITE) + if user.scopes.contains(Scopes::MODERATION_WRITE) && !ENV.MODERATION_SLACK_WEBHOOK.is_empty() { crate::util::webhook::send_slack_project_webhook( @@ -855,7 +857,7 @@ pub async fn project_edit_internal( old_status: project_item.inner.status, }, thread_id: project_item.thread_id, - hide_identity: user.has_scopes(Scopes::MODERATION_WRITE), + hide_identity: user.scopes.contains(Scopes::MODERATION_WRITE), } .insert(&mut transaction) .await @@ -880,7 +882,7 @@ pub async fn project_edit_internal( if sync_archival_disclosure && archival_disclosure.is_some_and(|disclosure| { - user.has_scopes(Scopes::MODERATION_WRITE) + user.scopes.contains(Scopes::MODERATION_WRITE) || disclosure.lock_status.allows_removal() }) { @@ -888,7 +890,7 @@ pub async fn project_edit_internal( project_item.inner.id, ProjectDisclosureType::Archived, user.user.id.into(), - user.has_scopes(Scopes::MODERATION_WRITE), + user.scopes.contains(Scopes::MODERATION_WRITE), &mut transaction, ) .await @@ -1155,7 +1157,7 @@ pub async fn project_edit_internal( } } if let Some(moderation_message) = &new_project.moderation_message { - if !user.has_scopes(Scopes::MODERATION_WRITE) + if !user.scopes.contains(Scopes::MODERATION_WRITE) && (!project_item.inner.status.is_approved() || moderation_message.is_some()) { @@ -1180,7 +1182,7 @@ pub async fn project_edit_internal( if let Some(moderation_message_body) = &new_project.moderation_message_body { - if !user.has_scopes(Scopes::MODERATION_WRITE) + if !user.scopes.contains(Scopes::MODERATION_WRITE) && (!project_item.inner.status.is_approved() || moderation_message_body.is_some()) { @@ -1234,7 +1236,7 @@ pub async fn project_edit_internal( if (*monetization_status == MonetizationStatus::ForceDemonetized || project_item.inner.monetization_status == MonetizationStatus::ForceDemonetized) - && !user.has_scopes(Scopes::MODERATION_WRITE) + && !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You do not have the permissions to edit the monetization status of this project!", @@ -1607,7 +1609,7 @@ async fn submit_project_for_review( .wrap_internal_err("querying database for `project_edit_internal`")?; if state.status().is_approved() - || !user.has_scopes(Scopes::MODERATION_WRITE) + || !user.scopes.contains(Scopes::MODERATION_WRITE) && matches!( state.status(), ProjectStatus::Rejected | ProjectStatus::Withheld @@ -2164,7 +2166,7 @@ pub async fn projects_edit( let mut changed_projects = Vec::new(); for project in projects_data { - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { let team_member = team_members.iter().find(|x| { x.team_id == project.inner.team_id && x.user_id == user.user.id.into() @@ -2308,7 +2310,7 @@ pub async fn projects_edit( } } - if !user.has_scopes(Scopes::MODERATION_WRITE) + if !user.scopes.contains(Scopes::MODERATION_WRITE) && project.inner.status == ProjectStatus::Processing { validate::ensure_project_is_valid_for_review( @@ -2515,7 +2517,7 @@ pub async fn project_icon_edit_internal( "the specified project does not exist!".to_string() })?; - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, @@ -2693,7 +2695,7 @@ pub async fn delete_project_icon_internal( "the specified project does not exist!".to_string() })?; - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, @@ -2874,7 +2876,7 @@ pub async fn add_gallery_item_internal( ))); } - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, @@ -2981,7 +2983,7 @@ pub async fn add_gallery_item_internal( .await .wrap_internal_err("inserting galleries into database")?; - let validation_error = if !user.has_scopes(Scopes::MODERATION_WRITE) + let validation_error = if !user.scopes.contains(Scopes::MODERATION_WRITE) && project_item.inner.status == ProjectStatus::Processing { validate::ensure_project_is_valid_for_review( @@ -3127,7 +3129,7 @@ pub async fn edit_gallery_item_internal( "the specified project does not exist!".to_string() })?; - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, @@ -3340,7 +3342,7 @@ pub async fn delete_gallery_item_internal( "the specified project does not exist!".to_string() })?; - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project_item.inner, @@ -3486,7 +3488,7 @@ pub async fn project_delete_internal( .wrap_internal_err("failed to get project")? .wrap_auth_err("the specified project does not exist")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { let (team_member, organization_team_member) = db_models::DBTeamMember::get_for_project_permissions( &project.inner, diff --git a/apps/labrinth/src/routes/v3/projects/mutation.rs b/apps/labrinth/src/routes/v3/projects/mutation.rs index 84034ae1231..eafbc23bcbf 100644 --- a/apps/labrinth/src/routes/v3/projects/mutation.rs +++ b/apps/labrinth/src/routes/v3/projects/mutation.rs @@ -472,7 +472,7 @@ pub(crate) async fn prepare_mutations( } if project_editor - .is_some_and(|user| !user.has_scopes(Scopes::MODERATION_WRITE)) + .is_some_and(|user| !user.scopes.contains(Scopes::MODERATION_WRITE)) { for state in &states { if state.project.status == ProjectStatus::Processing @@ -538,7 +538,7 @@ pub(crate) async fn finalize_project_edit( || validate_review_state || entered_approved_state) && !project_editor - .is_some_and(|user| user.has_scopes(Scopes::MODERATION_WRITE)) + .is_some_and(|user| user.scopes.contains(Scopes::MODERATION_WRITE)) && state.has_required_nags { return Err(ApiError::Request(eyre!( @@ -548,7 +548,7 @@ pub(crate) async fn finalize_project_edit( if validate_review_state && !project_editor - .is_some_and(|user| user.has_scopes(Scopes::MODERATION_WRITE)) + .is_some_and(|user| user.scopes.contains(Scopes::MODERATION_WRITE)) && state .thread_issues .iter() diff --git a/apps/labrinth/src/routes/v3/reports.rs b/apps/labrinth/src/routes/v3/reports.rs index 19d026d16cc..c4a0deea3cf 100644 --- a/apps/labrinth/src/routes/v3/reports.rs +++ b/apps/labrinth/src/routes/v3/reports.rs @@ -88,7 +88,7 @@ pub async fn report_create( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; let new_report: CreateReport = read_typed_from_payload(&mut body) .await diff --git a/apps/labrinth/src/routes/v3/teams.rs b/apps/labrinth/src/routes/v3/teams.rs index 36d86ddc2d4..1d67e5b456c 100644 --- a/apps/labrinth/src/routes/v3/teams.rs +++ b/apps/labrinth/src/routes/v3/teams.rs @@ -1106,7 +1106,7 @@ pub async fn transfer_ownership( } } - if !current_user.has_scopes(Scopes::ADMIN_WRITE) { + if !current_user.scopes.contains(Scopes::ADMIN_WRITE) { let member = DBTeamMember::get_from_user_id( id.into(), current_user.user.id.into(), diff --git a/apps/labrinth/src/routes/v3/threads.rs b/apps/labrinth/src/routes/v3/threads.rs index 8fc2b32b67b..bc28220cfe0 100644 --- a/apps/labrinth/src/routes/v3/threads.rs +++ b/apps/labrinth/src/routes/v3/threads.rs @@ -48,7 +48,7 @@ pub async fn is_authorized_thread( moderation_scope: Scopes, pool: &PgPool, ) -> Result { - if user.has_scopes(moderation_scope) { + if user.scopes.contains(moderation_scope) { return Ok(true); } @@ -110,7 +110,7 @@ pub async fn filter_authorized_threads( redis: &RedisPool, ) -> Result, ApiError> { let user_id: database::models::DBUserId = user.user.id.into(); - let is_moderator = user.has_scopes(Scopes::MODERATION_READ); + let is_moderator = user.scopes.contains(Scopes::MODERATION_READ); let mut return_threads = Vec::new(); let mut check_threads = Vec::new(); @@ -324,7 +324,7 @@ pub async fn thread_get( .await .wrap_api_err("checking thread authorization")? { - let is_moderator = user.has_scopes(Scopes::MODERATION_READ); + let is_moderator = user.scopes.contains(Scopes::MODERATION_READ); let authors = &mut data.members; authors.append( @@ -597,7 +597,7 @@ pub async fn thread_issues_create( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; if new_issues.issues.is_empty() { return Err(ApiError::Request(eyre::eyre!( @@ -700,7 +700,7 @@ pub async fn thread_issue_facet_edit( let project_exists = project_exists(project_id, &pool).await?; if edit.moderator_verified.is_some() - && !user.has_scopes(Scopes::MODERATION_WRITE) + && !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "only moderators can verify thread issues" @@ -714,7 +714,7 @@ pub async fn thread_issue_facet_edit( prepare_thread_issue_mutation(project_id, &mut transaction).await?; } if edit.user_addressed.is_some() - && !user.has_scopes(Scopes::MODERATION_WRITE) + && !user.scopes.contains(Scopes::MODERATION_WRITE) && !is_project_team_member( project_id, user.user.id.into(), @@ -923,7 +923,7 @@ pub async fn thread_send_message_internal( ))); } - if *private && !user.has_scopes(Scopes::MODERATION_WRITE) { + if *private && !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Request(eyre::eyre!( "You are not allowed to send private messages!", ))); @@ -978,7 +978,7 @@ pub async fn thread_send_message_internal( author_id: Some(user.user.id.into()), body: new_message.body.clone(), thread_id: thread.id, - hide_identity: user.has_scopes(Scopes::MODERATION_WRITE), + hide_identity: user.scopes.contains(Scopes::MODERATION_WRITE), } .insert(&mut transaction) .await @@ -994,7 +994,7 @@ pub async fn thread_send_message_internal( if let Some(project) = project && project.inner.status != ProjectStatus::Processing - && user.has_scopes(Scopes::MODERATION_WRITE) + && user.scopes.contains(Scopes::MODERATION_WRITE) && !is_private { let members = @@ -1042,7 +1042,9 @@ pub async fn thread_send_message_internal( .wrap_internal_err("fetching report from database")?; if let Some(report) = report { - if report.closed && !user.has_scopes(Scopes::MODERATION_WRITE) { + if report.closed + && !user.scopes.contains(Scopes::MODERATION_WRITE) + { return Err(ApiError::Request(eyre::eyre!( "You may not reply to a closed report", ))); @@ -1164,7 +1166,7 @@ pub async fn message_delete( .wrap_internal_err("fetching thread message from database")?; if let Some(thread) = result { - if !user.has_scopes(Scopes::MODERATION_WRITE) + if !user.scopes.contains(Scopes::MODERATION_WRITE) && thread.author_id != Some(user.user.id.into()) { return Err(ApiError::Auth(eyre::eyre!( diff --git a/apps/labrinth/src/routes/v3/users.rs b/apps/labrinth/src/routes/v3/users.rs index e8b446ef5d8..cd0fe35b7b4 100644 --- a/apps/labrinth/src/routes/v3/users.rs +++ b/apps/labrinth/src/routes/v3/users.rs @@ -242,7 +242,7 @@ pub async fn admin_user_email( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_READ) { + if !user.scopes.contains(Scopes::ADMIN_READ) { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to get a user from their email!", ))); @@ -301,7 +301,7 @@ pub async fn admin_user_discord( .await .wrap_auth_err("authenticating API request")?; - if !user.has_scopes(Scopes::ADMIN_READ) { + if !user.scopes.contains(Scopes::ADMIN_READ) { return Err(ApiError::Auth(eyre!( "you must be an admin to look up users by discord ID" ))); @@ -602,7 +602,7 @@ pub async fn users_get( let notes = if auth_user .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)) + .is_some_and(|x| x.scopes.contains(Scopes::MODERATION_READ)) { DBModerationNote::get_many_users( &users_data.iter().map(|x| x.id).collect::>(), @@ -617,7 +617,7 @@ pub async fn users_get( let is_mod = auth_user .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)); + .is_some_and(|x| x.scopes.contains(Scopes::MODERATION_READ)); let users: Vec = users_data .into_iter() @@ -674,10 +674,10 @@ pub async fn user_get( let is_admin = auth_user .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::ADMIN_READ)); + .is_some_and(|x| x.scopes.contains(Scopes::ADMIN_READ)); let is_mod = auth_user .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)); + .is_some_and(|x| x.scopes.contains(Scopes::MODERATION_READ)); let user_id = data.id; let is_self = auth_user .as_ref() @@ -738,7 +738,7 @@ pub async fn user_notes_edit( ) .await .wrap_auth_err("authenticating API request")? - .into_user(); + .user; new_note .validate_not_empty() @@ -1230,7 +1230,7 @@ pub async fn user_icon_edit( if let Some(actual_user) = id_option { if user.user.id != actual_user.id.into() - && !user.has_scopes(Scopes::MODERATION_WRITE) + && !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You don't have permission to edit this user's icon.", @@ -1331,7 +1331,7 @@ pub async fn user_icon_delete( if let Some(actual_user) = id_option { if user.user.id != actual_user.id.into() - && !user.has_scopes(Scopes::MODERATION_WRITE) + && !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You don't have permission to edit this user's icon.", @@ -1404,7 +1404,7 @@ pub async fn user_delete( let id = id_option .map(|x| x.id) .wrap_not_found_err("resource not found")?; - if !user.has_scopes(Scopes::ADMIN_WRITE) && user.user.id != id.into() { + if !user.scopes.contains(Scopes::ADMIN_WRITE) && user.user.id != id.into() { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to delete this user!", ))); @@ -1464,7 +1464,9 @@ pub async fn user_follows( .wrap_internal_err("fetching user from database")?; if let Some(id) = id_option.map(|x| x.id) { - if !user.has_scopes(Scopes::ADMIN_READ) && user.user.id != id.into() { + if !user.scopes.contains(Scopes::ADMIN_READ) + && user.user.id != id.into() + { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to see the projects this user follows!", ))); @@ -1523,7 +1525,9 @@ pub async fn user_notifications( .wrap_internal_err("fetching user from database")?; if let Some(id) = id_option.map(|x| x.id) { - if !user.has_scopes(Scopes::ADMIN_READ) && user.user.id != id.into() { + if !user.scopes.contains(Scopes::ADMIN_READ) + && user.user.id != id.into() + { return Err(ApiError::Auth(eyre::eyre!( "You do not have permission to see the notifications of this user!", ))); diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index 2f13a48293a..79820064aed 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -813,7 +813,7 @@ async fn upload_file_to_version_inner( )); } - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { let team_member = models::DBTeamMember::get_from_user_id_project( version.inner.project_id, user.user.id.into(), diff --git a/apps/labrinth/src/routes/v3/version_file.rs b/apps/labrinth/src/routes/v3/version_file.rs index dbf16f4d5f9..e2b5f87dcd1 100644 --- a/apps/labrinth/src/routes/v3/version_file.rs +++ b/apps/labrinth/src/routes/v3/version_file.rs @@ -673,7 +673,7 @@ async fn update_files_internal( .collect::>(), user_option .as_ref() - .is_some_and(|x| x.has_scopes(Scopes::MODERATION_READ)), + .is_some_and(|x| x.scopes.contains(Scopes::MODERATION_READ)), user_option .as_ref() .map(|x| database::models::DBUserId::from(x.user.id).0), @@ -999,7 +999,7 @@ pub async fn delete_file( .wrap_internal_err("fetching version from database")?; if let Some(row) = file { - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { let team_member = database::models::DBTeamMember::get_from_user_id_version( row.version_id, diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index df7e2114537..ee7eb23bcca 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -863,7 +863,7 @@ pub(crate) async fn version_edit_in_transaction( } if let Some(downloads) = &new_version.downloads { - if !user.has_scopes(Scopes::MODERATION_WRITE) { + if !user.scopes.contains(Scopes::MODERATION_WRITE) { return Err(ApiError::Auth(eyre::eyre!( "You don't have permission to set the downloads of this mod", ))); @@ -1373,7 +1373,7 @@ pub async fn version_delete( "the specified version does not exist!".to_string() })?; - if !user.has_scopes(Scopes::ADMIN_WRITE) { + if !user.scopes.contains(Scopes::ADMIN_WRITE) { let team_member = database::models::DBTeamMember::get_from_user_id_project( version.inner.project_id,