History

Add check_is_channel_participant

Refactor permission checks to load ancestor permissions into memory for all checks to make the different logics more explicit.

Conrad Irwin committed 3 years ago

Commit a7db2aa1 parent 78432d07 files · +285 −31Browse files

Unified · Split

7 files
  1. crates/collab/migrations.sqlite/20221109000000_test_schema.sql +2 −1
  2. crates/collab/src/db/ids.rs +4 −0
  3. crates/collab/src/db/queries/channels.rs +154 −26
  4. crates/collab/src/db/tables/channel.rs +1 −1
  5. crates/collab/src/db/tests/channel_tests.rs +119 −2
  6. crates/collab/src/tests/channel_tests.rs +4 −1
  7. crates/rpc/proto/zed.proto +1 −0
crates/collab/migrations.sqlite/20221109000000_test_schema.sql+2−1
@@ -192,7 +192,8 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); · 191 unmodified lines
192192 CREATE TABLE "channels" (
193193 "id" INTEGER PRIMARY KEY AUTOINCREMENT,
194194 "name" VARCHAR NOT NULL,
195− "created_at" TIMESTAMP NOT NULL DEFAULT now
195+ "created_at" TIMESTAMP NOT NULL DEFAULT now,
196+ "visibility" VARCHAR NOT NULL
196197 );
197198
198199 CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
crates/collab/src/db/ids.rs+4−0
@@ -91,6 +91,8 @@ pub enum ChannelRole { · 90 unmodified lines
9191 Member,
9292 #[sea_orm(string_value = "guest")]
9393 Guest,
94+ #[sea_orm(string_value = "banned")]
95+ Banned,
9496 }
9597
9698 impl From<proto::ChannelRole> for ChannelRole {
@@ -99,6 +101,7 @@ impl From<proto::ChannelRole> for ChannelRole { · 2 unmodified lines
99101 proto::ChannelRole::Admin => ChannelRole::Admin,
100102 proto::ChannelRole::Member => ChannelRole::Member,
101103 proto::ChannelRole::Guest => ChannelRole::Guest,
104+ proto::ChannelRole::Banned => ChannelRole::Banned,
102105 }
103106 }
104107 }
@@ -109,6 +112,7 @@ impl Into<proto::ChannelRole> for ChannelRole { · 4 unmodified lines
109112 ChannelRole::Admin => proto::ChannelRole::Admin,
110113 ChannelRole::Member => proto::ChannelRole::Member,
111114 ChannelRole::Guest => proto::ChannelRole::Guest,
115+ ChannelRole::Banned => proto::ChannelRole::Banned,
112116 }
113117 }
114118 }
crates/collab/src/db/queries/channels.rs+154−26
@@ -37,8 +37,9 @@ impl Database { · 36 unmodified lines
3737 }
3838
3939 let channel = channel::ActiveModel {
40+ id: ActiveValue::NotSet,
4041 name: ActiveValue::Set(name.to_string()),
41− ..Default::default()
42+ visibility: ActiveValue::Set(ChannelVisibility::ChannelMembers),
4243 }
4344 .insert(&*tx)
4445 .await?;
@@ -89,6 +90,29 @@ impl Database { · 44 unmodified lines
8990 .await
9091 }
9192
93+ pub async fn set_channel_visibility(
94+ &self,
95+ channel_id: ChannelId,
96+ visibility: ChannelVisibility,
97+ user_id: UserId,
98+ ) -> Result<()> {
99+ self.transaction(move |tx| async move {
100+ self.check_user_is_channel_admin(channel_id, user_id, &*tx)
101+ .await?;
102+
103+ channel::ActiveModel {
104+ id: ActiveValue::Unchanged(channel_id),
105+ visibility: ActiveValue::Set(visibility),
106+ ..Default::default()
107+ }
108+ .update(&*tx)
109+ .await?;
110+
111+ Ok(())
112+ })
113+ .await
114+ }
115+
92116 pub async fn delete_channel(
93117 &self,
94118 channel_id: ChannelId,
@@ -160,11 +184,11 @@ impl Database { · 65 unmodified lines
160184 &self,
161185 channel_id: ChannelId,
162186 invitee_id: UserId,
163− inviter_id: UserId,
187+ admin_id: UserId,
164188 role: ChannelRole,
165189 ) -> Result<()> {
166190 self.transaction(move |tx| async move {
167− self.check_user_is_channel_admin(channel_id, inviter_id, &*tx)
191+ self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
168192 .await?;
169193
170194 channel_member::ActiveModel {
@@ -262,10 +286,10 @@ impl Database { · 91 unmodified lines
262286 &self,
263287 channel_id: ChannelId,
264288 member_id: UserId,
265− remover_id: UserId,
289+ admin_id: UserId,
266290 ) -> Result<()> {
267291 self.transaction(|tx| async move {
268− self.check_user_is_channel_admin(channel_id, remover_id, &*tx)
292+ self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
269293 .await?;
270294
271295 let result = channel_member::Entity::delete_many()
@@ -481,12 +505,12 @@ impl Database { · 209 unmodified lines
481505 pub async fn set_channel_member_role(
482506 &self,
483507 channel_id: ChannelId,
484− from: UserId,
508+ admin_id: UserId,
485509 for_user: UserId,
486510 role: ChannelRole,
487511 ) -> Result<()> {
488512 self.transaction(|tx| async move {
489− self.check_user_is_channel_admin(channel_id, from, &*tx)
513+ self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
490514 .await?;
491515
492516 let result = channel_member::Entity::update_many()
@@ -613,43 +637,147 @@ impl Database { · 120 unmodified lines
613637 Ok(user_ids)
614638 }
615639
640+ pub async fn check_user_is_channel_admin(
641+ &self,
642+ channel_id: ChannelId,
643+ user_id: UserId,
644+ tx: &DatabaseTransaction,
645+ ) -> Result<()> {
646+ match self.channel_role_for_user(channel_id, user_id, tx).await? {
647+ Some(ChannelRole::Admin) => Ok(()),
648+ Some(ChannelRole::Member)
649+ | Some(ChannelRole::Banned)
650+ | Some(ChannelRole::Guest)
651+ | None => Err(anyhow!(
652+ "user is not a channel admin or channel does not exist"
653+ ))?,
654+ }
655+ }
656+
616657 pub async fn check_user_is_channel_member(
617658 &self,
618659 channel_id: ChannelId,
619660 user_id: UserId,
620661 tx: &DatabaseTransaction,
621662 ) -> Result<()> {
622− let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;
623− channel_member::Entity::find()
624− .filter(
625− channel_member::Column::ChannelId
626− .is_in(channel_ids)
627− .and(channel_member::Column::UserId.eq(user_id)),
628− )
629− .one(&*tx)
630− .await?
631− .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?;
632− Ok(())
663+ match self.channel_role_for_user(channel_id, user_id, tx).await? {
664+ Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(()),
665+ Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
666+ "user is not a channel member or channel does not exist"
667+ ))?,
668+ }
633669 }
634670
635− pub async fn check_user_is_channel_admin(
671+ pub async fn check_user_is_channel_participant(
636672 &self,
637673 channel_id: ChannelId,
638674 user_id: UserId,
639675 tx: &DatabaseTransaction,
640676 ) -> Result<()> {
677+ match self.channel_role_for_user(channel_id, user_id, tx).await? {
678+ Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => {
679+ Ok(())
680+ }
681+ Some(ChannelRole::Banned) | None => Err(anyhow!(
682+ "user is not a channel participant or channel does not exist"
683+ ))?,
684+ }
685+ }
686+
687+ pub async fn channel_role_for_user(
688+ &self,
689+ channel_id: ChannelId,
690+ user_id: UserId,
691+ tx: &DatabaseTransaction,
692+ ) -> Result<Option<ChannelRole>> {
641693 let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;
642− channel_member::Entity::find()
694+
695+ #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
696+ enum QueryChannelMembership {
697+ ChannelId,
698+ Role,
699+ Admin,
700+ Visibility,
701+ }
702+
703+ let mut rows = channel_member::Entity::find()
704+ .left_join(channel::Entity)
643705 .filter(
644706 channel_member::Column::ChannelId
645707 .is_in(channel_ids)
646− .and(channel_member::Column::UserId.eq(user_id))
647− .and(channel_member::Column::Admin.eq(true)),
708+ .and(channel_member::Column::UserId.eq(user_id)),
648709 )
649− .one(&*tx)
650− .await?
651− .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?;
652− Ok(())
710+ .select_only()
711+ .column(channel_member::Column::ChannelId)
712+ .column(channel_member::Column::Role)
713+ .column(channel_member::Column::Admin)
714+ .column(channel::Column::Visibility)
715+ .into_values::<_, QueryChannelMembership>()
716+ .stream(&*tx)
717+ .await?;
718+
719+ let mut is_admin = false;
720+ let mut is_member = false;
721+ let mut is_participant = false;
722+ let mut is_banned = false;
723+ let mut current_channel_visibility = None;
724+
725+ // note these channels are not iterated in any particular order,
726+ // our current logic takes the highest permission available.
727+ while let Some(row) = rows.next().await {
728+ let (ch_id, role, admin, visibility): (
729+ ChannelId,
730+ Option<ChannelRole>,
731+ bool,
732+ ChannelVisibility,
733+ ) = row?;
734+ match role {
735+ Some(ChannelRole::Admin) => is_admin = true,
736+ Some(ChannelRole::Member) => is_member = true,
737+ Some(ChannelRole::Guest) => {
738+ if visibility == ChannelVisibility::Public {
739+ is_participant = true
740+ }
741+ }
742+ Some(ChannelRole::Banned) => is_banned = true,
743+ None => {
744+ // rows created from pre-role collab server.
745+ if admin {
746+ is_admin = true
747+ } else {
748+ is_member = true
749+ }
750+ }
751+ }
752+ if channel_id == ch_id {
753+ current_channel_visibility = Some(visibility);
754+ }
755+ }
756+ // free up database connection
757+ drop(rows);
758+
759+ Ok(if is_admin {
760+ Some(ChannelRole::Admin)
761+ } else if is_member {
762+ Some(ChannelRole::Member)
763+ } else if is_banned {
764+ Some(ChannelRole::Banned)
765+ } else if is_participant {
766+ if current_channel_visibility.is_none() {
767+ current_channel_visibility = channel::Entity::find()
768+ .filter(channel::Column::Id.eq(channel_id))
769+ .one(&*tx)
770+ .await?
771+ .map(|channel| channel.visibility);
772+ }
773+ if current_channel_visibility == Some(ChannelVisibility::Public) {
774+ Some(ChannelRole::Guest)
775+ } else {
776+ None
777+ }
778+ } else {
779+ None
780+ })
653781 }
654782
655783 /// Returns the channel ancestors, deepest first
crates/collab/src/db/tables/channel.rs+1−1
@@ -7,7 +7,7 @@ pub struct Model { · 6 unmodified lines
77 #[sea_orm(primary_key)]
88 pub id: ChannelId,
99 pub name: String,
10− pub visbility: ChannelVisibility,
10+ pub visibility: ChannelVisibility,
1111 }
1212
1313 impl ActiveModelBehavior for ActiveModel {}
crates/collab/src/db/tests/channel_tests.rs+119−2
@@ -8,11 +8,14 @@ use crate::{ · 7 unmodified lines
88 db::{
99 queries::channels::ChannelGraph,
1010 tests::{graph, TEST_RELEASE_CHANNEL},
11− ChannelId, ChannelRole, Database, NewUserParams,
11+ ChannelId, ChannelRole, Database, NewUserParams, UserId,
1212 },
1313 test_both_dbs,
1414 };
15−use std::sync::Arc;
15+use std::sync::{
16+ atomic::{AtomicI32, Ordering},
17+ Arc,
18+};
1619
1720 test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
1821
@@ -850,6 +853,101 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) { · 831 unmodified lines
850853 );
851854 }
852855
856+test_both_dbs!(
857+ test_user_is_channel_participant,
858+ test_user_is_channel_participant_postgres,
859+ test_user_is_channel_participant_sqlite
860+);
861+
862+async fn test_user_is_channel_participant(db: &Arc<Database>) {
863+ let admin_id = new_test_user(db, "admin@example.com").await;
864+ let member_id = new_test_user(db, "member@example.com").await;
865+ let guest_id = new_test_user(db, "guest@example.com").await;
866+
867+ let zed_id = db.create_root_channel("zed", admin_id).await.unwrap();
868+ let intermediate_id = db
869+ .create_channel("active", Some(zed_id), admin_id)
870+ .await
871+ .unwrap();
872+ let public_id = db
873+ .create_channel("active", Some(intermediate_id), admin_id)
874+ .await
875+ .unwrap();
876+
877+ db.set_channel_visibility(public_id, crate::db::ChannelVisibility::Public, admin_id)
878+ .await
879+ .unwrap();
880+ db.invite_channel_member(intermediate_id, member_id, admin_id, ChannelRole::Member)
881+ .await
882+ .unwrap();
883+ db.invite_channel_member(public_id, guest_id, admin_id, ChannelRole::Guest)
884+ .await
885+ .unwrap();
886+
887+ db.transaction(|tx| async move {
888+ db.check_user_is_channel_participant(public_id, admin_id, &*tx)
889+ .await
890+ })
891+ .await
892+ .unwrap();
893+ db.transaction(|tx| async move {
894+ db.check_user_is_channel_participant(public_id, member_id, &*tx)
895+ .await
896+ })
897+ .await
898+ .unwrap();
899+ db.transaction(|tx| async move {
900+ db.check_user_is_channel_participant(public_id, guest_id, &*tx)
901+ .await
902+ })
903+ .await
904+ .unwrap();
905+
906+ db.set_channel_member_role(public_id, admin_id, guest_id, ChannelRole::Banned)
907+ .await
908+ .unwrap();
909+ assert!(db
910+ .transaction(|tx| async move {
911+ db.check_user_is_channel_participant(public_id, guest_id, &*tx)
912+ .await
913+ })
914+ .await
915+ .is_err());
916+
917+ db.remove_channel_member(public_id, guest_id, admin_id)
918+ .await
919+ .unwrap();
920+
921+ db.set_channel_visibility(zed_id, crate::db::ChannelVisibility::Public, admin_id)
922+ .await
923+ .unwrap();
924+
925+ db.invite_channel_member(zed_id, guest_id, admin_id, ChannelRole::Guest)
926+ .await
927+ .unwrap();
928+
929+ db.transaction(|tx| async move {
930+ db.check_user_is_channel_participant(zed_id, guest_id, &*tx)
931+ .await
932+ })
933+ .await
934+ .unwrap();
935+ assert!(db
936+ .transaction(|tx| async move {
937+ db.check_user_is_channel_participant(intermediate_id, guest_id, &*tx)
938+ .await
939+ })
940+ .await
941+ .is_err(),);
942+
943+ db.transaction(|tx| async move {
944+ db.check_user_is_channel_participant(public_id, guest_id, &*tx)
945+ .await
946+ })
947+ .await
948+ .unwrap();
949+}
950+
853951 #[track_caller]
854952 fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
855953 let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
@@ -874,3 +972,22 @@ fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) · 18 unmodified lines
874972
875973 pretty_assertions::assert_eq!(actual_map, expected_map)
876974 }
975+
976+static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
977+
978+async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
979+ let gid = GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst);
980+
981+ db.create_user(
982+ email,
983+ false,
984+ NewUserParams {
985+ github_login: email[0..email.find("@").unwrap()].to_string(),
986+ github_user_id: GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst),
987+ invite_count: 0,
988+ },
989+ )
990+ .await
991+ .unwrap()
992+ .user_id
993+}
crates/collab/src/tests/channel_tests.rs+4−1
@@ -6,7 +6,10 @@ use call::ActiveCall; · 5 unmodified lines
66 use channel::{ChannelId, ChannelMembership, ChannelStore};
77 use client::User;
88 use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
9−use rpc::{proto, RECEIVE_TIMEOUT};
9+use rpc::{
10+ proto::{self},
11+ RECEIVE_TIMEOUT,
12+};
1013 use std::sync::Arc;
1114
1215 #[gpui::test]
crates/rpc/proto/zed.proto+1−0
@@ -1040,6 +1040,7 @@ enum ChannelRole { · 1039 unmodified lines
10401040 Admin = 0;
10411041 Member = 1;
10421042 Guest = 2;
1043+ Banned = 3;
10431044 }
10441045
10451046 message SetChannelMemberRole {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t