use crate::constants::internal::REVOKED_CONTROLLERS;
use crate::constants::shared::{MAX_NUMBER_OF_ACCESS_KEYS, MAX_NUMBER_OF_ADMIN_CONTROLLERS};
use crate::env::{CONSOLE, OBSERVATORY};
use crate::errors::{
JUNO_ERROR_CONTROLLERS_ADMIN_NO_EXPIRY, JUNO_ERROR_CONTROLLERS_ANONYMOUS_NOT_ALLOWED,
JUNO_ERROR_CONTROLLERS_EXPIRY_IN_PAST, JUNO_ERROR_CONTROLLERS_MAX_NUMBER,
JUNO_ERROR_CONTROLLERS_REVOKED_NOT_ALLOWED,
};
use crate::ic::api::{id, is_canister_controller, time};
use crate::types::interface::SetAccessKey;
use crate::types::state::{AccessKey, AccessKeyId, AccessKeyScope, AccessKeys};
use crate::utils::{principal_anonymous, principal_equal, principal_not_anonymous};
use candid::Principal;
use std::collections::HashMap;
pub fn init_admin_access_keys(new_access_keys: &[AccessKeyId]) -> AccessKeys {
let mut access_keys: AccessKeys = AccessKeys::new();
let access_key_data: SetAccessKey = SetAccessKey {
metadata: HashMap::new(),
expires_at: None,
scope: AccessKeyScope::Admin,
kind: None,
};
set_access_keys(new_access_keys, &access_key_data, &mut access_keys);
access_keys
}
pub fn set_access_keys(
new_access_key_ids: &[AccessKeyId],
access_key_data: &SetAccessKey,
access_keys: &mut AccessKeys,
) {
for access_key_id in new_access_key_ids {
let existing_access_key = access_keys.get(access_key_id);
let now = time();
let created_at: u64 = match existing_access_key {
None => now,
Some(existing_access_key) => existing_access_key.created_at,
};
let updated_at: u64 = now;
let access_key: AccessKey = AccessKey {
metadata: access_key_data.metadata.clone(),
created_at,
updated_at,
expires_at: access_key_data.expires_at,
scope: access_key_data.scope.clone(),
kind: access_key_data.kind.clone(),
};
access_keys.insert(*access_key_id, access_key);
}
}
pub fn delete_access_keys(remove_access_key_ids: &[AccessKeyId], access_keys: &mut AccessKeys) {
for id in remove_access_key_ids {
access_keys.remove(id);
}
}
pub fn is_write_access_key(id: Principal, access_keys: &AccessKeys) -> bool {
principal_not_anonymous(id)
&& (is_self(id)
|| access_keys
.iter()
.any(|(&access_key_id, access_key)| match access_key.scope {
AccessKeyScope::Submit => false,
_ => {
principal_equal(access_key_id, id) && is_access_key_not_expired(access_key)
}
}))
}
pub fn is_valid_access_key(id: Principal, access_keys: &AccessKeys) -> bool {
principal_not_anonymous(id)
&& (is_self(id)
|| access_keys.iter().any(|(&access_key_id, access_key)| {
principal_equal(access_key_id, id) && is_access_key_not_expired(access_key)
}))
}
fn is_access_key_not_expired(access_key: &AccessKey) -> bool {
!is_access_key_expired(access_key)
}
fn is_access_key_expired(access_key: &AccessKey) -> bool {
if matches!(access_key.scope, AccessKeyScope::Admin) {
return false;
}
access_key
.expires_at
.is_some_and(|expires_at| expires_at < time())
}
pub fn is_admin_controller(id: Principal, access_keys: &AccessKeys) -> bool {
is_canister_controller(&id)
&& principal_not_anonymous(id)
&& access_keys
.iter()
.any(|(&access_key_id, access_key)| match access_key.scope {
AccessKeyScope::Admin => principal_equal(access_key_id, id),
_ => false,
})
}
pub fn into_access_key_ids(access_keys: &AccessKeys) -> Vec<AccessKeyId> {
access_keys
.clone()
.into_keys()
.collect::<Vec<AccessKeyId>>()
}
pub fn assert_max_number_of_access_keys(
current_access_keys: &AccessKeys,
access_keys_ids: &[AccessKeyId],
scope: &AccessKeyScope,
max_access_keys: Option<usize>,
) -> Result<(), String> {
let filtered_access_keys = filter_access_keys(current_access_keys, scope);
let max_length = match scope {
AccessKeyScope::Admin => max_access_keys.unwrap_or(MAX_NUMBER_OF_ADMIN_CONTROLLERS),
_ => max_access_keys.unwrap_or(MAX_NUMBER_OF_ACCESS_KEYS),
};
assert_access_keys_length(&filtered_access_keys, access_keys_ids, max_length)
}
fn assert_access_keys_length(
current_access_keys: &AccessKeys,
access_keys_ids: &[AccessKeyId],
max_access_keys: usize,
) -> Result<(), String> {
let current_access_key_ids = into_access_key_ids(current_access_keys);
let new_access_key_ids = access_keys_ids.iter().copied().filter(|id| {
!current_access_key_ids
.iter()
.any(|current_id| current_id == id)
});
if current_access_key_ids.len() + new_access_key_ids.count() > max_access_keys {
return Err(format!(
"{JUNO_ERROR_CONTROLLERS_MAX_NUMBER} ({max_access_keys})"
));
}
Ok(())
}
pub fn assert_controllers(access_keys_ids: &[AccessKeyId]) -> Result<(), String> {
assert_no_anonymous_access_key(access_keys_ids)?;
assert_no_revoked_controller(access_keys_ids)?;
Ok(())
}
fn assert_no_anonymous_access_key(access_keys_ids: &[AccessKeyId]) -> Result<(), String> {
let has_anonymous = access_keys_ids
.iter()
.any(|controller_id| principal_anonymous(*controller_id));
match has_anonymous {
true => Err(JUNO_ERROR_CONTROLLERS_ANONYMOUS_NOT_ALLOWED.to_string()),
false => Ok(()),
}
}
fn assert_no_revoked_controller(access_keys_ids: &[AccessKeyId]) -> Result<(), String> {
let has_revoked = access_keys_ids.iter().any(controller_revoked);
match has_revoked {
true => Err(JUNO_ERROR_CONTROLLERS_REVOKED_NOT_ALLOWED.to_string()),
false => Ok(()),
}
}
pub fn assert_access_key_expiration(access_key: &SetAccessKey) -> Result<(), String> {
if matches!(access_key.scope, AccessKeyScope::Admin) && access_key.expires_at.is_some() {
return Err(JUNO_ERROR_CONTROLLERS_ADMIN_NO_EXPIRY.to_string());
}
if let Some(expires_at) = access_key.expires_at {
if expires_at < time() {
return Err(JUNO_ERROR_CONTROLLERS_EXPIRY_IN_PAST.to_string());
}
}
Ok(())
}
pub fn is_console(id: Principal) -> bool {
let console = Principal::from_text(CONSOLE).unwrap();
principal_equal(id, console)
}
pub fn is_observatory(id: Principal) -> bool {
let observatory = Principal::from_text(OBSERVATORY).unwrap();
principal_equal(id, observatory)
}
pub fn is_self(provided_id: Principal) -> bool {
let itself = id();
principal_equal(provided_id, itself)
}
pub fn filter_admin_access_keys(access_keys: &AccessKeys) -> AccessKeys {
filter_access_keys(access_keys, &AccessKeyScope::Admin)
}
fn filter_access_keys(controllers: &AccessKeys, scope: &AccessKeyScope) -> AccessKeys {
#[allow(clippy::match_like_matches_macro)]
controllers
.clone()
.into_iter()
.filter(|(_, controller)| match scope {
AccessKeyScope::Write => matches!(controller.scope, AccessKeyScope::Write),
AccessKeyScope::Admin => matches!(controller.scope, AccessKeyScope::Admin),
AccessKeyScope::Submit => matches!(controller.scope, AccessKeyScope::Submit),
})
.collect()
}
fn controller_revoked(access_key_id: &AccessKeyId) -> bool {
REVOKED_CONTROLLERS.iter().any(|revoked_controller_id| {
principal_equal(
Principal::from_text(revoked_controller_id).unwrap(),
*access_key_id,
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::state::{AccessKey, AccessKeyKind, AccessKeyScope, AccessKeys};
use candid::Principal;
use std::collections::HashMap;
fn mock_time() -> u64 {
1_000_000_000_000
}
fn test_principal(id: u8) -> Principal {
Principal::from_slice(&[id])
}
fn create_access_key(
scope: AccessKeyScope,
expires_at: Option<u64>,
kind: Option<AccessKeyKind>,
) -> AccessKey {
AccessKey {
metadata: HashMap::new(),
created_at: mock_time(),
updated_at: mock_time(),
expires_at,
scope,
kind,
}
}
#[test]
fn test_is_access_key_expired_admin_never_expires() {
let admin = create_access_key(AccessKeyScope::Admin, Some(mock_time() - 1000), None);
assert!(!is_access_key_expired(&admin));
}
#[test]
fn test_is_access_key_expired_no_expiration() {
let access_key = create_access_key(AccessKeyScope::Write, None, None);
assert!(!is_access_key_expired(&access_key));
}
#[test]
fn test_is_access_key_expired_future_expiration() {
let access_key = create_access_key(AccessKeyScope::Write, Some(time() + 1_000_000), None);
assert!(!is_access_key_expired(&access_key));
}
#[test]
fn test_is_access_key_not_expired() {
let admin = create_access_key(AccessKeyScope::Admin, Some(time() - 1000), None);
assert!(is_access_key_not_expired(&admin));
let expired = create_access_key(AccessKeyScope::Write, Some(time() - 1), None);
assert!(!is_access_key_not_expired(&expired));
let valid = create_access_key(AccessKeyScope::Write, Some(time() + 1000), None);
assert!(is_access_key_not_expired(&valid));
}
#[test]
fn test_is_access_key_expired_past_expiration() {
let access_key = create_access_key(AccessKeyScope::Write, Some(mock_time() - 1), None);
assert!(is_access_key_expired(&access_key));
}
#[test]
fn test_access_key_can_write_anonymous_rejected() {
let access_keys = AccessKeys::new();
assert!(!is_write_access_key(Principal::anonymous(), &access_keys));
}
#[test]
fn test_access_key_can_write_self_allowed() {
let access_keys = AccessKeys::new();
let self_principal = id();
assert!(is_write_access_key(self_principal, &access_keys));
}
#[test]
fn test_access_key_can_write_admin_allowed() {
let mut access_keys = AccessKeys::new();
let admin_principal = test_principal(1);
access_keys.insert(
admin_principal,
create_access_key(AccessKeyScope::Admin, None, None),
);
assert!(is_write_access_key(admin_principal, &access_keys));
}
#[test]
fn test_access_key_can_write_write_scope_allowed() {
let mut access_keys = AccessKeys::new();
let write_principal = test_principal(2);
access_keys.insert(
write_principal,
create_access_key(AccessKeyScope::Write, None, None),
);
assert!(is_write_access_key(write_principal, &access_keys));
}
#[test]
fn test_access_key_can_write_submit_rejected() {
let mut access_keys = AccessKeys::new();
let submit_principal = test_principal(3);
access_keys.insert(
submit_principal,
create_access_key(AccessKeyScope::Submit, None, None),
);
assert!(!is_write_access_key(submit_principal, &access_keys));
}
#[test]
fn test_access_key_can_write_expired_rejected() {
let mut access_keys = AccessKeys::new();
let expired_principal = test_principal(4);
access_keys.insert(
expired_principal,
create_access_key(AccessKeyScope::Write, Some(mock_time() - 1), None),
);
assert!(!is_write_access_key(expired_principal, &access_keys));
}
#[test]
fn test_is_valid_access_key_anonymous_rejected() {
let access_keys = AccessKeys::new();
assert!(!is_valid_access_key(Principal::anonymous(), &access_keys));
}
#[test]
fn test_is_valid_access_key_self_allowed() {
let access_keys = AccessKeys::new();
let self_principal = id();
assert!(is_valid_access_key(self_principal, &access_keys));
}
#[test]
fn test_is_valid_access_key_all_scopes_allowed() {
let mut access_keys = AccessKeys::new();
let admin = test_principal(1);
let write = test_principal(2);
let submit = test_principal(3);
access_keys.insert(admin, create_access_key(AccessKeyScope::Admin, None, None));
access_keys.insert(write, create_access_key(AccessKeyScope::Write, None, None));
access_keys.insert(
submit,
create_access_key(AccessKeyScope::Submit, None, None),
);
assert!(is_valid_access_key(admin, &access_keys));
assert!(is_valid_access_key(write, &access_keys));
assert!(is_valid_access_key(submit, &access_keys));
}
#[test]
fn test_is_valid_access_key_expired_rejected() {
let mut access_keys = AccessKeys::new();
let expired_principal = test_principal(4);
access_keys.insert(
expired_principal,
create_access_key(AccessKeyScope::Write, Some(mock_time() - 1), None),
);
assert!(!is_valid_access_key(expired_principal, &access_keys));
}
#[test]
fn test_is_valid_access_key_admin_expired_still_valid() {
let mut access_keys = AccessKeys::new();
let admin_principal = test_principal(5);
access_keys.insert(
admin_principal,
create_access_key(AccessKeyScope::Admin, Some(mock_time() - 1000), None),
);
assert!(is_valid_access_key(admin_principal, &access_keys));
}
#[test]
fn test_init_admin_access_keys() {
let principals = vec![test_principal(1), test_principal(2)];
let access_keys = init_admin_access_keys(&principals);
assert_eq!(access_keys.len(), 2);
for principal in principals {
let access_key = access_keys.get(&principal).unwrap();
assert!(matches!(access_key.scope, AccessKeyScope::Admin));
assert!(access_key.expires_at.is_none());
assert!(access_key.kind.is_none());
}
}
#[test]
fn test_set_access_keys_new() {
let mut access_keys = AccessKeys::new();
let principals = vec![test_principal(1)];
let access_key_data = SetAccessKey {
metadata: HashMap::new(),
expires_at: Some(mock_time() + 1000),
scope: AccessKeyScope::Write,
kind: Some(AccessKeyKind::Automation),
};
set_access_keys(&principals, &access_key_data, &mut access_keys);
assert_eq!(access_keys.len(), 1);
let access_key = access_keys.get(&principals[0]).unwrap();
assert!(matches!(access_key.scope, AccessKeyScope::Write));
assert_eq!(access_key.expires_at, Some(mock_time() + 1000));
assert!(matches!(access_key.kind, Some(AccessKeyKind::Automation)));
}
#[test]
fn test_set_access_keys_update_preserves_created_at() {
let mut access_keys = AccessKeys::new();
let principal = test_principal(1);
let initial_data = SetAccessKey {
metadata: HashMap::new(),
expires_at: None,
scope: AccessKeyScope::Write,
kind: None,
};
set_access_keys(&[principal], &initial_data, &mut access_keys);
let original_created_at = access_keys.get(&principal).unwrap().created_at;
let update_data = SetAccessKey {
metadata: HashMap::new(),
expires_at: Some(mock_time() + 1000),
scope: AccessKeyScope::Admin,
kind: Some(AccessKeyKind::Automation),
};
set_access_keys(&[principal], &update_data, &mut access_keys);
let updated = access_keys.get(&principal).unwrap();
assert_eq!(updated.created_at, original_created_at);
assert!(matches!(updated.scope, AccessKeyScope::Admin));
}
#[test]
fn test_delete_access_keys() {
let mut access_keys = AccessKeys::new();
let principals = vec![test_principal(1), test_principal(2), test_principal(3)];
for principal in &principals {
access_keys.insert(
*principal,
create_access_key(AccessKeyScope::Write, None, None),
);
}
delete_access_keys(&principals[0..2], &mut access_keys);
assert_eq!(access_keys.len(), 1);
assert!(access_keys.contains_key(&principals[2]));
assert!(!access_keys.contains_key(&principals[0]));
assert!(!access_keys.contains_key(&principals[1]));
}
#[test]
fn test_filter_admin_access_keys() {
let mut access_keys = AccessKeys::new();
access_keys.insert(
test_principal(1),
create_access_key(AccessKeyScope::Admin, None, None),
);
access_keys.insert(
test_principal(2),
create_access_key(AccessKeyScope::Write, None, None),
);
access_keys.insert(
test_principal(3),
create_access_key(AccessKeyScope::Admin, None, None),
);
access_keys.insert(
test_principal(4),
create_access_key(AccessKeyScope::Submit, None, None),
);
let admin_only = filter_admin_access_keys(&access_keys);
assert_eq!(admin_only.len(), 2);
assert!(admin_only.contains_key(&test_principal(1)));
assert!(admin_only.contains_key(&test_principal(3)));
}
#[test]
fn test_filter_access_keys_write() {
let mut access_keys = AccessKeys::new();
access_keys.insert(
test_principal(1),
create_access_key(AccessKeyScope::Admin, None, None),
);
access_keys.insert(
test_principal(2),
create_access_key(AccessKeyScope::Write, None, None),
);
access_keys.insert(
test_principal(3),
create_access_key(AccessKeyScope::Write, None, None),
);
let write_only = filter_access_keys(&access_keys, &AccessKeyScope::Write);
assert_eq!(write_only.len(), 2);
assert!(write_only.contains_key(&test_principal(2)));
assert!(write_only.contains_key(&test_principal(3)));
}
#[test]
fn test_filter_access_keys_submit() {
let mut access_keys = AccessKeys::new();
access_keys.insert(
test_principal(1),
create_access_key(AccessKeyScope::Submit, None, None),
);
access_keys.insert(
test_principal(2),
create_access_key(AccessKeyScope::Write, None, None),
);
let submit_only = filter_access_keys(&access_keys, &AccessKeyScope::Submit);
assert_eq!(submit_only.len(), 1);
assert!(submit_only.contains_key(&test_principal(1)));
}
#[test]
fn test_is_admin_controller() {
let mut access_keys = AccessKeys::new();
let admin_principal = id();
access_keys.insert(
admin_principal,
create_access_key(AccessKeyScope::Admin, None, None),
);
assert!(is_admin_controller(admin_principal, &access_keys));
}
#[test]
fn test_is_admin_controller_not_canister_access_key() {
let mut access_keys = AccessKeys::new();
let not_canister = test_principal(99);
access_keys.insert(
not_canister,
create_access_key(AccessKeyScope::Admin, None, None),
);
assert!(!is_admin_controller(not_canister, &access_keys));
}
#[test]
fn test_assert_expiration_access_key_admin_with_expiry_rejected() {
let access_key = SetAccessKey {
metadata: HashMap::new(),
expires_at: Some(time() + 1000),
scope: AccessKeyScope::Admin,
kind: None,
};
let result = assert_access_key_expiration(&access_key);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
JUNO_ERROR_CONTROLLERS_ADMIN_NO_EXPIRY.to_string()
);
}
#[test]
fn test_assert_expiration_access_key_admin_without_expiry_allowed() {
let access_key = SetAccessKey {
metadata: HashMap::new(),
expires_at: None,
scope: AccessKeyScope::Admin,
kind: None,
};
let result = assert_access_key_expiration(&access_key);
assert!(result.is_ok());
}
#[test]
fn test_assert_expiration_access_key_past_expiry_rejected() {
let access_key = SetAccessKey {
metadata: HashMap::new(),
expires_at: Some(time() - 1000),
scope: AccessKeyScope::Write,
kind: None,
};
let result = assert_access_key_expiration(&access_key);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
JUNO_ERROR_CONTROLLERS_EXPIRY_IN_PAST.to_string()
);
}
#[test]
fn test_assert_expiration_access_key_future_expiry_allowed() {
let access_key = SetAccessKey {
metadata: HashMap::new(),
expires_at: Some(time() + 1000),
scope: AccessKeyScope::Write,
kind: None,
};
let result = assert_access_key_expiration(&access_key);
assert!(result.is_ok());
}
#[test]
fn test_assert_expiration_access_key_no_expiry_allowed() {
let access_key = SetAccessKey {
metadata: HashMap::new(),
expires_at: None,
scope: AccessKeyScope::Write,
kind: None,
};
let result = assert_access_key_expiration(&access_key);
assert!(result.is_ok());
}
#[test]
fn test_assert_expiration_access_key_submit_scope_with_future_expiry() {
let access_key = SetAccessKey {
metadata: HashMap::new(),
expires_at: Some(time() + 1000),
scope: AccessKeyScope::Submit,
kind: None,
};
let result = assert_access_key_expiration(&access_key);
assert!(result.is_ok());
}
#[test]
fn test_assert_max_number_of_access_keys_admin_default() {
let mut access_keys = AccessKeys::new();
for i in 0..(MAX_NUMBER_OF_ADMIN_CONTROLLERS - 1) {
access_keys.insert(
test_principal(i as u8),
create_access_key(AccessKeyScope::Admin, None, None),
);
}
let new_access_keys = vec![test_principal(99)];
let result = assert_max_number_of_access_keys(
&access_keys,
&new_access_keys,
&AccessKeyScope::Admin,
None,
);
assert!(result.is_ok());
let new_access_keys = vec![test_principal(98), test_principal(99)];
let result = assert_max_number_of_access_keys(
&access_keys,
&new_access_keys,
&AccessKeyScope::Admin,
None,
);
assert!(result.is_err());
}
#[test]
fn test_assert_max_number_of_access_keys_write_default() {
let mut access_keys = AccessKeys::new();
for i in 0..(MAX_NUMBER_OF_ACCESS_KEYS - 1) {
access_keys.insert(
test_principal(i as u8),
create_access_key(AccessKeyScope::Write, None, None),
);
}
let new_access_keys = vec![test_principal(255)];
let result = assert_max_number_of_access_keys(
&access_keys,
&new_access_keys,
&AccessKeyScope::Write,
None,
);
assert!(result.is_ok());
let new_access_keys = vec![test_principal(254), test_principal(255)];
let result = assert_max_number_of_access_keys(
&access_keys,
&new_access_keys,
&AccessKeyScope::Write,
None,
);
assert!(result.is_err());
}
#[test]
fn test_assert_max_number_of_access_keys_custom_limit() {
let mut access_keys = AccessKeys::new();
for i in 0..2 {
access_keys.insert(
test_principal(i),
create_access_key(AccessKeyScope::Admin, None, None),
);
}
let new_access_keys = vec![test_principal(3)];
let result = assert_max_number_of_access_keys(
&access_keys,
&new_access_keys,
&AccessKeyScope::Admin,
Some(3),
);
assert!(result.is_ok());
let new_access_keys = vec![test_principal(3), test_principal(4)];
let result = assert_max_number_of_access_keys(
&access_keys,
&new_access_keys,
&AccessKeyScope::Admin,
Some(3),
);
assert!(result.is_err());
}
#[test]
fn test_assert_max_number_of_access_keys_filters_by_scope() {
let mut access_keys = AccessKeys::new();
for i in 0..5 {
access_keys.insert(
test_principal(i),
create_access_key(AccessKeyScope::Admin, None, None),
);
}
for i in 10..15 {
access_keys.insert(
test_principal(i),
create_access_key(AccessKeyScope::Write, None, None),
);
}
let new_access_keys = vec![test_principal(20)];
let result = assert_max_number_of_access_keys(
&access_keys,
&new_access_keys,
&AccessKeyScope::Write,
Some(6), );
assert!(result.is_ok());
let new_access_keys = vec![test_principal(21)];
let result = assert_max_number_of_access_keys(
&access_keys,
&new_access_keys,
&AccessKeyScope::Admin,
Some(6), );
assert!(result.is_ok());
}
}