Skip to content

ROLE

The ROLE command family manages roles and users at runtime. It requires role-based access control to be enabled (--rbac-config) and the caller to hold the ROLE permission (granted by the admin category) — otherwise the server replies -NOPERM.

Mutations are atomic: each one clones the active policy, applies the change, and republishes the snapshot in a single swap. Readers never observe a half-applied policy.

Terminal window
./bin/tellstone --rbac-config policy.yaml
ROLE CREATE <name> <rule>...

Defines a new role. Rules use the Redis-style tokens +cmd, -cmd, +@cat, -@cat, and ~prefix (see Rule syntax). Fails if the role already exists — updating a role is DELETE then CREATE.

Reply: +OK

> ROLE CREATE operator +get '~users:*'
+OK
ROLE SETUSER <username> <role> [>password] [nopass]

Creates or updates a user, binding it to a role. At least one password option is required: >password bcrypt-hashes the plaintext server-side, nopass marks a passwordless user. When both are given, the last option wins.

Reply: +OK

> ROLE SETUSER bob operator '>bobpw'
+OK
> ROLE SETUSER carol readonly nopass
+OK
ROLE DELUSER <username>

Removes a user.

Reply: +OK

ROLE DELETE <role>

Removes a role. Users referencing the deleted role fall back to the default role on their next policy lookup (fail-safe).

Reply: +OK, or -ERR when the role does not exist.

ROLE LIST

Enumerates every role with its name, granted commands, and namespace whitelist. Names and commands are sorted for deterministic output. Password hashes are never rendered.

Reply: an array with the role’s name, command array, and namespace array.

ROLE GETUSER <username>

Returns the username, its assigned role (or null when the default role applies), and 1/0 for whether a password is set. The hash is never exposed.

Reply:

> ROLE GETUSER bob
bob / operator / 1

The client package exposes the same operations as typed Go calls:

CallEquivalent
c.AuthUser(username, password, scratch)AUTH <username> <password>
c.RoleCreate(role, rules, scratch)ROLE CREATE <name> <rule>...
c.RoleSetUser(username, role, passOptions, scratch)ROLE SETUSER <user> <role> [>pw] [nopass]passOptions are []byte(">pw") / []byte("nopass")
c.RoleDelUser(username, scratch)ROLE DELUSER <user>
c.RoleDelete(role, scratch)ROLE DELETE <role>
c.RoleList(scratch)ROLE LIST — returns []RoleListEntry
c.RoleGetUser(username, scratch)ROLE GETUSER <user> — returns RoleUser{Role, HasPass}
c, _ := client.Dial("127.0.0.1:9988", 2*time.Second)
defer c.Close()
scratch := make([]byte, 4096)
if err := c.AuthUser("admin", "adminsecret", scratch); err != nil {
log.Fatal(err)
}
if err := c.RoleCreate("user-reader", []string{"+get", "~users:*"}, scratch); err != nil {
log.Fatal(err)
}
if err := c.RoleSetUser("alice", "user-reader", [][]byte{[]byte(">alicepw")}, scratch); err != nil {
log.Fatal(err)
}

The binary protocol carries its own wire encoding for each subcommand, separate from the RESP2 reply format.

A full walkthrough ships in the main repository at cmd/example/role: it authenticates as an admin, creates a role and a user, lists the roles, then connects as the new user to prove the role’s limits — GET on a matching key passes, SET and keys outside the whitelist are denied as NOT_AUTHORIZED.