-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
test: PriorityRpc
, RegisterProvider
, and ManageCanister
caller authorizations
#122
Changes from all commits
0a48258
304257e
eabb728
d2bfa31
7e592cf
8f3c536
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ pub fn is_authorized(principal: &Principal, auth: Auth) -> bool { | |
|
||
pub fn require_admin_or_controller() -> Result<(), String> { | ||
let caller = ic_cdk::caller(); | ||
if is_authorized(&caller, Auth::ManageService) || ic_cdk::api::is_controller(&caller) { | ||
if is_authorized(&caller, Auth::ManageCanister) || ic_cdk::api::is_controller(&caller) { | ||
Ok(()) | ||
} else { | ||
Err("You are not authorized".to_string()) | ||
|
@@ -30,7 +30,7 @@ pub fn require_register_provider() -> Result<(), String> { | |
} | ||
|
||
pub fn is_rpc_allowed(caller: &Principal) -> bool { | ||
METADATA.with(|m| m.borrow().get().open_rpc_access) || is_authorized(caller, Auth::Rpc) | ||
METADATA.with(|m| m.borrow().get().open_rpc_access) || is_authorized(caller, Auth::PriorityRpc) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was supposed to be named |
||
} | ||
|
||
pub fn do_authorize(principal: Principal, auth: Auth) { | ||
|
@@ -69,16 +69,16 @@ fn test_authorization() { | |
let principal2 = | ||
Principal::from_text("yxhtl-jlpgx-wqnzc-ysego-h6yqe-3zwfo-o3grn-gvuhm-nz3kv-ainub-6ae") | ||
.unwrap(); | ||
assert!(!is_authorized(&principal1, Auth::Rpc)); | ||
assert!(!is_authorized(&principal2, Auth::Rpc)); | ||
assert!(!is_authorized(&principal1, Auth::PriorityRpc)); | ||
assert!(!is_authorized(&principal2, Auth::PriorityRpc)); | ||
|
||
do_authorize(principal1, Auth::Rpc); | ||
assert!(is_authorized(&principal1, Auth::Rpc)); | ||
assert!(!is_authorized(&principal2, Auth::Rpc)); | ||
do_authorize(principal1, Auth::PriorityRpc); | ||
assert!(is_authorized(&principal1, Auth::PriorityRpc)); | ||
assert!(!is_authorized(&principal2, Auth::PriorityRpc)); | ||
|
||
do_deauthorize(principal1, Auth::Rpc); | ||
assert!(!is_authorized(&principal1, Auth::Rpc)); | ||
assert!(!is_authorized(&principal2, Auth::Rpc)); | ||
do_deauthorize(principal1, Auth::PriorityRpc); | ||
assert!(!is_authorized(&principal1, Auth::PriorityRpc)); | ||
assert!(!is_authorized(&principal2, Auth::PriorityRpc)); | ||
|
||
do_authorize(principal1, Auth::RegisterProvider); | ||
assert!(is_authorized(&principal1, Auth::RegisterProvider)); | ||
|
@@ -87,11 +87,10 @@ fn test_authorization() { | |
do_deauthorize(principal1, Auth::RegisterProvider); | ||
assert!(!is_authorized(&principal1, Auth::RegisterProvider)); | ||
|
||
do_authorize(principal2, Auth::ManageService); | ||
assert!(!is_authorized(&principal1, Auth::ManageService)); | ||
assert!(is_authorized(&principal2, Auth::ManageService)); | ||
do_authorize(principal2, Auth::ManageCanister); | ||
assert!(!is_authorized(&principal1, Auth::ManageCanister)); | ||
assert!(is_authorized(&principal2, Auth::ManageCanister)); | ||
|
||
assert!(!is_authorized(&principal2, Auth::Rpc)); | ||
assert!(!is_authorized(&principal2, Auth::FreeRpc)); | ||
assert!(!is_authorized(&principal2, Auth::PriorityRpc)); | ||
assert!(!is_authorized(&principal2, Auth::RegisterProvider)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -172,8 +172,19 @@ impl EvmRpcSetup { | |
self.call_query("getProviders", Encode!().unwrap()) | ||
} | ||
|
||
pub fn register_provider(&self, args: RegisterProviderArgs) -> CallFlow<u64> { | ||
pub fn register_provider(&self, args: RegisterProviderArgs) -> u64 { | ||
self.call_update("registerProvider", Encode!(&args).unwrap()) | ||
.wait() | ||
} | ||
|
||
pub fn unregister_provider(&self, provider_id: u64) -> bool { | ||
self.call_update("unregisterProvider", Encode!(&provider_id).unwrap()) | ||
.wait() | ||
} | ||
|
||
pub fn update_provider(&self, args: UpdateProviderArgs) { | ||
self.call_update("updateProvider", Encode!(&args).unwrap()) | ||
Comment on lines
176
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume panicking in these functions is okay, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep; these functions are only used by us to manage the canister. |
||
.wait() | ||
} | ||
|
||
pub fn authorize_caller(self, auth: Auth) -> Self { | ||
|
@@ -267,7 +278,7 @@ impl EvmRpcSetup { | |
pub fn eth_send_raw_transaction( | ||
&self, | ||
source: RpcSource, | ||
signed_raw_transaction_hex: String, | ||
signed_raw_transaction_hex: &str, | ||
) -> CallFlow<MultiRpcResult<SendRawTransactionResult>> { | ||
self.call_update( | ||
"eth_sendRawTransaction", | ||
|
@@ -441,29 +452,25 @@ fn should_register_provider() { | |
.collect::<Vec<_>>() | ||
); | ||
let n_providers = 2; | ||
let a_id = setup | ||
.register_provider(RegisterProviderArgs { | ||
chain_id: 1, | ||
hostname: ANKR_HOSTNAME.to_string(), | ||
credential_path: "".to_string(), | ||
credential_headers: None, | ||
cycles_per_call: 0, | ||
cycles_per_message_byte: 0, | ||
}) | ||
.wait(); | ||
let b_id = setup | ||
.register_provider(RegisterProviderArgs { | ||
chain_id: 5, | ||
hostname: CLOUDFLARE_HOSTNAME.to_string(), | ||
credential_path: "/test-path".to_string(), | ||
credential_headers: Some(vec![HttpHeader { | ||
name: "Test-Authorization".to_string(), | ||
value: "---".to_string(), | ||
}]), | ||
cycles_per_call: 0, | ||
cycles_per_message_byte: 0, | ||
}) | ||
.wait(); | ||
let a_id = setup.register_provider(RegisterProviderArgs { | ||
chain_id: 1, | ||
hostname: ANKR_HOSTNAME.to_string(), | ||
credential_path: "".to_string(), | ||
credential_headers: None, | ||
cycles_per_call: 0, | ||
cycles_per_message_byte: 0, | ||
}); | ||
let b_id = setup.register_provider(RegisterProviderArgs { | ||
chain_id: 5, | ||
hostname: CLOUDFLARE_HOSTNAME.to_string(), | ||
credential_path: "/test-path".to_string(), | ||
credential_headers: Some(vec![HttpHeader { | ||
name: "Test-Authorization".to_string(), | ||
value: "---".to_string(), | ||
}]), | ||
cycles_per_call: 0, | ||
cycles_per_message_byte: 0, | ||
}); | ||
assert_eq!(a_id + 1, b_id); | ||
let providers = setup.get_providers(); | ||
assert_eq!(providers.len(), get_default_providers().len() + n_providers); | ||
|
@@ -493,6 +500,44 @@ fn should_register_provider() { | |
) | ||
} | ||
|
||
#[test] | ||
#[should_panic(expected = "You are not authorized")] | ||
fn should_panic_if_unauthorized_register_provider() { | ||
let setup = EvmRpcSetup::new(); | ||
setup.register_provider(RegisterProviderArgs { | ||
chain_id: 1, | ||
hostname: ANKR_HOSTNAME.to_string(), | ||
credential_path: "".to_string(), | ||
credential_headers: None, | ||
cycles_per_call: 0, | ||
cycles_per_message_byte: 0, | ||
}); | ||
} | ||
|
||
#[test] | ||
#[should_panic(expected = "Provider owner != caller")] | ||
fn should_panic_if_unauthorized_update_provider() { | ||
// Providers can only be updated by the original owner | ||
let setup = EvmRpcSetup::new().authorize_caller(Auth::RegisterProvider); | ||
setup.update_provider(UpdateProviderArgs { | ||
provider_id: 0, | ||
hostname: Some("unauthorized.host".to_string()), | ||
credential_path: None, | ||
credential_headers: None, | ||
cycles_per_call: None, | ||
cycles_per_message_byte: None, | ||
primary: None, | ||
}); | ||
} | ||
|
||
#[test] | ||
#[should_panic(expected = "Not authorized")] | ||
fn should_panic_if_unauthorized_unregister_provider() { | ||
// Only the `ManageCanister` authorization may unregister a provider | ||
let setup = EvmRpcSetup::new().authorize_caller(Auth::RegisterProvider); | ||
setup.unregister_provider(0); | ||
} | ||
|
||
fn mock_request(builder_fn: impl Fn(MockOutcallBuilder) -> MockOutcallBuilder) { | ||
let setup = EvmRpcSetup::new().authorize_caller(Auth::FreeRpc); | ||
|
||
|
@@ -845,7 +890,7 @@ fn eth_send_raw_transaction_should_succeed() { | |
let response = setup | ||
.eth_send_raw_transaction( | ||
RpcSource::EthMainnet(None), | ||
"0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83".to_string(), | ||
"0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83", | ||
) | ||
.mock_http(MockOutcallBuilder::new( | ||
200, | ||
|
@@ -1030,7 +1075,7 @@ fn candid_rpc_should_represent_inconsistent_results() { | |
let results = setup | ||
.eth_send_raw_transaction( | ||
RpcSource::EthMainnet(Some(vec![EthMainnetService::Ankr, EthMainnetService::Cloudflare])), | ||
"0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83".to_string(), | ||
"0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83", | ||
) | ||
.mock_http_once(MockOutcallBuilder::new( | ||
200, | ||
|
@@ -1078,7 +1123,7 @@ fn candid_rpc_should_handle_already_known() { | |
let result = setup | ||
.eth_send_raw_transaction( | ||
RpcSource::EthMainnet(Some(vec![EthMainnetService::Ankr, EthMainnetService::Cloudflare])), | ||
"0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83".to_string(), | ||
"0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83", | ||
) | ||
.mock_http_once(MockOutcallBuilder::new( | ||
200, | ||
|
@@ -1107,3 +1152,61 @@ fn candid_rpc_should_handle_already_known() { | |
} | ||
); | ||
} | ||
|
||
#[test] | ||
#[should_panic(expected = "You are not authorized")] | ||
fn should_panic_if_unauthorized_set_rpc_access() { | ||
// Only `ManageCanister` can restrict RPC access | ||
let setup = EvmRpcSetup::new(); | ||
setup.set_open_rpc_access(false); | ||
} | ||
|
||
#[test] | ||
fn should_restrict_rpc_access() { | ||
let mut setup = EvmRpcSetup::new().authorize_caller(Auth::FreeRpc); | ||
setup.clone().as_controller().set_open_rpc_access(false); | ||
let result = setup | ||
.eth_get_transaction_count( | ||
RpcSource::EthMainnet(Some(vec![EthMainnetService::Ankr])), | ||
candid_types::GetTransactionCountArgs { | ||
address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(), | ||
block: candid_types::BlockTag::Latest, | ||
}, | ||
) | ||
.wait() | ||
.expect_consistent(); | ||
assert_eq!( | ||
result, | ||
Err(RpcError::ProviderError(ProviderError::NoPermission)) | ||
); | ||
setup = setup.authorize_caller(Auth::PriorityRpc); | ||
let result = setup | ||
.eth_get_transaction_count( | ||
RpcSource::EthMainnet(Some(vec![EthMainnetService::Ankr])), | ||
candid_types::GetTransactionCountArgs { | ||
address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(), | ||
block: candid_types::BlockTag::Latest, | ||
}, | ||
) | ||
.mock_http(MockOutcallBuilder::new( | ||
200, | ||
r#"{"jsonrpc":"2.0","id":0,"result":"0x1"}"#, | ||
)) | ||
.wait() | ||
.expect_consistent(); | ||
assert_eq!(result, Ok(1.into())); | ||
let rpc_method = || RpcMethod("eth_getTransactionCount".to_string()); | ||
assert_eq!( | ||
setup.get_metrics(), | ||
Metrics { | ||
requests: hashmap! { | ||
(rpc_method(), RpcHost(ANKR_HOSTNAME.to_string())) => 1, | ||
}, | ||
responses: hashmap! { | ||
(rpc_method(), RpcHost(ANKR_HOSTNAME.to_string())) => 1, | ||
}, | ||
err_no_permission: 1, | ||
..Default::default() | ||
} | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renaming
ManageService
toManageCanister
for clarity. In all other cases, the term "service" refers to third-party RPC services rather than the EVM RPC canister itself. Updated the design document accordingly.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ManageCanister
could be confused with the management canister (or a management canister function), so this name is also not idea...What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I see what you're saying. Maybe just
Manage
then? I'll do this refactor in a separate PR so we can try a few different options.