Guide

Set up permissions on a Counter-Strike 2 server, manage groups and players with commands and check permissions from your own plugin.

The guide has two parts. The first is for server owners: storage, settings and commands. The second is for plugin authors: how to check and change permissions through the API.

Permission lines

A permission is a line of segments separated by dots. Plugins choose their own lines, usually starting with the plugin name.

LineMeaning
admin.kickOne permission
admin.*Everything under admin.
*Everything
-admin.banA denial: forbids admin.ban
  • * may only be the last segment. Empty segments (a..b, a.) are not allowed.
  • admin.* covers admin.kick, but the plain line admin does not.
  • A denial is how you take away one permission from someone who has a wildcard: admin.* together with -admin.ban.

How a check is resolved

When a permission is checked for a player, the first place that knows the answer wins:

  1. temporary permissions of the player;
  2. permanent permissions of the player;
  3. the player's groups, from the highest priority to the lowest. Each group is asked first, then its parent, then the parent of the parent.

The player's own permissions therefore override the groups, and a group with a higher priority overrides a lower one.

Immunity

Every player has an immunity level. An admin can act on a player only when the admin's immunity is equal or higher. An immunity of -1 means "use the priority of my highest group".

For server owners

Installation

All three plugins are part of the Plugify Source 2 build and work out of the box with an SQLite database. To install them on their own, see Installation.

Storage

The storage plugin keeps its settings in permissions_storage.settings.yaml. The defaults lie next to the plugin. To change them, create configs/permissions/permissions_storage.settings.yaml with the keys you change:

configs/permissions/permissions_storage.settings.yaml
storage_method: mysql
server_id: 1
data:
  host: "127.0.0.1"
  port: 3306
  username: "plugify"
  password: "secret"
  database: "permissions"
SettingMeaning
storage_methodsqlite3 (default), mysql or postgres
dataConnection to MySQL or PostgreSQL: host, port, username, password, database, schema (PostgreSQL), params and pool_settings
server_idId of this server. 0 means all servers: groups and permissions given on server 0 apply everywhere
global_cookieShare cookies between all servers instead of keeping them per server
delete_expired_recordsDelete expired groups and permissions from the database
delete_removed_recordsDelete removed groups and permissions from the database instead of marking them
  • SQLite needs no setup. The database is data/permissions/permissions.db, created on the first start with a Default group that every player receives. SQLite always works as server 0.
  • MySQL and PostgreSQL tables are not created by the plugin. Create them before the first start with tables_mysql.sql or tables_postgres.sql from the permissions-storage repository.

Connector settings

The connector reads permissions_connector_s2.settings.yaml the same way, with overrides in configs/permissions/:

SettingMeaning
notify_modeWhere players are notified: console, chat, hint, each true or false
hint_durationHow long a hint stays on screen, in seconds
time_modeUnit of <duration> in commands: 0 seconds, 1 minutes, 2 hours, 3 days
abbreviate_timeShort units (5 min.) instead of full words
format_timestampDate format in Go layout, for example 2006-01-02 15:04:05
display_active_groupsShow players their active groups when they join
hidden_groupsGroups that are not shown to players, Default by default

Phrases live in permissions_connector_s2.translations.yaml. To change them, put your copy into data/permissions/. Apply changes without a restart with perms_reload_cfg and perms_reload_translations.

Commands

The commands work in the server console and in chat. From the server console everything is allowed. A player needs the permission from the table and cannot act on players with higher immunity.

<player> is #userid, $steamid64, &account_id or a name. <duration> is a number in the unit of time_mode, and 0 means forever. In perms_creategroup pass "" for no parent and put several permissions in quotes, separated by spaces.

CommandPermissionWhat it does
permsnoneList of the commands available to you
perms_reload_cfgpermissions.reload_configReload the settings
perms_reload_translationspermissions.reload_translationsReload the phrases
perms_userspermissions.user.listList of players on the server
perms_user_info <player>permissions.user.infoEverything about a player
perms_user_permissions <player>permissions.user.permission.infoPermissions and denials of a player
perms_user_groups <player>permissions.user.group.infoGroups of a player
perms_user_check_permission <player> <permission>permissions.user.permission.checkCheck a permission
perms_user_check_group <player> <group>permissions.user.group.checkCheck whether a player is in a group
perms_user_add_permission <player> <permission> <duration>permissions.user.permission.addAdd a permission or denial
perms_user_set_permission <player> <permission> <duration>permissions.user.permission.setSet a permission or denial, replacing the current one
perms_user_remove_permission <player> <permission>permissions.user.permission.removeRemove a permission or denial
perms_user_add_group <player> <group> <duration>permissions.user.group.addAdd a group
perms_user_set_group <player> <group> <duration>permissions.user.group.setSet a group, replacing its duration
perms_user_remove_group <player> <group>permissions.user.group.removeRemove a group
perms_groupspermissions.listgroupsList of groups
perms_group_info <group>permissions.group.infoInformation about a group
perms_group_permissions <group>permissions.group.permissionsPermissions of a group
perms_group_check_permission <group> <permission>permissions.group.permission.checkCheck a permission of a group
perms_creategroup <group> <priority> <parent> <permissions>permissions.creategroupCreate a group
perms_deletegroup <group>permissions.deletegroupDelete a group
perms_group_parent_set <child> <parent>permissions.group.parent.setSet the parent group
perms_group_parent_info <group>permissions.group.parent.infoShow the parent group
perms_group_parent_check <child> <parent>permissions.group.parent.checkCheck whether a group is a parent of another
perms_group_set_option <group> <option> <value>permissions.group.option.setSet a group option
perms_group_option <group> <option>permissions.group.optionShow a group option
perms_group_add_permission <group> <permission>permissions.group.permission.addAdd a permission
perms_group_set_permission <group> <permission>permissions.group.permission.setSet a permission, replacing the current one
perms_group_remove_permission <group> <permission>permissions.group.permission.removeRemove a permission

