Skip to content

Commit f795d07

Browse files
committed
Merge branch 'main' into fetch/fix-billing
2 parents f2db343 + bb9ce52 commit f795d07

File tree

9 files changed

+129
-40
lines changed

9 files changed

+129
-40
lines changed

apps/labrinth/.env.local

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ PYRO_API_KEY=none
129129
BREX_API_URL=https://platform.brexapis.com/v2/
130130
BREX_API_KEY=none
131131

132-
DELPHI_URL=none
132+
DELPHI_URL=http://localhost:59999
133133
DELPHI_SLACK_WEBHOOK=none
134134

135135
AVALARA_1099_API_URL=https://www.track1099.com/api

apps/labrinth/.sqlx/query-eb792d5033d7079fe3555593d8731f8853235275e4d5614636b5db524a4920d5.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/labrinth/src/auth/checks.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use crate::database;
2-
use crate::database::models::DBCollection;
32
use crate::database::models::project_item::ProjectQueryResult;
43
use crate::database::models::version_item::VersionQueryResult;
4+
use crate::database::models::{DBCollection, DBOrganization, DBTeamMember};
55
use crate::database::redis::RedisPool;
66
use crate::database::{DBProject, DBVersion, models};
77
use crate::models::users::User;
88
use crate::routes::ApiError;
9+
use futures::TryStreamExt;
910
use itertools::Itertools;
1011
use sqlx::PgPool;
1112

