Access Control

Access control is the process of defining who has access to what actions. In Scrypto, access control rules not only define who can access component methods, they also define who has access to a resource behavior. In this article, we describe the basic principles of role-based access control.

Access control exists everywhere in our everyday life, be it a corporate employee who needs a badge to access a building or an intelligence officer who needs clearance to access contained information. With Scrypto, access control will feel relatable to how we intuitively think about defining who is allowed to do what. There are four cornerstones to Scrypto’s authorization model: Roles, Badges, AccessRules, and Proofs.

Different from Ethereum, where access control is based on the caller address, in Scrypto, authorization is granted in the form of badges. Any actor is allowed to take a privileged action if the proper authority to do so is present (more on this later). This gives us much more flexibility in defining security rules.

Roles

Roles are defined by the level of access you want to permit. In your dApp, you may want to define three levels of authority: an admin, super admin, and an owner. Intuitively, in this three-level hierarchy, the admin has the lowest level of authority. They may carry a single badge to access a handful of method calls. A super admin, may have access to methods an admin doesn’t. Because the level of access super admins have may have higher consequences, badges provided to super admins are more priviliged. Finally, an owner, may have total control over the dApp, therefore, the badge given to the owner may supersede the badges from other Roles. While your needs may vary depending on your use-case, defining Roles is a way to organize access control within your dApp. Roles are first defined and methods can be partitioned amongst each role. Here’s a contrived example:

#[blueprint]
mod token_sale
    enable_method_auth! { (1)
        roles {
            super_admin => updatable_by: [] (2)
            admin => updatable_by: [super_admin] (3)
        },
        methods { (4)
            buy => PUBLIC; (5)
            create_admin => restrict_to: [super_admin, OWNER];
            change_price => restrict_to: [admin, super_admin, OWNER]; (6)
            redeem_profits => restrict_to: [OWNER];
        }
    struct GumballMachine {
        ..
    // -- snip --
    }
}
1 Roles are defined below the module and above the Blueprint struct.
2 Roles naming scheme are defined however makes sense for your use case. (OWNER role doesn’t need to be defined)
3 A role can be updatable, and we can choose which other role may have the authority to update the AccessRule of another role.
4 Methods are then mapped to each role.
5 Methods mapped to PUBLIC allows everyone to call on the method.
6 Methods can be mapped to multiple Roles.

Badges

When Roles are defined, badge(s) associated to the role also need to be defined. A badge isn’t a primitive type - specifically, it is a way of referring to a resource that is used mainly for authorization. Anytime a user or component needs some form of authorization or authentication, the badge specified needs to be present before that action can be performed. A badge can either be a fungible or non-fungible resource, depending on your use case. For example, you may consider using a non-fungible badge if you want to associate data to an individual badge unit. Or a fungible badge may be appropriate to define an authorization "role" that is provided to many users/components.

To create a new badge type, we use the ResourceBuilder just as we would to create any other fungible or non-fungible token. For example:

// Using `DIVISIBILITY_NONE` to make sure nobody can divide a badge into multiple parts.
let owner_badge: Bucket = ResourceBuilder::new_fungible(OwnerRole::None)
    .divisibility(DIVISIBILITY_NONE)
    .metadata(metadata!(
        init {
            "name" => "Owner Badge", locked;
        }
    )
    .mint_initial_supply(1);

let super_admin_badge: Bucket = ResourceBuilder::new_fungible(OwnerRole::None)
    .divisibility(DIVISIBILITY_NONE)
    .metadata(metadata!(
        init {
            "name" => "Super Admin Badge", locked;
        }
    )
    .mint_initial_supply(2);

let admin_badge: Bucket = ResourceBuilder::new_fungible(OwnerRole::None)
    .divisibility(DIVISIBILITY_NONE)
    .metadata(metadata!(
        init {
            "name" => "Admin Badge", locked;
        }
    )
    .mint_initial_supply(1);

AccessRules

AccessRule is used to define security measures around your methods and resource behaviors. More specifically, an AccessRule defines what or how many badges need to be present before a method call or resource action is performed. Here’s an example:

// -- snip --
.instantiate()
.prepare_to_globalize(
    OwnerRole::Fixed(
        rule!(require(owner_badge.resource_address()) (1)
    )
))
.roles( (2)
    roles!(
        super_admin => rule!(require_amount(dec!(2), super_admin_badge.resource_address())); (3)
        admin => rule!(require(admin_manager.resource_address()));
    )
)
.globalize();
1 The OWNER is defined separately from other Roles. This is because the OWNER not only has access to component methods, but also component modules such as Authority, Metadata, and Royalties.
2 Roles are constructed in the instantiation phase before a component is globalized (addressable).
3 Roles are mapped to an AccessRule which specifies the badges associated.

Proofs

One of the important conventions of badge usage is that, under normal usage, they are not actually withdrawn from a Vault and passed around. Instead, a Proof is created and used to prove that an actor owns that badge - or at least access to it.

You can create a Proof for a particular resource from a Vault or (rarely) a Bucket, and then do things with that Proof without changing ownership of the underlying contents. For example, if my account holds a FLIX token signifying that I am a member of Radflix, I can create a Proof of that token and present it to a Radflix component so that it will allow me to access it. I’m not actually transferring it…​even if the Radflix component was buggy or malicious it would have no ability to take control of the underlying token from which the Proof was generated. Think of it just like flashing a badge in the real world. Whoever you show it to can see that you possess it, and can inspect it, but you’re not actually handing it to them so they can’t hang on to it.

Proofs have a quantity associated with them, and a Proof can not be created with a quantity of 0. That is, if you have a Vault which is configured to hold a specified resource, but that Vault is empty, you can’t create a Proof of that resource.

The Authorization Zone

The top level of every transaction, accessible by the transaction manifest, contains a worktop where resources are stored, and an authorization zone where Proofs are stored. When calling any Scrypto method from the manifest, the rules governing access to that method are automatically compared to the contents of the authorization zone. If the rules can be met, access to the method is granted and the call succeeds. If the rules can’t be met, then the transaction immediately aborts. There’s no need to specify what Proofs you think are necessary to meet the rules; the system just figures it out for you.

The same logic applies within a component called directly from the manifest. If it attempts a privileged action on a resource, such as trying to mint additional supply, the rules are checked against the contents of the authorization zone. If the rules can be met, the action succeeds. If the rules can’t be met, the transaction aborts.

That’s it. In the vast majority of use cases, you don’t have to think about access control or the authorization zone. The system just takes care of it.

Passing by Intent

There are times when you actually need your code to see what Proof a caller is using. For example, what if you issue a non-fungible badge to each of your system members, which contains some metadata like a user ID that your code will care about. In these cases, you add a Proof parameter to your method, and your caller can make a clone of a Proof sitting in the authorization zone to then pass it to the method just like any other parameter. We call this method of explicitly providing a Proof "passing by intent".

Here is an example where we display data contained in the NFT that the user passed a proof of:

pub fn query_ticket(&mut self, ticket: Proof) {
    // Make sure the provided proof is of the right resource address
    let checked_proof = ticket.check(self.ticket_manager.resource_address());

    // Get the data associated with the passed NFT proof
    let non_fungible: NonFungible<TicketData> = validated_ticket.non_fungible()
    let ticket_data: TicketData = non_fungible.data();

    info!("You inserted {} XRD into this component", ticket_data.xrd_amount);
}

Before we get the proof’s data with non_fungible().data() we have to do a validation step. If we didn’t, people could send us any NFT that has the field xrd_amount and trick our components.

Limitations on Proof Visibility

Because Proofs provide authorization to privileged methods or resource actions, it’s important to understand how they are allowed to move, who can see them, and how they are prevented from being abused by malicious or buggy components.

The short explanation is that Proofs can freely move "up" the callstack as many times as you like, but can only move "down" the stack once. That is, if your transaction manifest calls a method on ComponentAlpha which returns a Proof back to the authorization zone, a later call from your manifest to ComponentBravo will be able to utilize the Proof. But if ComponentBravo calls ComponentCharlie directly, that Proof won’t be considered when determining if Charlie’s method can be called, and won’t be considered for any privileged resource actions within Charlie’s method.

This same logic applies when you pass a Proof by intent, directly in the method parameters. The method you called can see and use the Proof for resource actions, but it can’t pass it on to another component, nor use it for accessing a privileged method.

While executing, any called component has its own local authorization zone which it can add or remove from. So if Bravo needs to call a method on Charlie, and needs a badge to do so, it will have to create a Proof of that badge and place it in its local authorization zone.

This sounds more complicated to explain than it is to practice. Stuff you call directly can utilize your authorization zone (either the transaction authorization zone, if calling directly from the manifest, or the local authorization zone of your component, if doing a component-to-component call). If the stuff you called wants to call some other component, then they’re on their own for authorization. In other words, if you pass a Proof of your user ID badge to some component, you can be confident that it’s not then going to be able to call something else and masquerade as you.

Next step: setting access rules

Looking for the syntax of rule setting and some examples? Please see the appropriate subsection here.