First setup

Run these in the server console to create an admin group and give it to yourself:

perms_creategroup Admin 100 "" "admin.* permissions.*"
perms_user_add_group $76561198000000000 Admin 0

The player must be on the server: users are loaded when they join and unloaded when they leave.

For plugin authors

Add the dependency

my_plugin.pplugin
{
  "$schema": "https://raw.githubusercontent.com/untrustedmodders/plugify/refs/heads/main/schemas/plugin.schema.json",
  "name": "my_plugin",
  "version": "1.0.0",
  "description": "An example plugin",
  "author": "Your Name",
  "website": "https://example.com",
  "license": "MIT",
  "language": "golang",
  "entry": "bin/my_plugin",
  "dependencies": [
    { "name": "permissions", "constraints": ">=1.3.0" }
  ]
}

The examples below are in Go and use the ready-made bindings, go get github.com/fr0nch/go-plugify-permissions. In other languages the same functions are imported from the permissions plugin, see the Import Functions page of your language.

Check a permission

package main

import (
    permissions "github.com/fr0nch/go-plugify-permissions"
)

// May this player use the kick feature of my plugin?
func canKick(steamID64 uint64) bool {
    return permissions.HasPermission(steamID64, "my_plugin.kick") == permissions.Status_Allow
}

// May the admin act on the target at all?
func canTarget(adminID, targetID uint64) bool {
    return permissions.CanAffectUser(adminID, targetID) == permissions.Status_Allow
}

HasPermission returns a Status: Allow, Disallow, PermNotFound, or TargetUserNotFound when the player is not loaded. Compare with Allow, so a missing permission means "no".

Change permissions and listen to changes

// Give the VIP group for 30 days. pluginID is the id of your plugin.
expires := time.Now().Add(30 * 24 * time.Hour).Unix()
status := permissions.AddGroup(pluginID, steamID64, "VIP", expires, false)
if status > permissions.Status_ErrorsStart {
    // GroupNotFound, TargetUserNotFound, DBNotReady and so on
}

// React to changes
permissions.OnUserGroupChange_Register(func(pluginID int64, action permissions.Action,
    targetID uint64, group string, oldTimestamp int64, newTimestamp int64) {
    // action is Action_Add, Action_Remove or Action_Replace
})
  • Times are Unix timestamps of the moment the permission or group expires. 0 means forever.
  • Functions that change something take pluginID, which is passed to the callbacks, and dontBroadcast, which applies the change without notifying listeners.
  • Every status above ErrorsStart is an error.
  • AddPermission refuses to change a permission that already exists with the opposite state (PermConflict). SetPermission replaces it.

Main functions

FunctionPurpose
HasPermission(targetID, perm)Check a permission of a user
HasPermissionExtended(targetID, perm, exact, permSource, timestamp)Same, and tells where the permission came from and when it expires
AddPermission, SetPermission, RemovePermissionChange permissions of a user
HasGroup, AddGroup, RemoveGroup, GetUserGroupsGroups of a user
GetImmunity, SetImmunity, CanAffectUserImmunity
GetCookie, SetCookie, GetAllCookiesNamed values stored per user
CreateGroup, DeleteGroup, GroupExists, GetAllGroupsGroups
SetParent, GetParent, HasParentGroup, GetPriorityGroupGroup hierarchy
HasPermissionGroup, AddPermissionGroup, SetPermissionGroup, RemovePermissionGroupPermissions of a group
GetOptionGroup, SetOptionGroup, GetAllOptionsGroupOptions of a group
UserExists, DumpUsersList, DumpPermissionsWhat is loaded now

Events

Every event has a pair of functions, On<Event>_Register and On<Event>_Unregister.

EventWhen it fires
OnUserLoadedA user has been loaded with all groups and permissions
OnUserCreate, OnUserDeleteA user appeared in the core or was removed
OnUserPermissionChange, OnUserGroupChangeA permission or group of a user was added, replaced or removed
OnUserCookieChange, OnUserImmunityChangeA cookie or the immunity of a user changed
OnPermissionExpiration, OnGroupExpirationA temporary permission or group expired
OnGroupCreate, OnGroupDelete, OnSetParentGroups were created, deleted or linked
OnGroupPermissionChange, OnGroupOptionChangeA permission or option of a group changed
OnGroupsLoadedThe groups of the server have been loaded

Writing your own storage or connector

The core loads nothing by itself. LoadGroups and LoadUser call the OnGroupsLoad and OnUserRequest callbacks, and the plugin that registered them fills the core with CreateGroup, CreateUser, AddGroup, AddPermission and SetCookie.

To save changes, register the storage callbacks: OnUserPermissionChangeStorage, OnUserGroupChangeStorage, OnUserCookieChangeStorage, OnUserImmunityChangeStorage, OnGroupCreateStorage, OnGroupDeleteStorage, OnGroupPermissionChangeStorage, OnGroupOptionChangeStorage and OnSetParentStorage. They are called before the change is applied and return bool. Returning false cancels the change, and the caller gets the DBNotReady status.

The full list of functions, callbacks and status codes is on the API Hub.