@@ -132,8 +133,6 @@ pub async fn filter_enlisted_projects_ids(
132133
if let Some(user) = user_option {
133134
let user_id: models::ids::DBUserId = user.id.into();
134135

135-
use futures::TryStreamExt;
136-
137136
sqlx::query!(
138137
"
139138
SELECT m.id id, m.team_id team_id FROM team_members tm
@@ -364,3 +363,36 @@ pub async fn filter_visible_collections(
364363

365364
Ok(return_collections)
366365
}
366+
367+
pub async fn is_visible_organization(
368+
organization: &DBOrganization,
369+
viewing_user: &Option<User>,
370+
pool: &PgPool,
371+
redis: &RedisPool,
372+
) -> Result<bool, ApiError> {
373+
let members =
374+
DBTeamMember::get_from_team_full(organization.team_id, pool, redis)
375+
.await?;
376+
377+
// This is meant to match the same projects as the `Project::is_searchable` method, but we're not using
378+
// it here because that'd entail pulling in all projects for the organization
379+
let has_searchable_projects = sqlx::query_scalar!(
380+
"SELECT TRUE FROM mods WHERE organization_id = $1 AND status IN ('public', 'archived') LIMIT 1",
381+
organization.id as database::models::ids::DBOrganizationId
382+
)
383+
.fetch_optional(pool)
384+
.await?
385+
.flatten()
386+
.unwrap_or(false);
387+
388+
let visible = has_searchable_projects
389+
|| members.iter().filter(|member| member.accepted).count() > 1
390+
|| viewing_user.as_ref().is_some_and(|viewing_user| {
391+
viewing_user.role.is_mod()
392+
|| members
393+
.iter()
394+
.any(|member| member.user_id == viewing_user.id.into())
395+
});
396+
397+
Ok(visible)
398+
}

apps/labrinth/src/models/v3/projects.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -524,9 +524,13 @@ impl ProjectStatus {
524524
}
525525

526526
// Project can be displayed in search
527-
// IMPORTANT: if this is changed, make sure to update the `mods_searchable_ids_gist`
528-
// index in the DB to keep random project queries fast (see the
529-
// `20250609134334_spatial-random-project-index.sql` migration)
527+
// IMPORTANT: if this is changed, make sure to:
528+
// - update the `mods_searchable_ids_gist`
529+
// index in the DB to keep random project queries fast (see the
530+
// `20250609134334_spatial-random-project-index.sql` migration).
531+
// - update the `is_visible_organization` function in
532+
// `apps/labrinth/src/auth/checks.rs`, which duplicates this logic
533+
// in a SQL query for efficiency.
530534
pub fn is_searchable(&self) -> bool {
531535
matches!(self, ProjectStatus::Approved | ProjectStatus::Archived)
532536
}

apps/labrinth/src/routes/v3/organizations.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::collections::HashMap;
22
use std::sync::Arc;
33

44
use super::ApiError;
5+
use crate::auth::checks::is_visible_organization;
56
use crate::auth::{filter_visible_projects, get_user_from_headers};
67
use crate::database::models::team_item::DBTeamMember;
78
use crate::database::models::{
@@ -70,7 +71,10 @@ pub async fn organization_projects_get(
7071
.ok();
7172

7273
let organization_data = DBOrganization::get(&id, &**pool, &redis).await?;
73-
if let Some(organization) = organization_data {
74+
if let Some(organization) = organization_data
75+
&& is_visible_organization(&organization, &current_user, &pool, &redis)
76+
.await?
77+
{
7478
let project_ids = sqlx::query!(
7579
"
7680
SELECT m.id FROM organizations o
@@ -232,7 +236,9 @@ pub async fn organization_get(
232236
let user_id = current_user.as_ref().map(|x| x.id.into());
233237

234238
let organization_data = DBOrganization::get(&id, &**pool, &redis).await?;
235-
if let Some(data) = organization_data {
239+
if let Some(data) = organization_data
240+
&& is_visible_organization(&data, &current_user, &pool, &redis).await?
241+
{
236242
let members_data =
237243
DBTeamMember::get_from_team_full(data.team_id, &**pool, &redis)
238244
.await?;
@@ -328,6 +334,11 @@ pub async fn organizations_get(
328334
}
329335

330336
for data in organizations_data {
337+
if !is_visible_organization(&data, &current_user, &pool, &redis).await?
338+
{
339+
continue;
340+
}
341+
331342
let members_data = team_groups.remove(&data.team_id).unwrap_or(vec![]);
332343
let logged_in = current_user
333344
.as_ref()

apps/labrinth/src/routes/v3/teams.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::auth::checks::is_visible_project;
1+
use crate::auth::checks::{is_visible_organization, is_visible_project};
22
use crate::auth::get_user_from_headers;
33
use crate::database::DBProject;
44
use crate::database::models::notification_item::NotificationBuilder;
@@ -134,18 +134,21 @@ pub async fn team_members_get_organization(
134134
crate::database::models::DBOrganization::get(&string, &**pool, &redis)
135135
.await?;
136136

137-
if let Some(organization) = organization_data {
138-
let current_user = get_user_from_headers(
139-
&req,
140-
&**pool,
141-
&redis,
142-
&session_queue,
143-
Scopes::ORGANIZATION_READ,
144-
)
145-
.await
146-
.map(|x| x.1)
147-
.ok();
137+
let current_user = get_user_from_headers(
138+
&req,
139+
&**pool,
140+
&redis,
141+
&session_queue,
142+
Scopes::ORGANIZATION_READ,
143+
)
144+
.await
145+
.map(|x| x.1)
146+
.ok();
148147

148+
if let Some(organization) = organization_data
149+
&& is_visible_organization(&organization, &current_user, &pool, &redis)
150+
.await?
151+
{
149152
let members_data = DBTeamMember::get_from_team_full(
150153
organization.team_id,
151154
&**pool,

apps/labrinth/tests/scopes.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use common::environment::{
2222
use common::{database::*, scopes::ScopeTest};
2323
use labrinth::models::ids::ProjectId;
2424
use labrinth::models::pats::Scopes;
25+
use labrinth::models::teams::ProjectPermissions;
2526
use serde_json::json;
2627
// For each scope, we (using test_scope):
2728
// - create a PAT with a given set of scopes for a function
@@ -1093,6 +1094,7 @@ pub async fn organization_scopes() {
10931094
.await
10941095
.unwrap();
10951096
let organization_id = success["id"].as_str().unwrap();
1097+
let organization_team_id = success["team_id"].as_str().unwrap();
10961098

10971099
// Patch organization
10981100
let organization_edit = Scopes::ORGANIZATION_WRITE;
@@ -1154,6 +1156,27 @@ pub async fn organization_scopes() {
11541156
.await
11551157
.unwrap();
11561158

1159+
// Add two members to the organization
1160+
api.add_user_to_team(
1161+
organization_team_id,
1162+
FRIEND_USER_ID,
1163+
Some(ProjectPermissions::all()),
1164+
None,
1165+
USER_USER_PAT,
1166+
)
1167+
.await;
1168+
api.join_team(organization_team_id, FRIEND_USER_PAT).await;
1169+
1170+
api.add_user_to_team(
1171+
organization_team_id,
1172+
ENEMY_USER_ID,
1173+
Some(ProjectPermissions::all()),
1174+
None,
1175+
USER_USER_PAT,
1176+
)
1177+
.await;
1178+
api.join_team(organization_team_id, ENEMY_USER_PAT).await;
1179+
11571180
// Organization reads
11581181
let organization_read = Scopes::ORGANIZATION_READ;
11591182
let req_gen = |pat: Option<String>| async move {

apps/labrinth/tests/teams.rs

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ async fn test_get_team_organization() {
176176
&test_env.dummy.organization_zeta.organization_id;
177177
let zeta_team_id = &test_env.dummy.organization_zeta.team_id;
178178

179-
// A non-member of the team should get basic info but not be able to see private data
179+
// A non-member of the team should get basic team info
180180
let members = api
181181
.get_team_members_deserialized_common(
182182
zeta_team_id,
@@ -187,15 +187,6 @@ async fn test_get_team_organization() {
187187
assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64);
188188
assert!(members[0].permissions.is_none());
189189

190-
let members = api
191-
.get_organization_members_deserialized_common(
192-
zeta_organization_id,
193-
FRIEND_USER_PAT,
194-
)
195-
.await;
196-
assert_eq!(members.len(), 1);
197-
assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64);
198-
199190
// A non-accepted member of the team should:
200191
// - not be able to see private data about the team, but see all members including themselves
201192
// - should not appear in the team members list to enemy users
@@ -262,15 +253,6 @@ async fn test_get_team_organization() {
262253
.await;
263254
assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team
264255

265-
// enemy team check via association
266-
let members = api
267-
.get_organization_members_deserialized_common(
268-
zeta_organization_id,
269-
ENEMY_USER_PAT,
270-
)
271-
.await;
272-
assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team
273-
274256
// An accepted member of the team should appear in the team members list
275257
// and should be able to see private data about the team
276258
let resp = api.join_team(zeta_team_id, FRIEND_USER_PAT).await;

packages/utils/changelog.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ export type VersionEntry = {
1010
}
1111

1212
const VERSIONS: VersionEntry[] = [
13+
{
14+
date: `2025-09-26T13:00:00+02:00`,
15+
product: 'web',
16+
body: `### Improvements
17+
- Re-enabled the creation of Organizations.`,
18+
},
19+
{
20+
date: `2025-09-25T19:15:00-07:00`,
21+
product: 'web',
22+
body: `### Improvements
23+
- Temporarily disabled the creation of Organizations.`,
24+
},
1325
{
1426
date: `2025-09-21T15:45:00-07:00`,
1527
product: 'web',

0 commit comments

Comments
 (0)