summaryrefslogtreecommitdiffstats
path: root/freeradius3-model.lua
diff options
context:
space:
mode:
Diffstat (limited to 'freeradius3-model.lua')
-rw-r--r--freeradius3-model.lua290
1 files changed, 290 insertions, 0 deletions
diff --git a/freeradius3-model.lua b/freeradius3-model.lua
index 473219e..1879437 100644
--- a/freeradius3-model.lua
+++ b/freeradius3-model.lua
@@ -6,6 +6,7 @@ posix = require("posix")
fs = require("acf.fs")
format = require("acf.format")
validator = require("acf.validator")
+subprocess = require("subprocess")
-- Set variables
local processname = "radiusd"
@@ -14,6 +15,9 @@ local baseurl = "/etc/raddb"
local owner = "radius"
local group = "root"
+local config
+local configtable
+
-- ################################################################################
-- LOCAL FUNCTIONS
@@ -22,6 +26,146 @@ local is_valid_filename = function(filename)
return validator.is_valid_filename(filename) and string.match(dirname, baseurl) and not string.match(dirname, "%.%.")
end
+local get_config = function()
+ if config then return true end
+ local code, cmdresult = subprocess.call_capture({"radiusd", "-XC"})
+ if 0 ~= code then
+ return false, string.match(cmdresult, "([^\n]+)\n$")
+ end
+ config = {}
+ for line in string.gmatch(cmdresult, "[^\n]+") do
+ if string.match(line, "^including") then
+ elseif string.match(line, "^Using") then
+ elseif string.match(line, "^reading") then
+ elseif string.match(line, "^Ignoring") then
+ --elseif string.match(line, "^Configuration ") then
+ elseif string.match(line, "^radiusd: ") then
+ elseif string.match(line, "^rlm_passwd: ") then
+ elseif string.match(line, "^%[/etc/raddb/") then
+ elseif string.match(line, "^%s*#") then
+ elseif string.match(line, "^%c") then
+ else
+ -- We want to remove spaces at beginning or end, and comments from end (being careful of quotes)
+ local tmp = string.match(line, "%S.*")
+ local nextcomment = string.find(tmp, "#")
+ local quotestart, quoteend
+ quotestart, quoteend = string.find(tmp, '%b""')
+
+ while nextcomment and (nextcomment < #tmp) and quotestart and (quotestart < nextcomment) do
+ if nextcomment < quoteend then
+ nextcomment = string.find(tmp, "#", nextcomment+1)
+ else
+ quotestart, quoteend = string.find(tmp, '%b""', quoteend+1)
+ end
+ end
+
+ if nextcomment then
+ tmp = string.sub(tmp, 1, nextcomment-1)
+ end
+ config[#config+1] = tmp
+ end
+ end
+ config[#config] = nil
+
+ -- At this point, every line should have {, =, or }
+ -- We will parse the lines to create a table structure
+ function parselines(index)
+ local result = {}
+ local i=index
+ local name,value
+ while i<#config do
+ -- Look for = first because line might contain { or }
+ if string.find(config[i], '=') then
+ name, value = string.match(config[i], "(.*%S)%s*=%s*(.*)")
+ if string.find(value, "^\".*\"$") then
+ value = string.sub(value, 2, -2)
+ end
+ result[#result+1] = {name=name, value=value}
+ i = i+1
+ elseif string.find(config[i], '}') then
+ break
+ elseif string.find(config[i], '{') then
+ name = string.match(config[i], "(.*%S)%s*{")
+ value, i = parselines(i+1)
+ result[#result+1] = {name=name, value=value}
+ else
+ mymodule.logevent("radiusd bad config line:"..config[i])
+ end
+ end
+
+ return result, i+1
+ end
+
+ configtable = parselines(1)
+ config = table.concat(config,"\n")
+
+ return true
+end
+
+local get_passwd_files = function()
+ local files
+ local configs
+ local result,errtxt = get_config()
+ if result then
+ -- Find the files by searching for modules / passwd
+ files = {}
+ configs = {}
+ for i,first in ipairs(configtable) do
+ if string.find(first.name, "^modules$") then
+ for j,second in ipairs(first.value) do
+ if string.find(second.name, "^passwd ") then
+ for k,third in ipairs(second.value) do
+ if string.find(third.name, "^filename$") then
+ files[#files+1] = third.value
+ configs[#configs+1] = second.value
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ return files, errtxt, configs
+end
+
+local parse_passwd_config = function(config)
+ local retval = {filename="", delimiter=":", fields={}, key={}, allowmultiplekeys=false}
+ for i,l in ipairs(config) do
+ if string.match(l.name, "^delimiter$") then
+ retval.delimiter = l.value
+ elseif string.match(l.name, "^allowmultiplekeys$") then
+ retval.allowmultiplekeys = (l.value == "yes")
+ elseif string.match(l.name, "^format$") then
+ -- ex. format = "*User-Name:Crypt-Password:"
+ for field in string.gmatch(l.value, "([^:]*):?") do
+ field = field or ""
+ retval.fields[#retval.fields+1] = string.match(field, "^[*~=,]*(.*)")
+ if string.match(field, "^%*,") then
+ retval.key.index = #retval.fields
+ retval.key.multiple = true
+ elseif string.match(field, "^%*") then
+ retval.key.index = #retval.fields
+ retval.key.multiple = false
+ end
+ end
+ end
+ end
+
+ return retval
+end
+
+local b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./"
+local mksalt = function()
+ local file = io.open("/dev/urandom")
+ local str = ""
+ if file == nil then return nil end
+ for i = 1,16 do
+ local offset = (string.byte(file:read(1)) % 64) + 1
+ str = str .. string.sub (b64, offset, offset)
+ end
+ return str
+end
+
-- ################################################################################
-- PUBLIC FUNCTIONS
@@ -115,4 +259,150 @@ function mymodule.deletefile(self, delfile)
return delfile
end
+function mymodule.list_passwd_files()
+ local retval = {}
+ local files,errtxt = get_passwd_files()
+ if files then
+ for i,file in ipairs(files) do
+ local details = fs.stat(file)
+ details.filename = file
+ table.insert(retval, details)
+ end
+ table.sort(retval, function(a,b) return a.filename < b.filename end)
+ end
+
+ return cfe({ type="structure", value=retval, label="List of Freeradius passwd files", errtxt=errtxt })
+end
+
+local get_passwd_file = function(self, clientdata)
+ local retval = cfe({ type="group", value={}, label="Freeradius passwd file" })
+ retval.value.filename = cfe({ type="select", label="File name", option ={}, key=true, seq=1 })
+ local files,configs,passwdconfig
+ files,retval.errtxt,configs = get_passwd_files()
+ if files then
+ retval.value.filename.option = files
+ self.handle_clientdata(retval, clientdata)
+ if retval.value.filename.value then
+ retval.value.filename.errtxt = "Invalid selection"
+ for i,f in ipairs(files) do
+ if f == retval.value.filename.value then
+ retval.value.filename.errtxt = nil
+ retval.value.filename.readonly = true
+ passwdconfig = parse_passwd_config(configs[i])
+ break
+ end
+ end
+ end
+ end
+
+ -- return the CFE structure and the config info for this filename
+ return retval, passwdconfig
+end
+
+function mymodule.view_passwd_file(self, clientdata)
+ local retval,passwdconfig = get_passwd_file(self, clientdata)
+ if passwdconfig then
+ -- We have a valid filename, now parse the file using the format from the config
+ retval.value.fields = cfe({ type="structure", value=passwdconfig.fields, label="Fields" })
+ retval.value.data = cfe({ type="structure", value={}, label="Data" })
+ local content = fs.read_file(retval.value.filename.value) or ""
+ for line in string.gmatch(content, "[^\n]+") do
+ retval.value.data.value[#retval.value.data.value+1] = format.string_to_table(line, format.escapemagiccharacters(passwdconfig.delimiter))
+ end
+ end
+ return retval
+end
+
+function mymodule.get_passwd_entry(self, clientdata)
+ local retval,passwdconfig = get_passwd_file(self, clientdata)
+ retval.label = "Freeradius passwd entry"
+ retval.value.entry = cfe({ label="Entry index", key=true, seq=2 })
+ self.handle_clientdata(retval, clientdata)
+ if passwdconfig then
+ -- The filename is valid, and we should create the fields
+ retval.value.fields = cfe({ type="group", value={}, label="Fields", seq=3, delimiter=passwdconfig.delimiter })
+ retval.value.entry.errtxt = "Invalid entry"
+ local content = fs.read_file(retval.value.filename.value) or ""
+ local maxcount = 0
+ local entry = tonumber(retval.value.entry.value) or 0
+ local entryline = ""
+ local i=1
+ for line in string.gmatch(content, "[^\n]+") do
+ local _,count = string.gsub(line, format.escapemagiccharacters(passwdconfig.delimiter), " ")
+ maxcount = math.max(maxcount, count)
+ if i == entry then
+ retval.value.entry.readonly = true
+ retval.value.entry.errtxt = nil
+ entryline = format.string_to_table(line, format.escapemagiccharacters(passwdconfig.delimiter))
+ end
+ i = i+1
+ end
+ for i=1, math.max(#passwdconfig.fields, maxcount+1), 1 do
+ local label = passwdconfig.fields[i]
+ if not label or label == "" then
+ label = "Unlabeled field "..i
+ end
+ if label == "Crypt-Password" then
+ -- We do not return the encrypted password, but will leave unchanged if blank
+ retval.value.fields.value[tostring(i)] = cfe({ type="password", label=label, descr="Leave blank to leave unchanged", seq=i })
+ retval.value.fields.value["algorithm"..i] = cfe({ type="select", value="$6$", label="Algorithm", option={{value="", label="DES"}, {value="$1$", label="MD5"}, {value="$2$", label="Blowfish"}, {value="$2a$", label="eksblowfish"}, {value="$5$", label="SHA-256"}, {value="$6$", label="SHA-512"}}, seq=i })
+ -- Hide the algorithm so user does not use insecure algorithms unless they REALLY want to
+ retval.value.fields.value["algorithm"..i].type = "hidden"
+ else
+ retval.value.fields.value[tostring(i)] = cfe({ label=label, value=entryline[i] or "", seq=i })
+ end
+ end
+ end
+ return retval
+end
+
+function mymodule.update_passwd_entry(self, entry)
+ -- The password/index fields have already been validated
+ if not entry.value.fields then
+ entry.errtxt = "Invalid passwd entry"
+ else
+ -- The only fields we can validate are the password algorithms
+ -- Don't search for 'select' cfe's because they have been changed to hidden
+ for n,v in pairs(entry.value.fields.value) do
+ if v.option and not modelfunctions.validateselect(v) then
+ entry.errtxt = "Invalid passwd entry"
+ end
+ end
+ if not entry.errtxt then
+ -- Set the value
+ local content = fs.read_file_as_array(entry.value.filename.value) or {}
+ local values = {}
+ for n,v in pairs(entry.value.fields.value) do
+ if v.type=="password" and v.value=="" then
+ -- Keep the same password
+ local line = format.string_to_table(content[tonumber(entry.value.entry.value)], entry.value.fields.delimiter) or {}
+ values[tonumber(n)] = line[tonumber(n)] or ""
+ elseif v.type=="password" then
+ local salt = entry.value.fields.value["algorithm"..n].value
+ if salt == "" then
+ salt = string.sub(mksalt(), 1, 2)
+ else
+ salt = salt..(mksalt() or "").."$"
+ end
+ local crypt,errtxt = posix.crypt(v.value, salt)
+ if crypt then
+ values[tonumber(n)] = crypt
+ else
+ v.errtxt = errtxt
+ entry.errtxt = "Invalid passwd entry"
+ break
+ end
+ elseif tonumber(n) then
+ values[tonumber(n)] = v.value
+ end
+ end
+ if not entry.errtxt then
+ content[tonumber(entry.value.entry.value)] = table.concat(values, entry.value.fields.delimiter)
+ fs.write_file(entry.value.filename.value, table.concat(content, "\n"))
+ end
+ end
+ end
+ return entry
+end
+
return mymodule