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
192CREATE TABLE "channels" (192CREATE TABLE "channels" (
193 "id" INTEGER PRIMARY KEY AUTOINCREMENT,193 "id" INTEGER PRIMARY KEY AUTOINCREMENT,
194 "name" VARCHAR NOT NULL,194 "name" VARCHAR NOT NULL,
195 "created_at" TIMESTAMP NOT NULL DEFAULT now195 "created_at" TIMESTAMP NOT NULL DEFAULT now,
196 "visibility" VARCHAR NOT NULL
196);197);
197 198
198CREATE TABLE IF NOT EXISTS "channel_chat_participants" (199CREATE 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
91 Member,91 Member,
92 #[sea_orm(string_value = "guest")]92 #[sea_orm(string_value = "guest")]
93 Guest,93 Guest,
94 #[sea_orm(string_value = "banned")]
95 Banned,
94}96}
95 97
96impl From<proto::ChannelRole> for ChannelRole {98impl From<proto::ChannelRole> for ChannelRole {
@@ -99,6 +101,7 @@ impl From<proto::ChannelRole> for ChannelRole { · 2 unmodified lines
99 proto::ChannelRole::Admin => ChannelRole::Admin,101 proto::ChannelRole::Admin => ChannelRole::Admin,
100 proto::ChannelRole::Member => ChannelRole::Member,102 proto::ChannelRole::Member => ChannelRole::Member,
101 proto::ChannelRole::Guest => ChannelRole::Guest,103 proto::ChannelRole::Guest => ChannelRole::Guest,
104 proto::ChannelRole::Banned => ChannelRole::Banned,
102 }105 }
103 }106 }
104}107}
@@ -109,6 +112,7 @@ impl Into<proto::ChannelRole> for ChannelRole { · 4 unmodified lines
109 ChannelRole::Admin => proto::ChannelRole::Admin,112 ChannelRole::Admin => proto::ChannelRole::Admin,
110 ChannelRole::Member => proto::ChannelRole::Member,113 ChannelRole::Member => proto::ChannelRole::Member,
111 ChannelRole::Guest => proto::ChannelRole::Guest,114 ChannelRole::Guest => proto::ChannelRole::Guest,
115 ChannelRole::Banned => proto::ChannelRole::Banned,
112 }116 }
113 }117 }
114}118}
crates/collab/src/db/queries/channels.rs+154−26
@@ -37,8 +37,9 @@ impl Database { · 36 unmodified lines
37 }37 }
38 38
39 let channel = channel::ActiveModel {39 let channel = channel::ActiveModel {
40 id: ActiveValue::NotSet,
40 name: ActiveValue::Set(name.to_string()),41 name: ActiveValue::Set(name.to_string()),
41 ..Default::default()42 visibility: ActiveValue::Set(ChannelVisibility::ChannelMembers),
42 }43 }
43 .insert(&*tx)44 .insert(&*tx)
44 .await?;45 .await?;
@@ -89,6 +90,29 @@ impl Database { · 44 unmodified lines
89 .await90 .await
90 }91 }
91 92
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
92 pub async fn delete_channel(116 pub async fn delete_channel(
93 &self,117 &self,
94 channel_id: ChannelId,118 channel_id: ChannelId,
@@ -160,11 +184,11 @@ impl Database { · 65 unmodified lines
160 &self,184 &self,
161 channel_id: ChannelId,185 channel_id: ChannelId,
162 invitee_id: UserId,186 invitee_id: UserId,
163 inviter_id: UserId,187 admin_id: UserId,
164 role: ChannelRole,188 role: ChannelRole,
165 ) -> Result<()> {189 ) -> Result<()> {
166 self.transaction(move |tx| async move {190 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)
168 .await?;192 .await?;
169 193
170 channel_member::ActiveModel {194 channel_member::ActiveModel {
@@ -262,10 +286,10 @@ impl Database { · 91 unmodified lines
262 &self,286 &self,
263 channel_id: ChannelId,287 channel_id: ChannelId,
264 member_id: UserId,288 member_id: UserId,
265 remover_id: UserId,289 admin_id: UserId,
266 ) -> Result<()> {290 ) -> Result<()> {
267 self.transaction(|tx| async move {291 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)
269 .await?;293 .await?;
270 294
271 let result = channel_member::Entity::delete_many()295 let result = channel_member::Entity::delete_many()
@@ -481,12 +505,12 @@ impl Database { · 209 unmodified lines
481 pub async fn set_channel_member_role(505 pub async fn set_channel_member_role(
482 &self,506 &self,
483 channel_id: ChannelId,507 channel_id: ChannelId,
484 from: UserId,508 admin_id: UserId,
485 for_user: UserId,509 for_user: UserId,
486 role: ChannelRole,510 role: ChannelRole,
487 ) -> Result<()> {511 ) -> Result<()> {
488 self.transaction(|tx| async move {512 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)
490 .await?;514 .await?;
491 515
492 let result = channel_member::Entity::update_many()516 let result = channel_member::Entity::update_many()
@@ -613,43 +637,147 @@ impl Database { · 120 unmodified lines
613 Ok(user_ids)637 Ok(user_ids)
614 }638 }
615 639
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
616 pub async fn check_user_is_channel_member(657 pub async fn check_user_is_channel_member(
617 &self,658 &self,
618 channel_id: ChannelId,659 channel_id: ChannelId,
619 user_id: UserId,660 user_id: UserId,
620 tx: &DatabaseTransaction,661 tx: &DatabaseTransaction,
621 ) -> Result<()> {662 ) -> Result<()> {
622 let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;663 match self.channel_role_for_user(channel_id, user_id, tx).await? {
623 channel_member::Entity::find()664 Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(()),
624 .filter(665 Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
625 channel_member::Column::ChannelId666 "user is not a channel member or channel does not exist"
626 .is_in(channel_ids)667 ))?,
627 .and(channel_member::Column::UserId.eq(user_id)),668 }
628 )
629 .one(&*tx)
630 .await?
631 .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?;
632 Ok(())
633 }669 }
634 670
635 pub async fn check_user_is_channel_admin(671 pub async fn check_user_is_channel_participant(
636 &self,672 &self,
637 channel_id: ChannelId,673 channel_id: ChannelId,
638 user_id: UserId,674 user_id: UserId,
639 tx: &DatabaseTransaction,675 tx: &DatabaseTransaction,
640 ) -> Result<()> {676 ) -> 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>> {
641 let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;693 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)
643 .filter(705 .filter(
644 channel_member::Column::ChannelId706 channel_member::Column::ChannelId
645 .is_in(channel_ids)707 .is_in(channel_ids)
646 .and(channel_member::Column::UserId.eq(user_id))708 .and(channel_member::Column::UserId.eq(user_id)),
647 .and(channel_member::Column::Admin.eq(true)),
648 )709 )
649 .one(&*tx)710 .select_only()
650 .await?711 .column(channel_member::Column::ChannelId)
651 .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?;712 .column(channel_member::Column::Role)
652 Ok(())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 })
653 }781 }
654 782
655 /// Returns the channel ancestors, deepest first783 /// 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
7 #[sea_orm(primary_key)]7 #[sea_orm(primary_key)]
8 pub id: ChannelId,8 pub id: ChannelId,
9 pub name: String,9 pub name: String,
10 pub visbility: ChannelVisibility,10 pub visibility: ChannelVisibility,
11}11}
12 12
13impl ActiveModelBehavior for ActiveModel {}13impl ActiveModelBehavior for ActiveModel {}
crates/collab/src/db/tests/channel_tests.rs+119−2
@@ -8,11 +8,14 @@ use crate::{ · 7 unmodified lines
8 db::{8 db::{
9 queries::channels::ChannelGraph,9 queries::channels::ChannelGraph,
10 tests::{graph, TEST_RELEASE_CHANNEL},10 tests::{graph, TEST_RELEASE_CHANNEL},
11 ChannelId, ChannelRole, Database, NewUserParams,11 ChannelId, ChannelRole, Database, NewUserParams, UserId,
12 },12 },
13 test_both_dbs,13 test_both_dbs,
14};14};
15use std::sync::Arc;15use std::sync::{
16 atomic::{AtomicI32, Ordering},
17 Arc,
18};
16 19
17test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);20test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
18 21
@@ -850,6 +853,101 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) { · 831 unmodified lines
850 );853 );
851}854}
852 855
856test_both_dbs!(
857 test_user_is_channel_participant,
858 test_user_is_channel_participant_postgres,
859 test_user_is_channel_participant_sqlite
860);
861
862async 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
853#[track_caller]951#[track_caller]
854fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {952fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
855 let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();953 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
874 972
875 pretty_assertions::assert_eq!(actual_map, expected_map)973 pretty_assertions::assert_eq!(actual_map, expected_map)
876}974}
975
976static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
977
978async 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
6use channel::{ChannelId, ChannelMembership, ChannelStore};6use channel::{ChannelId, ChannelMembership, ChannelStore};
7use client::User;7use client::User;
8use gpui::{executor::Deterministic, ModelHandle, TestAppContext};8use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
9use rpc::{proto, RECEIVE_TIMEOUT};9use rpc::{
10 proto::{self},
11 RECEIVE_TIMEOUT,
12};
10use std::sync::Arc;13use std::sync::Arc;
11 14
12#[gpui::test]15#[gpui::test]
crates/rpc/proto/zed.proto+1−0
@@ -1040,6 +1040,7 @@ enum ChannelRole { · 1039 unmodified lines
1040 Admin = 0;1040 Admin = 0;
1041 Member = 1;1041 Member = 1;
1042 Guest = 2;1042 Guest = 2;
1043 Banned = 3;
1043}1044}
1044 1045
1045message SetChannelMemberRole {1046message 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