summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTed Trask <ttrask01@yahoo.com>2014-09-14 00:29:35 +0000
committerTed Trask <ttrask01@yahoo.com>2014-09-14 00:29:35 +0000
commitabaa1ce203d44749ca6073cb55c2016b6787deb0 (patch)
tree6632012da7a8e775c3e9646b167ed941644065e5
parent3449c5940995b0486cddc8ff8389a381649b961a (diff)
downloadacf-freeradius3-abaa1ce203d44749ca6073cb55c2016b6787deb0.tar.bz2
acf-freeradius3-abaa1ce203d44749ca6073cb55c2016b6787deb0.tar.xz
Add listpasswdfiles, viewpasswdfile, and editpasswdentry actions for managing passwd files
We obtain the config by running 'radiusd -XC' and parsing the result passwd files are parsed/displayed based upon the format defined in freeradius config Use posix.crypt to set encrypted password generating salt from /dev/urandom, defaulting to SHA-512
-rw-r--r--freeradius3-controller.lua12
-rw-r--r--freeradius3-listpasswdfiles-html.lsp63
-rw-r--r--freeradius3-model.lua290
-rw-r--r--freeradius3-viewpasswdfile-html.lsp58
-rw-r--r--freeradius3.menu1
-rw-r--r--freeradius3.roles4
6 files changed, 426 insertions, 2 deletions
diff --git a/freeradius3-controller.lua b/freeradius3-controller.lua
index fbd149e..5184ac4 100644
--- a/freeradius3-controller.lua
+++ b/freeradius3-controller.lua
@@ -26,4 +26,16 @@ function mymodule.deletefile(self)
return self.handle_form(self, self.model.getdeletefile, self.model.deletefile, self.clientdata, "Delete", "Delete Freeradius File", "Freeradius File Deleted")
end
+function mymodule.listpasswdfiles(self)
+ return self.model.list_passwd_files()
+end
+
+function mymodule.viewpasswdfile(self)
+ return self.model.view_passwd_file(self, self.clientdata)
+end
+
+function mymodule.editpasswdentry(self)
+ return self.handle_form(self, self.model.get_passwd_entry, self.model.update_passwd_entry, self.clientdata, "Save", "Edit Passwd Entry", "Entry Saved")
+end
+
return mymodule
diff --git a/freeradius3-listpasswdfiles-html.lsp b/freeradius3-listpasswdfiles-html.lsp
new file mode 100644
index 0000000..a3b454c
--- /dev/null
+++ b/freeradius3-listpasswdfiles-html.lsp
@@ -0,0 +1,63 @@
+<% local view, viewlibrary, page_info, session = ...
+htmlviewfunctions = require("htmlviewfunctions")
+html = require("acf.html")
+%>
+
+<%
+function convertsize(size)
+ if string.find(size, "k$") then
+ return tonumber(string.match(size, "[%d.]*")) * 1024.0
+ elseif string.find(size, "M$") then
+ return tonumber(string.match(size, "[%d.]*")) * 1024.0 * 1024.0
+ elseif string.find(size, "G$") then
+ return tonumber(string.match(size, "[%d.]*")) + 1024.0 * 1024.0 * 1024.0
+ else
+ return tonumber(string.match(size, "[%d.]*"))
+ end
+end
+%>
+
+<script type="text/javascript">
+ if (typeof jQuery == 'undefined') {
+ document.write('<script type="text/javascript" src="<%= html.html_escape(page_info.wwwprefix) %>/js/jquery-latest.js"><\/script>');
+ }
+</script>
+
+<script type="text/javascript">
+ if (typeof $.tablesorter == 'undefined') {
+ document.write('<script type="text/javascript" src="<%= html.html_escape(page_info.wwwprefix) %>/js/jquery.tablesorter.js"><\/script>');
+ }
+</script>
+
+<script type="text/javascript">
+ $(document).ready(function() {
+ $("#list").tablesorter({headers: {0:{sorter: false}}, widgets: ['zebra']});
+ });
+</script>
+
+<% local header_level = htmlviewfunctions.displaysectionstart(cfe({label="Passwd Configuration"}), page_info) %>
+<table id="list" class="tablesorter"><thead>
+ <tr>
+ <th>Action</th>
+ <th>File</th>
+ <th>Size</th>
+ <th>Last Modified</th>
+ </tr>
+</thead><tbody>
+<% local filename = cfe({ type="hidden", value="" }) %>
+<% local redir = cfe({ type="hidden", value=page_info.orig_action }) %>
+<% for k,v in ipairs( view.value ) do %>
+ <tr>
+ <td>
+ <% filename.value = v.filename %>
+ <% if viewlibrary.check_permission("viewpasswdfile") then %>
+ <% htmlviewfunctions.displayitem(cfe({type="link", value={filename=filename, redir=redir}, label="", option="View", action="viewpasswdfile"}), page_info, -1) %>
+ <% end %>
+ </td>
+ <td><%= html.html_escape(v.filename) %></td>
+ <td><span class="hide"><%= convertsize(v.size) %>b</span><%= html.html_escape(v.size) %></td>
+ <td><%= html.html_escape(v.mtime) %></td>
+ </tr>
+<% end %>
+</tbody></table>
+<% htmlviewfunctions.displaysectionend(header_level) %>
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
diff --git a/freeradius3-viewpasswdfile-html.lsp b/freeradius3-viewpasswdfile-html.lsp
new file mode 100644
index 0000000..b4e8ce5
--- /dev/null
+++ b/freeradius3-viewpasswdfile-html.lsp
@@ -0,0 +1,58 @@
+<% local view, viewlibrary, page_info, session = ...
+htmlviewfunctions = require("htmlviewfunctions")
+html = require("acf.html")
+%>
+
+<script type="text/javascript">
+ if (typeof jQuery == 'undefined') {
+ document.write('<script type="text/javascript" src="<%= html.html_escape(page_info.wwwprefix) %>/js/jquery-latest.js"><\/script>');
+ }
+</script>
+
+<script type="text/javascript">
+ if (typeof $.tablesorter == 'undefined') {
+ document.write('<script type="text/javascript" src="<%= html.html_escape(page_info.wwwprefix) %>/js/jquery.tablesorter.js"><\/script>');
+ }
+</script>
+
+<script type="text/javascript">
+ $(document).ready(function() {
+ $("#list").tablesorter({headers: {0:{sorter: false}}, widgets: ['zebra']});
+ });
+</script>
+
+<% htmlviewfunctions.displaycommandresults({"editpasswdentry"}, session) %>
+
+<% local header_level = htmlviewfunctions.displaysectionstart(view, page_info) %>
+<% htmlviewfunctions.displayitem(view.value.filename) %>
+<% if view.value.data then %>
+<table id="list" class="tablesorter"><thead>
+ <tr>
+ <th>Action</th>
+<% for i,f in ipairs(view.value.fields.value) do %>
+ <th><%= html.html_escape(f) %></th>
+<% end %>
+ </tr>
+</thead><tbody>
+<% local filename = cfe({ type="hidden", value=view.value.filename.value }) %>
+<% local entry = cfe({ type="hidden", value="" }) %>
+<% local redir = cfe({ type="hidden", value=page_info.orig_action }) %>
+<% -- This is a hack to redirect back to viewing the same file
+redir.value = redir.value.."?filename="..html.url_encode(view.value.filename.value)
+%>
+<% for i,r in ipairs( view.value.data.value ) do %>
+ <tr>
+ <td>
+ <% entry.value = i %>
+ <% if viewlibrary.check_permission("editpasswdentry") then %>
+ <% htmlviewfunctions.displayitem(cfe({type="link", value={filename=filename, entry=entry, redir=redir}, label="", option="Edit", action="editpasswdentry"}), page_info, -1) %>
+ <% end %>
+ </td>
+<% for j,f in ipairs(r) do %>
+ <td><%= html.html_escape(f) %></td>
+<% end %>
+ </tr>
+<% end %>
+</tbody></table>
+<% end %>
+<% htmlviewfunctions.displaysectionend(header_level) %>
diff --git a/freeradius3.menu b/freeradius3.menu
index 5dbfa92..463ad8f 100644
--- a/freeradius3.menu
+++ b/freeradius3.menu
@@ -1,5 +1,6 @@
# Prefix and controller are already known at this point
# Cat Group Tab Action
Applications 15Freeradius Status status
+Applications 15Freeradius Passwd listpasswdfiles
Applications 15Freeradius Expert listfiles
Applications 15Freeradius Logfile logfile
diff --git a/freeradius3.roles b/freeradius3.roles
index 49d3fa7..501d761 100644
--- a/freeradius3.roles
+++ b/freeradius3.roles
@@ -1,3 +1,3 @@
USER=freeradius3:status,freeradius3:startstop,freeradius3:logfile
-EXPERT=freeradius3:listfiles,freeradius3:editfile,freeradius3:createfile,freeradius3:deletefile
-ADMIN=freeradius3:status,freeradius3:startstop,freeradius3:logfile,freeradius3:listfiles,freeradius3:editfile,freeradius3:createfile,freeradius3:deletefile
+EXPERT=freeradius3:listfiles,freeradius3:editfile,freeradius3:createfile,freeradius3:deletefile,freeradius3:listpasswdfiles,freeradius3:viewpasswdfile,freeradius3:editpasswdentry
+ADMIN=freeradius3:status,freeradius3:startstop,freeradius3:logfile,freeradius3:listfiles,freeradius3:editfile,freeradius3:createfile,freeradius3:deletefile,freeradius3:listpasswdfiles,freeradius3:viewpasswdfile,freeradius3:editpasswdentry