Skip to main content

Roles

A Role is a core element of the M10 permissions system. Roles group together a set of permissions on resources into a single object. For example:

  • a Role called bank-admin that allows a user to create accounts and read data from accounts.
  • a Role called support that enables users to only read account information.

Roles must be "bound" to users (public keys) using a Role Bindings for them to come in effect.

Rules

A Role is made up of one or more Rules. A rule contains:

  • collection - the type of resource
  • permissions - the operations granted on the collection (each entry is either a bare verb such as "Read", or a verb qualified by an action such as "Update:set_issuance_limit")
  • effect - Allow (default) or Deny; a Deny rule denies the listed permissions on the matching scope
  • instance - [optional] the particular instance of the collection
  • when - [optional] additional custom condition
  • types - [optional] type annotations of any custom variables defined in when-statement

Collection

The collection is the type of object to which you are granting permissions. The current supported collections are:

  • Accounts
  • AccountSets
  • AccountMetadata
  • Banks
  • Roles
  • RoleBindings

Permission

A permission is a string that names an operation on a collection. Each entry in permissions is one of:

  • a bare verb, e.g. "Read" – the rule covers every action of that verb;
  • a verb qualified by an action, e.g. "Update:set_issuance_limit" – the rule covers only that specific action.

The following verbs are supported for all collection types:

  • Read - Read data
  • Create - Create data
  • Update - Update data
  • Delete - Delete data

The following verbs are only used for Accounts collections:

  • Transact - Perform transactions (Initiate + Commit).
  • Initiate - Initiate transactions. In a pending state until committed.
  • Commit - Commit the transaction. Finalize the transaction and release the pending state.

The following verb controls who can provision roles and role-bindings:

  • Grant - Authorize the creation, update, or deletion of roles and role-bindings that include rules on this collection and scope. See The GRANT Verb for details. Grant cannot be qualified by an action; it is a delegation gate evaluated at role-creation time.

The following verb controls who can revoke roles and role-bindings:

  • Revoke – Authorize the deletion of role-bindings within scope, without the ability to create or update them. See The REVOKE Verb for details. Revoke cannot be qualified by an action.

Action-qualified permissions

Each entry in permissions may be a bare Verb or a Verb:action pair. A bare verb is a wildcard over every action of that verb; a Verb:action entry matches only the named action. For example, an unqualified Update rule on ledger-accounts grants every Update:* operation on those accounts, while a rule with permissions: ["Update:set_issuance_limit"] grants only that one operation. See Action-qualified permissions for the full list of recognized action qualifiers.

Effect

A rule's effect is either:

  • Allow (default) – the listed permissions are granted on the matching scope.
  • Deny – the listed permissions are explicitly denied on the matching scope. A Deny rule takes precedence over any matching Allow rule. See The Deny Effect for details.

Instance

Optionally, each rule can apply to only a certain part of the collection, which can be specified by:

  • instance - apply to only a particular instance
  • instance_keys - apply to only a particular instance key

For example, to give a user access to their account only, you create a rule on the collection Accounts with the desired verbs and accountId for the user's account listed in instance_keys.

The format of the instance or instance_keys depends on the collection it applies to.

Collectioninstance / instance_key
AccountsAccount_Id
AccountMetadataAccountMetadata_Id
AccountSetsAccountSet_Id
BanksBank_Id
RolesRole_Id
RoleBindingsRoleBinding_Id

For example, to create a role with a rule on the collection Roles that gives the subject Read, Update, and Delete capabilities on a specific role instance, you would use:

--rules "rule -c roles -p Read -p Update -p Delete -i <RoleId>"

When-statement

Optionally, custom condition may be bound to any rule. Such condition MUST always evaluate to boolean result type (True/False) and is checked everytime the rule is applied. If condition evaluates to False for any reason (including errors), whole transaction is cancelled.

When-statement uses simplified version of MQL syntax (MetaQuotes language), which is similar to C++.

For example, to set per-transfer limits for account, you define such when-statement:

rule:
- collection: ledger-accounts
verbs: [Initiate, Transact, Commit]
when: "transfer.amount < 10000"

transfer.amount is automatically injected keyword.

Keywords may be simple and complex (see Injected keywords).

Account, that is bound to Role with such when-statement would be limited to 10,000 tokens to debit per transfer.

If you want to define custom limits per Role-binding, you can do so using field Types. See types.

