diff options
Diffstat (limited to 'freeradius3-model.lua')
-rw-r--r-- | freeradius3-model.lua | 290 |
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 |