04 Auth
4.6 System-level Auth
Component auth have similar principles to resource auth. We still use the core concept of auth such as badges, AccessRules, and roles, but some nuances in its setup. Particularly, unlike resource auth where each resource behavior comes along with pre-defined roles, component auth can be more flexible where we can create custom defined roles. But before we get into it, let’s establish the following premises we learned up to this point far:
- Roles are used to define the boundaries of access.
- While roles define boundaries of access, roles are also accompanied by AccessRules which is used to define conditions of access.
- Conditions set by the AccessRules involve the requirement of badges to be present before a permissioned action can be performed.
- Badges are simply resources used for auth and to prove ownership of a badge, we send a proof of the badge rather than the physical badge itself.
//Setting up Component Auth on our TokenSale Blueprint
Let’s review the TokenSale blueprint we created at the end of Chapter 3.7: Building a Token Sale. The TokenSale blueprint is a simple blueprint which allows us to create a fungible resource with fixed supply. We were also able to determine its price and provided two methods:
get_price- Retrieves the price we determined our token will sell for.buy_useful_token- Allows users to send in payment in XRD to purchase our token.
use scrypto::prelude::*;#[blueprint]mod token_sale { struct TokenSale { useful_tokens_vault: FungibleVault, collected_xrd: FungibleVault, price_per_token: Decimal, } impl TokenSale { pub fn instantiate_token_sale(price_per_token: Decimal) -> Global<TokenSale> { let bucket_of_useful_tokens = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Useful Token", locked; "symbol" => "USEFUL", locked; } )) .mint_initial_supply(100); Self { useful_tokens_vault: FungibleVault::with_bucket(bucket_of_useful_tokens), collected_xrd: FungibleVault::new(XRD), price_per_token: price_per_token, } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } pub fn buy_useful_token(&mut self, mut payment: FungibleBucket) -> (FungibleBucket, FungibleBucket) { let our_share = payment.take(self.price_per_token); self.collected_xrd.put(our_share); (self.useful_tokens_vault.take(1), payment) } }}However, providing these two methods only enables the basic mechanism for a token sale dApp. We’re missing other functionalities such as the ability for the component owner to take profits from the sale that’s been made. So let’s create this simple method:
pub fn redeem_profits(&mut self, amount: Decimal) -> FungibleBucket { self.xrd_tokens_vault.take(amount)}That’s simple enough, but is that it? If we had concluded there, we’d have forgotten a critical piece of information when developing on an open and decentralized ledger: that unless specified, anyone can access and call this method! But we only want the owner of the component to redeem the profits of the token sales! So how do we protect our method where only the owner of the component can access this method? We use the
enable_method_auth! macro to set up a system-level auth approach to create roles and manage access to our methods.//Configuring Component Method Auth
The approach to use system-level auth relies on the Radix Engine to manage access to our component. Meaning, we can make use of built-in auth system to have the engine apply and check auth for us instead of building custom logic to create our own auth process. For this, we will use
enable_method_auth! The setup looks like this:#[blueprint]mod token_sale { enable_method_auth! { methods { buy_useful_token => PUBLIC; redeem_profits => restrict_to: [OWNER]; } } struct TokenSale {A few things to notice here. First, is that this macro is actually placed within the mod which is above the blueprint
Some of these pre-defined roles are quite self-explanatory such as the
struct. This is an important placement and in fact the only place where enable_method_auth! should be. Second, notice that each of the methods in our TokenSale blueprint is mapped to some kind of value. These values are pre-defined roles much like how resource behaviors have pre-defined roles and a summary of them are listed below:OWNER- Denotes the component ownerPUBLIC- Denotes the method is premissionlessSELF- Denotes the component itself
Some of these pre-defined roles are quite self-explanatory such as the
OWNER role, which we already saw in Chapter 4.2: Resource Auth - Revisiting Resource Behaviors. OWNER works the same way here, which maps back to the OwnerRole configuration for the component; a different placement which we will get to momentarily. The PUBLIC role simply represents the role that the method this role is attributed to can be callable by anyone or any component on the network. The SELF role is a bit interesting. In Scrypto, components can act on behalf of itself, so having a method restricted to SELF prevents anyone but the component itself to call it. We won’t get much into it this course, but it’s a good exposure as you learn more about Scrypto.//Configuring Component Owner AccessRule
So now that we’ve set our
redeem_profits method to only be accessible by the OWNER role, we need to configure the AccessRule of the OWNER. Like resource auth, configuring the component OwnerRole has a special place where it can be set. We can configure the OwnerRole by passing it as an argument to the .prepare_to_globalize() within the globalization process of an instantiation function. This setup is reserved for the component owner and the only place where component owners can be configured..instantiate().prepare_to_globalize( OwnerRole::Fixed( rule!(require(owner_badge) ))).globalize();We’ve now configured the
AccessRule of the OWNER to require an owner_badge. This means whenever redeem_profits method is called, the owner_badge needs to be present as part of the transaction before the method can be accessed.//Defining Custom Roles
With the existence of an
OWNER, auth can be simplified by defaulting every permissioned actions to the OWNER. However, your component may need more flexibility and granularity with its auth system outside of just the OWNER. Unlike resource auth, component auth allows you define your own roles and its boundaries based on your needs.Earlier we’ve added a method to redeem the profits we collect from selling our token. We’ve restricted this method to the OWNER. But let's say our TokenSale blueprint has multiple permissioned methods which require roles outside of just an OWNER. We want to make our component more complex so we add a few more methods such as the ability to create additional admins that can manage our component and the ability for admins to change the price of the tokens when there may be more demand. We can do so by attaching a “roles” section within the enable_method_auth! macro, like so:#[blueprint]mod token_sale { enable_method_auth! { roles { admin => updatable_by: [super_admin, OWNER]; super_admin => updatable_by: [OWNER]; }, methods { buy_useful_token => PUBLIC; create_admin => restrict_to: [super_admin]; change_price => restrict_to: [admin, super_admin]; redeem_profits => restrict_to: [OWNER]; } } struct TokenSale {Notice above the “methods” section of the
enable_method_auth! macro, we’ve introduced the “roles”. This is a place where we can define our own custom roles. The roles we’ve defined ourselves are admin and super_admin. Additionally, along with defining our own roles, we can also define some hierarchy between the roles as well. Notice with the admin role we’ve set it to be updatable by the super_admin and the OWNER while the super_admin role is updatable by the OWNER only. This means that the super_admin and the OWNER has the power to later update the AccessRule associated with the admin whereas only the OWNER can update the AccessRule of the super_admin. The admin in this case does not have any power to update any other roles.//Configuring Role AccessRules
Much like resource auth, roles are accompanied with their access rules and need to be configured. To do that, we call
.roles() within the globalization section of our blueprint like so:.instantiate().prepare_to_globalize( OwnerRole::Fixed( rule!(require(owner_badge) ))).roles(roles!( super_admin => rule!(require(super_admin_badge.resource_address())); admin => rule!(require(admin_manager.address()));)).globalize();Within the
.roles() method we can call a convenient roles!() macro where we can structure the roles with their respective access rules configuration. Each role’s AccessRule is conditioned to require their respective badges that we will later distribute.Owner Fallback
Also like resource auth, we can also have the option to fallback to the
OWNER as well. This would configure the AccessRule of each role to the same configuration defined within the OwnerRole in .prepare_to_globalize() method..instantiate().prepare_to_globalize( OwnerRole::Fixed( rule!(require(owner_badge) ))).roles(roles!( super_admin => OWNER; admin => OWNER;)).globalize();Line 8-9: The
OWNER is mapped to the AccessRule defined in Line 4.//Updating Role AccessRule
Earlier, we allowed the
admin role to be updatable by the super_admin and the OWNER whereas the super_admin can be updatable by the OWNER. There are many ways which we can use to update the AccessRule of a role. One of which can be through a transaction manifest explained in Chapter 5: Introduction to Transactions on Radix with the Transaction Manifest. Or within Scrypto, we can create a method that we can call to update a role’s AccessRule.pub fn update_role(&mut self, role_name: String, new_badge: ResourceAddress) { let access_rule = rule!(require(new_badge)); Runtime::global_component().set_role(&role_name, access_rule);}Line 4:
Here, we created a method to allow us to call and update the
Of course, it may be wise to include this method as part of the methods within the
Runtime is a Scrypto API which provides convenient methods to run at execution. In this case, we want to reference our assumed instantiated component with global_component() and update the roles we established with our component. Here, we created a method to allow us to call and update the
AccessRule of a role we defined. This method takes in a String where we will indicate the role we want to update (OWNER if we set it as OwnerRole::Updatable, super_admin, and admin) and the ResourceAddress of the badge we want to replace it with. Of course, it may be wise to include this method as part of the methods within the
enable_method_auth! we set up. Here’s a fun fact: even if this method was set to PUBLIC within enable_method_auth!, therefore callable by anyone, not anyone can simply update the roles AccessRule. This is because you still need to assume the superseding role that’s allowed to update another role. In other words, if someone were to attempt to update the super_admin by calling the update_role method we just created but doesn’t have the badge required to assume the OWNER role, the action will be rejected.//Component Modules
The last few small things in this chapter is to introduce component modules and there are three configurable modules a component has. In fact, we’ve already configured one of them which is component roles. Other modules that you can configure for your component are metadata and royalties.
//Royalty
Components can have royalties and when enabled, components can charge a determined fee every time a method is called. The fee is then deposited into a virtual vault of the component that can be claimed. To enable this module, simply call
.enable_component_royalties() within the component instantiation section. It’s important to note that royalties can only be configured upfront within the blueprint. Deferring this process will prevent you from enabling component royalties in the future..instantiate().prepare_to_globalize( OwnerRole::Fixed( rule!(require(owner_badge) ))).roles(roles!( super_admin => rule!( require(super_admin_badge.resource_address()) ); admin => rule!(require(admin_badge.address()));)).enable_component_royalties(component_royalties!( roles { royalty_setter => rule!(require(admin_badge.address())); royalty_setter_updater => OWNER; royalty_locker => rule!(require(super_admin_badge.resource_address())); royalty_locker_updater => OWNER; royalty_claimer => OWNER; royalty_claimer_updater => OWNER; }, init { get_price => Usd(1.into()), updatable; buy_useful_token => Xrd(1.into()), updatable; create_admin => Free, locked; change_price => Free, locked; redeem_profits => Free, locked; })).globalize();Claiming Royalties
Like the case of updating roles, we can create a method to claim royalties using an API provided by
Runtime. Note that even if this method we create is set as PUBLIC within enable_method_auth! the royalty_claimer role still needs to be accessed before component royalties can be claimed.pub fn claim_royalty(&mut self) -> Bucket { let royalty_bucket: Bucket = Runtime::global_component().claim_component_royalties(); return royalty_bucket}//Metadata
We covered the concept of a resource owner and how resource owner has metadata privileges over the resource. The same exact concept applies to component owners. Much like resources, components also have metadata and as long as you’ve configured a component owner, the component owner has the default power (unless divested) to configure metadata after a components been instantiated. The component metadata module will feel very familiar to configuring resource metadata. To do so, we’ll need to call
.metadata() within the component instantiation section..instantiate().prepare_to_globalize( OwnerRole::Fixed( rule!(require(owner_badge) ))).metadata(metadata!( roles { metadata_setter => rule!(require(admin_badge.address())); metadata_setter_updater => OWNER; metadata_locker => rule!(require(super_admin_badge.resource_address())); metadata_locker_updater => OWNER; }, init { "name" => "Token Sale Component", locked; "description" => "A component that sells useful tokens", locked; })).roles(roles!( super_admin => rule!( require(super_admin_badge.resource_address()) ); admin => rule!(require(admin_badge.address()));)).globalize();Unlike the component royalty module, metadata does not need to be configured upfront. Similar to the way resource metadata works, as long as you configure the component owner, you may defer component metadata configuration at a later point.
//Enable Function Auth
With
enable_method_auth! we can choose to protect the methods of our component, but we can also protect our functions. This can mean protecting our blueprint from being instantiated by requiring specified badges before the instantiation function can be called. Your blueprint may also have additional functions that allow for more flexible instantiations of your component that can be protected. We can use enable_function_auth! for this.#[blueprint]mod token_sale { enable_function_auth! { instantiate_token_sale => rule!(allow_all); } struct TokenSale {Similarly to
enable_method_auth!, the set up requires placement of the enable_function_auth! macro within the mod section. Although, notice that there are no roles involved. It’s a straightforward function to AccessRule mapping. This is because blueprint functions are generally simple and most auth requirements are confined within protecting the instantiation function. However, there is a tricky nuance when it comes to specifying badges required within the AccessRule.Specifying Badges for Function Auth AccessRule
Using badges to protect blueprint functions are slightly more complicated. This is because this is outside the realm of your functions and methods where badges can either be created and referenced from or retrieved from your function and method parameters. With blueprint functions, badges used should be created outside of the blueprint either by using the transaction manifest as we’ll explain in Chapter 5: Introduction to Transactions on Radix with the Transaction Manifest, or in our local world, using
resim. Once we create a badge with resim we can statically import it to our blueprint by using resource_manager! while also placing it within the mod.#[blueprint]mod token_sale { const SIMPLE_BADGE: ResourceManager = resource_manager!( "resource_sim1n27mf55xqp3hadmwlt9tsyf6frsl4hw4ufhadtq5u5arhgy5vjftyh" ); enable_function_auth! { instantiate_token_sale => rule!(require(SIMPLE_BADGE.address())); } struct TokenSale {