When-statement is similar to expressions in Role-bindings. They both use MQL syntax, return boolean values and are used to authorize transactions. But, unlike expressions, when-statements are evaluated strictly. If account is bound to multiple roles through its public key, all when-statements must evaluate to True for transaction to be successful.

Example 1

// Role 1
rule:
- collection: ledger-accounts
verbs: [Initiate, Transact, Commit]
when: "transfer.amount < 10000"

// Role 2
rule:
- collection: ledger-accounts
verbs: [Initiate, Transact, Commit]
when: "transfer.amount > 5000"

Because Role 1 limits transfers to maximum of 10,000 tokens and Role 2 sets transfers to be minimum of 5,000 tokens, all transfers must be between 5,000 and 10,000 tokens strictly to pass authorization.

Example 2

// Role 1
rule:
- collection: ledger-accounts
verbs: [Initiate, Transact, Commit]
when: "transfer.amount < 50000"

// Role 2
rule:
- collection: ledger-accounts
verbs: [Initiate, Transact, Commit]
when: "transfer.amount < 20000"

Role 1 sets transfer limits to 50,000 tokens, but, because when-statements are strictly evaluated, Role 2 overrides the limit and sets it to 20,000, so all transfers must be under 20,000 tokens.

Note: when-statement from Role 1 still would be evaluated internally, but, in fact, is overriden by Role 2

It is possible to limit the scope of when-statement. For example:

rule:
- collection: ledger-accounts
verbs: [Initiate, Transact]
- collection: ledger-accounts
verbs: [Commit]
when: "transfer.amount < 20000"

In this example, when-statement will only be evaluated during Commit stage of transfer.

Injected keywords

Simple keywords hold values of primitive types.

Complex keywords are objects that hold many simple keywords.

Simple

KeywordTypeCollectionVerbsDescription
nowu64AnyAnyTime in seconds since UNIX EPOCH (1st January 1970)

Complex

KeywordValuesCollectionVerbsDescription
transferamount: U64ledger-accountsInitiate / Transact / Commit / AnyTransfer context

Types

Instead of hard values, custom variables may be defined in when-statements. For example:

rule:
- collection: ledger-accounts
verbs: [Initiate, Transact, Commit]
when: "transfer.amount < transfer_limit"
types:
- [transfer_limit, U64]

In this example, transfer_limit is custom defined variable. There are no naming restrictions, as long as the name stays valid string.

Because actual values of custom variables are not known during Role setup, their types must be specified:

types:
- [transfer_limit, U64]

Supported types

AnnotationTypeSize
U64Unsigned integer64
U32Unsigned integer32
U16Unsigned integer16
U8Unsigned integer8
I64Integer64
I32Integer32
I16Integer16
I8Integer8
F64Float64
F32Float32
BOOLBoolean8
STRINGStringDynamic
BYTESBytes arrayDynamic

Actual values of custom variables must be given in Role-bindings that are bound to the Role.

Example:

- create:
roleBinding:
attributes:
transfer_limit: 10000

The GRANT Verb

GRANT is a delegation authority verb. It authorizes a principal to create, update, or delete roles and role-bindings that include rules on a given collection and scope.

Three properties distinguish GRANT from all other verbs:

Delegation-only. Holding GRANT on a collection does not grant operational access to that collection. A principal with ledger-accounts: [Grant] cannot read, update, or transact on accounts – they can only provision others with access to those accounts.

Required for all role and role-binding operations. Creating, updating, or deleting any role or role-binding requires GRANT coverage over every collection and scope in that role's rules. Holding roles: [Create] alone is not sufficient; the creating principal must also hold GRANT matching the scope of each rule in the role being created or modified.

Scope-bounded. A principal can only delegate scopes they hold GRANT for. An unrestricted GRANT (no instance_keys) can delegate any scope. A scoped GRANT (with instance_keys) can only delegate an equal or narrower scope – it is not possible to escalate beyond your own GRANT boundary.

Example: provisioning a bank admin

In a typical deployment, an operator holds unrestricted GRANT and creates a bank admin role with scoped GRANT on a specific bank's accounts. The bank admin can then provision maker and checker roles, but only within the accounts they were delegated GRANT over.

Operator
└─ unrestricted GRANT on ledger-accounts, roles, role-bindings
└─ creates bank-x-admin role:
ledger-accounts: [Read, Update, Grant] – scoped to accounts A and B
roles: [Create, Read, Update, Delete]
role-bindings: [Create, Read, Update, Delete]

