xref: /freebsd/libexec/nuageinit/nuage.lua (revision d21e322d563e0fd1f92c22205c2ced4bcd22dc23)
1---
2-- SPDX-License-Identifier: BSD-2-Clause
3--
4-- Copyright(c) 2022 Baptiste Daroussin <bapt@FreeBSD.org>
5
6local unistd = require("posix.unistd")
7local sys_stat = require("posix.sys.stat")
8local lfs = require("lfs")
9
10local function warnmsg(str, prepend)
11	if not str then
12		return
13	end
14	local tag = ""
15	if prepend ~= false then
16		tag = "nuageinit: "
17	end
18	io.stderr:write(tag .. str .. "\n")
19end
20
21local function errmsg(str, prepend)
22	warnmsg(str, prepend)
23	os.exit(1)
24end
25
26local function dirname(oldpath)
27	if not oldpath then
28		return nil
29	end
30	local path = oldpath:gsub("[^/]+/*$", "")
31	if path == "" then
32		return nil
33	end
34	return path
35end
36
37local function mkdir_p(path)
38	if lfs.attributes(path, "mode") ~= nil then
39		return true
40	end
41	local r, err = mkdir_p(dirname(path))
42	if not r then
43		return nil, err .. " (creating " .. path .. ")"
44	end
45	return lfs.mkdir(path)
46end
47
48local function sethostname(hostname)
49	if hostname == nil then
50		return
51	end
52	local root = os.getenv("NUAGE_FAKE_ROOTDIR")
53	if not root then
54		root = ""
55	end
56	local hostnamepath = root .. "/etc/rc.conf.d/hostname"
57
58	mkdir_p(dirname(hostnamepath))
59	local f, err = io.open(hostnamepath, "w")
60	if not f then
61		warnmsg("Impossible to open " .. hostnamepath .. ":" .. err)
62		return
63	end
64	f:write('hostname="' .. hostname .. '"\n')
65	f:close()
66end
67
68local function splitlist(list)
69	local ret = {}
70	if type(list) == "string" then
71		for str in list:gmatch("([^, ]+)") do
72			ret[#ret + 1] = str
73		end
74	elseif type(list) == "table" then
75		ret = list
76	else
77		warnmsg("Invalid type " .. type(list) .. ", expecting table or string")
78	end
79	return ret
80end
81
82local function adduser(pwd)
83	if (type(pwd) ~= "table") then
84		warnmsg("Argument should be a table")
85		return nil
86	end
87	local root = os.getenv("NUAGE_FAKE_ROOTDIR")
88	local cmd = "pw "
89	if root then
90		cmd = cmd .. "-R " .. root .. " "
91	end
92	local f = io.popen(cmd .. " usershow " .. pwd.name .. " -7 2> /dev/null")
93	local pwdstr = f:read("*a")
94	f:close()
95	if pwdstr:len() ~= 0 then
96		return pwdstr:match("%a+:.+:%d+:%d+:.*:(.*):.*")
97	end
98	if not pwd.gecos then
99		pwd.gecos = pwd.name .. " User"
100	end
101	if not pwd.homedir then
102		pwd.homedir = "/home/" .. pwd.name
103	end
104	local extraargs = ""
105	if pwd.groups then
106		local list = splitlist(pwd.groups)
107		extraargs = " -G " .. table.concat(list, ",")
108	end
109	-- pw will automatically create a group named after the username
110	-- do not add a -g option in this case
111	if pwd.primary_group and pwd.primary_group ~= pwd.name then
112		extraargs = extraargs .. " -g " .. pwd.primary_group
113	end
114	if not pwd.no_create_home then
115		extraargs = extraargs .. " -m "
116	end
117	if not pwd.shell then
118		pwd.shell = "/bin/sh"
119	end
120	local precmd = ""
121	local postcmd = ""
122	if pwd.passwd then
123		precmd = "echo '" .. pwd.passwd .. "' | "
124		postcmd = " -H 0"
125	elseif pwd.plain_text_passwd then
126		precmd = "echo '" .. pwd.plain_text_passwd .. "' | "
127		postcmd = " -h 0"
128	end
129	cmd = precmd .. "pw "
130	if root then
131		cmd = cmd .. "-R " .. root .. " "
132	end
133	cmd = cmd .. "useradd -n " .. pwd.name .. " -M 0755 -w none "
134	cmd = cmd .. extraargs .. " -c '" .. pwd.gecos
135	cmd = cmd .. "' -d '" .. pwd.homedir .. "' -s " .. pwd.shell .. postcmd
136
137	local r = os.execute(cmd)
138	if not r then
139		warnmsg("fail to add user " .. pwd.name)
140		warnmsg(cmd)
141		return nil
142	end
143	if pwd.locked then
144		cmd = "pw "
145		if root then
146			cmd = cmd .. "-R " .. root .. " "
147		end
148		cmd = cmd .. "lock " .. pwd.name
149		os.execute(cmd)
150	end
151	return pwd.homedir
152end
153
154local function addgroup(grp)
155	if (type(grp) ~= "table") then
156		warnmsg("Argument should be a table")
157		return false
158	end
159	local root = os.getenv("NUAGE_FAKE_ROOTDIR")
160	local cmd = "pw "
161	if root then
162		cmd = cmd .. "-R " .. root .. " "
163	end
164	local f = io.popen(cmd .. " groupshow " .. grp.name .. " 2> /dev/null")
165	local grpstr = f:read("*a")
166	f:close()
167	if grpstr:len() ~= 0 then
168		return true
169	end
170	local extraargs = ""
171	if grp.members then
172		local list = splitlist(grp.members)
173		extraargs = " -M " .. table.concat(list, ",")
174	end
175	cmd = "pw "
176	if root then
177		cmd = cmd .. "-R " .. root .. " "
178	end
179	cmd = cmd .. "groupadd -n " .. grp.name .. extraargs
180	local r = os.execute(cmd)
181	if not r then
182		warnmsg("fail to add group " .. grp.name)
183		warnmsg(cmd)
184		return false
185	end
186	return true
187end
188
189local function addsshkey(homedir, key)
190	local chownak = false
191	local chowndotssh = false
192	local root = os.getenv("NUAGE_FAKE_ROOTDIR")
193	if root then
194		homedir = root .. "/" .. homedir
195	end
196	local ak_path = homedir .. "/.ssh/authorized_keys"
197	local dotssh_path = homedir .. "/.ssh"
198	local dirattrs = lfs.attributes(ak_path)
199	if dirattrs == nil then
200		chownak = true
201		dirattrs = lfs.attributes(dotssh_path)
202		if dirattrs == nil then
203			assert(lfs.mkdir(dotssh_path))
204			chowndotssh = true
205			dirattrs = lfs.attributes(homedir)
206		end
207	end
208
209	local f = io.open(ak_path, "a")
210	if not f then
211		warnmsg("impossible to open " .. ak_path)
212		return
213	end
214	f:write(key .. "\n")
215	f:close()
216	if chownak then
217		sys_stat.chmod(ak_path, 384)
218		unistd.chown(ak_path, dirattrs.uid, dirattrs.gid)
219	end
220	if chowndotssh then
221		sys_stat.chmod(dotssh_path, 448)
222		unistd.chown(dotssh_path, dirattrs.uid, dirattrs.gid)
223	end
224end
225
226local n = {
227	warn = warnmsg,
228	err = errmsg,
229	dirname = dirname,
230	mkdir_p = mkdir_p,
231	sethostname = sethostname,
232	adduser = adduser,
233	addgroup = addgroup,
234	addsshkey = addsshkey
235}
236
237return n
238