radix_engine/system/system_modules/auth/
auth_module.rs

1use super::Authorization;
2use crate::blueprints::package::PackageAuthNativeBlueprint;
3use crate::blueprints::resource::AuthZone;
4use crate::errors::*;
5use crate::internal_prelude::*;
6use crate::kernel::call_frame::ReferenceOrigin;
7use crate::kernel::kernel_api::{KernelInternalApi, KernelNodeApi, KernelSubstateApi};
8use crate::object_modules::role_assignment::RoleAssignmentNativePackage;
9use crate::system::actor::Actor;
10use crate::system::module::*;
11use crate::system::node_init::type_info_partition;
12use crate::system::system::SystemService;
13use crate::system::system_callback::*;
14use crate::system::type_info::TypeInfoSubstate;
15use radix_engine_interface::api::{AttachedModuleId, LockFlags, ModuleId, SystemBlueprintApi};
16use radix_engine_interface::blueprints::package::{
17    BlueprintVersion, BlueprintVersionKey, MethodAuthTemplate, RoleSpecification,
18};
19use radix_engine_interface::blueprints::resource::*;
20use radix_engine_interface::blueprints::transaction_processor::TRANSACTION_PROCESSOR_BLUEPRINT;
21use radix_engine_interface::types::*;
22use radix_transactions::model::AuthZoneInit;
23
24#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
25pub enum AuthError {
26    NoFunction(FnIdentifier),
27    NoMethodMapping(FnIdentifier),
28    Unauthorized(Box<Unauthorized>),
29    InnerBlueprintDoesNotExist(String),
30    InvalidOuterObjectMapping,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
34pub enum FailedAccessRules {
35    RoleList(Vec<(RoleKey, Vec<AccessRule>)>),
36    AccessRule(Vec<AccessRule>),
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
40pub struct Unauthorized {
41    pub failed_access_rules: FailedAccessRules,
42    pub fn_identifier: FnIdentifier,
43}
44
45#[derive(Debug, Clone)]
46pub struct AuthModule {
47    /// SystemV1 only - we special-case the initial transaction processor
48    /// function call and add virtual resources to the transaction processor
49    /// call frame
50    pub v1_transaction_processor_proofs_for_injection: Option<AuthZoneInit>,
51}
52
53pub enum AuthorizationCheckResult {
54    Authorized,
55    Failed(Vec<AccessRule>),
56}
57
58pub enum AuthorityListAuthorizationResult {
59    Authorized,
60    Failed(Vec<(RoleKey, Vec<AccessRule>)>),
61}
62
63pub enum ResolvedPermission {
64    RoleList {
65        role_assignment_of: GlobalAddress,
66        module_id: ModuleId,
67        role_list: RoleList,
68    },
69    AccessRule(AccessRule),
70    AllowAll,
71}
72
73impl AuthModule {
74    pub fn new() -> Self {
75        Self {
76            v1_transaction_processor_proofs_for_injection: None,
77        }
78    }
79
80    pub fn new_with_transaction_processor_auth_zone(auth_zone_init: AuthZoneInit) -> Self {
81        Self {
82            v1_transaction_processor_proofs_for_injection: Some(auth_zone_init),
83        }
84    }
85
86    // In SystemV1, the transaction processor is initiated via a call_function, and we
87    // used this to inject the signature proofs and resource simulation.
88    //
89    // In SystemV2 and later, we initialize the auth zone directly, so no longer have
90    // need to do this check.
91    fn system_v1_resolve_injectable_transaction_processor_proofs<Y: SystemBasedKernelApi>(
92        system: &mut SystemService<Y>,
93        blueprint_id: &BlueprintId,
94    ) -> Result<(BTreeSet<ResourceAddress>, BTreeSet<NonFungibleGlobalId>), RuntimeError> {
95        let is_root_call_frame = system
96            .kernel_get_system_state()
97            .current_call_frame
98            .is_root();
99        let is_root_thread = system.kernel_get_current_stack_id_uncosted() == 0;
100        if is_root_call_frame && is_root_thread {
101            let auth_module = &system.kernel_get_system().modules.auth;
102            if let Some(auth_zone_init) = &auth_module.v1_transaction_processor_proofs_for_injection
103            {
104                // This is an extra sanity check / defense in depth which I believe isn't strictly needed.
105                let is_transaction_processor_blueprint = blueprint_id
106                    .package_address
107                    .eq(&TRANSACTION_PROCESSOR_PACKAGE)
108                    && blueprint_id
109                        .blueprint_name
110                        .eq(TRANSACTION_PROCESSOR_BLUEPRINT);
111                if is_transaction_processor_blueprint {
112                    return Ok((
113                        auth_zone_init.simulate_every_proof_under_resources.clone(),
114                        auth_zone_init.initial_non_fungible_id_proofs.clone(),
115                    ));
116                }
117            }
118        }
119
120        Ok((BTreeSet::new(), BTreeSet::new()))
121    }
122
123    pub fn on_call_function<Y: SystemBasedKernelApi>(
124        system: &mut SystemService<Y>,
125        blueprint_id: &BlueprintId,
126        ident: &str,
127    ) -> Result<NodeId, RuntimeError> {
128        // Create AuthZone
129        let auth_zone = {
130            if system
131                .system()
132                .versioned_system_logic
133                .should_inject_transaction_processor_proofs_in_call_function()
134            {
135                let (simulate_all_proofs_under_resources, implicit_non_fungible_proofs) =
136                    Self::system_v1_resolve_injectable_transaction_processor_proofs(
137                        system,
138                        blueprint_id,
139                    )?;
140                Self::create_auth_zone(
141                    system,
142                    None,
143                    simulate_all_proofs_under_resources,
144                    implicit_non_fungible_proofs,
145                )?
146            } else {
147                Self::create_auth_zone(system, None, Default::default(), Default::default())?
148            }
149        };
150
151        // Check authorization
152        {
153            // Step 1: Resolve method to permission
154            let permission = PackageAuthNativeBlueprint::resolve_function_permission(
155                blueprint_id.package_address.as_node_id(),
156                &BlueprintVersionKey::new_default(blueprint_id.blueprint_name.as_str()),
157                ident,
158                system.api(),
159            )?;
160
161            // Step 2: Check permission
162            let fn_identifier = FnIdentifier {
163                blueprint_id: blueprint_id.clone(),
164                ident: ident.to_string(),
165            };
166            Self::check_permission(&auth_zone, permission, fn_identifier, system)?;
167        }
168
169        Ok(auth_zone)
170    }
171
172    pub fn on_call_function_finish<Y: SystemBasedKernelApi>(
173        api: &mut SystemService<Y>,
174        auth_zone: NodeId,
175    ) -> Result<(), RuntimeError> {
176        Self::teardown_auth_zone(api, auth_zone)
177    }
178
179    pub fn on_call_method<Y: SystemBasedKernelApi>(
180        api: &mut SystemService<Y>,
181        receiver: &NodeId,
182        module_id: ModuleId,
183        direct_access: bool,
184        ident: &str,
185        args: &IndexedScryptoValue,
186    ) -> Result<NodeId, RuntimeError> {
187        let auth_zone = AuthModule::create_auth_zone(
188            api,
189            Some((receiver, direct_access)),
190            btreeset!(),
191            btreeset!(),
192        )?;
193
194        // Step 1: Resolve method to permission
195        let attached_module_id = match module_id {
196            ModuleId::Main => None,
197            ModuleId::Metadata => Some(AttachedModuleId::Metadata),
198            ModuleId::Royalty => Some(AttachedModuleId::Royalty),
199            ModuleId::RoleAssignment => Some(AttachedModuleId::RoleAssignment),
200        };
201
202        let blueprint_id = api
203            .get_blueprint_info(receiver, attached_module_id)?
204            .blueprint_id;
205
206        let permission =
207            Self::resolve_method_permission(api, &blueprint_id, receiver, &module_id, ident, args)?;
208
209        // Step 2: Check permission
210        let fn_identifier = FnIdentifier {
211            blueprint_id: blueprint_id.clone(),
212            ident: ident.to_string(),
213        };
214        Self::check_permission(&auth_zone, permission, fn_identifier, api)?;
215
216        Ok(auth_zone)
217    }
218
219    pub fn on_call_method_finish<Y: SystemBasedKernelApi>(
220        api: &mut SystemService<Y>,
221        auth_zone: NodeId,
222    ) -> Result<(), RuntimeError> {
223        Self::teardown_auth_zone(api, auth_zone)
224    }
225
226    /// On CALL_FUNCTION or CALL_METHOD, when auth module is disabled.
227    pub fn on_call_fn_mock<Y: SystemBasedKernelApi>(
228        system: &mut SystemService<Y>,
229        receiver: Option<(&NodeId, bool)>,
230        simulate_all_proofs_under_resources: BTreeSet<ResourceAddress>,
231        implicit_non_fungible_proofs: BTreeSet<NonFungibleGlobalId>,
232    ) -> Result<NodeId, RuntimeError> {
233        Self::create_auth_zone(
234            system,
235            receiver,
236            simulate_all_proofs_under_resources,
237            implicit_non_fungible_proofs,
238        )
239    }
240
241    #[allow(clippy::type_complexity)]
242    fn copy_global_caller<Y: SystemBasedKernelApi>(
243        system: &mut SystemService<Y>,
244        direct_caller_auth_zone_id: &NodeId,
245    ) -> Result<(Option<(GlobalCaller, Reference)>, Option<SubstateHandle>), RuntimeError> {
246        let direct_caller_auth_zone_handle = system.kernel_open_substate(
247            direct_caller_auth_zone_id,
248            MAIN_BASE_PARTITION,
249            &AuthZoneField::AuthZone.into(),
250            LockFlags::read_only(),
251            SystemLockData::default(),
252        )?;
253
254        let direct_caller_auth_zone = system
255            .kernel_read_substate(direct_caller_auth_zone_handle)?
256            .as_typed::<FieldSubstate<AuthZone>>()
257            .unwrap();
258
259        Ok((
260            direct_caller_auth_zone.into_payload().global_caller,
261            Some(direct_caller_auth_zone_handle),
262        ))
263    }
264
265    pub(crate) fn create_auth_zone<Y: SystemBasedKernelApi>(
266        system: &mut SystemService<Y>,
267        receiver: Option<(&NodeId, bool)>,
268        simulate_all_proofs_under_resources: BTreeSet<ResourceAddress>,
269        implicit_non_fungible_proofs: BTreeSet<NonFungibleGlobalId>,
270    ) -> Result<NodeId, RuntimeError> {
271        let (auth_zone, parent_lock_handle) = {
272            let is_global_context_change = if let Some((receiver, direct_access)) = receiver {
273                let object_info = system.get_object_info(receiver)?;
274                object_info.is_global() || direct_access
275            } else {
276                true
277            };
278
279            let direct_caller = system.current_actor();
280            let direct_caller_package_address = direct_caller.package_address();
281
282            // Retrieve global caller property of next auth zone
283            let (global_caller, parent_lock_handle) = match direct_caller {
284                Actor::Root | Actor::BlueprintHook(..) => (None, None),
285                Actor::Method(direct_caller_method_actor) => {
286                    let direct_caller_ancestor_visibility_origin = system
287                        .kernel_get_node_visibility_uncosted(&direct_caller_method_actor.node_id)
288                        .reference_origin(direct_caller_method_actor.node_id)
289                        .unwrap();
290                    let direct_caller_auth_zone = direct_caller_method_actor.auth_zone;
291
292                    // The `direct_caller_ancestor_visibility_origin` is rather indirect, but it is intended to
293                    // capture the concept:  "Who is the direct caller's ancestor for the purpose of auth?"
294                    //
295                    // In particular:
296                    // * If the direct caller is a global object, then it has ReferenceOrigin::Global
297                    // * If the direct caller was loaded from a substate belonging to a global object,
298                    //   then it gets a Borrowed visibility, which transforms into a ReferenceOrigin::Global.
299                    //   This also works transitively.
300                    // * If the direct caller was made visible by being passed to the call frame, (i.e. it didn't
301                    //   arise from track), then it is `ReferenceOrigin::FrameOwned`
302                    //
303                    // At some point we should refactor this to make this all much more explicit.
304                    match (
305                        direct_caller_ancestor_visibility_origin,
306                        is_global_context_change,
307                    ) {
308                        // Direct caller's ancestor is global AND this call is a global context change
309                        (ReferenceOrigin::Global(global_root_address), true) => {
310                            let global_caller_address = global_root_address.into();
311                            let global_caller_leaf_auth_zone_reference =
312                                Reference(direct_caller_auth_zone);
313                            (
314                                Some((
315                                    global_caller_address,
316                                    global_caller_leaf_auth_zone_reference,
317                                )),
318                                None,
319                            )
320                        }
321                        // Direct caller's ancestor is global AND this call is NOT a global context change
322                        (ReferenceOrigin::Global(..), false) => {
323                            Self::copy_global_caller(system, &direct_caller_auth_zone)?
324                        }
325                        // Direct caller's ancestor was directly accessed
326                        (ReferenceOrigin::DirectlyAccessed, _) => (None, None),
327                        // Direct caller's ancestor was borrowed from an internal referance in a substate
328                        (ReferenceOrigin::SubstateNonGlobalReference(..), _) => (None, None),
329                        // Direct caller's ancestor was passed into the call frame
330                        (ReferenceOrigin::FrameOwned, _) => {
331                            // In the past, all frame-owned direct callers copied their global caller to their callee.
332                            // This was a mistake, as it could allow frame-owned objects to use proofs from e.g.
333                            // the transaction processor.
334                            //
335                            // A fix needed to be backwards-compatible (without changing the size of substates, which would
336                            // affect the fee costs), and whilst the auth zone reference could be fixed by using a `Reference`
337                            // to `self_auth_zone`, the global caller was harder.
338                            //
339                            // As a work-around, the `FRAME_OWNED_GLOBAL_MARKER = TRANSACTION_TRACKER` was used as a marker
340                            // that the global caller was invalid and shouldn't be used. It is checked used to avoid adding
341                            // a global caller implicit proof in this case.
342
343                            let (caller, lock_handle) =
344                                Self::copy_global_caller(system, &direct_caller_auth_zone)?;
345
346                            // To avoid changing the size of the substate, we need to make sure that we replace Some
347                            // with Some and None with None.
348                            let global_caller = match caller {
349                                Some(_) => {
350                                    let global_caller_address = FRAME_OWNED_GLOBAL_MARKER.into();
351                                    // NOTE: This results in both the global caller stack and the parent stack being the same.
352                                    // This won't cause any critical issues, but should be reworked at some point.
353                                    let global_caller_leaf_auth_zone_reference =
354                                        Reference(direct_caller_auth_zone);
355                                    Some((
356                                        global_caller_address,
357                                        global_caller_leaf_auth_zone_reference,
358                                    ))
359                                }
360                                None => None,
361                            };
362
363                            (global_caller, lock_handle)
364                        }
365                    }
366                }
367                Actor::Function(function_actor) => {
368                    let direct_caller_auth_zone = function_actor.auth_zone;
369                    let global_caller = function_actor.as_global_caller();
370                    if is_global_context_change {
371                        (
372                            Some((global_caller, Reference(direct_caller_auth_zone))),
373                            None,
374                        )
375                    } else {
376                        Self::copy_global_caller(system, &direct_caller_auth_zone)?
377                    }
378                }
379            };
380
381            let auth_zone_parent = if is_global_context_change {
382                None
383            } else {
384                system.current_actor().self_auth_zone().map(Reference)
385            };
386
387            let auth_zone = AuthZone::new(
388                vec![],
389                simulate_all_proofs_under_resources,
390                implicit_non_fungible_proofs,
391                direct_caller_package_address,
392                global_caller,
393                auth_zone_parent,
394            );
395
396            (auth_zone, parent_lock_handle)
397        };
398
399        // Create node
400        let new_auth_zone = system
401            .api()
402            .kernel_allocate_node_id(EntityType::InternalGenericComponent)?;
403
404        system.api().kernel_create_node(
405            new_auth_zone,
406            btreemap!(
407                MAIN_BASE_PARTITION => btreemap!(
408                    AuthZoneField::AuthZone.into() => IndexedScryptoValue::from_typed(&FieldSubstate::new_unlocked_field(auth_zone))
409                ),
410                TYPE_INFO_FIELD_PARTITION => type_info_partition(TypeInfoSubstate::Object(ObjectInfo {
411                    blueprint_info: BlueprintInfo {
412                        blueprint_id: BlueprintId::new(&RESOURCE_PACKAGE, AUTH_ZONE_BLUEPRINT),
413                        blueprint_version: BlueprintVersion::default(),
414                        outer_obj_info: OuterObjectInfo::default(),
415                        features: indexset!(),
416                        generic_substitutions: vec![],
417                    },
418                    object_type: ObjectType::Owned,
419                }))
420            ),
421        )?;
422        system.api().kernel_pin_node(new_auth_zone)?;
423
424        if let Some(parent_lock_handle) = parent_lock_handle {
425            system.kernel_close_substate(parent_lock_handle)?;
426        }
427
428        Ok(new_auth_zone)
429    }
430
431    pub fn teardown_auth_zone<Y: SystemBasedKernelApi>(
432        api: &mut SystemService<Y>,
433        self_auth_zone: NodeId,
434    ) -> Result<(), RuntimeError> {
435        // Detach proofs from the auth zone
436        let handle = api.kernel_open_substate(
437            &self_auth_zone,
438            MAIN_BASE_PARTITION,
439            &AuthZoneField::AuthZone.into(),
440            LockFlags::MUTABLE,
441            SystemLockData::Default,
442        )?;
443        let mut auth_zone = api
444            .kernel_read_substate(handle)?
445            .as_typed::<FieldSubstate<AuthZone>>()
446            .unwrap()
447            .into_payload();
448        let proofs = core::mem::take(&mut auth_zone.proofs);
449        api.kernel_write_substate(
450            handle,
451            IndexedScryptoValue::from_typed(&FieldSubstate::new_unlocked_field(auth_zone)),
452        )?;
453        api.kernel_close_substate(handle)?;
454
455        // Drop all proofs (previously) owned by the auth zone
456        for proof in proofs {
457            let object_info = api.get_object_info(proof.0.as_node_id())?;
458            api.call_function(
459                RESOURCE_PACKAGE,
460                &object_info.blueprint_info.blueprint_id.blueprint_name,
461                PROOF_DROP_IDENT,
462                scrypto_encode(&ProofDropInput { proof }).unwrap(),
463            )?;
464        }
465
466        // Drop the auth zone
467        api.kernel_drop_node(&self_auth_zone)?;
468
469        Ok(())
470    }
471
472    fn check_permission<Y: SystemBasedKernelApi>(
473        auth_zone: &NodeId,
474        resolved_permission: ResolvedPermission,
475        fn_identifier: FnIdentifier,
476        api: &mut SystemService<Y>,
477    ) -> Result<(), RuntimeError> {
478        match resolved_permission {
479            ResolvedPermission::AllowAll => Ok(()),
480            ResolvedPermission::AccessRule(rule) => {
481                let result =
482                    Authorization::check_authorization_against_access_rule(api, auth_zone, &rule)?;
483
484                match result {
485                    AuthorizationCheckResult::Authorized => Ok(()),
486                    AuthorizationCheckResult::Failed(access_rule_stack) => Err(
487                        RuntimeError::SystemModuleError(SystemModuleError::AuthError(
488                            AuthError::Unauthorized(Box::new(Unauthorized {
489                                failed_access_rules: FailedAccessRules::AccessRule(
490                                    access_rule_stack,
491                                ),
492                                fn_identifier,
493                            })),
494                        )),
495                    ),
496                }
497            }
498            ResolvedPermission::RoleList {
499                role_assignment_of,
500                role_list,
501                module_id,
502            } => {
503                let result = Authorization::check_authorization_against_role_list(
504                    auth_zone,
505                    &role_assignment_of,
506                    module_id,
507                    &role_list,
508                    api,
509                )?;
510
511                match result {
512                    AuthorityListAuthorizationResult::Authorized => Ok(()),
513                    AuthorityListAuthorizationResult::Failed(auth_list_fail) => Err(
514                        RuntimeError::SystemModuleError(SystemModuleError::AuthError(
515                            AuthError::Unauthorized(Box::new(Unauthorized {
516                                failed_access_rules: FailedAccessRules::RoleList(auth_list_fail),
517                                fn_identifier,
518                            })),
519                        )),
520                    ),
521                }
522            }
523        }
524    }
525
526    fn resolve_method_permission<Y: SystemBasedKernelApi>(
527        system: &mut SystemService<Y>,
528        blueprint_id: &BlueprintId,
529        receiver: &NodeId,
530        module_id: &ModuleId,
531        ident: &str,
532        args: &IndexedScryptoValue,
533    ) -> Result<ResolvedPermission, RuntimeError> {
534        let method_key = MethodKey::new(ident);
535
536        if let ModuleId::RoleAssignment = module_id {
537            // Only global objects have role assignment modules
538            let global_address = GlobalAddress::new_or_panic(receiver.0);
539            return RoleAssignmentNativePackage::authorization(
540                &global_address,
541                ident,
542                args,
543                system,
544            );
545        }
546
547        let auth_template = PackageAuthNativeBlueprint::get_bp_auth_template(
548            blueprint_id.package_address.as_node_id(),
549            &BlueprintVersionKey::new_default(blueprint_id.blueprint_name.as_str()),
550            system.api(),
551        )?
552        .method_auth;
553
554        let receiver_object_info = system.get_object_info(receiver)?;
555
556        let (role_assignment_of, method_permissions) = match auth_template {
557            MethodAuthTemplate::StaticRoleDefinition(static_roles) => {
558                let role_assignment_of = match static_roles.roles {
559                    RoleSpecification::Normal(..) => {
560                        // Non-globalized objects do not have access rules module
561                        if !receiver_object_info.is_global() {
562                            return Ok(ResolvedPermission::AllowAll);
563                        }
564
565                        GlobalAddress::new_or_panic(receiver.0)
566                    }
567                    RoleSpecification::UseOuter => receiver_object_info.get_outer_object(),
568                };
569
570                (role_assignment_of, static_roles.methods)
571            }
572            MethodAuthTemplate::AllowAll => return Ok(ResolvedPermission::AllowAll),
573        };
574
575        match method_permissions.get(&method_key) {
576            Some(MethodAccessibility::Public) => Ok(ResolvedPermission::AllowAll),
577            Some(MethodAccessibility::OwnPackageOnly) => {
578                let package = blueprint_id.package_address;
579                Ok(ResolvedPermission::AccessRule(rule!(require(
580                    package_of_direct_caller(package)
581                ))))
582            }
583            Some(MethodAccessibility::OuterObjectOnly) => match module_id {
584                ModuleId::Main => {
585                    let outer_object_info = &receiver_object_info.blueprint_info.outer_obj_info;
586                    match outer_object_info {
587                        OuterObjectInfo::Some { outer_object } => {
588                            Ok(ResolvedPermission::AccessRule(rule!(require(
589                                global_caller(*outer_object)
590                            ))))
591                        }
592                        OuterObjectInfo::None => Err(RuntimeError::SystemModuleError(
593                            SystemModuleError::AuthError(AuthError::InvalidOuterObjectMapping),
594                        )),
595                    }
596                }
597                _ => Err(RuntimeError::SystemModuleError(
598                    SystemModuleError::AuthError(AuthError::InvalidOuterObjectMapping),
599                )),
600            },
601            Some(MethodAccessibility::RoleProtected(role_list)) => {
602                Ok(ResolvedPermission::RoleList {
603                    role_assignment_of,
604                    role_list: role_list.clone(),
605                    module_id: *module_id,
606                })
607            }
608            None => {
609                let fn_identifier = FnIdentifier {
610                    blueprint_id: blueprint_id.clone(),
611                    ident: ident.to_string(),
612                };
613                Err(RuntimeError::SystemModuleError(
614                    SystemModuleError::AuthError(AuthError::NoMethodMapping(fn_identifier)),
615                ))
616            }
617        }
618    }
619}
620
621impl Default for AuthModule {
622    fn default() -> Self {
623        Self::new()
624    }
625}
626
627impl InitSystemModule for AuthModule {}
628impl ResolvableSystemModule for AuthModule {
629    #[inline]
630    fn resolve_from_system(system: &mut impl HasModules) -> &mut Self {
631        &mut system.modules_mut().auth
632    }
633}
634impl PrivilegedSystemModule for AuthModule {}
635impl<ModuleApi: SystemModuleApiFor<Self>> SystemModule<ModuleApi> for AuthModule {}