Permissions

The permissions system of Harmony resembles that of the permissions system common to open-source Minecraft server software, featuring rules and querying a node string against those rules.

Permission

In a community, there will be features, management functionality, bot commands, and other things which exist. Most of these actions have a permission associated with them, allowing you to control which members have access to each feature.

A permission is just a string, such as roles.user.manage, separated into parts using periods.

This string is also referred to as a permission node, or just node for short.

Rule

A rule is a string that matches permissions, using UNIX-style globbing and parameter expansion. This matching string is paired with a mode, which will either allow or deny permissions that match the string.

The rule roles.* will match roles.user.manage and roles.user.view. This is a star-expression, meaning that anything beginning with the text before the star and ending with the text after the star will match. Only one star-expression is permitted within a rule.

The rule roles.user.{manage,view} will match roles.user.manage and roles.user.view, but not roles.user.share. This is an or-expression, meaning that a.{b,c} will match both a.b and a.c. Any amount of items can be put in the or-expression’s braces: a.{b,c,d} will match both a.b, a.c, and a.d. Multiple or-expressions can compound, in which they effectively “multiply” the amount of items they expand to.

a.{b,c}.{d,e} will expand to a.b.d, a.b.e, a.c.d, and a.c.e.

Role

A role is a collection of rules, which can be assigned to users. These are named and coloured for aesthetic reasons.

Each guild MUST have a default role. This role MUST have the ID 0. The default role will not be included in GetGuildRoles or GetUserRoles. Clients are free to show it however they want.

Overrides

A role can also have channel and category-specific overrides, which are a collection of rules that take precedence over the guild-level set of rules. Actions may query permissions in the context of a specific channel. For example, you may want to allow @everyone to messages.send in most channels, but deny them that permission to send messages in an announcements channel.

Some actions may not query permissions in a channel or category, such as changing the name of a community.

If an action queries in the context of a channel, it’ll first check the overrides of the channel, then the rules of the category of the channel if applicable, then the rules associated with a guild.

Evaluation

// Mode determines whether a permission will glob or not
type Mode int

// RoleID is the ID of a role
type RoleID uint64

// ChannelID is the ID of a channel
type ChannelID uint64

const (
    // Allow permission
    Allow Mode = iota
    // Deny permission
    Deny
)

type PermissionGlob struct {
}

func (p *PermissionGlob) Match(str string) bool {
    // return true if the string matches this glob,
    // otherwise false
}

type PermissionNode struct {
    Glob PermissionGlob
    Mode
}

type GuildState struct {
    // map of role IDs to list of permission nodes
    Roles      map[RoleID][]PermissionNode

    // map of channel IDs to parent channel IDs (categories)
    Categories map[ChannelID]ChannelID

    // map of channel IDs to map of role IDs to list of permission nodes
    Channels   map[ChannelID]map[RoleID][]PermissionNode
}

func (g GuildState) Check(permission string, userRoles []uint64, in ChannelID) bool {
    userRoles = append(userRoles, uint64(Everyone))

    if in != 0 {
        if channelData, ok := g.Channels[in]; ok {
            for _, role := range userRoles {
                nodes, ok := channelData[RoleID(role)]
                _ = ok
                for _, node := range nodes {
                    if node.Glob.Match(permission) {
                        return node.Mode == Allow
                    }
                }
            }
        }

        if category, ok := g.Categories[in]; ok {
            if channelData, ok := g.Channels[category]; ok {
                for _, role := range userRoles {
                    nodes, ok := channelData[RoleID(role)]
                    _ = ok
                    for _, node := range nodes {
                        if node.Glob.Match(permission) {
                            return node.Mode == Allow
                        }
                    }
                }
            }
        }
    }

    for _, role := range userRoles {
        nodes, ok := g.Roles[RoleID(role)]
        _ = ok
        for _, node := range nodes {
            if node.Glob.Match(permission) {
                return node.Mode == Allow
            }
        }
    }

    return false
}