Skip to content
Merged
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
281 changes: 281 additions & 0 deletions contract/contracts/hello-world/src/tests/access_control_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
//! positive tests confirm the same function succeeds when the correct
//! role calls it, ensuring we are not accidentally asserting a failure
//! caused by something unrelated to authorization (e.g. NotFound).
//!
//! Additionally, authorization boundary tests verify that authenticated
//! users cannot access or modify notification resources outside their
//! permitted scope (cross-user access rejection, ownership validation,
//! covering both read and write operations).

use crate::base::events::NotificationCategory;
use crate::base::types::GroupMember;
Expand Down Expand Up @@ -692,3 +697,279 @@ mod creator_or_admin_notifications {
client.reduce_usage(&id, &attacker);
}
}

// ============================================================================
// AUTHORIZATION BOUNDARY TESTS (Notification Resources)
// ============================================================================
//
// These tests verify that an authenticated user cannot access or modify
// notification resources owned by another user. They cover:
// * Cross-user access rejection (read + write).
// * Resource ownership validation.
// * Both read and write operations on notification resources.

mod authorization_boundary {
use super::*;

fn schedule(
client: &AutoShareContractClient<'_>,
env: &Env,
id: &BytesN<32>,
creator: &Address,
) {
set_now(env, 1_000);
client.schedule_notification(id, creator, &ONE_HOUR, &title(env, "boundary test"));
}

// ————————————————————————————————————————————————————————————————————————
// READ operations — cross-user access must be rejected
// ————————————————————————————————————————————————————————————————————————

#[test]
fn test_get_notification_owner_can_read() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let id = make_id(&test_env.env, 100);
schedule(&client, &test_env.env, &id, &owner);

let notification = client.get_notification(&id);
assert_eq!(notification.creator, owner);
}

#[test]
fn test_get_notification_cross_user_read_is_rejected() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let other_user = test_env.users.get(1).unwrap().clone();
let id = make_id(&test_env.env, 101);
schedule(&client, &test_env.env, &id, &owner);

// A different authenticated user must not be able to read the
// notification resource owned by `owner`.
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.get_notification_for(&id, &other_user);
}));
assert!(
result.is_err(),
"cross-user read of notification resource must be rejected"
);
}

#[test]
fn test_get_notification_admin_can_read() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let id = make_id(&test_env.env, 102);
schedule(&client, &test_env.env, &id, &owner);

// Admin retains privileged read access.
let notification = client.get_notification_for(&id, &test_env.admin);
assert_eq!(notification.creator, owner);
}

#[test]
fn test_is_notification_revoked_cross_user_read_is_rejected() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let other_user = test_env.users.get(1).unwrap().clone();
let id = make_id(&test_env.env, 103);
schedule(&client, &test_env.env, &id, &owner);

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.is_notification_revoked_for(&id, &other_user);
}));
assert!(
result.is_err(),
"cross-user read of revocation status must be rejected"
);
}

// ————————————————————————————————————————————————————————————————————————
// WRITE operations — cross-user access must be rejected
// ————————————————————————————————————————————————————————————————————————

#[test]
fn test_cancel_notification_cross_user_write_is_rejected() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let other_user = test_env.users.get(1).unwrap().clone();
let id = make_id(&test_env.env, 104);
schedule(&client, &test_env.env, &id, &owner);

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.cancel_notification(&id, &other_user);
}));
assert!(
result.is_err(),
"cross-user write (cancel) must be rejected"
);

// Ownership must be preserved: the original notification still exists.
let notification = client.get_notification(&id);
assert_eq!(notification.creator, owner);
}

#[test]
fn test_revoke_notification_cross_user_write_is_rejected() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let other_user = test_env.users.get(1).unwrap().clone();
let id = make_id(&test_env.env, 105);
schedule(&client, &test_env.env, &id, &owner);

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.revoke_notification(&id, &other_user);
}));
assert!(
result.is_err(),
"cross-user write (revoke) must be rejected"
);

// Revocation flag must not have been flipped by the unauthorized user.
assert!(!client.is_notification_revoked(&id));
}

#[test]
fn test_extend_notification_expiry_cross_user_write_is_rejected() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let other_user = test_env.users.get(1).unwrap().clone();
let id = make_id(&test_env.env, 106);
schedule(&client, &test_env.env, &id, &owner);

let before = client.get_notification(&id).expires_at;
set_now(&test_env.env, 2_000);

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.extend_notification_expiry(&id, &other_user, &ONE_HOUR);
}));
assert!(
result.is_err(),
"cross-user write (extend expiry) must be rejected"
);

// Expiry must be unchanged after the rejected write.
let after = client.get_notification(&id).expires_at;
assert_eq!(after, before);
}

// ————————————————————————————————————————————————————————————————————————
// Resource ownership validation
// ————————————————————————————————————————————————————————————————————————

#[test]
fn test_notification_ownership_is_recorded_on_schedule() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let id = make_id(&test_env.env, 107);
schedule(&client, &test_env.env, &id, &owner);

let notification = client.get_notification(&id);
assert_eq!(
notification.creator, owner,
"scheduled notification must record its creator as owner"
);
}

#[test]
fn test_ownership_validation_rejects_non_owner_on_write() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let non_owner = test_env.users.get(1).unwrap().clone();
let id = make_id(&test_env.env, 108);
schedule(&client, &test_env.env, &id, &owner);

// Non-owner must be rejected on every mutating operation.
assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.cancel_notification(&id, &non_owner);
}))
.is_err());

assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.revoke_notification(&id, &non_owner);
}))
.is_err());

assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.extend_notification_expiry(&id, &non_owner, &ONE_HOUR);
}))
.is_err());
}

#[test]
fn test_ownership_validation_allows_owner_on_write() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let id = make_id(&test_env.env, 109);
schedule(&client, &test_env.env, &id, &owner);

// Owner must be allowed to mutate their own resource.
client.revoke_notification(&id, &owner);
assert!(client.is_notification_revoked(&id));
}

// ————————————————————————————————————————————————————————————————————————
// Cross-user access across distinct resources
// ————————————————————————————————————————————————————————————————————————

#[test]
fn test_user_cannot_access_other_users_notification() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let user_a = test_env.users.get(0).unwrap().clone();
let user_b = test_env.users.get(1).unwrap().clone();

let id_a = make_id(&test_env.env, 110);
let id_b = make_id(&test_env.env, 111);
schedule(&client, &test_env.env, &id_a, &user_a);
schedule(&client, &test_env.env, &id_b, &user_b);

// user_b must not be able to cancel user_a's notification.
assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.cancel_notification(&id_a, &user_b);
}))
.is_err());

// user_a must not be able to cancel user_b's notification.
assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.cancel_notification(&id_b, &user_a);
}))
.is_err());

// Both resources remain intact and owned by their respective creators.
assert_eq!(client.get_notification(&id_a).creator, user_a);
assert_eq!(client.get_notification(&id_b).creator, user_b);
}

#[test]
fn test_cross_user_access_emits_authorization_failure_event() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let owner = test_env.users.get(0).unwrap().clone();
let other_user = test_env.users.get(1).unwrap().clone();
let id = make_id(&test_env.env, 112);
schedule(&client, &test_env.env, &id, &owner);

let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.cancel_notification(&id, &other_user);
}));

let event = latest_event_topics(&test_env.env, "authorization_failure")
.expect("cross-user access must emit AuthorizationFailure event");
// Topics: [name, caller, category, priority, action]
assert_eq!(event.len(), 5);
let topic_caller =
Address::try_from_val(&test_env.env, &event.get(1).unwrap()).unwrap();
assert_eq!(topic_caller, other_user);
}
}
102 changes: 102 additions & 0 deletions contract/contracts/hello-world/src/tests/access_log_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,105 @@ fn test_multiple_access_events_can_be_emitted() {
client.record_notification_access(&notification_id, &accessor2);
// Both succeed — audit trail is append-only.
}

#[test]
#[should_panic]
fn test_cross_user_read_access_is_rejected() {
let env = Env::default();
env.mock_all_auths();
let (admin, client) = setup(&env);
let notification_id = schedule_test_notification(&client, &env, &admin);
let unauthorized = Address::generate(&env);

// Unauthorized user must not be able to read another user's notification.
client.get_notification(&notification_id, &unauthorized);
}

#[test]
#[should_panic]
fn test_cross_user_write_access_is_rejected() {
let env = Env::default();
env.mock_all_auths();
let (admin, client) = setup(&env);
let notification_id = schedule_test_notification(&client, &env, &admin);
let unauthorized = Address::generate(&env);

// Unauthorized user must not be able to modify another user's notification.
client.cancel_notification(&notification_id, &unauthorized);
}

#[test]
fn test_resource_ownership_is_validated_for_owner() {
let env = Env::default();
env.mock_all_auths();
let (admin, client) = setup(&env);
let notification_id = schedule_test_notification(&client, &env, &admin);

// Owner should be able to read their own notification.
let notification = client.get_notification(&notification_id, &admin);
assert_eq!(notification.id, notification_id);
}

#[test]
#[should_panic]
fn test_resource_ownership_is_validated_for_non_owner() {
let env = Env::default();
env.mock_all_auths();
let (admin, client) = setup(&env);
let notification_id = schedule_test_notification(&client, &env, &admin);
let non_owner = Address::generate(&env);

// Non-owner must not be able to read the notification.
client.get_notification(&notification_id, &non_owner);
}

#[test]
#[should_panic]
fn test_owner_cannot_modify_other_users_notification() {
let env = Env::default();
env.mock_all_auths();
let (admin, client) = setup(&env);
let notification_id = schedule_test_notification(&client, &env, &admin);

let other_user = Address::generate(&env);
let mut other_id_bytes = [0u8; 32];
other_id_bytes[0] = 77;
let other_notification_id = BytesN::from_array(&env, &other_id_bytes);
client.schedule_notification(
&other_notification_id,
&other_user,
&3600u64,
&String::from_str(&env, "Other"),
&NotificationPriority::Medium,
);

// Admin must not be able to cancel another user's notification.
client.cancel_notification(&other_notification_id, &admin);
}

#[test]
fn test_owner_can_modify_own_notification() {
let env = Env::default();
env.mock_all_auths();
let (admin, client) = setup(&env);
let notification_id = schedule_test_notification(&client, &env, &admin);

// Owner should be able to cancel their own notification.
client.cancel_notification(&notification_id, &admin);
}

#[test]
#[should_panic]
fn test_cross_user_access_rejected_for_nonexistent_notification() {
let env = Env::default();
env.mock_all_auths();
let (_admin, client) = setup(&env);

let mut id_bytes = [0u8; 32];
id_bytes[0] = 123;
let notification_id = BytesN::from_array(&env, &id_bytes);
let unauthorized = Address::generate(&env);

// Access to a nonexistent notification must be rejected.
client.get_notification(&notification_id, &unauthorized);
}
Loading