All pastes #450952 Raw Edit

Mine

public unlisted text v1 · immutable
#450952 ·published 2007-04-21 04:53 UTC
rendered paste body
--[[
	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