Bank X Admin (bound to bank-x-admin)
└─ can create maker/checker roles scoped to accounts A and B
└─ cannot create roles covering accounts outside A and B

The following example creates the bank-x-admin role described above:

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

Authorization failures

AttemptResult
Create any role or role-binding without GRANT rulesUnauthorized
Use GRANT on ledger-accounts to initiate a transferUnauthorized – GRANT is delegation-only
Scoped GRANT on accounts A and B; create a role covering account CUnauthorized
Update a role to extend its scope beyond your own GRANTUnauthorized
Delete a role whose scope exceeds your GRANTUnauthorized

The REVOKE Verb

REVOKE is a scoped removal verb. It authorizes a principal to delete role-bindings whose bound role falls within their REVOKE scope, without the ability to create or update roles or role-bindings.

How scope works. REVOKE on a collection does not grant operational access to that collection – it defines which role-bindings can be deleted. For example, REVOKE on ledger-accounts scoped to accounts A and B means the principal can delete any role-binding whose bound role contains rules on accounts A or B. The principal cannot read, update, or transact on those accounts through the REVOKE rule alone. To actually find and delete the role-bindings, the principal also needs Read and Delete on role-bindings.

Three properties distinguish REVOKE from GRANT:

Deletion-only. A principal holding REVOKE can delete role-bindings within scope but cannot create new ones or update existing ones.

Implied by GRANT. GRANT implies REVOKE for the same scope – a principal who can delegate permissions can also remove them. REVOKE does not imply GRANT.

Scope-bounded. Like GRANT, REVOKE is scope-bounded. A principal can only revoke role-bindings whose bound role's rules fall within the principal's REVOKE (or GRANT) scope.

Example: security operations role

A security operations team needs to terminate compromised sessions (delete role-bindings) without being able to mint new permissions. The role below uses two rules that work together:

  1. ledger-accounts with REVOKE – defines the scope: this principal can revoke access to accounts A and B. This rule does not grant any operational access to the accounts themselves.
  2. role-bindings with Read and Delete – grants the operational ability to find and delete role-bindings.

Together, the sec-ops principal can delete any role-binding whose bound role grants access to accounts A or B, but cannot create new permissions or access the accounts directly.

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

Authorization behavior

AttemptResult
Delete a role-binding whose role is within REVOKE scopePermitted
Delete a role-binding whose role is within GRANT scopePermitted – GRANT implies REVOKE
Create a role-binding with only REVOKE (no GRANT)Unauthorized – REVOKE does not imply GRANT
Update a role-binding with only REVOKE (no GRANT)Unauthorized
Delete a role-binding whose role exceeds REVOKE scopeUnauthorized

The Deny Effect

Setting effect: Deny on a rule explicitly denies the listed permissions on the matching scope, regardless of any matching Allow rules.

Combination requirement. A Deny rule must list at least one permission. The listed permissions specify which operations are denied. A Deny rule with an empty permissions list is rejected at creation time with a BadRequest error.

Evaluation order. When checking a request, the engine evaluates all matching rules. If any matching rule has effect: Deny and lists a permission that the request needs, the request is rejected – deny always wins.

Aggregation behavior (sticky deny). A single request may have several acceptable permissions (e.g., a transfer may be authorized by either Transact or by Initiate + Commit). If a Deny rule matches any one of those acceptable permissions on the matching scope, the entire request is denied – even if another acceptable permission would have been allowed. Deny is sticky across the set of acceptable permissions for a transaction.

Use case. A single Deny rule on a specific instance composes freely with any Allow rules without needing to be repeated across every rule that touches the same collection.

Example: read all accounts except the issuance account

The following example creates a role that can read all ledger accounts but denies read access to a specific issuance account:

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

Authorization behavior

ScenarioResult
Rule with effect: Deny and empty permissions listRejected at creation – a Deny rule must list at least one permission
Request matches a Deny rule on a required permissionRejected – deny takes precedence
Request has multiple acceptable permissions; Deny matches any one of themRejected – deny is sticky across acceptable permissions
Request matches both Deny and Allow rulesRejected – deny always wins
Request matches only Allow rulesPermitted
No rules matchRejected (implicit deny)

Action-qualified permissions

Each entry in a rule's permissions list is a string of the form Verb or Verb:action. A bare verb is a wildcard over every action of that verb on the matching scope; a Verb:action entry matches only that specific action.

