local mymodule = {} -- Load libraries modelfunctions = require("modelfunctions") fs = require("acf.fs") format = require("acf.format") validator = require("acf.validator") authenticator = require("authenticator") roles = require("roles") posix = require("posix") -- Set variables local allzonefiles = {} local cachedzonefiles = {} local cached_user local packagename = "nsd" local processname = "nsd" local configfile = "/etc/nsd/nsd.conf" local configdir = "/etc/nsd/" local descr = { prefix={ ['.']="Name server for your domain (NS + A + SOA)", ['&']="Delegate subdomain (NS + A)", ['=']="Host and reverse record (A + PTR)", ['+']="Host record (A, no PTR)", ['@']="Mail exchanger (MX)", ["'"]="Text record (TXT)", ['^']="Reverse record (PTR)", ['C']="Canonical name (CNAME)", ['Z']="SOA record (SOA)", [':']="Generic record", ['%']="Client location", ['S']="Service location (SRV)", ['N']="Naming authority pointer (NAPTR)", }, fieldlabels={ ['.']={"Domain", "IP address", "Name server", "Time to live", "Timestamp", "Location", }, ['&']={"Domain", "IP address", "Name server", "Time to live", "Timestamp", "Location", }, ['=']={"Host", "IP address", "Time to live", "Timestamp", "Location", }, ['+']={"Host", "IP address", "Time to live", "Timestamp", "Location", }, ['@']={"Domain", "IP address", "Mail exchanger", "Distance", "Time to live", "Timestamp", "Location", }, ['\'']={"Domain", "Text Record", "Time to live", "Timestamp", "Location", }, ['^']={"PTR", "Domain name", "Time to live", "Timestamp", "Location", }, ['C']={"Domain", "Canonical name", "Time to live", "Timestamp", "Location", }, ['Z']={"Domain", "Primary name server", "Contact address", "Serial number", "Refresh time", "Retry time", "Expire time", "Minimum time", "Time to live", "Timestamp", "Location",}, [':']={"Domain", "Record type", "Record data", "Time to live", "Timestamp", "Location", }, ['%']={"Location", "IP prefix", }, ['S']={"Domain Service", "IP address", "Server", "Port", "Priority", "Weight", "Time to live", "Timestamp", }, ['N']={"Domain", "Order", "Preference", "Flags", "Service", "Regular expression", "Replacement", "Time to live", "Timestamp", }, }, } -- ################################################################################ -- LOCAL FUNCTIONS -- Function to recursively inserts all file details in a dir into an array local function recursedir(path, filearray) filearray = filearray or {} local k,v for k,v in pairs(posix.dir(path) or {}) do -- Ignore files that begins with a '.' if not string.match(v, "^%.") then local f = path .. v local details = posix.stat(f) -- If subfolder exists, list files in this subfolder if details.type == "directory" then recursedir(f.."/", filearray) elseif details.type == "regular" then details.filename = f table.insert(filearray, details) end end end return filearray end local is_valid_filename = function(filename) local dirname = posix.dirname(filename or "").."/" return validator.is_valid_filename(filename) and string.match(dirname, "^"..format.escapemagiccharacters(configdir)) and not string.match(dirname, "%.%.") end local function getallowedlist(self, userid) local allowedlist = {} local auth = authenticator.get_subauth(self) local entry = auth.read_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, userid) or "" for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end -- also check to see if there are allowed files for this user's roles local userinfo = authenticator.get_userinfo(self, userid) -- add in the guest role userinfo.roles[#userinfo.roles + 1] = roles.guest_role for i,role in ipairs(userinfo.roles) do local entry = auth.read_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, role) or "" for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end end return allowedlist end -- Feed the allzonefiles table with list of all zonefiles in the config local function listzonefiles() if #allzonefiles > 0 then return allzonefiles end -- Parse the config -- TODO - handle include statements -- TODO - should properly parse the whole file, not just look for lines local file = io.open(configfile) local zonesdir local zonefiles = {} for line in file:lines() do if string.find(line, "^%s*zonesdir:") then zonesdir = string.match(line, "^%s*zonesdir:%s*\"(.*)\"") elseif string.find(line, "^%s*zonefile:") then local z = string.match(line, "^%s*zonefile:%s*\"(.*)\"") zonefiles[#zonefiles+1] = z end end file:close() if not zonesdir or zonesdir == "" then zonesdir = configdir elseif not string.find(zonesdir, "/$") then zonesdir = zonesdir.."/" end for i,z in ipairs(zonefiles) do allzonefiles[#allzonefiles+1] = zonesdir..z end return allzonefiles end -- Feed the cachedzonefiles table with list of all zonefiles that are available and allowed -- Default to allowing all files if no userid or allowed list local function searchforzonefiles(self, userid) if #cachedzonefiles > 0 and cached_user == userid then return cachedzonefiles end allzonefiles = listzonefiles(configdir) local allowedlist = getallowedlist(self, userid) if allowedlist and #allowedlist > 0 then local reverseallowed = {} for x,name in ipairs(allowedlist) do reverseallowed[name] = x end for k,v in pairs(allzonefiles) do if reverseallowed[v] then table.insert(cachedzonefiles, v) end end else cachedzonefiles = allzonefiles end cached_user = userid table.sort(cachedzonefiles) return cachedzonefiles end local function is_valid_zonefile(path) for k,v in pairs(cachedzonefiles) do if (v == path) then return true end end return false, "Not a valid filename!" end -- ################################################################################ -- PUBLIC FUNCTIONS function mymodule.get_startstop(self, clientdata) return modelfunctions.get_startstop(processname) end function mymodule.startstop_service(self, startstop, action) return modelfunctions.startstop_service(startstop, action) end function mymodule.getstatus() return modelfunctions.getstatus(processname, packagename, "NSD Status") end function mymodule.list_files(self) local filearray = recursedir(configdir) table.sort(filearray, function(a,b) return a.filename < b.filename end) return cfe({ type="structure", value=filearray, label="List of NSD files" }) end function mymodule.get_file(self, clientdata) local retval = cfe({ type="group", value={ filename=cfe({}) } }) self.handle_clientdata(retval, clientdata) return modelfunctions.getfiledetails(retval.value.filename.value, is_valid_filename) end function mymodule.update_file(self, filedetails) return modelfunctions.setfiledetails(self, filedetails, is_valid_filename) end function mymodule.get_new_file(self, clientdata) local retval = cfe({ type="group", value={}, label="NSD File" }) retval.value.filename = cfe({ label="File Name", descr="Must be in "..configdir }) self.handle_clientdata(retval, clientdata) return retval end function mymodule.create_file(self, filedetails) local success = true local path = string.match(filedetails.value.filename.value, "^%s*(.*%S)%s*$") or "" if not string.find(path, "/") then path = configdir..path end if not is_valid_filename(path) then success = false filedetails.value.filename.errtxt = "Invalid filename" else if not fs.is_dir(configdir) then fs.create_directory(configdir) end if posix.stat(path) then success = false filedetails.value.filename.errtxt = "Filename already exists" end end if success then fs.create_file(path) else filedetails.errtxt = "Failed to Create File" end return filedetails end function mymodule.get_delete_file(self, clientdata) local retval = cfe({ type="group", value={}, label="Delete NSD File" }) retval.value.filename = cfe({ label="File Name" }) self.handle_clientdata(retval, clientdata) return retval end function mymodule.delete_file(self, delfile) delfile.errtxt = "Failed to delete NSD File - invalid filename" for i,file in ipairs(mymodule.list_files().value) do if delfile.value.filename.value == file.filename then delfile.errtxt = nil os.remove(delfile.value.filename.value) break end end return delfile end function mymodule.getzonefilelist(self, clientdata, userid) cachedzonefiles = searchforzonefiles(self, userid) local listed_files = {} for i,name in pairs(cachedzonefiles) do local filedetails = posix.stat(name) or {} filedetails.filename = name table.insert(listed_files, filedetails) end return cfe({ type="structure", value=listed_files, label="Zone files" }) end function mymodule.get_zonefile(self, clientdata, userid) cachedzonefiles = searchforzonefiles(self, userid) local retval = cfe({ type="group", value={ filename=cfe({}) } }) self.handle_clientdata(retval, clientdata) return modelfunctions.getfiledetails(retval.value.filename.value, is_valid_zonefile) end function mymodule.set_zonefile (self, filedetails, userid) cachedzonefiles = searchforzonefiles(self, userid) return modelfunctions.setfiledetails(self, filedetails, is_valid_zonefile) end function mymodule.getpermissionslist(self) local auth = authenticator.get_subauth(self) local users = authenticator.list_users(self) local userlist = {} for i,user in ipairs(users) do local allowedlist = {} local entry = auth.read_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, user) or "" for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end userlist[#userlist + 1] = {id=user, allowed=allowedlist} end -- Need to check for roles as well as users local rolelist = {} local rols = roles.list_all_roles(self) for i,role in ipairs(rols) do local allowedlist = {} local entry = auth.read_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, role) or "" for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end rolelist[#rolelist + 1] = {id=role, allowed=allowedlist} end table.sort(userlist, function(a,b) return a.id < b.id end) return cfe({ type="structure", value={user=userlist, role=rolelist}, label="NSD Permissions" }) end local function validateuserpermissions(self, userpermissions) local success = true local userinfo = authenticator.get_userinfo(self, userpermissions.value.userid.value) if not userinfo then userpermissions.value.userid.errtxt = "Invalid user" success = false end success = modelfunctions.validatemulti(userpermissions.value.allowed) and success return success, userpermissions end local function validaterolepermissions(self, rolepermissions) local success = false rolepermissions.value.role.errtxt = "Invalid role" local rols = roles.list_all_roles(self) for i,role in ipairs(rols) do if rolepermissions.value.role.value == role then rolepermissions.value.role.errtxt = nil success = true break end end success = modelfunctions.validatemulti(rolepermissions.value.allowed) and success return success, rolepermissions end function mymodule.getuserpermissions(self, clientdata) local retval = cfe({ type="group", value={}, label="NSD User Permissions" }) retval.value.userid = cfe({ label="User Name", seq=0 }) self.handle_clientdata(retval, clientdata) local allzonefiles = listzonefiles(configdir) table.sort(allzonefiles) retval.value.allowed = cfe({ type="multi", value={}, label="NSD Permissions", option=allzonefiles, descr="If no permissions are defined, then all are allowed", seq=1 }) if #allzonefiles == 0 then retval.value.allowed.errtxt = "No zones defined" end local userid = retval.value.userid.value if userid ~= "" then retval.value.userid.readonly = true local auth = authenticator.get_subauth(self) local entry = auth.read_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, userid) or "" for x in string.gmatch(entry, "([^,]+),?") do retval.value.allowed.value[#retval.value.allowed.value + 1] = x end validateuserpermissions(self, retval) end return retval end function mymodule.setuserpermissions(self, userpermissions) local success, userpermissions = validateuserpermissions(self, userpermissions) if success then local auth = authenticator.get_subauth(self) auth.write_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, userpermissions.value.userid.value, table.concat(userpermissions.value.allowed.value, ",")) else userpermissions.errtxt = "Failed to set user permissions" end return userpermissions end function mymodule.getrolepermissions(self, clientdata) local retval = cfe({ type="group", value={}, label="NSD Role Permissions" }) retval.value.role = cfe({ label="Role", seq=0 }) self.handle_clientdata(retval, clientdata) local allzonefiles = listzonefiles(configdir) table.sort(allzonefiles) retval.value.allowed = cfe({ type="multi", value={}, label="NSD Permissions", option=allzonefiles, descr="If no permissions are defined, then all are allowed", seq=1 }) if #allzonefiles == 0 then retval.value.allowed.errtxt = "No zones defined" end local role = retval.value.role.value if role ~= "" then retval.value.role.readonly = true local auth = authenticator.get_subauth(self) local entry = auth.read_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, role) or "" for x in string.gmatch(entry, "([^,]+),?") do retval.value.allowed.value[#retval.value.allowed.value + 1] = x end validaterolepermissions(self, retval) end return retval end function mymodule.setrolepermissions(self, rolepermissions) local success, rolepermissions = validaterolepermissions(self, rolepermissions) if success then local auth = authenticator.get_subauth(self) auth.write_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, rolepermissions.value.role.value, table.concat(rolepermissions.value.allowed.value, ",")) else rolepermissions.errtxt = "Failed to set role permissions" end return rolepermissions end return mymodule