From e412ebddc699f80a5a96c5f89bd072ccb8a63b71 Mon Sep 17 00:00:00 2001 From: thesuzerain Date: Wed, 13 Dec 2023 14:07:51 -0700 Subject: [PATCH] version changes --- ...747c60aca09862d39590431ac12f6ad585aab.json | 23 +++ ...54abf0f67f225622935802379de495dce18b.json} | 8 +- ...1525ed4f616d17bb3aa76430e95492caa5c74.json | 22 +++ ...7d8cd467e589504c6e754f1f6836203946590.json | 22 +++ ...359f34d8cdd7ef4e8a4e447878b5a0e04cfff.json | 23 --- ...0d83da45214984f8a2ec48f3f1343a28240e.json} | 7 +- ...19cf7caf9a0ebc53b0232c6107b2995453c14.json | 23 +++ ...ae92ebe04b0d4d1fe1762208167cee23b645.json} | 5 +- ...2b80ce0f7feb780644b15c3e3f68e0a18d0fd.json | 15 -- ...19b9921d86f0fff6b7ba63c0c54488d12f1a8.json | 14 -- ...3b30832f1caaa295b1a3ffa55209d1cda01ba.json | 14 -- ...5768180e8d4fc103239806d2da7ea2540e5d.json} | 7 +- ...759cb80863c36d1eaf373139f325102e2068b.json | 14 -- ...1667baa7bb9157f58a5017e710d09fd295eb0.json | 14 ++ ...8b259e0226e7dac16c635927ca74abc78cea9.json | 23 --- .../20231213103100_enforces-owner-unique.sql | 10 + src/auth/checks.rs | 58 +++++- src/database/models/team_item.rs | 28 ++- src/routes/v3/organizations.rs | 129 ++++++------ src/routes/v3/teams.rs | 83 +++++++- src/routes/v3/version_creation.rs | 2 + src/routes/v3/version_file.rs | 1 + src/routes/v3/versions.rs | 3 + tests/organizations.rs | 185 +++++++++++++++--- tests/scopes.rs | 14 +- tests/teams.rs | 3 +- 26 files changed, 505 insertions(+), 245 deletions(-) create mode 100644 .sqlx/query-1997511538affe27f229df10ddc747c60aca09862d39590431ac12f6ad585aab.json rename .sqlx/{query-6ed8a0eadaa72fafc49538ed9be33c9621a763d2d4e1fbd1f541be50b48db4d2.json => query-23829a50afd2a4fd1b2d1770d2f854abf0f67f225622935802379de495dce18b.json} (51%) create mode 100644 .sqlx/query-26c8f1dbb233bfcdc555344e9d41525ed4f616d17bb3aa76430e95492caa5c74.json create mode 100644 .sqlx/query-2b097a9a1b24b9648d3558e348c7d8cd467e589504c6e754f1f6836203946590.json delete mode 100644 .sqlx/query-323c32b24bb89dfda356f34da2e359f34d8cdd7ef4e8a4e447878b5a0e04cfff.json rename .sqlx/{query-2e3ce3eafee2cd110085a94b122884fe25591aa6f48256abbb6c8d973efe932e.json => query-389a48e2e5dec4370013f3cada400d83da45214984f8a2ec48f3f1343a28240e.json} (87%) create mode 100644 .sqlx/query-7f039929345f6421ab7de7a75ac19cf7caf9a0ebc53b0232c6107b2995453c14.json rename .sqlx/{query-cb82bb6e22690fd5fee18bbc2975503371814ef1cbf95f32c195bfe7542b2b20.json => query-8d6166509c910b6400efc1d1398aae92ebe04b0d4d1fe1762208167cee23b645.json} (61%) delete mode 100644 .sqlx/query-8f9d42b02fbef278e2bf7f53dc72b80ce0f7feb780644b15c3e3f68e0a18d0fd.json delete mode 100644 .sqlx/query-b18328c8570ad6e140e54634b2219b9921d86f0fff6b7ba63c0c54488d12f1a8.json delete mode 100644 .sqlx/query-cc89fe059b80fb7d7387016d1a53b30832f1caaa295b1a3ffa55209d1cda01ba.json rename .sqlx/{query-740c4343d7357af6820e28a3e1f165cbbc3f967c4dfbeeb13a0c63f78e072895.json => query-ebdcc29fc24bd31514ccdf0202a35768180e8d4fc103239806d2da7ea2540e5d.json} (88%) delete mode 100644 .sqlx/query-efc5806b3d0f0429854e62031b0759cb80863c36d1eaf373139f325102e2068b.json create mode 100644 .sqlx/query-f40153d1516fe93e4179c14d00e1667baa7bb9157f58a5017e710d09fd295eb0.json delete mode 100644 .sqlx/query-fa1b92b15cc108fa046998f789c8b259e0226e7dac16c635927ca74abc78cea9.json create mode 100644 migrations/20231213103100_enforces-owner-unique.sql diff --git a/.sqlx/query-1997511538affe27f229df10ddc747c60aca09862d39590431ac12f6ad585aab.json b/.sqlx/query-1997511538affe27f229df10ddc747c60aca09862d39590431ac12f6ad585aab.json new file mode 100644 index 00000000..635fbd40 --- /dev/null +++ b/.sqlx/query-1997511538affe27f229df10ddc747c60aca09862d39590431ac12f6ad585aab.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(\n SELECT 1 FROM mods m \n INNER JOIN organizations o ON m.organization_id = o.id\n INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 \n WHERE m.id = $1\n )", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "1997511538affe27f229df10ddc747c60aca09862d39590431ac12f6ad585aab" +} diff --git a/.sqlx/query-6ed8a0eadaa72fafc49538ed9be33c9621a763d2d4e1fbd1f541be50b48db4d2.json b/.sqlx/query-23829a50afd2a4fd1b2d1770d2f854abf0f67f225622935802379de495dce18b.json similarity index 51% rename from .sqlx/query-6ed8a0eadaa72fafc49538ed9be33c9621a763d2d4e1fbd1f541be50b48db4d2.json rename to .sqlx/query-23829a50afd2a4fd1b2d1770d2f854abf0f67f225622935802379de495dce18b.json index 8b1c27b1..8f77eadb 100644 --- a/.sqlx/query-6ed8a0eadaa72fafc49538ed9be33c9621a763d2d4e1fbd1f541be50b48db4d2.json +++ b/.sqlx/query-23829a50afd2a4fd1b2d1770d2f854abf0f67f225622935802379de495dce18b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, m.team_id team_id FROM team_members tm\n INNER JOIN mods m ON m.team_id = tm.team_id\n LEFT JOIN organizations o ON o.team_id = tm.team_id\n WHERE (tm.team_id = ANY($1) or o.id = ANY($2)) AND tm.user_id = $3\n ", + "query": "\n SELECT m.id id, m.team_id team_id FROM team_members tm\n INNER JOIN mods m ON m.team_id = tm.team_id\n WHERE tm.team_id = ANY($1) AND tm.user_id = $3\n\n UNION\n\n SELECT m.id id, m.team_id team_id FROM team_members tm\n INNER JOIN organizations o ON o.team_id = tm.team_id\n INNER JOIN mods m ON m.organization_id = o.id\n WHERE o.id = ANY($2) AND tm.user_id = $3\n ", "describe": { "columns": [ { @@ -22,9 +22,9 @@ ] }, "nullable": [ - false, - false + null, + null ] }, - "hash": "6ed8a0eadaa72fafc49538ed9be33c9621a763d2d4e1fbd1f541be50b48db4d2" + "hash": "23829a50afd2a4fd1b2d1770d2f854abf0f67f225622935802379de495dce18b" } diff --git a/.sqlx/query-26c8f1dbb233bfcdc555344e9d41525ed4f616d17bb3aa76430e95492caa5c74.json b/.sqlx/query-26c8f1dbb233bfcdc555344e9d41525ed4f616d17bb3aa76430e95492caa5c74.json new file mode 100644 index 00000000..69fc76be --- /dev/null +++ b/.sqlx/query-26c8f1dbb233bfcdc555344e9d41525ed4f616d17bb3aa76430e95492caa5c74.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT m.team_id FROM organizations o\n INNER JOIN mods m ON m.organization_id = o.id\n WHERE o.id = $1 AND $1 IS NOT NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "team_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "26c8f1dbb233bfcdc555344e9d41525ed4f616d17bb3aa76430e95492caa5c74" +} diff --git a/.sqlx/query-2b097a9a1b24b9648d3558e348c7d8cd467e589504c6e754f1f6836203946590.json b/.sqlx/query-2b097a9a1b24b9648d3558e348c7d8cd467e589504c6e754f1f6836203946590.json new file mode 100644 index 00000000..1ccb3854 --- /dev/null +++ b/.sqlx/query-2b097a9a1b24b9648d3558e348c7d8cd467e589504c6e754f1f6836203946590.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT u.id \n FROM team_members\n INNER JOIN users u ON u.id = team_members.user_id\n WHERE team_id = $1 AND is_owner = TRUE\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "2b097a9a1b24b9648d3558e348c7d8cd467e589504c6e754f1f6836203946590" +} diff --git a/.sqlx/query-323c32b24bb89dfda356f34da2e359f34d8cdd7ef4e8a4e447878b5a0e04cfff.json b/.sqlx/query-323c32b24bb89dfda356f34da2e359f34d8cdd7ef4e8a4e447878b5a0e04cfff.json deleted file mode 100644 index 7c4929c8..00000000 --- a/.sqlx/query-323c32b24bb89dfda356f34da2e359f34d8cdd7ef4e8a4e447878b5a0e04cfff.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT t.id FROM teams t\n WHERE t.id = ANY($1) AND NOT EXISTS (\n SELECT 1 FROM team_members tm\n WHERE tm.team_id = t.id AND tm.user_id = $2\n )\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8Array", - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "323c32b24bb89dfda356f34da2e359f34d8cdd7ef4e8a4e447878b5a0e04cfff" -} diff --git a/.sqlx/query-2e3ce3eafee2cd110085a94b122884fe25591aa6f48256abbb6c8d973efe932e.json b/.sqlx/query-389a48e2e5dec4370013f3cada400d83da45214984f8a2ec48f3f1343a28240e.json similarity index 87% rename from .sqlx/query-2e3ce3eafee2cd110085a94b122884fe25591aa6f48256abbb6c8d973efe932e.json rename to .sqlx/query-389a48e2e5dec4370013f3cada400d83da45214984f8a2ec48f3f1343a28240e.json index 3580feb9..31403a77 100644 --- a/.sqlx/query-2e3ce3eafee2cd110085a94b122884fe25591aa6f48256abbb6c8d973efe932e.json +++ b/.sqlx/query-389a48e2e5dec4370013f3cada400d83da45214984f8a2ec48f3f1343a28240e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering\n FROM organizations o\n INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = TRUE\n WHERE o.id = $1\n ", + "query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering\n FROM organizations o\n INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = ANY($3)\n WHERE o.id = $1\n ", "describe": { "columns": [ { @@ -57,7 +57,8 @@ "parameters": { "Left": [ "Int8", - "Int8" + "Int8", + "BoolArray" ] }, "nullable": [ @@ -73,5 +74,5 @@ false ] }, - "hash": "2e3ce3eafee2cd110085a94b122884fe25591aa6f48256abbb6c8d973efe932e" + "hash": "389a48e2e5dec4370013f3cada400d83da45214984f8a2ec48f3f1343a28240e" } diff --git a/.sqlx/query-7f039929345f6421ab7de7a75ac19cf7caf9a0ebc53b0232c6107b2995453c14.json b/.sqlx/query-7f039929345f6421ab7de7a75ac19cf7caf9a0ebc53b0232c6107b2995453c14.json new file mode 100644 index 00000000..3dad0e1c --- /dev/null +++ b/.sqlx/query-7f039929345f6421ab7de7a75ac19cf7caf9a0ebc53b0232c6107b2995453c14.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(\n SELECT 1 FROM mods m \n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 \n WHERE m.id = $1\n )", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + null + ] + }, + "hash": "7f039929345f6421ab7de7a75ac19cf7caf9a0ebc53b0232c6107b2995453c14" +} diff --git a/.sqlx/query-cb82bb6e22690fd5fee18bbc2975503371814ef1cbf95f32c195bfe7542b2b20.json b/.sqlx/query-8d6166509c910b6400efc1d1398aae92ebe04b0d4d1fe1762208167cee23b645.json similarity index 61% rename from .sqlx/query-cb82bb6e22690fd5fee18bbc2975503371814ef1cbf95f32c195bfe7542b2b20.json rename to .sqlx/query-8d6166509c910b6400efc1d1398aae92ebe04b0d4d1fe1762208167cee23b645.json index fef6fe52..03686661 100644 --- a/.sqlx/query-cb82bb6e22690fd5fee18bbc2975503371814ef1cbf95f32c195bfe7542b2b20.json +++ b/.sqlx/query-8d6166509c910b6400efc1d1398aae92ebe04b0d4d1fe1762208167cee23b645.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO team_members (\n id, team_id, user_id, role, permissions, organization_permissions, accepted\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7\n )\n ", + "query": "\n INSERT INTO team_members (\n id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n )\n ", "describe": { "columns": [], "parameters": { @@ -11,10 +11,11 @@ "Varchar", "Int8", "Int8", + "Bool", "Bool" ] }, "nullable": [] }, - "hash": "cb82bb6e22690fd5fee18bbc2975503371814ef1cbf95f32c195bfe7542b2b20" + "hash": "8d6166509c910b6400efc1d1398aae92ebe04b0d4d1fe1762208167cee23b645" } diff --git a/.sqlx/query-8f9d42b02fbef278e2bf7f53dc72b80ce0f7feb780644b15c3e3f68e0a18d0fd.json b/.sqlx/query-8f9d42b02fbef278e2bf7f53dc72b80ce0f7feb780644b15c3e3f68e0a18d0fd.json deleted file mode 100644 index 2b9024d6..00000000 --- a/.sqlx/query-8f9d42b02fbef278e2bf7f53dc72b80ce0f7feb780644b15c3e3f68e0a18d0fd.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE team_members tm\n SET is_owner = TRUE\n WHERE tm.team_id = ANY($1) AND tm.user_id = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8Array", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "8f9d42b02fbef278e2bf7f53dc72b80ce0f7feb780644b15c3e3f68e0a18d0fd" -} diff --git a/.sqlx/query-b18328c8570ad6e140e54634b2219b9921d86f0fff6b7ba63c0c54488d12f1a8.json b/.sqlx/query-b18328c8570ad6e140e54634b2219b9921d86f0fff6b7ba63c0c54488d12f1a8.json deleted file mode 100644 index d03dc926..00000000 --- a/.sqlx/query-b18328c8570ad6e140e54634b2219b9921d86f0fff6b7ba63c0c54488d12f1a8.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE team_members\n SET is_owner = FALSE\n WHERE (team_id = $1)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "b18328c8570ad6e140e54634b2219b9921d86f0fff6b7ba63c0c54488d12f1a8" -} diff --git a/.sqlx/query-cc89fe059b80fb7d7387016d1a53b30832f1caaa295b1a3ffa55209d1cda01ba.json b/.sqlx/query-cc89fe059b80fb7d7387016d1a53b30832f1caaa295b1a3ffa55209d1cda01ba.json deleted file mode 100644 index 71cf655b..00000000 --- a/.sqlx/query-cc89fe059b80fb7d7387016d1a53b30832f1caaa295b1a3ffa55209d1cda01ba.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE team_members\n SET is_owner = TRUE\n WHERE (id = $1)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "cc89fe059b80fb7d7387016d1a53b30832f1caaa295b1a3ffa55209d1cda01ba" -} diff --git a/.sqlx/query-740c4343d7357af6820e28a3e1f165cbbc3f967c4dfbeeb13a0c63f78e072895.json b/.sqlx/query-ebdcc29fc24bd31514ccdf0202a35768180e8d4fc103239806d2da7ea2540e5d.json similarity index 88% rename from .sqlx/query-740c4343d7357af6820e28a3e1f165cbbc3f967c4dfbeeb13a0c63f78e072895.json rename to .sqlx/query-ebdcc29fc24bd31514ccdf0202a35768180e8d4fc103239806d2da7ea2540e5d.json index 82e5ec33..e9788a30 100644 --- a/.sqlx/query-740c4343d7357af6820e28a3e1f165cbbc3f967c4dfbeeb13a0c63f78e072895.json +++ b/.sqlx/query-ebdcc29fc24bd31514ccdf0202a35768180e8d4fc103239806d2da7ea2540e5d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering\n FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE\n WHERE m.id = $1\n ", + "query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering\n FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = ANY($3)\n WHERE m.id = $1\n ", "describe": { "columns": [ { @@ -57,7 +57,8 @@ "parameters": { "Left": [ "Int8", - "Int8" + "Int8", + "BoolArray" ] }, "nullable": [ @@ -73,5 +74,5 @@ false ] }, - "hash": "740c4343d7357af6820e28a3e1f165cbbc3f967c4dfbeeb13a0c63f78e072895" + "hash": "ebdcc29fc24bd31514ccdf0202a35768180e8d4fc103239806d2da7ea2540e5d" } diff --git a/.sqlx/query-efc5806b3d0f0429854e62031b0759cb80863c36d1eaf373139f325102e2068b.json b/.sqlx/query-efc5806b3d0f0429854e62031b0759cb80863c36d1eaf373139f325102e2068b.json deleted file mode 100644 index f3192d61..00000000 --- a/.sqlx/query-efc5806b3d0f0429854e62031b0759cb80863c36d1eaf373139f325102e2068b.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE team_members tm\n SET is_owner = FALSE\n WHERE tm.team_id = ANY($1)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [] - }, - "hash": "efc5806b3d0f0429854e62031b0759cb80863c36d1eaf373139f325102e2068b" -} diff --git a/.sqlx/query-f40153d1516fe93e4179c14d00e1667baa7bb9157f58a5017e710d09fd295eb0.json b/.sqlx/query-f40153d1516fe93e4179c14d00e1667baa7bb9157f58a5017e710d09fd295eb0.json new file mode 100644 index 00000000..c1d94524 --- /dev/null +++ b/.sqlx/query-f40153d1516fe93e4179c14d00e1667baa7bb9157f58a5017e710d09fd295eb0.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE team_members\n SET \n is_owner = TRUE,\n accepted = TRUE,\n permissions = $1,\n organization_permissions = NULL,\n role = 'Inherited Owner'\n WHERE (id = $1)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "f40153d1516fe93e4179c14d00e1667baa7bb9157f58a5017e710d09fd295eb0" +} diff --git a/.sqlx/query-fa1b92b15cc108fa046998f789c8b259e0226e7dac16c635927ca74abc78cea9.json b/.sqlx/query-fa1b92b15cc108fa046998f789c8b259e0226e7dac16c635927ca74abc78cea9.json deleted file mode 100644 index bcc6250b..00000000 --- a/.sqlx/query-fa1b92b15cc108fa046998f789c8b259e0226e7dac16c635927ca74abc78cea9.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 WHERE m.id = $1)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "fa1b92b15cc108fa046998f789c8b259e0226e7dac16c635927ca74abc78cea9" -} diff --git a/migrations/20231213103100_enforces-owner-unique.sql b/migrations/20231213103100_enforces-owner-unique.sql new file mode 100644 index 00000000..f02a0352 --- /dev/null +++ b/migrations/20231213103100_enforces-owner-unique.sql @@ -0,0 +1,10 @@ +-- Enforces that there can only be one owner per team +CREATE UNIQUE INDEX idx_one_owner_per_team +ON team_members (team_id) +WHERE is_owner = TRUE; + +-- Enforces one team_member per user/team +CREATE UNIQUE INDEX idx_unique_user_team +ON team_members (user_id, team_id); + + diff --git a/src/auth/checks.rs b/src/auth/checks.rs index 4d47e72c..7794e8f7 100644 --- a/src/auth/checks.rs +++ b/src/auth/checks.rs @@ -103,13 +103,32 @@ pub async fn filter_authorized_projects( let user_id: models::ids::UserId = user.id.into(); use futures::TryStreamExt; - + println!( + "Checking team ids: {:?}", + check_projects + .iter() + .map(|x| x.inner.team_id.0) + .collect::>() + ); + println!( + "Checking organization ids: {:?}", + check_projects + .iter() + .filter_map(|x| x.inner.organization_id.map(|x| x.0)) + .collect::>() + ); sqlx::query!( " SELECT m.id id, m.team_id team_id FROM team_members tm INNER JOIN mods m ON m.team_id = tm.team_id - LEFT JOIN organizations o ON o.team_id = tm.team_id - WHERE (tm.team_id = ANY($1) or o.id = ANY($2)) AND tm.user_id = $3 + WHERE tm.team_id = ANY($1) AND tm.user_id = $3 + + UNION + + SELECT m.id id, m.team_id team_id FROM team_members tm + INNER JOIN organizations o ON o.team_id = tm.team_id + INNER JOIN mods m ON m.organization_id = o.id + WHERE o.id = ANY($2) AND tm.user_id = $3 ", &check_projects .iter() @@ -125,7 +144,8 @@ pub async fn filter_authorized_projects( .try_for_each(|e| { if let Some(row) = e.right() { check_projects.retain(|x| { - let bool = x.inner.id.0 == row.id && x.inner.team_id.0 == row.team_id; + let bool = + Some(x.inner.id.0) == row.id && Some(x.inner.team_id.0) == row.team_id; if bool { return_projects.push(x.clone().into()); @@ -159,15 +179,35 @@ pub async fn is_authorized_version( let user_id: models::ids::UserId = user.id.into(); let version_exists = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 WHERE m.id = $1)", + "SELECT EXISTS( + SELECT 1 FROM mods m + INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 + WHERE m.id = $1 + )", + version_data.project_id as database::models::ids::ProjectId, + user_id as database::models::ids::UserId, + ) + .fetch_one(&***pool) + .await? + .exists; + + let version_organization_exists = sqlx::query!( + "SELECT EXISTS( + SELECT 1 FROM mods m + INNER JOIN organizations o ON m.organization_id = o.id + INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 + WHERE m.id = $1 + )", version_data.project_id as database::models::ids::ProjectId, user_id as database::models::ids::UserId, ) - .fetch_one(&***pool) - .await? - .exists; + .fetch_one(&***pool) + .await? + .exists; - authorized = version_exists.unwrap_or(false); + authorized = version_exists + .or(version_organization_exists) + .unwrap_or(false); } } } diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index 704d229a..5b1acd33 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -412,10 +412,10 @@ impl TeamMember { sqlx::query!( " INSERT INTO team_members ( - id, team_id, user_id, role, permissions, organization_permissions, accepted + id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted ) VALUES ( - $1, $2, $3, $4, $5, $6, $7 + $1, $2, $3, $4, $5, $6, $7, $8 ) ", self.id as TeamMemberId, @@ -424,6 +424,7 @@ impl TeamMember { self.role, self.permissions.bits() as i64, self.organization_permissions.map(|p| p.bits() as i64), + self.is_owner, self.accepted, ) .execute(&mut **transaction) @@ -576,20 +577,28 @@ impl TeamMember { pub async fn get_from_user_id_project<'a, 'b, E>( id: ProjectId, user_id: UserId, + allow_pending: bool, executor: E, ) -> Result, super::DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let accepted = if allow_pending { + vec![true, false] + } else { + vec![true] + }; + let result = sqlx::query!( " SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering FROM mods m - INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE + INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = ANY($3) WHERE m.id = $1 ", id as ProjectId, - user_id as UserId + user_id as UserId, + &accepted ) .fetch_optional(executor) .await?; @@ -618,20 +627,27 @@ impl TeamMember { pub async fn get_from_user_id_organization<'a, 'b, E>( id: OrganizationId, user_id: UserId, + allow_pending: bool, executor: E, ) -> Result, super::DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { + let accepted = if allow_pending { + vec![true, false] + } else { + vec![true] + }; let result = sqlx::query!( " SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering FROM organizations o - INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = TRUE + INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = ANY($3) WHERE o.id = $1 ", id as OrganizationId, - user_id as UserId + user_id as UserId, + &accepted ) .fetch_optional(executor) .await?; diff --git a/src/routes/v3/organizations.rs b/src/routes/v3/organizations.rs index eff3f142..f779e57e 100644 --- a/src/routes/v3/organizations.rs +++ b/src/routes/v3/organizations.rs @@ -66,8 +66,11 @@ pub async fn organization_projects_get( .map(|x| x.1) .ok(); + println!("\n\nHas user: {:?}", current_user); + let possible_organization_id: Option = parse_base62(&info).ok(); + println!("Parsed id: {:?}", possible_organization_id); let project_ids = sqlx::query!( " SELECT m.id FROM organizations o @@ -82,10 +85,14 @@ pub async fn organization_projects_get( .try_collect::>() .await?; + println!("Found: {:?}", project_ids); + let projects_data = crate::database::models::Project::get_many_ids(&project_ids, &**pool, &redis).await?; + println!("Found1: {:?}", projects_data.len()); let projects = filter_authorized_projects(projects_data, ¤t_user, &pool).await?; + println!("Found2: {:?}", projects.len()); Ok(HttpResponse::Ok().json(projects)) } @@ -504,6 +511,7 @@ pub async fn organization_delete( let team_member = database::models::TeamMember::get_from_user_id_organization( organization.id, user.id.into(), + false, &**pool, ) .await @@ -538,6 +546,8 @@ pub async fn organization_delete( let mut transaction = pool.begin().await?; // Handle projects- every project that is in this organization needs to have its owner changed the organization owner + // Now, no project should have an owner if it is in an organization, and also + // the owner of an organization should not be a team member in any project let organization_project_teams = sqlx::query!( " SELECT t.id FROM organizations o @@ -552,75 +562,24 @@ pub async fn organization_delete( .try_collect::>() .await?; - // Get all project teams from the organization that do not have owner as a team_member - let project_teams_without_owner = sqlx::query!( - " - SELECT t.id FROM teams t - WHERE t.id = ANY($1) AND NOT EXISTS ( - SELECT 1 FROM team_members tm - WHERE tm.team_id = t.id AND tm.user_id = $2 - ) - ", - &organization_project_teams - .iter() - .map(|x| x.0 as i64) - .collect::>(), - owner_id.0 - ) - .fetch_many(&mut *transaction) - .try_filter_map(|e| async { Ok(e.right().map(|c| crate::database::models::TeamId(c.id))) }) - .try_collect::>() - .await?; - - for project_team_without_owner in project_teams_without_owner { + for organization_project_team in organization_project_teams.iter() { let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?; let member = TeamMember { id: new_id, - team_id: project_team_without_owner, + team_id: *organization_project_team, user_id: owner_id, role: "Inherited Owner".to_string(), - is_owner: false, + is_owner: true, permissions: ProjectPermissions::all(), organization_permissions: None, accepted: true, payouts_split: Decimal::ZERO, ordering: 0, }; + println!("Added new team_member {:?}", serde_json::to_string(&member)); member.insert(&mut transaction).await?; } - - // Set is_owner to false for all team_members of projects in the organization - sqlx::query!( - " - UPDATE team_members tm - SET is_owner = FALSE - WHERE tm.team_id = ANY($1) - ", - &organization_project_teams - .iter() - .map(|x| x.0 as i64) - .collect::>() - ) - .execute(&mut *transaction) - .await?; - - // Now that every project has 'owner' as a team member, we can set those projects to have the owner as the owner - sqlx::query!( - " - UPDATE team_members tm - SET is_owner = TRUE - WHERE tm.team_id = ANY($1) AND tm.user_id = $2 - ", - &organization_project_teams - .iter() - .map(|x| x.0 as i64) - .collect::>(), - owner_id.0 - ) - .execute(&mut *transaction) - .await?; - // Safely remove the organization let result = database::models::Organization::remove(organization.id, &mut transaction, &redis).await?; @@ -684,6 +643,7 @@ pub async fn organization_projects_add( let project_team_member = database::models::TeamMember::get_from_user_id_project( project_item.inner.id, current_user.id.into(), + false, &**pool, ) .await? @@ -692,6 +652,7 @@ pub async fn organization_projects_add( let organization_team_member = database::models::TeamMember::get_from_user_id_organization( organization.id, current_user.id.into(), + false, &**pool, ) .await? @@ -742,6 +703,31 @@ pub async fn organization_projects_add( .await?; } + // The owner of the organization, should be removed as a member of the project, if they are + // (As it is an organization project now, and they should not have more specific permissions) + let organization_owner_user_id = sqlx::query!( + " + SELECT u.id + FROM team_members + INNER JOIN users u ON u.id = team_members.user_id + WHERE team_id = $1 AND is_owner = TRUE + ", + organization.team_id as database::models::ids::TeamId + ) + .fetch_one(&mut *transaction) + .await?; + let organization_owner_user_id = + database::models::ids::UserId(organization_owner_user_id.id); + println!("Adding org owner id: {}", organization_owner_user_id.0); + + // If the owner of the organization is a member of the project, remove them + database::models::TeamMember::delete( + project_item.inner.team_id, + organization_owner_user_id, + &mut transaction, + ) + .await?; + transaction.commit().await?; database::models::TeamMember::clear_cache(project_item.inner.team_id, &redis).await?; @@ -811,6 +797,7 @@ pub async fn organization_projects_remove( let organization_team_member = database::models::TeamMember::get_from_user_id_organization( organization.id, current_user.id.into(), + false, &**pool, ) .await? @@ -828,6 +815,7 @@ pub async fn organization_projects_remove( database::models::TeamMember::get_from_user_id_organization( organization.id, data.new_owner.into(), + false, &**pool, ) .await? @@ -838,12 +826,21 @@ pub async fn organization_projects_remove( })?; // Then, we get the team member of the project and that user (if it exists) + // We use the team member get directly + println!("Looking for new owner from user id: {}", data.new_owner.0); + println!( + "Looking for new owner from project id: {}", + project_item.inner.id.0 + ); + let new_owner = database::models::TeamMember::get_from_user_id_project( project_item.inner.id, data.new_owner.into(), + true, &**pool, ) .await?; + println!("Did we find a new owner? {:?}", new_owner.is_some()); let mut transaction = pool.begin().await?; @@ -871,24 +868,18 @@ pub async fn organization_projects_remove( } }; - // Remove any owners from the team (likely redundant) - sqlx::query!( - " - UPDATE team_members - SET is_owner = FALSE - WHERE (team_id = $1) - ", - project_item.inner.team_id as database::models::ids::TeamId - ) - .execute(&mut *transaction) - .await?; - println!("Setting new owner to {}", new_owner.id.0); - // Set the new owner + + // Set the new owner to fit owner sqlx::query!( " UPDATE team_members - SET is_owner = TRUE + SET + is_owner = TRUE, + accepted = TRUE, + permissions = $1, + organization_permissions = NULL, + role = 'Inherited Owner' WHERE (id = $1) ", new_owner.id as database::models::ids::TeamMemberId diff --git a/src/routes/v3/teams.rs b/src/routes/v3/teams.rs index acef3b10..23d1babf 100644 --- a/src/routes/v3/teams.rs +++ b/src/routes/v3/teams.rs @@ -496,9 +496,30 @@ pub async fn add_team_member( )); } } - crate::database::models::User::get_id(new_member.user_id.into(), &**pool, &redis) - .await? - .ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?; + let new_user = + crate::database::models::User::get_id(new_member.user_id.into(), &**pool, &redis) + .await? + .ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?; + + if let TeamAssociationId::Project(pid) = team_association { + // Get user from the organization of this team, if applicable + + let organization = + Organization::get_associated_organization_project_id(pid, &**pool).await?; + let organization_team_member = if let Some(organization) = &organization { + TeamMember::get_from_user_id(organization.team_id, new_user.id, &**pool).await? + } else { + None + }; + if organization_team_member + .map(|tm| tm.is_owner) + .unwrap_or(false) + { + return Err(ApiError::InvalidInput( + "You cannot add the owner of an organization to a project team owned by that organization".to_string(), + )); + } + } let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?; TeamMember { @@ -595,7 +616,9 @@ pub async fn edit_team_member( let mut transaction = pool.begin().await?; - if edit_member_db.is_owner && edit_member.permissions.is_some() { + if edit_member_db.is_owner + && (edit_member.permissions.is_some() || edit_member.organization_permissions.is_some()) + { return Err(ApiError::InvalidInput( "The owner's permission's in a team cannot be edited".to_string(), )); @@ -732,8 +755,8 @@ pub async fn transfer_ownership( // 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 // There shouldnt be an ownr on these projects in these cases, but just in case. - let pid = Team::get_association(id.into(), &**pool).await?; - if let Some(TeamAssociationId::Project(pid)) = pid { + let team_association_id = Team::get_association(id.into(), &**pool).await?; + if let Some(TeamAssociationId::Project(pid)) = team_association_id { let result = Project::get_id(pid, &**pool, &redis).await?; if let Some(project_item) = result { if project_item.inner.organization_id.is_some() { @@ -798,7 +821,14 @@ pub async fn transfer_ownership( id.into(), new_owner.user_id.into(), Some(ProjectPermissions::all()), - Some(OrganizationPermissions::all()), + if matches!( + team_association_id, + Some(TeamAssociationId::Organization(_)) + ) { + Some(OrganizationPermissions::all()) + } else { + None + }, None, None, None, @@ -808,7 +838,46 @@ pub async fn transfer_ownership( ) .await?; + let project_teams_edited = + if let Some(TeamAssociationId::Organization(oid)) = team_association_id { + println!("Transferring ownership of an ORG"); + // The owner of ALL projects that this organization owns, if applicable, should be removed as members of the project, + // if they are members of those projects. + // (As they are the org owners for them, and they should not have more specific permissions) + + // First, get team id for every project owned by this organization + let team_ids = sqlx::query!( + " + SELECT m.team_id FROM organizations o + INNER JOIN mods m ON m.organization_id = o.id + WHERE o.id = $1 AND $1 IS NOT NULL + ", + oid.0 as i64 + ) + .fetch_all(&mut *transaction) + .await?; + + let team_ids: Vec = team_ids + .into_iter() + .map(|x| TeamId(x.team_id as u64).into()) + .collect(); + + // If the owner of the organization is a member of the project, remove them + for team_id in team_ids.iter() { + println!("Removing from team: {}", team_id.0); + println!("New owner: {}", new_owner.user_id.0); + TeamMember::delete(*team_id, new_owner.user_id.into(), &mut transaction).await?; + } + + team_ids + } else { + vec![] + }; + TeamMember::clear_cache(id.into(), &redis).await?; + for team_id in project_teams_edited { + TeamMember::clear_cache(team_id, &redis).await?; + } transaction.commit().await?; diff --git a/src/routes/v3/version_creation.rs b/src/routes/v3/version_creation.rs index 62661bf9..11294975 100644 --- a/src/routes/v3/version_creation.rs +++ b/src/routes/v3/version_creation.rs @@ -217,6 +217,7 @@ async fn version_create_inner( let team_member = models::TeamMember::get_from_user_id_project( project_id, user.id.into(), + false, &mut **transaction, ) .await?; @@ -609,6 +610,7 @@ async fn upload_file_to_version_inner( let team_member = models::TeamMember::get_from_user_id_project( version.inner.project_id, user.id.into(), + false, &mut **transaction, ) .await?; diff --git a/src/routes/v3/version_file.rs b/src/routes/v3/version_file.rs index 2006d481..bbd448a2 100644 --- a/src/routes/v3/version_file.rs +++ b/src/routes/v3/version_file.rs @@ -585,6 +585,7 @@ pub async fn delete_file( database::models::TeamMember::get_from_user_id_organization( organization.id, user.id.into(), + false, &**pool, ) .await diff --git a/src/routes/v3/versions.rs b/src/routes/v3/versions.rs index 54da5fbb..ea40a9cf 100644 --- a/src/routes/v3/versions.rs +++ b/src/routes/v3/versions.rs @@ -275,6 +275,7 @@ pub async fn version_edit_helper( let team_member = database::models::TeamMember::get_from_user_id_project( version_item.inner.project_id, user.id.into(), + false, &**pool, ) .await?; @@ -871,6 +872,7 @@ pub async fn version_schedule( let team_member = database::models::TeamMember::get_from_user_id_project( version_item.inner.project_id, user.id.into(), + false, &**pool, ) .await?; @@ -959,6 +961,7 @@ pub async fn version_delete( let team_member = database::models::TeamMember::get_from_user_id_project( version.inner.project_id, user.id.into(), + false, &**pool, ) .await diff --git a/tests/organizations.rs b/tests/organizations.rs index de98226f..f628344b 100644 --- a/tests/organizations.rs +++ b/tests/organizations.rs @@ -305,7 +305,9 @@ async fn add_remove_organization_projects() { async fn add_remove_organization_project_ownership_to_user() { with_test_environment(None, |test_env: TestEnvironment| async move { let alpha_project_id: &str = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let beta_project_id: &str = &test_env.dummy.as_ref().unwrap().project_beta.project_id; let alpha_team_id: &str = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let beta_team_id: &str = &test_env.dummy.as_ref().unwrap().project_beta.team_id; let zeta_team_id: &str = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; let zeta_organization_id: &str = &test_env .dummy @@ -314,52 +316,143 @@ async fn add_remove_organization_project_ownership_to_user() { .organization_zeta .organization_id; - // Add FRIEND + // Add friend to alpha, beta, and zeta + for (team, organization) in [ + (alpha_team_id, false), + (beta_team_id, false), + (zeta_team_id, true), + ] { + let org_permissions = if organization { + Some(OrganizationPermissions::all()) + } else { + None + }; + let resp = test_env + .api + .add_user_to_team( + team, + FRIEND_USER_ID, + Some(ProjectPermissions::all()), + org_permissions, + USER_USER_PAT, + ) + .await; + println!("{:?}", resp.response().body()); + assert_eq!(resp.status(), 204); + + // Accept invites + let resp = test_env.api.join_team(team, FRIEND_USER_PAT).await; + assert_eq!(resp.status(), 204); + } + + // For each team, confirm there are two members, but only one owner of the project, and it is USER_USER_ID + for team in [alpha_team_id, beta_team_id, zeta_team_id] { + let members = test_env + .api + .get_team_members_deserialized(team, USER_USER_PAT) + .await; + assert_eq!(members.len(), 2); + let user_member = members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 1); + assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID); + } + + // Transfer ownership of beta project to FRIEND let resp = test_env .api - .add_user_to_team(zeta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) + .transfer_team_ownership(beta_team_id, FRIEND_USER_ID, USER_USER_PAT) .await; assert_eq!(resp.status(), 204); - // Accept invite - let resp = test_env.api.join_team(zeta_team_id, FRIEND_USER_PAT).await; - assert_eq!(resp.status(), 204); - - // Confirm there is only one owner of the project, and it is USER_USER_ID + // Confirm there are still two users, but now FRIEND_USER_ID is the owner let members = test_env .api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) + .get_team_members_deserialized(beta_team_id, USER_USER_PAT) .await; + assert_eq!(members.len(), 2); let user_member = members.iter().filter(|m| m.is_owner).collect::>(); assert_eq!(user_member.len(), 1); - assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID); + assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID); - // Confirm there is only one owner of the organization, and it is USER_USER_ID - let members = test_env + // Add alpha, beta to zeta organization + for (project_id, pat) in [ + (alpha_project_id, USER_USER_PAT), + (beta_project_id, FRIEND_USER_PAT), + ] { + let resp = test_env + .api + .organization_add_project(zeta_organization_id, project_id, pat) + .await; + println!("{:?}", resp.response().body()); + assert_eq!(resp.status(), 200); + + // Get and confirm it has been added + let project = test_env.api.get_project_deserialized(project_id, pat).await; + assert_eq!( + project.organization.unwrap().to_string(), + zeta_organization_id + ); + } + + // Both alpha and beta project should have: + // - 1 member, FRIEND_USER_ID + // - No owner. + // -> For alpha, user was removed as owner when it was added to the organization + // -> For beta, user was removed as owner when ownership was transferred to friend + // then friend was removed as owner when it was added to the organization + // -> In both cases, user was removed entirely as a team_member as it is now the owner of the organization + for team_id in [alpha_team_id, beta_team_id] { + println!("team_id: {:?}", team_id); + let members = test_env + .api + .get_team_members_deserialized(team_id, USER_USER_PAT) + .await; + println!("{:?}", serde_json::to_string(&members).unwrap()); + assert_eq!(members.len(), 1); + assert_eq!(members[0].user.id.to_string(), FRIEND_USER_ID); + let user_member = members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 0); + } + + // Transfer ownership of zeta organization to FRIEND + let resp = test_env .api - .get_organization_members_deserialized(zeta_organization_id, USER_USER_PAT) + .transfer_team_ownership(zeta_team_id, FRIEND_USER_ID, USER_USER_PAT) .await; - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 1); - assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID); + assert_eq!(resp.status(), 204); - // Add alpha to zeta organization + // Confirm there are no members of the alpha project OR the beta project + // - Friend was removed as a member of these projects when ownership was transferred to them + for team_id in [alpha_team_id, beta_team_id] { + println!("Team_id: {:?}", team_id); + let members = test_env + .api + .get_team_members_deserialized(team_id, USER_USER_PAT) + .await; + println!("{:?}", serde_json::to_string(&members).unwrap()); + assert!(members.is_empty()); + } + + // As user, cannot add friend to alpha project, as they are the org owner let resp = test_env .api - .organization_add_project(zeta_organization_id, alpha_project_id, USER_USER_PAT) + .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) .await; - assert_eq!(resp.status(), 200); + assert_eq!(resp.status(), 400); - // Confirm there is NO owner of the project, as it is owned by the organization - let members = test_env + // As friend, can add user to alpha project, as they are not the org owner + let resp = test_env .api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) + .add_user_to_team(alpha_team_id, USER_USER_ID, None, None, FRIEND_USER_PAT) .await; - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 0); + assert_eq!(resp.status(), 204); + + // At this point, friend owns the org + // Alpha member has user as a member, but not as an owner + // Neither project has an owner, as they are owned by the org // Remove project from organization with a user that is not an organization member - // This should fail + // This should fail as we cannot give a project to a user that is not a member of the organization let resp = test_env .api .organization_remove_project( @@ -371,28 +464,56 @@ async fn add_remove_organization_project_ownership_to_user() { .await; assert_eq!(resp.status(), 400); - // Remove project from organization with a user that is an organization member, but not a project member + // Remove project from organization with a user that is an organization member, and a project member // This should succeed let resp = test_env .api .organization_remove_project( zeta_organization_id, alpha_project_id, - UserId(FRIEND_USER_ID_PARSED as u64), + UserId(USER_USER_ID_PARSED as u64), USER_USER_PAT, ) .await; println!("{:?}", resp.response().body()); assert_eq!(resp.status(), 200); - // Confirm there is only one owner of the project, and it is now FRIEND_USER_ID - let members = test_env + // Remove project from organization with a user that is an organization member, but not a project member + // This should succeed + let resp = test_env .api - .get_team_members_deserialized(alpha_team_id, USER_USER_PAT) + .organization_remove_project( + zeta_organization_id, + beta_project_id, + UserId(USER_USER_ID_PARSED as u64), + USER_USER_PAT, + ) .await; - let user_member = members.iter().filter(|m| m.is_owner).collect::>(); - assert_eq!(user_member.len(), 1); - assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID); + assert_eq!(resp.status(), 200); + + // For each of alpha and beta, confirm: + // - There is one member of each project, the owner, USER_USER_ID + // - They no longer have an attached organization + for team_id in [alpha_team_id, beta_team_id] { + println!("Team: {:?}", team_id); + let members = test_env + .api + .get_team_members_deserialized(team_id, USER_USER_PAT) + .await; + println!("{:?}", serde_json::to_string(&members).unwrap()); + assert_eq!(members.len(), 1); + let user_member = members.iter().filter(|m| m.is_owner).collect::>(); + assert_eq!(user_member.len(), 1); + assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID); + } + + for project_id in [alpha_project_id, beta_project_id] { + let project = test_env + .api + .get_project_deserialized(project_id, USER_USER_PAT) + .await; + assert!(project.organization.is_none()); + } }) .await; } diff --git a/tests/scopes.rs b/tests/scopes.rs index d0484fa0..a8dc7eea 100644 --- a/tests/scopes.rs +++ b/tests/scopes.rs @@ -1172,7 +1172,7 @@ pub async fn collections_scopes() { } // Organization scopes (and a couple PROJECT_WRITE scopes that are only allowed for orgs) -#[actix_rt::test] +#[actix_rt::test] pub async fn organization_scopes() { // Test setup and dummy data with_test_environment_all(None, |test_env| async move { @@ -1293,11 +1293,13 @@ pub async fn organization_scopes() { // remove project (now that we've checked) let req_gen = || { - test::TestRequest::delete().uri(&format!( - "/v3/organization/{organization_id}/projects/{beta_project_id}" - )).set_json(json!({ - "new_owner": USER_USER_ID - })) + test::TestRequest::delete() + .uri(&format!( + "/v3/organization/{organization_id}/projects/{beta_project_id}" + )) + .set_json(json!({ + "new_owner": USER_USER_ID + })) }; ScopeTest::new(&test_env) .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE) diff --git a/tests/teams.rs b/tests/teams.rs index b25044e8..0665ee7d 100644 --- a/tests/teams.rs +++ b/tests/teams.rs @@ -186,6 +186,7 @@ async fn test_get_team_project_orgs() { // The team members route from teams (on a project's team): // - the members of the project team specifically // - not the ones from the organization + // - Remember: the owner of an org will not be included in the org's team members list let req = test::TestRequest::get() .uri(&format!("/v3/team/{alpha_team_id}/members")) .append_pat(FRIEND_USER_PAT) @@ -194,7 +195,7 @@ async fn test_get_team_project_orgs() { assert_eq!(resp.status(), 200); let value: serde_json::Value = test::read_body_json(resp).await; let members = value.as_array().unwrap(); - assert_eq!(members.len(), 1); + assert_eq!(members.len(), 0); // The team members route from project should show: // - the members of the project team including the ones from the organization