Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions app/src/drive/sharing/dialog/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ use crate::word_block_editor::{
WordBlockStyles,
};
use crate::workspace::{ToastStack, WorkspaceAction};
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::workspaces::user_workspaces::{TeamContext, UserWorkspaces, UserWorkspacesEvent};
use crate::{TelemetryEvent, send_telemetry_from_ctx};

mod inheritance;
Expand Down Expand Up @@ -262,6 +262,10 @@ impl SharingDialog {
},
);

ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, _, event, ctx| {
me.handle_user_workspaces_event(event, ctx);
});

let invite_form = EmailInviteForm {
email_editor: ctx.add_typed_action_view(|ctx| {
let mut view = WordBlockEditorView::new(
Expand Down Expand Up @@ -302,6 +306,26 @@ impl SharingDialog {
}
}

/// The link-sharing controls are drawn from this window's team policy, so a team change has
/// to redraw: the invite form and the link-sharing row appear and disappear with it, and a
/// control left on screen under the old team's policy is one the action guards now refuse.
fn handle_user_workspaces_event(
&mut self,
event: &UserWorkspacesEvent,
ctx: &mut ViewContext<Self>,
) {
let changes_this_windows_policy = match event {
UserWorkspacesEvent::TeamsChanged => true,
UserWorkspacesEvent::WindowTeamChanged { window_id } => *window_id == ctx.window_id(),
// Everything else this model emits is workspace administration, which does not
// change which team this window shares as.
_ => false,
};
if changes_this_windows_policy {
ctx.notify();
}
}

fn handle_update_manager_event(
&mut self,
event: &UpdateManagerEvent,
Expand Down Expand Up @@ -475,12 +499,33 @@ impl SharingDialog {
self.target.is_some() && self.access_level(app).can_edit_access()
}

/// The team this dialog's window is sharing as, resolved afresh on every read so a window
/// that moves to another team is never governed by the team it opened with.
///
/// `None` only when no scope can be resolved at all: this view is no longer in a window,
/// its window was never registered with `UserWorkspaces`, or its window names a team that
/// has since left the current workspace. A window that simply *has* no team is a different
/// thing — it resolves a scope, whose `team_uid()` is then `None`.
fn team_scope<'a>(&self, app: &'a AppContext) -> Option<TeamContext<'a>> {
UserWorkspaces::as_ref(app).team_context(&self.self_handle, app)
}

/// Whether this window's team permits sharing the target with anyone who holds its link.
///
/// A window with no team permits it, per `TeamScope`'s contract. Only a window with no
/// resolvable scope denies, because offering a sharing channel when we cannot say which
/// policy governs it is the unsafe direction to guess in.
fn can_anyone_with_link_share(&self, app: &AppContext) -> bool {
UserWorkspaces::as_ref(app).is_anyone_with_link_sharing_enabled()
self.team_scope(app).is_some_and(|scope| {
UserWorkspaces::as_ref(app).is_anyone_with_link_sharing_enabled(&scope)
})
}

/// Whether this window's team permits sharing the target directly with named people. See
/// [`Self::can_anyone_with_link_share`] for what an unresolvable scope means.
fn can_direct_link_share(&self, app: &AppContext) -> bool {
UserWorkspaces::as_ref(app).is_direct_link_sharing_enabled()
self.team_scope(app)
.is_some_and(|scope| UserWorkspaces::as_ref(app).is_direct_link_sharing_enabled(&scope))
}

/// The editability state of the object.
Expand Down Expand Up @@ -1615,6 +1660,15 @@ impl SharingDialog {

/// Send all pending email invitations.
fn send_invitations(&mut self, ctx: &mut ViewContext<Self>) {
// Re-read the policy instead of trusting the render that put the form on screen: an
// open dialog whose window moved to a team that forbids direct sharing must not send
// the invitations that team now disallows. Redraw on the way out so the form the user
// just submitted disappears rather than sitting there as a dead control.
if !self.can_direct_link_share(ctx) {
ctx.notify();
return;
}

let form_state = self.invite_form_state(ctx);
if !form_state.is_valid() {
return;
Expand Down Expand Up @@ -2930,6 +2984,15 @@ impl TypedActionView for SharingDialog {
}
SharingDialogAction::SetLinkPermissions(access_level) => {
self.set_open_menu(OpenMenuState::None, ctx);
// The menu's items were built when it opened. Re-read the policy so a window
// that has since moved to a team forbidding link sharing cannot act on the
// permission it was offered under the old one. Only granting is blocked:
// `None` revokes link access, and a user tightening an over-shared object must
// not be turned away by the very policy that wants it tightened.
if access_level.is_some() && !self.can_anyone_with_link_share(ctx) {
ctx.notify();
return;
}
if let Some(ShareableObject::WarpDriveObject(id)) = self.target.as_ref() {
UpdateManager::handle(ctx).update(ctx, move |update_manager, ctx| {
update_manager.set_object_link_permissions(*id, *access_level, ctx);
Expand Down
Loading
Loading