Skip to content

Commit 9521ed2

Browse files
committed
rbac implementation
1 parent 8034593 commit 9521ed2

File tree

5 files changed

+506
-0
lines changed

5 files changed

+506
-0
lines changed

Diff for: contract/p/gnoswap/rbac/doc.gno

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Package rbac provides a flexible, upgradeable Role-Based Access Control (RBAC)
2+
// system for Gno smart contracts and related applications. It decouples authorization
3+
// logic from fixed addresses, enabling dynamic registration, update, and removal of roles
4+
// and permissions.
5+
//
6+
// ## Overview
7+
//
8+
// The RBAC package encapsulates a manager that maintains an internal registry of roles.
9+
// Each role is defined by a unique name and a set of permissions. A permission is
10+
// represented by a `PermissionChecker` function that validates whether a given caller
11+
// (`std.Address`) satisfies the required access conditions.
12+
//
13+
// Key components of this package include:
14+
//
15+
// 1. **Role**: Represents a role with a name and a collection of permission-checking functions.
16+
// 2. **PermissionChecker**: A function type defined as `func(caller std.Address) error`,
17+
// used to verify access for a given permission.
18+
// 3. **RBAC Manager**: The core type (RBAC) that encapsulates role registration, permission
19+
// assignment, verification, updating, and removal.
20+
//
21+
// ## Key Features
22+
//
23+
// - **Dynamic Role Management**: Roles can be registered, and permissions can be assigned
24+
// or updated at runtime without requiring contract redeployment.
25+
// - **Multiple Permissions per Role**: A single role can have multiple permissions,
26+
// each with its own validation logic.
27+
// - **Declarative Role Definition**: The package supports a Functional Option pattern,
28+
// allowing roles and their permissions to be defined declaratively via functions like
29+
// `DeclareRole` and `WithPermission`.
30+
// - **Encapsulation**: Internal state (roles registry) is encapsulated within the RBAC
31+
// manager, preventing unintended external modifications.
32+
// - **Flexible Validation**: Permission checkers can implement custom logic, supporting
33+
// arbitrary access control policies.
34+
//
35+
// ## Workflow
36+
//
37+
// Typical usage of the RBAC package includes the following steps:
38+
//
39+
// 1. **Initialization**: Create a new RBAC manager using `NewRBAC()`.
40+
// 2. **Role Registration**: Register roles using `RegisterRole` or declaratively with
41+
// `DeclareRole`.
42+
// 3. **Permission Assignment**: Add permissions to roles using `RegisterPermission` or the
43+
// `WithPermission` option during role declaration.
44+
// 4. **Permission Verification**: Validate access by invoking `CheckPermission` with the
45+
// role name, permission name, and the caller's address (std.Address).
46+
//
47+
// ## Example Usage
48+
//
49+
// The following example demonstrates how to use the RBAC package in both traditional and
50+
// declarative styles:
51+
//
52+
// ```gno
53+
// package main
54+
//
55+
// import (
56+
// "std"
57+
//
58+
// "gno.land/p/demo/ufmt"
59+
// "gno.land/p/gnoswap/rbac"
60+
//
61+
// )
62+
//
63+
// func main() {
64+
// // Create a new RBAC manager
65+
// manager := rbac.NewRBAC()
66+
//
67+
// // Define example addresses
68+
// adminAddr := std.Address("admin")
69+
// userAddr := std.Address("user")
70+
//
71+
// // --- Traditional Role Registration ---
72+
// // Register an "admin" role
73+
// if err := manager.RegisterRole("admin"); err != nil {
74+
// panic(err)
75+
// }
76+
//
77+
// // Register an "access" permission for the "admin" role.
78+
// // The checker verifies that the caller matches adminAddr.
79+
// adminChecker := func(caller std.Address) error {
80+
// if caller != adminAddr {
81+
// return ufmt.Errorf("caller %s is not admin", caller)
82+
// }
83+
// return nil
84+
// }
85+
// if err := manager.RegisterPermission("admin", "access", adminChecker); err != nil {
86+
// panic(err)
87+
// }
88+
//
89+
// // --- Declarative Role Registration ---
90+
// // Register an "editor" role with a "modify" permission using the Functional Option pattern.
91+
// editorChecker := func(caller std.Address) error {
92+
// if caller != userAddr {
93+
// return ufmt.Errorf("caller %s is not editor", caller)
94+
// }
95+
// return nil
96+
// }
97+
// if err := manager.DeclareRole("editor", rbac.WithPermission("modify", editorChecker)); err != nil {
98+
// panic(err)
99+
// }
100+
//
101+
// // --- Permission Check ---
102+
// // Check if adminAddr has the "access" permission on the "admin" role.
103+
// if err := manager.CheckPermission("admin", "access", adminAddr); err != nil {
104+
// println("Access denied for admin:", err)
105+
// } else {
106+
// println("Admin access granted")
107+
// }
108+
// }
109+
//
110+
// ```
111+
//
112+
// ## Error Handling
113+
//
114+
// The package reports errors using the ufmt.Errorf function. Typical errors include:
115+
//
116+
// - Registering a role that already exists.
117+
// - Attempting to register a permission for a non-existent role.
118+
// - Verifying a permission that does not exist on a role.
119+
// - Failing a permission check due to a caller not meeting the required conditions.
120+
//
121+
// ## Limitations and Considerations
122+
//
123+
// - This RBAC implementation does not directly map addresses to roles; instead, it verifies
124+
// the caller against permission-checking functions registered for a role.
125+
// - Address validation relies on the logic provided within each PermissionChecker. Ensure that
126+
// your checkers properly validate `std.Address` values (which follow the Bech32 format).
127+
// - The encapsulated RBAC manager is designed to minimize external mutation, but integrating it
128+
// with other modules may require additional mapping between addresses and roles.
129+
//
130+
// # Notes
131+
//
132+
// - The RBAC system is designed to be upgradeable, enabling contracts to modify permission
133+
// logic without redeploying the entire contract.
134+
// - Both imperative and declarative styles are supported, providing flexibility to developers.
135+
//
136+
// Package rbac is intended for use in Gno smart contracts and other systems requiring dynamic,
137+
// upgradeable access control mechanisms.
138+
package rbac

