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    fn copy_global_caller<Y: SystemBasedKernelApi>(
242        system: &mut SystemService<Y>,
243        direct_caller_auth_zone_id: &NodeId,
244    ) -> Result<(Option<(GlobalCaller, Reference)>, Option<SubstateHandle>), RuntimeError> {
245        let direct_caller_auth_zone_handle = system.kernel_open_substate(
246            direct_caller_auth_zone_id,
247            MAIN_BASE_PARTITION,
248            &AuthZoneField::AuthZone.into(),
249            LockFlags::read_only(),
250            SystemLockData::default(),
251        )?;
252
253        let direct_caller_auth_zone = system
254            .kernel_read_substate(direct_caller_auth_zone_handle)?
255            .as_typed::<FieldSubstate<AuthZone>>()
256            .unwrap();
257
258        Ok((
259            direct_caller_auth_zone.into_payload().global_caller,
260            Some(direct_caller_auth_zone_handle),
261        ))
262    }
263
264    pub(crate) fn create_auth_zone<Y: SystemBasedKernelApi>(
265        system: &mut SystemService<Y>,
266        receiver: Option<(&NodeId, bool)>,
267        simulate_all_proofs_under_resources: BTreeSet<ResourceAddress>,
268        implicit_non_fungible_proofs: BTreeSet<NonFungibleGlobalId>,
269    ) -> Result<NodeId, RuntimeError> {
270        let (auth_zone, parent_lock_handle) = {
271            let is_global_context_change = if let Some((receiver, direct_access)) = receiver {
272                let object_info = system.get_object_info(receiver)?;
273                object_info.is_global() || direct_access
274            } else {
275                true
276            };
277
278            let direct_caller = system.current_actor();
279            let direct_caller_package_address = direct_caller.package_address();
280
281            // Retrieve global caller property of next auth zone
282            let (global_caller, parent_lock_handle) = match direct_caller {
283                Actor::Root | Actor::BlueprintHook(..) => (None, None),
284                Actor::Method(direct_caller_method_actor) => {
285                    let direct_caller_ancestor_visibility_origin = system
286                        .kernel_get_node_visibility_uncosted(&direct_caller_method_actor.node_id)
287                        .reference_origin(direct_caller_method_actor.node_id)
288                        .unwrap();
289                    let direct_caller_auth_zone = direct_caller_method_actor.auth_zone;
290
291                    // The `direct_caller_ancestor_visibility_origin` is rather indirect, but it is intended to
292                    // capture the concept:  "Who is the direct caller's ancestor for the purpose of auth?"
293                    //
294                    // In particular:
295                    // * If the direct caller is a global object, then it has ReferenceOrigin::Global
296                    // * If the direct caller was loaded from a substate belonging to a global object,
297                    //   then it gets a Borrowed visibility, which transforms into a ReferenceOrigin::Global.
298                    //   This also works transitively.
299                    // * If the direct caller was made visible by being passed to the call frame, (i.e. it didn't
300                    //   arise from track), then it is `ReferenceOrigin::FrameOwned`
301                    //
302                    // At some point we should refactor this to make this all much more explicit.
303                    match (
304                        direct_caller_ancestor_visibility_origin,
305                        is_global_context_change,
306                    ) {
307                        // Direct caller's ancestor is global AND this call is a global context change
308                        (ReferenceOrigin::Global(global_root_address), true) => {
309                            let global_caller_address = global_root_address.into();
310                            let global_caller_leaf_auth_zone_reference =
311                                Reference(direct_caller_auth_zone);
312                            (
313                                Some((
314                                    global_caller_address,
315                                    global_caller_leaf_auth_zone_reference,
316                                )),
317                                None,
318                            )
319                        }
320                        // Direct caller's ancestor is global AND this call is NOT a global context change
321                        (ReferenceOrigin::Global(..), false) => {
322                            Self::copy_global_caller(system, &direct_caller_auth_zone)?
323                        }
324                        // Direct caller's ancestor was directly accessed
325                        (ReferenceOrigin::DirectlyAccessed, _) => (None, None),
326                        // Direct caller's ancestor was borrowed from an internal referance in a substate
327                        (ReferenceOrigin::SubstateNonGlobalReference(..), _) => (None, None),
328                        // Direct caller's ancestor was passed into the call frame
329                        (ReferenceOrigin::FrameOwned, _) => {
330                            // In the past, all frame-owned direct callers copied their global caller to their callee.
331                            // This was a mistake, as it could allow frame-owned objects to use proofs from e.g.
332                            // the transaction processor.
333                            //
334                            // A fix needed to be backwards-compatible (without changing the size of substates, which would
335                            // affect the fee costs), and whilst the auth zone reference could be fixed by using a `Reference`
336                            // to `self_auth_zone`, the global caller was harder.
337                            //
338                            // As a work-around, the `FRAME_OWNED_GLOBAL_MARKER = TRANSACTION_TRACKER` was used as a marker
339                            // that the global caller was invalid and shouldn't be used. It is checked used to avoid adding
340                            // a global caller implicit proof in this case.
341
342                            let (caller, lock_handle) =
343                                Self::copy_global_caller(system, &direct_caller_auth_zone)?;
344
345                            // To avoid changing the size of the substate, we need to make sure that we replace Some
346                            // with Some and None with None.
347                            let global_caller = match caller {
348                                Some(_) => {
349                                    let global_caller_address = FRAME_OWNED_GLOBAL_MARKER.into();
350                                    // NOTE: This results in both the global caller stack and the parent stack being the same.
351                                    // This won't cause any critical issues, but should be reworked at some point.
352                                    let global_caller_leaf_auth_zone_reference =
353                                        Reference(direct_caller_auth_zone);
354                                    Some((
355                                        global_caller_address,
356                                        global_caller_leaf_auth_zone_reference,
357                                    ))
358                                }
359                                None => None,
360                            };
361
362                            (global_caller, lock_handle)
363                        }
364                    }
365                }
366                Actor::Function(function_actor) => {
367                    let direct_caller_auth_zone = function_actor.auth_zone;
368                    let global_caller = function_actor.as_global_caller();
369                    if is_global_context_change {
370                        (
371                            Some((global_caller, Reference(direct_caller_auth_zone))),
372                            None,
373                        )
374                    } else {
375                        Self::copy_global_caller(system, &direct_caller_auth_zone)?
376                    }
377                }
378            };
379
380            let auth_zone_parent = if is_global_context_change {
381                None
382            } else {
383                system
384                    .current_actor()
385                    .self_auth_zone()
386                    .map(|x| Reference(x))
387            };
388
389            let auth_zone = AuthZone::new(
390                vec![],
391                simulate_all_proofs_under_resources,
392                implicit_non_fungible_proofs,
393                direct_caller_package_address,
394                global_caller,
395                auth_zone_parent,
396            );
397
398            (auth_zone, parent_lock_handle)
399        };
400
401        // Create node
402        let new_auth_zone = system
403            .api()
404            .kernel_allocate_node_id(EntityType::InternalGenericComponent)?;
405
406        system.api().kernel_create_node(
407            new_auth_zone,
408            btreemap!(
409                MAIN_BASE_PARTITION => btreemap!(
410                    AuthZoneField::AuthZone.into() => IndexedScryptoValue::from_typed(&FieldSubstate::new_unlocked_field(auth_zone))
411                ),
412                TYPE_INFO_FIELD_PARTITION => type_info_partition(TypeInfoSubstate::Object(ObjectInfo {
413                    blueprint_info: BlueprintInfo {
414                        blueprint_id: BlueprintId::new(&RESOURCE_PACKAGE, AUTH_ZONE_BLUEPRINT),
415                        blueprint_version: BlueprintVersion::default(),
416                        outer_obj_info: OuterObjectInfo::default(),
417                        features: indexset!(),
418                        generic_substitutions: vec![],
419                    },
420                    object_type: ObjectType::Owned,
421                }))
422            ),
423        )?;
424        system.api().kernel_pin_node(new_auth_zone)?;
425
426        if let Some(parent_lock_handle) = parent_lock_handle {
427            system.kernel_close_substate(parent_lock_handle)?;
428        }
429
430        Ok(new_auth_zone)
431    }
432
433    pub fn teardown_auth_zone<Y: SystemBasedKernelApi>(
434        api: &mut SystemService<Y>,
435        self_auth_zone: NodeId,
436    ) -> Result<(), RuntimeError> {
437        // Detach proofs from the auth zone
438        let handle = api.kernel_open_substate(
439            &self_auth_zone,
440            MAIN_BASE_PARTITION,
441            &AuthZoneField::AuthZone.into(),
442            LockFlags::MUTABLE,
443            SystemLockData::Default,
444        )?;
445        let mut auth_zone = api
446            .kernel_read_substate(handle)?
447            .as_typed::<FieldSubstate<AuthZone>>()
448            .unwrap()
449            .into_payload();
450        let proofs = core::mem::replace(&mut auth_zone.proofs, Vec::new());
451        api.kernel_write_substate(
452            handle,
453            IndexedScryptoValue::from_typed(&FieldSubstate::new_unlocked_field(auth_zone)),
454        )?;
455        api.kernel_close_substate(handle)?;
456
457        // Drop all proofs (previously) owned by the auth zone
458        for proof in proofs {
459            let object_info = api.get_object_info(proof.0.as_node_id())?;
460            api.call_function(
461                RESOURCE_PACKAGE,
462                &object_info.blueprint_info.blueprint_id.blueprint_name,
463                PROOF_DROP_IDENT,
464                scrypto_encode(&ProofDropInput { proof }).unwrap(),
465            )?;
466        }
467
468        // Drop the auth zone
469        api.kernel_drop_node(&self_auth_zone)?;
470
471        Ok(())
472    }
473
474    fn check_permission<Y: SystemBasedKernelApi>(
475        auth_zone: &NodeId,
476        resolved_permission: ResolvedPermission,
477        fn_identifier: FnIdentifier,
478        api: &mut SystemService<Y>,
479    ) -> Result<(), RuntimeError> {
480        match resolved_permission {
481            ResolvedPermission::AllowAll => return Ok(()),
482            ResolvedPermission::AccessRule(rule) => {
483                let result =
484                    Authorization::check_authorization_against_access_rule(api, &auth_zone, &rule)?;
485
486                match result {
487                    AuthorizationCheckResult::Authorized => Ok(()),
488                    AuthorizationCheckResult::Failed(access_rule_stack) => Err(
489                        RuntimeError::SystemModuleError(SystemModuleError::AuthError(
490                            AuthError::Unauthorized(Box::new(Unauthorized {
491                                failed_access_rules: FailedAccessRules::AccessRule(
492                                    access_rule_stack,
493                                ),
494                                fn_identifier,
495                            })),
496                        )),
497                    ),
498                }
499            }
500            ResolvedPermission::RoleList {
501                role_assignment_of,
502                role_list,
503                module_id,
504            } => {
505                let result = Authorization::check_authorization_against_role_list(
506                    &auth_zone,
507                    &role_assignment_of,
508                    module_id,
509                    &role_list,
510                    api,
511                )?;
512
513                match result {
514                    AuthorityListAuthorizationResult::Authorized => Ok(()),
515                    AuthorityListAuthorizationResult::Failed(auth_list_fail) => Err(
516                        RuntimeError::SystemModuleError(SystemModuleError::AuthError(
517                            AuthError::Unauthorized(Box::new(Unauthorized {
518                                failed_access_rules: FailedAccessRules::RoleList(auth_list_fail),
519                                fn_identifier,
520                            })),
521                        )),
522                    ),
523                }
524            }
525        }
526    }
527
528    fn resolve_method_permission<Y: SystemBasedKernelApi>(
529        system: &mut SystemService<Y>,
530        blueprint_id: &BlueprintId,
531        receiver: &NodeId,
532        module_id: &ModuleId,
533        ident: &str,
534        args: &IndexedScryptoValue,
535    ) -> Result<ResolvedPermission, RuntimeError> {
536        let method_key = MethodKey::new(ident);
537
538        if let ModuleId::RoleAssignment = module_id {
539            // Only global objects have role assignment modules
540            let global_address = GlobalAddress::new_or_panic(receiver.0);
541            return RoleAssignmentNativePackage::authorization(
542                &global_address,
543                ident,
544                args,
545                system,
546            );
547        }
548
549        let auth_template = PackageAuthNativeBlueprint::get_bp_auth_template(
550            blueprint_id.package_address.as_node_id(),
551            &BlueprintVersionKey::new_default(blueprint_id.blueprint_name.as_str()),
552            system.api(),
553        )?
554        .method_auth;
555
556        let receiver_object_info = system.get_object_info(&receiver)?;
557
558        let (role_assignment_of, method_permissions) = match auth_template {
559            MethodAuthTemplate::StaticRoleDefinition(static_roles) => {
560                let role_assignment_of = match static_roles.roles {
561                    RoleSpecification::Normal(..) => {
562                        // Non-globalized objects do not have access rules module
563                        if !receiver_object_info.is_global() {
564                            return Ok(ResolvedPermission::AllowAll);
565                        }
566
567                        GlobalAddress::new_or_panic(receiver.0)
568                    }
569                    RoleSpecification::UseOuter => receiver_object_info.get_outer_object(),
570                };
571
572                (role_assignment_of, static_roles.methods)
573            }
574            MethodAuthTemplate::AllowAll => return Ok(ResolvedPermission::AllowAll),
575        };
576
577        match method_permissions.get(&method_key) {
578            Some(MethodAccessibility::Public) => Ok(ResolvedPermission::AllowAll),
579            Some(MethodAccessibility::OwnPackageOnly) => {
580                let package = blueprint_id.package_address;
581                Ok(ResolvedPermission::AccessRule(rule!(require(
582                    package_of_direct_caller(package)
583                ))))
584            }
585            Some(MethodAccessibility::OuterObjectOnly) => match module_id {
586                ModuleId::Main => {
587                    let outer_object_info = &receiver_object_info.blueprint_info.outer_obj_info;
588                    match outer_object_info {
589                        OuterObjectInfo::Some { outer_object } => {
590                            Ok(ResolvedPermission::AccessRule(rule!(require(
591                                global_caller(*outer_object)
592                            ))))
593                        }
594                        OuterObjectInfo::None { .. } => Err(RuntimeError::SystemModuleError(
595                            SystemModuleError::AuthError(AuthError::InvalidOuterObjectMapping),
596                        )),
597                    }
598                }
599                _ => Err(RuntimeError::SystemModuleError(
600                    SystemModuleError::AuthError(AuthError::InvalidOuterObjectMapping),
601                )),
602            },
603            Some(MethodAccessibility::RoleProtected(role_list)) => {
604                Ok(ResolvedPermission::RoleList {
605                    role_assignment_of,
606                    role_list: role_list.clone(),
607                    module_id: module_id.clone(),
608                })
609            }
610            None => {
611                let fn_identifier = FnIdentifier {
612                    blueprint_id: blueprint_id.clone(),
613                    ident: ident.to_string(),
614                };
615                Err(RuntimeError::SystemModuleError(
616                    SystemModuleError::AuthError(AuthError::NoMethodMapping(fn_identifier)),
617                ))
618            }
619        }
620    }
621}
622
623impl InitSystemModule for AuthModule {}
624impl ResolvableSystemModule for AuthModule {
625    #[inline]
626    fn resolve_from_system(system: &mut impl HasModules) -> &mut Self {
627        &mut system.modules_mut().auth
628    }
629}
630impl PrivilegedSystemModule for AuthModule {}
631impl<ModuleApi: SystemModuleApiFor<Self>> SystemModule<ModuleApi> for AuthModule {}