This lets roles distinguish between operations that share the same underlying verb. For example, several operations on a ledger account are all Update operations: changing the issuance limit, changing the balance limit, freezing the account, and so on. A role with permissions: ["Update"] covers all of them; a role with permissions: ["Update:set_issuance_limit"] covers only that one operation – the holder can change the issuance limit but cannot freeze accounts, set balance limits, or change other account fields.

Currently recognized action qualifiers. Core-state recognizes the following qualified permissions on the ledger-accounts collection:

  • Update:set_issuance_limit
  • Update:set_balance_limit
  • Update:set_freeze_state

Other Update operations on ledger accounts (e.g. SetInstrument, SetDisplayCode) currently match plain Update and have no distinct action qualifier. Future operations may add more.

Verbs that cannot be qualified. Grant and Revoke cannot accept action qualifiers; they are delegation gates evaluated at role-creation time over the rule's scope, not at runtime over a specific action.

Example: a role that can only adjust the issuance limit

The following role grants Update:set_issuance_limit on a specific issuance account, plus Read so the holder can inspect the account. The holder cannot freeze the account, change the balance limit, or modify any other field.

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

Labels

Roles support optional labels – key-value metadata pairs that can be attached for categorization, organization, and filtering. Both keys and values are strings with a maximum length of 100 characters each.

Labels can be set at creation time and updated later. When listing roles, you can filter by labels instead of name, description, or instance.

Creating a role with labels

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

Updating labels

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

Filtering by labels

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

note

The --labels filter is mutually exclusive with --name, --description, and --instance.

Immutable Roles

A role can be marked as immutable at creation time. Once set, the immutable flag cannot be changed: no principal – regardless of their permissions, can update or delete an immutable role.

Immutable roles are intended for foundational system roles (e.g. a root operator role or a fixed bootstrap role) where accidental or malicious modification must be ruled out entirely.

warning

Immutability is permanent. There is no way to unset it or delete an immutable role after creation.

To create an immutable role, set immutable: true in the role document or use the --immutable flag in the CLI:

  • Rust
  • CLI
M10 REPLFOO

Attempting to update or delete an immutable role returns an error:

AttemptResult
Update an immutable roleInvalidInput: Role is immutable and cannot be updated
Delete an immutable roleInvalidInput: Role is immutable and cannot be deleted

Model

The data model for the M10 Roles is defined in rbac.proto:

API Requests

Create

To create a role on the M10 platform.

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

note

For an easier way to define roles with multiple or complex rules using the CLI, use the --editor flag. This opens your default text editor to define the role in YAML format.

Update

You can update a role's metadata (like its name, description, or owner) or its rules.

warning

Immutable roles cannot be updated. Any update attempt will return an error.

Update Role Metadata

To update a role's metadata, such as its name, you can perform an update operation. The --editor flag is not supported for role-metadata; use the explicit flags instead.

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

Update Role Rules

To replace all existing rules for a role with a new set of rules, you can perform a rule update.

note

This operation replaces all existing rules on the role with the new ones provided.

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

note

For an easier way to update rules using the CLI, use the --editor flag with m10 update role-rules. This opens your default editor with the existing rules in YAML format (rule list only).

Delete

To delete a role.

warning

Immutable roles cannot be deleted. Any delete attempt will return an error.

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO


List (Find)

List roles using the filters --name (text or partial text), --instance, --description (text or partial text), or --labels (key=value pairs). These filters are mutually exclusive:

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

Get

To get role details.

  • Rust
  • TypeScript
  • Dart
  • CLI
M10 REPLFOO

Output

The response from the get role command should be something like this:

(
id: "6aa51062-452b-482a-aff0-3e7df1a1aae9",
owner: "ACMKVkeIt+L5z39xk5YHujjcN7bLhnq+UIkLBlymNM4=",
name: "bank-admin",
rules: [(
collection: "accounts",
instance_keys: [00800005000000000000000000000002],
permissions: ["Read", "Update"],
effect: Allow,
), (
collection: "banks",
instance_keys: [bc3b532d-6be0-45e1-b98c-5ddc6e8e239a],
permissions: ["Read"],
effect: Allow,
), (
collection: "ledger-accounts",
instance_keys: [00800005000000000000000000000002],
permissions: ["Create"],
effect: Allow,
)],
)

HTTP/1

To submit requests over HTTP/1 instead of HTTP/2 with the SDKs, use the http option.