|
1 | 1 | use chrono::Local; |
2 | 2 | use session_sharing_protocol::common::SessionId; |
3 | | -use warpui::{App, SingletonEntity, ViewHandle}; |
4 | | - |
5 | | -use super::{SharingDialog, SharingDialogMode}; |
6 | | -use crate::drive::sharing::ShareableObject; |
| 3 | +use warpui::{App, SingletonEntity, TypedActionView, ViewHandle}; |
| 4 | + |
| 5 | +use super::{SharingDialog, SharingDialogAction, SharingDialogMode}; |
| 6 | +use crate::auth::UserUid; |
| 7 | +use crate::cloud_object::model::persistence::CloudModel; |
| 8 | +use crate::cloud_object::{CloudObject, Owner}; |
| 9 | +use crate::drive::sharing::{ShareableObject, SharingAccessLevel}; |
| 10 | +use crate::server::ids::{ClientId, ServerId, SyncId}; |
7 | 11 | use crate::terminal::TerminalView; |
8 | 12 | use crate::terminal::shared_session::manager::Manager; |
9 | 13 | use crate::terminal::shared_session::{SharedSessionSource, SharedSessionStatus}; |
10 | 14 | use crate::test_util::add_window_with_terminal; |
11 | 15 | use crate::test_util::terminal::{ |
12 | 16 | add_window_with_id_and_terminal, initialize_app_for_terminal_view, |
13 | 17 | }; |
| 18 | +use crate::workflows::workflow::Workflow; |
| 19 | +use crate::workflows::{CloudWorkflow, CloudWorkflowModel}; |
14 | 20 | use crate::workspaces::team::{Team, TeamVisibility}; |
15 | 21 | use crate::workspaces::user_workspaces::UserWorkspaces; |
16 | 22 | use crate::workspaces::workspace::{ |
@@ -310,24 +316,139 @@ fn link_sharing_gates_follow_a_window_onto_its_new_team() { |
310 | 316 | }); |
311 | 317 | } |
312 | 318 |
|
| 319 | +/// Distinct from a window that has no team, which resolves a scope and is *permitted* to share. |
| 320 | +/// A window `UserWorkspaces` has never seen resolves no scope at all, and there is then no |
| 321 | +/// policy to consult. |
313 | 322 | #[test] |
314 | | -fn link_sharing_gates_deny_when_the_dialogs_window_has_no_team() { |
| 323 | +fn link_sharing_gates_deny_when_the_dialogs_window_cannot_be_resolved() { |
315 | 324 | App::test((), |mut app| async move { |
316 | 325 | initialize_app_for_terminal_view(&mut app); |
317 | 326 |
|
318 | 327 | let (window_id, _terminal) = add_window_with_id_and_terminal(&mut app, None); |
319 | 328 | install_workspace_with_teams(&mut app, vec![team_with_link_sharing(123, "team", true)]); |
320 | 329 |
|
321 | | - // Deliberately left unregistered: no window team can be resolved for this dialog. |
| 330 | + // Deliberately never registered: `add_window_with_id_and_terminal` roots the window in |
| 331 | + // a `TerminalView`, so nothing calls `UserWorkspaces::register_window` for it. |
322 | 332 | let dialog = app.add_typed_action_view(window_id, |ctx| SharingDialog::new(None, ctx)); |
323 | 333 |
|
324 | 334 | dialog.read(&app, |dialog, ctx| { |
325 | 335 | assert!( |
326 | 336 | !dialog.can_anyone_with_link_share(ctx), |
327 | | - "a dialog whose window team cannot be resolved must not offer a sharing channel \ |
328 | | - governed by an unknown policy" |
| 337 | + "a dialog with no resolvable scope must not offer a sharing channel governed by \ |
| 338 | + an unknown policy" |
329 | 339 | ); |
330 | 340 | assert!(!dialog.can_direct_link_share(ctx)); |
331 | 341 | }); |
332 | 342 | }); |
333 | 343 | } |
| 344 | + |
| 345 | +/// Puts a Warp Drive object in the cloud model under a server id, so the sharing dialog can |
| 346 | +/// target it and `UpdateManager` can find it. |
| 347 | +fn add_shareable_object(app: &mut App) -> ServerId { |
| 348 | + let object_uid: ServerId = 789.into(); |
| 349 | + let mut object = CloudWorkflow::new_local( |
| 350 | + CloudWorkflowModel { |
| 351 | + data: Workflow::new("shared workflow", "echo shared"), |
| 352 | + }, |
| 353 | + Owner::User { |
| 354 | + user_uid: UserUid::new("owner"), |
| 355 | + }, |
| 356 | + None, |
| 357 | + ClientId::default(), |
| 358 | + ); |
| 359 | + object.id = SyncId::ServerId(object_uid); |
| 360 | + |
| 361 | + let cloud_model = CloudModel::handle(&*app); |
| 362 | + cloud_model.update(app, |cloud_model, _| { |
| 363 | + cloud_model.add_object(object.id, object); |
| 364 | + }); |
| 365 | + object_uid |
| 366 | +} |
| 367 | + |
| 368 | +/// Whether a permissions change reached the object. `UpdateManager` marks the object |
| 369 | +/// synchronously, before it issues any request, so a dispatch the dialog refused leaves this |
| 370 | +/// clear. |
| 371 | +fn permissions_change_reached_object(app: &App, object_uid: ServerId) -> bool { |
| 372 | + app.read(|ctx| { |
| 373 | + CloudModel::as_ref(ctx) |
| 374 | + .get_by_uid(&object_uid.uid()) |
| 375 | + .expect("the targeted object should be in the cloud model") |
| 376 | + .metadata() |
| 377 | + .pending_changes_statuses |
| 378 | + .has_pending_permissions_change |
| 379 | + }) |
| 380 | +} |
| 381 | + |
| 382 | +fn set_link_permissions( |
| 383 | + dialog: &ViewHandle<SharingDialog>, |
| 384 | + access_level: Option<SharingAccessLevel>, |
| 385 | + app: &mut App, |
| 386 | +) { |
| 387 | + dialog.update(app, |dialog, ctx| { |
| 388 | + dialog.handle_action(&SharingDialogAction::SetLinkPermissions(access_level), ctx); |
| 389 | + }); |
| 390 | +} |
| 391 | + |
| 392 | +/// The link-sharing menu builds its items once, when it opens, so acting on one has to re-read |
| 393 | +/// the policy. Deleting the guard in the `SetLinkPermissions` handler must fail this test. |
| 394 | +#[test] |
| 395 | +fn set_link_permissions_refuses_to_grant_under_a_forbidding_team() { |
| 396 | + App::test((), |mut app| async move { |
| 397 | + initialize_app_for_terminal_view(&mut app); |
| 398 | + |
| 399 | + let (window_id, _terminal) = add_window_with_id_and_terminal(&mut app, None); |
| 400 | + let forbidden_team = team_with_link_sharing(456, "forbids-sharing", false); |
| 401 | + install_workspace_with_teams(&mut app, vec![forbidden_team.clone()]); |
| 402 | + |
| 403 | + let user_workspaces = UserWorkspaces::handle(&app); |
| 404 | + user_workspaces.update(&mut app, |user_workspaces, ctx| { |
| 405 | + user_workspaces.set_team_for_window(window_id, forbidden_team.uid, ctx); |
| 406 | + }); |
| 407 | + |
| 408 | + let object_uid = add_shareable_object(&mut app); |
| 409 | + let dialog = app.add_typed_action_view(window_id, |ctx| { |
| 410 | + SharingDialog::new(Some(ShareableObject::WarpDriveObject(object_uid)), ctx) |
| 411 | + }); |
| 412 | + |
| 413 | + set_link_permissions(&dialog, Some(SharingAccessLevel::View), &mut app); |
| 414 | + assert!( |
| 415 | + !permissions_change_reached_object(&app, object_uid), |
| 416 | + "granting link access under a team that forbids it must not reach the object" |
| 417 | + ); |
| 418 | + |
| 419 | + // Revoking is how a user tightens an over-shared object, so the guard must let it |
| 420 | + // through: the forbidding policy is the reason to allow this, not to block it. |
| 421 | + set_link_permissions(&dialog, None, &mut app); |
| 422 | + assert!( |
| 423 | + permissions_change_reached_object(&app, object_uid), |
| 424 | + "revoking link access must go through even under a team that forbids granting it" |
| 425 | + ); |
| 426 | + }); |
| 427 | +} |
| 428 | + |
| 429 | +#[test] |
| 430 | +fn set_link_permissions_grants_under_a_permitting_team() { |
| 431 | + App::test((), |mut app| async move { |
| 432 | + initialize_app_for_terminal_view(&mut app); |
| 433 | + |
| 434 | + let (window_id, _terminal) = add_window_with_id_and_terminal(&mut app, None); |
| 435 | + let permitted_team = team_with_link_sharing(123, "permits-sharing", true); |
| 436 | + install_workspace_with_teams(&mut app, vec![permitted_team.clone()]); |
| 437 | + |
| 438 | + let user_workspaces = UserWorkspaces::handle(&app); |
| 439 | + user_workspaces.update(&mut app, |user_workspaces, ctx| { |
| 440 | + user_workspaces.set_team_for_window(window_id, permitted_team.uid, ctx); |
| 441 | + }); |
| 442 | + |
| 443 | + let object_uid = add_shareable_object(&mut app); |
| 444 | + let dialog = app.add_typed_action_view(window_id, |ctx| { |
| 445 | + SharingDialog::new(Some(ShareableObject::WarpDriveObject(object_uid)), ctx) |
| 446 | + }); |
| 447 | + |
| 448 | + set_link_permissions(&dialog, Some(SharingAccessLevel::View), &mut app); |
| 449 | + assert!( |
| 450 | + permissions_change_reached_object(&app, object_uid), |
| 451 | + "the guard must let a grant through when the window's team permits link sharing" |
| 452 | + ); |
| 453 | + }); |
| 454 | +} |
0 commit comments