| gradle | ||
| src/main/kotlin/net/hareworks/permits_lib | ||
| .envrc | ||
| .gitattributes | ||
| .gitignore | ||
| AGENT.md | ||
| build.gradle.kts | ||
| flake.lock | ||
| flake.nix | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| README.md | ||
| settings.gradle.kts | ||
permits-lib
Permits Lib provides a declarative way to describe Bukkit/Paper permission hierarchies using a Kotlin
DSL. Once a tree is built you can hand it to PermissionRegistry (or the higher-level
MutationSession) and the library will register/unregister the relevant org.bukkit.permissions.Permission
instances and keep PermissionAttachments in sync.
Usage
import net.hareworks.permits_lib.domain.NodeRegistration
class ExamplePlugin : JavaPlugin() {
private val permits = PermitsLib.session(this)
override fun onEnable() {
val tree = permissionTree("example") {
node("command", NodeRegistration.STRUCTURAL) {
description = "Access to all example commands"
defaultValue = PermissionDefault.OP
wildcard {
exclude("cooldown") // example.command.* will skip cooldown
}
node("reload", NodeRegistration.PERMISSION) {
description = "Allows /example reload (permission example.command.reload)"
}
// Link to a helper node defined elsewhere under the command branch:
child("helper")
// Link to a permission outside the current branch (must be fully-qualified):
childAbsolute("example.tools.repair")
node("cooldown", NodeRegistration.PERMISSION) {
description = "Allows /example cooldown tweaks"
}
}
node("command.helper", NodeRegistration.PERMISSION) {
description = "Allows /example helper (referenced via child(\"helper\"))"
}
node("tools.repair", NodeRegistration.PERMISSION) {
description = "Allows /example tools repair (linked with childAbsolute(\"example.tools.repair\"))"
}
}
permits.applyTree(tree)
// The tree above materializes as permissions such as:
// example.command, example.command.reload, example.command.helper, example.command.cooldown,
// example.tools.repair,
// plus the auto-generated example.command.* wildcard (command opted in, cooldown was excluded).
// export to plugin.yml or inspect Bukkit's /permissions output).
configureRuntimePermissions()
}
fun grantHelper(player: Player) {
permits.attachments.grant(player, PermissionId.of("example.command.reload"))
}
private fun configureRuntimePermissions() {
// Later in runtime you can mutate the previously applied structure without rebuilding it:
permits.edit("example") {
// Update an existing node and link it to new children
node("command", NodeRegistration.STRUCTURAL) {
description = "Admins for every command path"
wildcard = true
node("debug", NodeRegistration.PERMISSION) {
description = "Allows /example debug"
defaultValue = PermissionDefault.OP
wildcard = true
}
}
// Remove deprecated permissions entirely
removeNode("command.cooldown")
}
}
}
Procedural Edits with MutablePermissionTree
If you prefer an imperative style before handing the structure to Bukkit, you can clone any existing tree, mutate it procedurally, and then apply the result:
val baseTree = permissionTree("example") {
node("command", NodeRegistration.STRUCTURAL) {
wildcard = true
node("reload", NodeRegistration.PERMISSION)
}
}
val mutable = MutablePermissionTree.from(baseTree)
mutable.node("command", NodeRegistration.STRUCTURAL) {
wildcard = true
excludeWildcardChild("helper") // keep helper out of command.*
node("debug", NodeRegistration.PERMISSION) {
description = "Allows /example debug"
defaultValue = PermissionDefault.OP
wildcard = true
}
}
mutable.removeNode("command.legacy")
permits.applyTree(mutable.build())
The mutable API mirrors the DSL (node, child, childAbsolute, removeNode, renameNode, etc.) so you can
stage edits procedurally before ever touching MutationSession.
Concepts
- Permission tree – immutable graph of
PermissionNodes. Nodes specify description, default value, boolean children map, and thewildcardflag (disabled by default) that, when enabled per node, keepsnamespace.path.*aggregate permissions in sync automatically. - DSL –
permissionTree("namespace") { ... }ensures consistent prefixes and validation (no cycles). Everynode("command", NodeRegistration.PERMISSION)(or.STRUCTURAL) is relative to that namespace, so you never include the namespace manually at the root. - Nested nodes –
node("command", NodeRegistration.STRUCTURAL) { node("reload", NodeRegistration.PERMISSION) { ... } }automatically producesnamespace.commandandnamespace.command.reloadplus wires the parent/child relationship so you don't have to repeat the full id. - Flexible references –
child("reload"),node("command", NodeRegistration.STRUCTURAL) { node("reload", NodeRegistration.PERMISSION) { ... } }, or evennode("command.reload", NodeRegistration.PERMISSION)insideeditall resolve to the same node; children are auto-created on first reference but you can demand explicit nodes by adding anodeblock later, and you can unlink specific children vianode("command", NodeRegistration.STRUCTURAL) { removeNode("cooldown") }and the entire subtree disappears. - Node registration –
NodeRegistration.PERMISSIONmaterializes the node as a Bukkit permission, whileNodeRegistration.STRUCTURALkeeps it purely for grouping (still participates in wildcard aggregation) so you can avoid ambiguous intermediate permissions likehoge.command. Nestedchild(...)calls are relative to the current node by default, whilechildAbsolute(...)now expects a fully-qualified permission ID (e.g.,example.tools.repair) so you can also point at nodes in other namespaces. - PermissionRegistry – calculates a diff between snapshots and performs the minimum additions,
removals, or updates via Bukkit's
PluginManager. - Wildcards – disabled by default; opt in via
wildcard = trueor the richerwildcard { ... }block. The block automatically enables the wildcard and lets youexclude("sub.path")so only selected DSL children end up undernamespace.command.*. Enabled nodes automatically add their wildcard descendants (e.g.,example.command.debug.*) so granting the wildcard cascades to the remaining children.
Selective wildcards
- DSL – call
wildcard { exclude("cooldown") }to enable the*. *permission while skipping specific literal/argument branches. You can chainexcludecalls and pass multi-segment paths (exclude("debug.logs")). - Mutable tree – after
wildcard = true, invokeexcludeWildcardChild("helper")(relative) orexcludeWildcardChildAbsolute("example.command.helper.extras")to trim wildcard membership imperatively. - Mutable edits –
permits.edit { ... }clones the currently registered tree, lets you mutate nodes imperatively, re-validates, and only pushes the structural diff to Bukkit. - AttachmentSynchronizer – manages identity-based
PermissionAttachments and exposes high-level helpers (grant,revoke,applyPatch). - MutationSession – ties everything together for plugins that just want to push new trees and manage attachments without worrying about the lower-level services.