Diff for: contract/p/gnoswap/rbac/gno.mod

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module gno.land/p/gnoswap/rbac

Diff for: contract/p/gnoswap/rbac/rbac.gno

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package rbac
2+
3+
import (
4+
"std"
5+
6+
"gno.land/p/demo/ufmt"
7+
)
8+
9+
// RBAC encapsulates and manages toles and their permissions.
10+
type RBAC struct {
11+
// roles maps role names to their respective `Role` objects
12+
roles map[string]*Role
13+
}
14+
15+
// New creates a new RBAC instance.
16+
func New() *RBAC {
17+
return &RBAC{
18+
roles: make(map[string]*Role),
19+
}
20+
}
21+
22+
func (rb *RBAC) hasRole(name string) bool {
23+
_, exists := rb.roles[name]
24+
return exists
25+
}
26+
27+
// RegisterRole registers a role with the given role name.
28+
// Returns an error if the role already exists.
29+
func (rb *RBAC) RegisterRole(roleName string) error {
30+
if rb.hasRole(roleName) {
31+
return ufmt.Errorf("role %s already exists", roleName)
32+
}
33+
rb.roles[roleName] = NewRole(roleName)
34+
return nil
35+
}
36+
37+
// RegisterPermission registers a permission name and checker
38+
// for the specific role.
39+
func (rb *RBAC) RegisterPermission(
40+
roleName, permissionName string,
41+
checker PermissionChecker,
42+
) error {
43+
role, exists := rb.roles[roleName]
44+
if !exists {
45+
return ufmt.Errorf("role %s does not exist", roleName)
46+
}
47+
role.AddPermission(permissionName, checker)
48+
return nil
49+
}
50+
51+
// CheckPermission verifies if the caller has the specific permission.
52+
func (rb *RBAC) CheckPermission(
53+
roleName, permissionName string,
54+
caller std.Address,
55+
) error {
56+
role, exists := rb.roles[roleName]
57+
if !exists {
58+
return ufmt.Errorf("role %s does not exist", roleName)
59+
}
60+
checker, exists := role.permissions[permissionName]
61+
if !exists {
62+
return ufmt.Errorf("permission %s does not exist for role %s", permissionName, roleName)
63+
}
64+
return checker(caller)
65+
}
66+
67+
// UpdatePermission updates the checker for a specific permission
68+
// in a role.
69+
func (rb *RBAC) UpdatePermission(
70+
roleName, permissionName string,
71+
newChecker PermissionChecker,
72+
) error {
73+
role, exists := rb.roles[roleName]
74+
if !exists {
75+
return ufmt.Errorf("role %s does not exist", roleName)
76+
}
77+
if !role.HasPermission(permissionName) {
78+
return ufmt.Errorf("permission %s does not exist for role %s", permissionName, roleName)
79+
}
80+
role.AddPermission(permissionName, newChecker)
81+
return nil
82+
}
83+
84+
// RemovePermission removes a permission from a role.
85+
func (rb *RBAC) RemovePermission(
86+
roleName, permissionName string,
87+
) error {
88+
role, exists := rb.roles[roleName]
89+
if !exists {
90+
return ufmt.Errorf("cannot remove permission from non-existent role %s", roleName)
91+
}
92+
if !role.HasPermission(permissionName) {
93+
return ufmt.Errorf("permission %s does not exist for role %s", permissionName, roleName)
94+
}
95+
delete(role.permissions, permissionName)
96+
return nil
97+
}

0 commit comments

Comments
 (0)