Skip to content

Commit

Permalink
Introduce Admin Library (#206)
Browse files Browse the repository at this point in the history
## Type of change

<!--Delete points that do not apply-->

- New feature

## Changes

The following changes have been made:

- Introduces the admin library which has the following functionality:
     - `add_admin()`
     - `revoke_admin()`
     - `is_admin()`
     - `only_admin()`
     - `only_owner_or_admin()`
- This library allows for multiple administrators rather than a single
user.

## Notes

- This library is built atop the ownership library where the owner is
the only user who may add administrators.
  • Loading branch information
bitzoic authored Nov 27, 2023
1 parent c1813e1 commit ad2f935
Show file tree
Hide file tree
Showing 23 changed files with 799 additions and 2 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ These libraries contain helper functions and other tools valuable to blockchain

#### Access Control and Security

- [Ownership](./libs/ownership/) is used to apply restrictions on functions such that only a single user may call them.
- [Ownership](./libs/ownership/) is used to apply restrictions on functions such that only a **single** user may call them.
- [Admin](./libs/admin/) is used to apply restrictions on functions such that only a select few users may call them like a whitelist.
- [Pausable](./libs/pausable/) allows contracts to implement an emergency stop mechanism.
- [Reentrancy](./libs/reentrancy) is used to detect and prevent reentrancy attacks.

Expand Down
1 change: 1 addition & 0 deletions libs/Forc.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"admin",
"fixed_point",
"merkle_proof",
"ownership",
Expand Down
Binary file added libs/admin/.docs/admin-logo-dark-theme.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added libs/admin/.docs/admin-logo-light-theme.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions libs/admin/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "admin.sw"
license = "Apache-2.0"
name = "admin"

[dependencies]
ownership = { path = "../ownership" }
src_5 = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.2.0" }
86 changes: 86 additions & 0 deletions libs/admin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".docs/admin-logo-dark-theme.png">
<img alt="SwayApps logo" width="400px" src=".docs/admin-logo-light-theme.png">
</picture>
</p>

# Overview

The Admin library provides a way to block users without an "adimistrative status" from calling functions within a contract. Admin is often used when needing administrative calls on a contract that involve multiple users or a whitelist.

This library extends the [Ownership Library](../ownership/). The Ownership library must be imported and used to enable the Admin library. Only the contract's owner may add and remove administrative users.

For more information please see the [specification](./SPECIFICATION.md).

# Using the Library

## Getting Started

In order to use the Admin library it must be added to the `Forc.toml` file and then imported into your Sway project. To add Sway-libs as a dependency to the `Forc.toml` file in your project please see the [README.md](../../README.md).

You may import the Admin library's functionalities like so:

```sway
use admin::*;
```

Once imported, the Admin library's functions will be available. To use them, the contract's owner must add a user as an admin with the `add_admin()` function. There is no limit to the number of admins a contract may have.

```sway
#[storage(read, write)]
fn add_a_admin(new_admin: Identity) {
// Can only be called by the Ownership Library's Owner
add_admin(new_admin);
}
```

## Basic Functionality

To restrict a function to only an admin, call the `only_admin()` function.

```sway
only_admin();
// Only an admin may reach this line.
```

> **NOTE:** Admins and the contract's owner are independent of one another. `only_admin()` will revert if called by the contract's owner.
To restrict a function to only an admin or the contract's owner, call the `only_owner_or_admin()` function.

```sway
only_owner_or_admin();
// Only an admin may reach this line.
```

To check the administrative privledges of a user, call the `is_admin()` function.

```sway
#[storage(read)]
fn check_if_admin(admin: Identity) {
let status = is_admin(admin);
assert(status);
}
```

## Integrating the Admin Library into the Ownership Library

To implement the Ownership library with the Admin library, be sure to set a contract owner for your contract. The following demonstrates the integration of the Ownership library with the Admin library.

```sway
use ownership::initialize_ownership;
use admin::add_admin;
#[storage(read, write)]
fn my_constructor(new_owner: Identity) {
initialize_ownership(new_owner);
}
#[storage(read, write)]
fn add_a_admin(new_admin: Identity) {
// Can only be called by contract's owner set in the constructor above.
add_admin(new_admin);
}
```

For more information please see the [specification](./SPECIFICATION.md).
31 changes: 31 additions & 0 deletions libs/admin/SPECIFICATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Overview

This document provides an overview of the Admin library.

It outlines the use cases, i.e. specification, and describes how to implement the library.

## Use Cases

The Admin library can be used anytime a function should be restricted to **multiple** users.

## Public Functions

### `only_admin()`

This function will ensure that the current caller is an admin.

### `only_owner_or_admin()`

This function will ensure that the current caller is an admin or the contract's owner.

### `is_admin()`

Returns whether a user is an admin.

### `add_admin()`

Only callable by the current owner, this function will add a new admin.

### `revoke_admin()`

Only callable by the current owner, this function will remove an admin.
180 changes: 180 additions & 0 deletions libs/admin/src/admin.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
library;

mod errors;

use errors::AdminError;
use ownership::{_owner, only_owner};
use src_5::State;
use std::{auth::msg_sender, storage::storage_api::clear,};

// Sets a new administrator.
///
/// # Arguments
///
/// * `new_admin`: [Identity] - The `Identity` which is to recieve administrator status.
///
/// # Reverts
///
/// * When the caller is not the contract owner.
///
/// # Number of Storage Accesses
///
/// * Reads: `1`
/// * Writes: `1`
///
/// # Examples
///
/// ```sway
/// use admin::{add_admin, is_admin};
///
/// fn foo(new_admin: Identity) {
/// add_admin(new_admin);
/// assert(is_admin(new_admin));
/// }
/// ```
#[storage(read, write)]
pub fn add_admin(new_admin: Identity) {
only_owner();
let admin_value = match new_admin {
Identity::Address(addr) => addr.value,
Identity::ContractId(contr) => contr.value,
};
let admin_key = StorageKey::<Identity>::new(admin_value, 0, admin_value);
admin_key.write(new_admin);
}

// Removes an administrator.
///
/// # Arguments
///
/// * `old_admin`: [Identity] - The `Identity` which the administrator status is to be removed.
///
/// # Reverts
///
/// * When the caller is not the contract owner.
///
/// # Number of Storage Accesses
///
/// * Reads: `1`
/// * Writes: `1`
///
/// # Examples
///
/// ```sway
/// use admin::{revoke_admin, is_admin};
///
/// fn foo(old_admin: Identity) {
/// revoke_admin(old_admin);
/// assert(!is_admin(old_admin));
/// }
/// ```
#[storage(read, write)]
pub fn revoke_admin(old_admin: Identity) {
only_owner();
let admin_value = match old_admin {
Identity::Address(addr) => addr.value,
Identity::ContractId(contr) => contr.value,
};
let admin_key = StorageKey::<Identity>::new(admin_value, 0, admin_value);
// TODO: Update to use StorageKey::clear() on next release
// https://github.com/FuelLabs/sway/pull/5284
let _ = clear::<Identity>(admin_key.slot, admin_key.offset);
}

// Returns whether `admin` is an administrator.
///
/// # Arguments
///
/// * `admin`: [Identity] - The `Identity` of which to check the administrator status.
///
/// # Returns
///
/// * [bool] - `true` if the `admin` is an administrator, otherwise `false`.
///
/// # Number of Storage Accesses
///
/// * Reads: `1`
///
/// # Examples
///
/// ```sway
/// use admin::{is_admin};
///
/// fn foo(admin: Identity) {
/// assert(is_admin(admin));
/// }
/// ```
#[storage(read)]
pub fn is_admin(admin: Identity) -> bool {
let admin_value = match admin {
Identity::Address(addr) => addr.value,
Identity::ContractId(contr) => contr.value,
};
let admin_key = StorageKey::<Identity>::new(admin_value, 0, admin_value);
match admin_key.try_read() {
Some(identity) => {
admin == identity
},
None => {
false
},
}
}

// Ensures that the sender is an administrator.
///
/// # Additional Information
///
/// NOTE: Owner and administrator are independent of one another. If an Owner calls this function, it will revert.
///
/// # Reverts
///
/// * When the caller is not an administrator.
///
/// # Number of Storage Accesses
///
/// * Reads: `1`
///
/// # Examples
///
/// ```sway
/// use admin::{only_admin};
///
/// fn foo() {
/// only_admin();
/// // Only reachable by an administrator
/// }
/// ```
#[storage(read)]
pub fn only_admin() {
require(is_admin(msg_sender().unwrap()), AdminError::NotAdmin);
}

// Ensures that the sender is an owner or administrator.
///
/// # Reverts
///
/// * When the caller is not an owner or administrator.
///
/// # Number of Storage Accesses
///
/// * Reads: `2`
///
/// # Examples
///
/// ```sway
/// use admin::{only_owner_or_admin};
///
/// fn foo() {
/// only_owner_or_admin();
/// // Only reachable by an owner or administrator
/// }
/// ```
#[storage(read)]
pub fn only_owner_or_admin() {
let sender = msg_sender().unwrap();
require(
_owner() == State::Initialized(sender) || is_admin(sender),
AdminError::NotAdmin,
);
}
7 changes: 7 additions & 0 deletions libs/admin/src/errors.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
library;

/// Error log for when access is denied.
pub enum AdminError {
/// Emiited when the caller is not an admin.
NotAdmin: (),
}
2 changes: 1 addition & 1 deletion libs/ownership/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Overview

The Ownership library provides a way to block users other than a single "owner" from calling functions. Ownership is often used when needing administrative calls on a contract.
The Ownership library provides a way to block users other than a **single** "owner" from calling functions. Ownership is often used when needing administrative calls on a contract.

For more information please see the [specification](./SPECIFICATION.md).

Expand Down
1 change: 1 addition & 0 deletions tests/Forc.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"./src/admin",
"./src/fixed_point/ufp32_div_test",
"./src/fixed_point/ufp32_exp_test",
"./src/fixed_point/ufp32_mul_test",
Expand Down
10 changes: 10 additions & 0 deletions tests/src/admin/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "admin_test"

[dependencies]
admin = { path = "../../../libs/admin" }
ownership = { path = "../../../libs/ownership" }
src_5 = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.2.0" }
1 change: 1 addition & 0 deletions tests/src/admin/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod tests;
Loading

0 comments on commit ad2f935

Please sign in to comment.