--[[
Title: UCL
ULib's Access Control List
]]
--[[
Variable: PASSWORD_TIME
Default password time for authenticating
]]
local PASSWORD_TIME = 30
ULib.convar( "ulib_passtime", PASSWORD_TIME, ULib.ACCESS_ADMIN )
--[[
Variable: PASSWORD_TIMEOUT
Default password timeout in minutes (at which point they'll have to re-enter their password).
]]
local PASSWORD_TIMEOUT = 60
ULib.convar( "ulib_passtimeout", PASSWORD_TIMEOUT, ULib.ACCESS_ADMIN )
--[[
Section: Format
This section will tell you how to format your strings/files for UCLs.
Format of admin account in users.txt--
"<account_name>"
{
"id" "<name|ip|steamid|clantag>"
"type" "<name|ip|steamid|clantag>"
"pass" "<password>"
"pass_req" "<0|1>"
"groups"
{
"superadmin"
"immunity"
}
"allow"
{
"ulx kick"
"ulx ban"
}
"deny"
{
"ulx cexec"
}
}
Format of group in groups.txt--
"<group_name>"
{
"allow"
{
"ulx kick"
"ulx ban"
}
"deny"
{
"ulx cexec"
}
}
]]
--[[
Table: ucl
Holds all of the ucl variables and functions
]]
ULib.ucl = {}
local ucl = ULib.ucl -- Make it easier for us to refer to
-----------------------------------
--// Setup default information //--
-----------------------------------
ucl.users = {}
ucl.groups = {}
ucl.authed = {}
ucl.awaitingauth = {}
ucl.callbacks = {}
local function ucl_initialspawn( ply )
ucl.authed[ ply ] = nil
ucl.awaitingauth[ ply ] = nil
ucl.probe( ply )
end
hook.Add( "PlayerInitialSpawn", "ucl_initialspawn", ucl_initialspawn )
local function ucl_disconnect( ply )
ucl.authed[ ply ] = nil
ucl.awaitingauth[ ply ] = nil
end
hook.Add( "PlayerDisconnected", "routeUserDisconnect", userDisconnect )
--[[
Function: ULib.ucl.addGroup
Adds a new group to the UCL.
Parameters:
name - A string of the group name. (IE: superadmin)
acl - The ACL for the group.
write - *(Optional, defaults to false)* Write this new or changed group to the file.
]]
function ucl.addGroup( name, acl, write )
ucl.groups[ name ] = ucl.groups[ name ] or {}
ucl.groups[ name ].allow = acl.allow or {}
ucl.groups[ name ].deny = acl.deny or {}
if write then
local groups = util.KeyValuesToTable( file.Read( ULib.UCL_GROUPS ) )
groups[ name ] = acl
file.Write( ULib.UCL_GROUPS, util.TableToKeyValues( groups ) )
end
end
--[[
Function: ULib.ucl.addUser
Adds a new user to the UCL.
Parameters:
account - The unique user account name. Has no effect on access, just for reference.
typ - The string type of this account. Valid options are steamid, ip, clantag, and name.
id - The ID (steamid, name, etc).
groups - A table of groups this user belongs to.
acl - *(Optional, defaults to {})* The ACL for this user.
pass - *(Optional, defaults to "")* The password for this user.
pass_req - *(Optional, defaults to false)* Whether or not the user can stay in the server without the correct password.
write - *(Optional, defaults to false)* Write this new or changed user to the file.
]]
function ucl.addUser( account, typ, id, groups, acl, pass, pass_req, write )
acl = acl or {}
pass = pass or ""
if not account or type( account ) ~= "string" or
not typ or type( typ ) ~= "string" or
not id or type( id ) ~= "string" or
not groups or type( groups ) ~= "table" or
not acl or type( acl ) ~= "table" or
not pass or type( "pass" ) ~= "string" then
ULib.error( "Bad user passed to addUser! User account name was " .. tostring( account ) )
return
end
typ = string.lower( typ ) -- Error catching
if typ ~= "name" and typ ~= "clantag" and typ ~= "ip" and typ ~= "steamid" then
ULib.error( "Invalid user type: " .. typ .. " for account: " .. account )
return
end
ucl.users[ account ] = {
type = typ,
id = id,
groups = groups,
allow = acl.allow or {},
deny = acl.deny or {},
pass = pass,
pass_req = pass_req,
}
for _, group in pairs( groups ) do
if not ucl.groups[ group ] then
ucl.addGroup( group, {allow={}, deny={}} )
end
end
-- Handle adding the user if they're connected
if typ == "name" then
local players = player.GetAll()
for _, player in ipairs( players ) do
if player:IsConnected() and player:Nick() == id then
ucl.probe( player )
break
end
end
elseif typ == "clantag" then
local users = ULib.getUsers( id, true )
if users then -- If there's users
for _, player in ipairs( users ) do
ucl.probe( player )
end
end
elseif typ == "ip" then
local players = player.GetAll()
for _, player in ipairs( players ) do
if player:IsConnected() then
local ip = string.gsub( player:IPAddress(), "^(.-):(.+)$", "%1" ) -- Strip the port
if ip == id then
ucl.probe( player )
break
end
end
end
elseif typ == "steamid" then
local players = player.GetAll()
for _, player in ipairs( players ) do
if player:IsConnected() and player:SteamID() == id then
ucl.probe( player )
-- We're not breaking in case they add ID_LAN
end
end
end
if write then
local users = util.KeyValuesToTable( file.Read( ULib.UCL_USERS ) )
users[ account ] = ucl.users[ account ]
file.Write( ULib.UCL_USERS, util.TableToKeyValues( users ) )
end
end
--[[
Function: ULib.ucl.removeUser
Removes a user from the UCL object "obj". Also removes them from the authed list.
Parameters:
account - The unique user account name. Has no effect on access, just for reference.
write - *(Optional, defaults to false)* Write this deletion to the file.
]]
function ucl.removeUser( account, write )
if not ucl.users[ account ] then
ULib.error( "removeUser was passed a non-existant account " .. tostring( account ) )
return
end
local typ = ucl.users[ account ].type
local id = ucl.users[ account ].id
if typ == "name" then
local players = player.GetAll()
for _, player in ipairs( players ) do
if player:IsConnected() and player:Nick() == id then
ucl.authed[ player ] = nil
break
end
end
elseif typ == "clantag" then
local users = ULib.getUsers( id, true )
if users then -- If there's users
for _, player in ipairs( users ) do
ucl.authed[ player ] = nil
end
end
elseif typ == "ip" then
local players = player.GetAll()
for _, player in ipairs( players ) do
if player:IsConnected() then
local ip = string.gsub( player:IPAddress(), "^(.-):(.+)$", "%1" ) -- Strip the port
if ip == id then
ucl.authed[ player ] = nil
break
end
end
end
elseif typ == "steamid" then
local players = player.GetAll()
for _, player in ipairs( players ) do
if player:IsConnected() and player:SteamID() == id then
ucl.authed[ player ] = nil
-- We're not breaking in case they add ID_LAN
end
end
end
ucl.users[ account ] = nil
if write then
local users = util.KeyValuesToTable( file.Read( ULib.UCL_USERS ) )
users[ account ] = nil
file.Write( ULib.UCL_USERS, util.TableToKeyValues( users ) )
end
end
--[[
Function: ULib.ucl.probe
Probes the user to assign access appropriately.
*DO NOT CALL THIS DIRECTLY, UCL HANDLES IT.*
Parameters:
ply - The player object to probe.
]]
function ucl.probe( ply )
if ucl.authed[ ply ] or ucl.awaitingauth[ ply ] then return end -- They've already been probed
local steamid = ply:SteamID()
local name = ply:Nick()
local ip = string.gsub( ply:IPAddress(), "^(.-):(.+)$", "%1" ) -- Strip the port
-- Let's make some quick lookup tables
local ips = {}
local steamids = {}
local names = {}
local clantags = {}
for _, v in pairs( ucl.users ) do
if v.type == "ip" then ips[ v.id ] = v
elseif v.type == "steamid" then steamids[ v.id ] = v
elseif v.type == "name" then names[ v.id ] = v
elseif v.type == "clantag" then clantags[ v.id ] = v end
end
local match
-- Let's figure out if they have any access
if steamids[ steamid ] then
match = steamids[ steamid ]
elseif ips[ ip ] then
match = ips[ ip ]
elseif names[ name ] then
match = names[ name ]
elseif ply:IsListenServerHost() then
match = ULib.LISTEN_ACCESS
else
local match
for _, v in ipairs( clantags ) do
local tag = v.id
if string.find( name, tag ) then
match = v
break
end
end
end
-- Now let's allocate access properly
if match then -- If they have some sort of access
local account -- Let's grab the account name
for name, v in pairs( ucl.users ) do
if v == match then
account = name
break
end
end
match = table.Copy( match ) -- We'll want to add some custom information
match.uniqueid = ply:UniqueID()
match.account = account
if match.pass and match.pass ~= "" then -- If there's a password, put them on the awaiting list.
local passtimeout = GetConVarNumber( "ulib_passtimeout" )
passtimeout = passtimeout * 60 -- Convert to minutes
if match.login_time and match.login_time + passtimeout > os.time() then -- If they still have an active password from a previous map change
ULib.tsay( ply, "[UCL] Access set." )
ucl.authed[ ply ] = match
ucl.callCallbacks( ply )
local timeago = os.time() - match.login_time -- How long ago did they login in?
timer.Simple( passtimeout - timeago, ucl.passTimeout, ply, match.pass )
return -- They have access, we're done.
end
ucl.awaitingauth[ ply ] = match -- They don't have an active password, put them on the backburner
if util.tobool( match.pass_req ) then -- If the password is required, we have special steps.
local passtime = GetConVarNumber( "ulib_passtimeout" )
ULib.tsay( ply, "[UCL] You *MUST* enter your user password using the command \"_pw\" within " .. passtime .. " seconds, or be kicked." )
timer.Simple( passtime, ucl.checkAuth, ply )
else
ULib.tsay( ply, "[UCL] Please enter your user password using the command \"_pw\" to claim your admin access." )
end
else -- If they don't need a password, they're set!
match.pass = nil
ULib.tsay( ply, "[UCL] ULX is Teh UberLol! MegiDdos Cat Can Bow Down TO MYNE! Jamminrs a cupcake lol ol ol ol o l!! wtchaxstuff!!!." )
ucl.authed[ ply ] = match
ucl.callCallbacks( ply ) -- Let others know they got access.
end
else -- They have no access, let's tell our callback.
ucl.callCallbacks( ply )
end
end
--[[
Function: ULib.ucl.checkPass
The callback for "_pw". *DO NOT CALL DIRECTLY*.
Parameters:
ply - The player to check
command - The command
argv - The argv
args - The password
]]
function ucl.checkPass( ply, command, argv, args )
if not ucl.awaitingauth[ ply ] then
if ucl.authed[ ply ] then
ULib.console( ply, "[UCL] You are already authenticated." )
return
else
ULib.console( ply, "[UCL] Why are you trying to enter a password when you have no access at all?" )
return
end
end
local pass = ULib.stripQuotes( args )
if pass == ucl.awaitingauth[ ply ].pass then -- They got the right pass.
ucl.authed[ ply ] = ucl.awaitingauth[ ply ]
-- TODO: Add a handler for saving login time.
local passtimeout = GetConVarNumber( "ulib_passtimeout" )
passtimeout = passtimeout * 60 -- Convert to minutes
timer.Simple( passtimeout, ucl.passTimeout, ply, pass ) -- Add a timer for their password timeout
ucl.awaitingauth[ ply ] = nil -- They are now authed, take them off awaiting.
ucl.authed[ ply ].pass = nil
ULib.console( ply, "[UCL] Correct password, thank you!" )
ucl.callCallbacks( ply ) -- Let others know they got access
else
ULib.console( ply, "[UCL] Incorrect password, please try again." )
end
end
--[[
Function: ULib.ucl.checkAuth
The callback to make sure users enter their password within the time limit. *DO NOT CALL DIRECTLY*.
Parameters:
ply - The player.
]]
function ucl.checkAuth( ply )
if not ply:IsConnected() then
ucl.awaitingauth[ ply ] = nil
ucl.authed[ ply ] = nil
end
if not ucl.awaitingauth[ ply ] or ply:UniqueID() ~= ucl.awaitingauth[ ply ].uniqueid then
return -- They already authed or a different player
end
local name = ply:Nick()
ULib.console( ply, "[UCL] Password timeout, good bye." )
ULib.kick( ply )
ULib.tsay( _, "[UCL] " .. name .. " was kicked due to password timeout." )
end
--[[
Function: ULib.ucl.passTimeout
The callback to time out user's passwords. *DO NOT CALL DIRECTLY*.
Parameters:
ply - The player.
pass - Their pass.
]]
function ucl.passTimeout( ply, pass )
if not ply:IsConnected() then
ucl.awaitingauth[ ply ] = nil
ucl.authed[ ply ] = nil
end
if not ucl.authed[ ply ] or ucl.authed[ ply ].uniqueid ~= ply:UniqueID() then
return -- Return if they're disconnected or a different player
end
ULib.tsay( ply, "[UCL] Your password has timed out, please enter your user password using the command \"_pw\" to reclaim your admin access." )
ucl.awaitingauth[ ply ] = ucl.authed[ ply ]
ucl.authed[ ply ] = nil
ucl.awaitingauth[ ply ].pass = pass -- Set them back up to get auth
end
--[[
Function: ULib.ucl.callCallbacks
*Internal function, DO NOT CALL DIRECTLY.* This function calls access callbacks
Parameters:
ply - The player object.
]]
function ucl.callCallbacks( ply )
for _, fn in ipairs( ucl.callbacks ) do
local hasAccess = not not ucl.authed[ ply ] -- Double not! We want it in boolean form.
pcall( fn, ply, hasAccess )
end
end
--[[
Function: ULib.ucl.addAccessCallback
Pass a function and it will be called when someone receives access. Parameters it passes to the function are the player and an access bool.
This function is useful for things like passing menus to users upon access. *This is also called when a user receives NO access*.
Parameters:
fn - The function to call.
]]
function ucl.addAccessCallback( fn )
table.insert( ucl.callbacks, fn )
end
-------------------------------------------
--//Here's the meat of our control list//--
-------------------------------------------
--[[
Function: ULib.ucl.query
This function is used to see if a user has access to a command.
Parameters:
ply - The player to check access for
command - The command you want to see if they have access to
hide - *(Optional, defaults to false)* Normally, a listen host is automatically given access to everything. Set this to true if you want to treat the listen host as a normal user.
]]
function ucl.query( ply, command, hide )
if not ply:IsValid() or (not hide and ply:IsListenServerHost()) then return true end -- Grant full access to server host.
local match -- This will hold the command matching ours
if not command:find( " " ) then
match = ULib.concommands[ command ]
else
local prefix_list = string.Explode( " ", command )
local num = #prefix_list
match = ULib.subconcommands
for i=1, num do -- Get to the table list we need
match = match[ prefix_list[ i ] ]
if not match then
break
end
end
end
if not match then -- Not under ULib control
return ply:IsUserGroup( ULib.DEFAULT_ACCESS )
end
if not ucl.authed[ ply ] then
return ply:IsUserGroup( match.access )
end
-- Now let's go through all applicable allows and denies for this user.
local denies = { ucl.authed[ ply ].deny }
local allows = { ucl.authed[ ply ].allow }
-- We grabbed their user allow/deny, now let's grab all the group allow/denys.
for _, v in pairs( ucl.authed[ ply ].groups ) do
if ucl.groups[ v ] then
table.insert( denies, ucl.groups[ v ].deny )
table.insert( allows, ucl.groups[ v ].allow )
end
end
for _, v in pairs( denies ) do
if table.HasValue( v, command ) then return false end
end
for _, v in pairs( allows ) do
if table.HasValue( v, command ) then return true end
end
-- No specific instruction, fall back on command default.
return ply:IsUserGroup( match.access )
end
---------------------
--//Initial Setup//--
---------------------
ULib.concommand( "_pw", ucl.checkPass )
-- Now let's load our database and add from the default users.txt as necessary.
do
if not file.Exists( ULib.UCL_USERS ) or not file.Exists( ULib.UCL_GROUPS ) then
ULib.error( "FATAL: Missing users.txt and/or groups.txt!" )
return
end
local users = util.KeyValuesToTable( file.Read( ULib.UCL_USERS ) )
local groups = util.KeyValuesToTable( file.Read( ULib.UCL_GROUPS ) )
if not users or not groups then
ULib.error( "FATAL: Unable to load users.txt and/or groups.txt" )
return
end
for group, acl in pairs( groups ) do
ucl.addGroup( group, acl )
end
for account, info in pairs( users ) do
ucl.addUser( account, info.type, info.id, info.groups, {allow=info.allow, deny=info.deny}, info.pass, info.pass_req )
end
-- Load's load garry-default users now
if ULib.UCL_LOAD_DEFAULT then
local garry = util.KeyValuesToTable( file.Read( "../settings/users.txt" ) )
for group, gusers in pairs( garry ) do
local acl
if groups[ group ] then
acl = groups[ group ]
else
acl = { allow={}, deny={} }
end
ucl.addGroup( group, acl, true )
for user, steamid in pairs( gusers ) do
local info
if users[ user ] then
info = users[ user ]
info.id = steamid
else
info = { type="steamid", id=steamid, groups={group}, allow={}, deny={}, pass="", pass_req=false}
end
ucl.addUser( user, info.type, info.id, info.groups, {allow=info.allow or {}, deny=info.deny or {}}, info.pass, info.pass_req, true )
end
end
end
end
-- Let's probe any players that may be connected right now.
do
local players = player.GetAll()
for _, player in ipairs( players ) do
if player:IsConnected() then
ucl.probe( player )
end
end
end
------------------
--//Meta hooks//--
------------------
local meta = FindMetaTable( "Player" )
if not meta then return end
function meta:IsAdmin()
if self:IsSuperAdmin() then return true end
if self:IsUserGroup( "admin" ) then return true end
return false
end
function meta:IsSuperAdmin()
-- Always a super admin if this is our server
if self:IsListenServerHost() then return true end
return self:IsUserGroup( "superadmin" )
end
function meta:IsUserGroup( name )
if name == ULib.ACCESS_ALL then return true end -- Everyone has default access
if name == ULib.ACCESS_NONE then return false end -- No one has this access
if not ucl.authed[ self ] then return false end
-- Let's work our way up the access chain.
if name == ULib.ACCESS_OPERATOR then
if table.HasValue( ucl.authed[ self ].groups, ULib.ACCESS_OPERATOR ) then return true end
if table.HasValue( ucl.authed[ self ].groups, ULib.ACCESS_ADMIN ) then return true end
if table.HasValue( ucl.authed[ self ].groups, ULib.ACCESS_SUPERADMIN ) then return true end
return false
end
if name == ULib.ACCESS_ADMIN then
if table.HasValue( ucl.authed[ self ].groups, ULib.ACCESS_ADMIN ) then return true end
if table.HasValue( ucl.authed[ self ].groups, ULib.ACCESS_SUPERADMIN ) then return true end
return false
end
if name == ULib.ACCESS_SUPERADMIN then
if table.HasValue( ucl.authed[ self ].groups, ULib.ACCESS_SUPERADMIN ) then return true end
return false
end
-- Else fall back on our default.
return table.HasValue( ucl.authed[ self ].groups, name )
end
function meta:query( command, hide )
return ULib.ucl.query( self, command, hide )
end