From def0ca408682ace43b416a5b3ed667fe986e6f9d Mon Sep 17 00:00:00 2001 From: Mophyr Avern Date: Thu, 27 Nov 2025 17:29:54 -0300 Subject: [PATCH 1/4] Automatically assign commands permission groups based on ranks / roles --- lib/cmdrhandler/src/Server/init.luau | 36 +++++++ lib/cmdrhandler/src/Shared/CmdrUtil.luau | 93 ++++++++++++------- .../src/Shared/PermissionsHandler.luau | 28 ++++-- 3 files changed, 118 insertions(+), 39 deletions(-) diff --git a/lib/cmdrhandler/src/Server/init.luau b/lib/cmdrhandler/src/Server/init.luau index 55f0e7f..27776b0 100644 --- a/lib/cmdrhandler/src/Server/init.luau +++ b/lib/cmdrhandler/src/Server/init.luau @@ -294,6 +294,41 @@ function CmdrServer:Init() end -- Sync initial player permissions to client + --[[ + [⚠️ • NOTE]: Player:GetRoleInGroup() still returns the player rank, + so this should not be used until Roblox fixes it. + + [⚠️ • NOTE]: When getting back to this, check again this implementation. + A player might have multiple roles with different permissions, and :GetRoleInGroup() + assumes only one role per player. + + local groupRolePermissionGroups = Client.GroupRolePermissionGroups:Get() + for groupId, roleData in groupRolePermissionGroups do + if player:IsInGroup(groupId) then + for role, permissionGroups in roleData do + if player:GetRoleInGroup(groupId) == role then + CmdrServer.PermissionsHandler:GivePlayerPermissionGroups(player, permissionGroups) + end + end + end + end + --]] + + local groupRankPermissionGroups = Client.GroupRankPermissionGroups:Get() + for groupId, rankData in groupRankPermissionGroups do + if player:IsInGroup(groupId) then + for _, rankPermissionsData in rankData do + local playerRank = player:GetRankInGroup(groupId) + if playerRank >= rankPermissionsData.Ranks.Min and playerRank <= rankPermissionsData.Ranks.Max then + CmdrServer.PermissionsHandler:GivePlayerPermissionGroups( + player, + rankPermissionsData.Permissions + ) + end + end + end + end + local playerPerms = CmdrServer.PermissionsHandler:_getPlayerPermissionGroupsData()[player] Client.Permissions:SetFor(player, playerPerms) end) @@ -309,6 +344,7 @@ function CmdrServer:Init() CmdrServer.PermissionsHandler.GroupRolePermissionGroupsChanged:Connect(function(newValue) Client.GroupRolePermissionGroups:Set(newValue) + warn("Changed to", newValue) end) CmdrServer.PermissionsHandler.PlayerPermissionGroupsChanged:Connect(function(player, newValue) diff --git a/lib/cmdrhandler/src/Shared/CmdrUtil.luau b/lib/cmdrhandler/src/Shared/CmdrUtil.luau index 7f50b46..49e20ae 100644 --- a/lib/cmdrhandler/src/Shared/CmdrUtil.luau +++ b/lib/cmdrhandler/src/Shared/CmdrUtil.luau @@ -6,40 +6,43 @@ local GroupService = game:GetService("GroupService") local Players = game:GetService("Players") --// Imports //-- -local GroupCache = (setmetatable({}, {__mode = "k"}) :: any) :: {[Player]: {{ - Name: string, - Id: number, - Rank: number, - Role: string, - IsPrimary: boolean, -}}} +local GroupCache = (setmetatable({}, { __mode = "k" }) :: any) :: { + [Player]: { + { + Name: string, + Id: number, + Rank: number, + Role: string, + IsPrimary: boolean, + } + }, +} local function RefreshGroupCache(plr: Player) - task.spawn(function() - local groups = GroupService:GetGroupsAsync(plr.UserId) - GroupCache[plr] = groups - -- if RunService:IsStudio() then - -- print("[Cmdr] Cached Groups for ", plr.Name, ":", groups) - -- end - end) + task.spawn(function() + local groups = GroupService:GetGroupsAsync(plr.UserId) + GroupCache[plr] = groups + -- if RunService:IsStudio() then + -- print("[Cmdr] Cached Groups for ", plr.Name, ":", groups) + -- end + end) end Players.PlayerAdded:Connect(RefreshGroupCache) for _, plr in ipairs(Players:GetPlayers()) do - RefreshGroupCache(plr) + RefreshGroupCache(plr) end - -------------------------------------------------------------------------------- - --// Class //-- +--// Class //-- -------------------------------------------------------------------------------- local Util = {} -function Util.getPlayerPermissions(cmdrWrapper, plr: Player, rawPlayerPerms: {string}): {string} - local playerPerms = table.clone(rawPlayerPerms) +function Util.getPlayerPermissions(cmdrWrapper, plr: Player, rawPlayerPerms: { string }): { string } + local playerPerms = table.clone(rawPlayerPerms) - for _, permissionGroup: string in playerPerms do + for _, permissionGroup: string in playerPerms do for _, inheritedPerm in cmdrWrapper:GetPermissionInheritance(permissionGroup) do if not table.find(playerPerms, inheritedPerm) then table.insert(playerPerms, inheritedPerm) @@ -47,20 +50,25 @@ function Util.getPlayerPermissions(cmdrWrapper, plr: Player, rawPlayerPerms: {st end end - for _, groupData in GroupCache[plr] or {} do - for _, groupPerm in Util.getGroupRankPermissions(cmdrWrapper, groupData.Id, groupData.Rank) do - if not table.find(playerPerms, groupPerm) then - table.insert(playerPerms, groupPerm) - end - end - end + for _, groupData in GroupCache[plr] or {} do + for _, groupPerm in Util.getGroupRankPermissions(cmdrWrapper, groupData.Id, groupData.Rank) do + if not table.find(playerPerms, groupPerm) then + table.insert(playerPerms, groupPerm) + end + end + for _, groupPerm in Util.getGroupRolePermissions(cmdrWrapper, groupData.Id, groupData.Role) do + if not table.find(playerPerms, groupPerm) then + table.insert(playerPerms, groupPerm) + end + end + end - return playerPerms + return playerPerms end -function Util.getGroupRankPermissions(cmdrWrapper, groupId: number, rank: number): {string} - local groupData = cmdrWrapper:_getRawGroupPerms(groupId) - local rankPerms = {} +function Util.getGroupRankPermissions(cmdrWrapper, groupId: number, rank: number): { string } + local groupData = cmdrWrapper:_getRawGroupPerms(groupId) + local rankPerms = {} -- This is terribly unoptimized, feel free to optimize it for _, groupPermData in groupData do @@ -81,4 +89,25 @@ function Util.getGroupRankPermissions(cmdrWrapper, groupId: number, rank: number return rankPerms end -return Util \ No newline at end of file +function Util.getGroupRolePermissions(cmdrWrapper, groupId: number, role: string): { string } + local groupData = cmdrWrapper:_getRawGroupPerms(groupId) + local rolePerms = {} + + -- This is terribly unoptimized, feel free to optimize it + for _, groupPermData in groupData do + for _, permissionGroup: string in groupPermData.Permissions do + if not table.find(rolePerms, permissionGroup) then + table.insert(rolePerms, permissionGroup) + end + for _, inheritedPerm in cmdrWrapper:GetPermissionInheritance(permissionGroup) do + if not table.find(rolePerms, inheritedPerm) then + table.insert(rolePerms, inheritedPerm) + end + end + end + end + + return rolePerms +end + +return Util diff --git a/lib/cmdrhandler/src/Shared/PermissionsHandler.luau b/lib/cmdrhandler/src/Shared/PermissionsHandler.luau index 4aef5c0..c1ff92d 100644 --- a/lib/cmdrhandler/src/Shared/PermissionsHandler.luau +++ b/lib/cmdrhandler/src/Shared/PermissionsHandler.luau @@ -192,9 +192,10 @@ function PermissionsHandler:GiveRobloxGroupRankPermissionGroups( if RunService:IsStudio() then print( - `[PermissionsHandler] - Granted group [{robloxGroupId}] permissions [{table.concat(permissionGroups :: any, ", ")}] for ranks [{( - ranks :: NumberRange - ).Min}, {(ranks :: NumberRange).Max}]` + `[PermissionsHandler] - Granted group [{robloxGroupId}] permissions [{table.concat( + permissionGroups :: any, + ", " + )}] for ranks [{(ranks :: NumberRange).Min}, {(ranks :: NumberRange).Max}]` ) end @@ -219,6 +220,9 @@ function PermissionsHandler:GiveRobloxGroupRolePermissionGroups( role: string, permissionGroups: string | { string } ): () -> () + error( + "Using group roles for permissions does not work properly yet since Player:GetRoleInGroup() returns the player rank instead of the role. Use :GiveRobloxGroupRankPermissionGroups() instead." + ) assert(typeof(robloxGroupId) == "number", "Bad robloxGroupId") assert(typeof(role) == "string", "Bad role") assert(typeof(permissionGroups) == "string" or typeof(permissionGroups) == "table", "Bad permissions") @@ -250,7 +254,10 @@ function PermissionsHandler:GiveRobloxGroupRolePermissionGroups( if RunService:IsStudio() then print( - `[PermissionsHandler] - Granted group [{robloxGroupId}] permissions [{table.concat(permissionGroups :: any, ", ")}] for role [{role}]` + `[PermissionsHandler] - Granted group [{robloxGroupId}] permissions [{table.concat( + permissionGroups :: any, + ", " + )}] for role [{role}]` ) end @@ -271,7 +278,10 @@ end @param permissionGroup -- The permission group to set the inheritance for @param inheritedGroups -- The group(s) to inherit permissions from ]=] -function PermissionsHandler:SetPermissionGroupInheritance(permissionGroup: string, inheritedGroups: string | { string }): () +function PermissionsHandler:SetPermissionGroupInheritance( + permissionGroup: string, + inheritedGroups: string | { string } +): () assert(typeof(permissionGroup) == "string", "Bad permissionGroup") if typeof(inheritedGroups) == "string" then inheritedGroups = { inheritedGroups } @@ -331,7 +341,9 @@ function PermissionsHandler:_getPlayerPermissionGroupsData(): { [Player]: { stri return playerPermissionGroups end -function PermissionsHandler:_getGroupRankPermissionGroupsData(): { [GroupId]: { { Ranks: NumberRange, Permissions: { string } } } } +function PermissionsHandler:_getGroupRankPermissionGroupsData(): { + [GroupId]: { { Ranks: NumberRange, Permissions: { string } } }, +} return groupRankPermissionGroups end @@ -352,7 +364,9 @@ function PermissionsHandler:_setPlayerPermissionGroupsData(data: { [Player]: { s playerPermissionGroups = data or {} end -function PermissionsHandler:_setGroupRankPermissionGroupsData(data: { [GroupId]: { { Ranks: NumberRange, Permissions: { string } } } }?): () +function PermissionsHandler:_setGroupRankPermissionGroupsData(data: { + [GroupId]: { { Ranks: NumberRange, Permissions: { string } } }, +}?): () groupRankPermissionGroups = data or {} end From 2ffd59acfc5c2c318387469949ac13413af5f0b5 Mon Sep 17 00:00:00 2001 From: Mophyr Avern Date: Thu, 27 Nov 2025 17:32:24 -0300 Subject: [PATCH 2/4] Remove debug warn --- lib/cmdrhandler/src/Server/init.luau | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/cmdrhandler/src/Server/init.luau b/lib/cmdrhandler/src/Server/init.luau index 27776b0..9cebc39 100644 --- a/lib/cmdrhandler/src/Server/init.luau +++ b/lib/cmdrhandler/src/Server/init.luau @@ -344,7 +344,6 @@ function CmdrServer:Init() CmdrServer.PermissionsHandler.GroupRolePermissionGroupsChanged:Connect(function(newValue) Client.GroupRolePermissionGroups:Set(newValue) - warn("Changed to", newValue) end) CmdrServer.PermissionsHandler.PlayerPermissionGroupsChanged:Connect(function(player, newValue) From 2b7dddaa4543fa2b3ffecde7876938dba860021b Mon Sep 17 00:00:00 2001 From: Logan Hunt <2dloganh@gmail.com> Date: Mon, 1 Dec 2025 17:45:05 -0500 Subject: [PATCH 3/4] Add server-only assertions to permission methods Added assertions to ensure key PermissionsHandler methods are only called on the server. This helps prevent misuse of permission group management functions from the client. --- lib/cmdrhandler/src/Shared/PermissionsHandler.luau | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/cmdrhandler/src/Shared/PermissionsHandler.luau b/lib/cmdrhandler/src/Shared/PermissionsHandler.luau index 046bd0f..58a4bd6 100644 --- a/lib/cmdrhandler/src/Shared/PermissionsHandler.luau +++ b/lib/cmdrhandler/src/Shared/PermissionsHandler.luau @@ -26,6 +26,7 @@ type CmdrRegistry = { local Packages = script.Parent.Parent.Parent local Signal = require(Packages.Signal) :: any +local IS_SERVER = RunService:IsServer() -- Default permission inheritance structure (shared constant) local DEFAULT_PERMISSION_INHERITANCE: { [string]: { string } } = { @@ -104,6 +105,7 @@ end @return {string} -- Array of permission group names ]=] function PermissionsHandler:GetPlayerPermissionGroups(plr: Player): { string } + assert(IS_SERVER, "PermissionsHandler:GetPlayerPermissionGroups can only be called on the server") local permissions = playerPermissionGroups[plr] or {} return Util.getPlayerPermissions(PermissionsHandler, plr, permissions) end @@ -115,6 +117,7 @@ end @param permissions -- The permission group(s) to set ]=] function PermissionsHandler:SetPlayerPermissionGroups(plr: Player, permissions: string | { string }): () + assert(IS_SERVER, "PermissionsHandler:SetPlayerPermissionGroups can only be called on the server") if typeof(permissions) == "string" then permissions = { permissions } end @@ -128,6 +131,7 @@ end @param permissionGroups -- The permission groups to grant ]=] function PermissionsHandler:GivePlayerPermissionGroups(plr: Player, permissionGroups: string | { string }): () + assert(IS_SERVER, "PermissionsHandler:GivePlayerPermissionGroups can only be called on the server") if typeof(permissionGroups) == "string" then permissionGroups = { permissionGroups } end @@ -209,6 +213,9 @@ function PermissionsHandler:GiveRobloxGroupRankPermissionGroups( end --[=[ + @ignore + @unreleased + Grants specified roles in a Roblox Group permission to use commands under a given PermissionGroup. @param robloxGroupId -- The Roblox group id to grant permissions to @param role -- The role name to grant permissions to @@ -282,6 +289,7 @@ function PermissionsHandler:SetPermissionGroupInheritance( permissionGroup: string, inheritedGroups: string | { string } ): () + assert(IS_SERVER, "PermissionsHandler:SetPermissionGroupInheritance can only be called on the server") assert(typeof(permissionGroup) == "string", "Bad permissionGroup") if typeof(inheritedGroups) == "string" then inheritedGroups = { inheritedGroups } From 64d783bce4a2ffa5f1df8d11f6c8d56bfaa22f45 Mon Sep 17 00:00:00 2001 From: Logan Hunt <2dloganh@gmail.com> Date: Mon, 1 Dec 2025 17:51:51 -0500 Subject: [PATCH 4/4] Remove server assertion from GetPlayerPermissionGroups Eliminated the IS_SERVER assertion in PermissionsHandler:GetPlayerPermissionGroups, allowing the function to be called in non-server contexts. This may improve flexibility for permission checks outside the server environment. --- lib/cmdrhandler/src/Shared/PermissionsHandler.luau | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/cmdrhandler/src/Shared/PermissionsHandler.luau b/lib/cmdrhandler/src/Shared/PermissionsHandler.luau index 58a4bd6..b58eab5 100644 --- a/lib/cmdrhandler/src/Shared/PermissionsHandler.luau +++ b/lib/cmdrhandler/src/Shared/PermissionsHandler.luau @@ -105,7 +105,6 @@ end @return {string} -- Array of permission group names ]=] function PermissionsHandler:GetPlayerPermissionGroups(plr: Player): { string } - assert(IS_SERVER, "PermissionsHandler:GetPlayerPermissionGroups can only be called on the server") local permissions = playerPermissionGroups[plr] or {} return Util.getPlayerPermissions(PermissionsHandler, plr, permissions) end