diff --git a/CHANGELOG.md b/CHANGELOG.md index 89870a39a..fdab064d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. ### Changes +- Serviceability + - Gate UpdateUser on `USER_ADMIN`, CheckAccessPass on `ACTIVATOR`, and accesspass CheckStatus on `ACTIVATOR|USER_ADMIN` via `authorize()`; user create and set_bgp_status remain owner-authorized (not part of the admin Permission system). (#3984) - Collector - Harden ledger writes against a slow/degraded RPC endpoint: bound each RPC request (default 15s, `--ledger-rpc-timeout`), size the connection pool above the submitter concurrency (default 128, `--ledger-rpc-max-conns`), and deadline each submission attempt so it fails fast and retries with a fresh blockhash instead of sending an expired one and failing preflight with `BlockhashNotFound`. (#3973) - E2E diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/check_status.rs b/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/check_status.rs index 073f7795f..88349fdcc 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/check_status.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/accesspass/check_status.rs @@ -1,16 +1,18 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_write, - state::{accesspass::AccessPass, globalstate::GlobalState}, + state::{accesspass::AccessPass, globalstate::GlobalState, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; use core::fmt; +#[cfg(test)] +use solana_program::msg; use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, - msg, pubkey::Pubkey, }; @@ -63,19 +65,17 @@ pub fn process_check_status_access_pass( "PDA Account is not writable" ); - // Parse the global state account & check if the payer is in the allowlist + // Authorization: ACTIVATOR or foundation, via a Permission account or the legacy + // activator_authority_pk / foundation_allowlist (ACTIVATOR covers the activator + // authority, USER_ADMIN covers foundation). let globalstate = GlobalState::try_from(globalstate_account)?; - if globalstate.activator_authority_pk != *payer_account.key - && !globalstate.foundation_allowlist.contains(payer_account.key) - { - msg!( - "activator_authority_pk: {} payer: {} foundation_allowlist: {:?}", - globalstate.activator_authority_pk, - payer_account.key, - globalstate.foundation_allowlist - ); - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::ACTIVATOR | permission_flags::USER_ADMIN, + )?; let mut accesspass = AccessPass::try_from(accesspass_account)?; // Update status diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/user/check_access_pass.rs b/smartcontract/programs/doublezero-serviceability/src/processors/user/check_access_pass.rs index 26b5c3d89..d188d859b 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/user/check_access_pass.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/user/check_access_pass.rs @@ -1,4 +1,5 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, pda::get_accesspass_pda, processors::validation::validate_program_account, @@ -6,6 +7,7 @@ use crate::{ state::{ accesspass::AccessPass, globalstate::GlobalState, + permission::permission_flags, user::{User, UserStatus}, }, }; @@ -71,10 +73,15 @@ pub fn process_check_access_pass_user( "Invalid System Program Account Owner" ); + // Authorization: ACTIVATOR (Permission account) or activator authority (legacy). let globalstate = GlobalState::try_from(globalstate_account)?; - if globalstate.activator_authority_pk != *payer_account.key { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::ACTIVATOR, + )?; let mut user: User = User::try_from(user_account)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/user/update.rs b/smartcontract/programs/doublezero-serviceability/src/processors/user/update.rs index 137058426..72437bbb8 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/user/update.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/user/update.rs @@ -1,4 +1,5 @@ use crate::{ + authorize::{authorize, split_trailing_permission}, error::DoubleZeroError, format_option, helper::format_option_displayable, @@ -9,7 +10,7 @@ use crate::{ }, resource::ResourceType, serializer::try_acc_write, - state::{globalstate::GlobalState, tenant::Tenant, user::*}, + state::{globalstate::GlobalState, permission::permission_flags, tenant::Tenant, user::*}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -97,23 +98,19 @@ pub fn process_update_user( dz_prefix_accounts.push(next_account_info(accounts_iter)?); } - // Tenant accounts are optional — present when tenant_pk is being updated. - // We compute the expected count of remaining accounts to detect their presence. - // Remaining accounts: [old_tenant?, new_tenant?, payer, system] - // With tenants: 4 remaining. Without tenants: 2 remaining. - let remaining: Vec<_> = accounts_iter.collect(); - let has_tenant_accounts = remaining.len() >= 4; - let (old_tenant_account, new_tenant_account, payer_account, _system_program) = - if has_tenant_accounts { - ( - Some(remaining[0]), - Some(remaining[1]), - remaining[2], - remaining[3], - ) - } else { - (None, None, remaining[0], remaining[1]) - }; + // Remaining tail: [old_tenant?, new_tenant?, payer, system, permission?]. The + // optional tenant pair is present when tenant_pk is being updated. + // split_trailing_permission peels payer/system — and the optional payer + // Permission PDA the SDK appends when it exists — off the tail by PDA match, so + // the tenant accounts are detected unambiguously via what's left (`leading`). + let remaining: Vec<&AccountInfo> = accounts_iter.collect(); + let (payer_account, _system_program, leading, permission_account) = + split_trailing_permission(program_id, &remaining)?; + let (old_tenant_account, new_tenant_account) = if leading.len() >= 2 { + (Some(leading[0]), Some(leading[1])) + } else { + (None, None) + }; #[cfg(test)] msg!("process_update_user({:?})", value); @@ -130,10 +127,16 @@ pub fn process_update_user( "GlobalState" ); + // Authorization: USER_ADMIN (Permission account) or foundation (legacy). This is + // an administrative operation — the User owner does not update via this path. let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + &mut permission_account.into_iter(), + payer_account.key, + &globalstate, + permission_flags::USER_ADMIN, + )?; let mut user: User = User::try_from(user_account)?; diff --git a/smartcontract/sdk/rs/src/commands/accesspass/check_status.rs b/smartcontract/sdk/rs/src/commands/accesspass/check_status.rs index 4030fab4f..8207b1d61 100644 --- a/smartcontract/sdk/rs/src/commands/accesspass/check_status.rs +++ b/smartcontract/sdk/rs/src/commands/accesspass/check_status.rs @@ -21,7 +21,7 @@ impl CheckStatusAccessPassCommand { let (pda_pubkey, _) = get_accesspass_pda(&client.get_program_id(), &self.client_ip, &self.user_payer); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::CheckStatusAccessPass(CheckStatusAccessPassArgs {}), vec![ AccountMeta::new(pda_pubkey, false), @@ -56,7 +56,7 @@ mod tests { let (pda_pubkey, _) = get_accesspass_pda(&client.get_program_id(), &client_ip, &payer); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::CheckStatusAccessPass( CheckStatusAccessPassArgs {}, diff --git a/smartcontract/sdk/rs/src/commands/user/check_access_pass.rs b/smartcontract/sdk/rs/src/commands/user/check_access_pass.rs index a6f176a56..997dde042 100644 --- a/smartcontract/sdk/rs/src/commands/user/check_access_pass.rs +++ b/smartcontract/sdk/rs/src/commands/user/check_access_pass.rs @@ -27,7 +27,7 @@ impl CheckUserAccessPassCommand { let (accesspass_pk, _) = get_accesspass_pda(&client.get_program_id(), &user.client_ip, &user.owner); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::CheckUserAccessPass(CheckUserAccessPassArgs {}), vec![ AccountMeta::new(self.user_pubkey, false), diff --git a/smartcontract/sdk/rs/src/commands/user/update.rs b/smartcontract/sdk/rs/src/commands/user/update.rs index 8f4d324f0..98ab9562d 100644 --- a/smartcontract/sdk/rs/src/commands/user/update.rs +++ b/smartcontract/sdk/rs/src/commands/user/update.rs @@ -109,7 +109,7 @@ impl UpdateUserCommand { accounts.push(AccountMeta::new(new_tenant_pk, false)); } - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::UpdateUser(UserUpdateArgs { user_type: self.user_type, cyoa_type: self.cyoa_type, @@ -211,7 +211,7 @@ mod tests { get_resource_extension_pda(&program_id, ResourceType::DzPrefixBlock(device_pk, 0)); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::UpdateUser(UserUpdateArgs { user_type: None,