parsav  mgtool.t at [f09cd18161]

File mgtool.t artifact ee3dfa15a8 part of check-in f09cd18161


-- vim: ft=terra
-- provides the functionality of the `parsav` utility that controls `parsavd`
local pstr = lib.mem.ptr(int8)
local ctloptions = lib.cmdparse({
	version = {'V', 'display information about the binary build and exit'};
	verbose = {'v', 'increase logging verbosity', inc=1};
	quiet = {'q', 'do not print to standard out'};
	help = {'h', 'display this list'};
	backend_file = {'B', 'init from specified backend file', consume=1};
	backend = {'b', 'operate on only the selected backend'};
	instance_id = {'i', 'specify the instance to control by name', consume=1};
	instance_serial = {'I', 'specify the instance to control by serial', consume=1};
	all = {'A', 'affect all running instances/backends'};
}, { subcmd = 1 })

local pbasic = lib.cmdparse {
	help = {'h', 'display this list'}
}
local subcmds = {
}

local ctlcmds = {
	{ 'start', 'start a new instance of the server' };
	{ 'stop', 'stop a running instance' };
	{ 'ls', 'list all running instances' };
	{ 'attach', 'capture log output from a running instance' };
	{ 'db', 'set up and manage the database' };
	{ 'user', 'create and manage users, privileges, and credentials'};
	{ 'actor', 'manage and purge actors, epithets, and ranks'};
	{ 'mkroot <handle>', 'establish a new root user with the given handle' };
	{ 'conf', 'manage the server configuration'};
	{ 'grow <count> [<acl>]', 'grant a new round of invites to all users, or those who match the given ACL' };
	{ 'serv dl', 'initiate an update cycle over foreign actors' };
	{ 'tl', 'print the current local timeline to standard out' };
	{ 'be pgsql setup-auth (managed|unmanaged)', '(PGSQL backends) select the authentication strategy to use' };
}

local cmdhelp = function(tbl)
	local str = '\ncommands:\n'
	for _, v in ipairs(tbl) do
		str = str .. string.format (
			'    \27[1m%s\27[m: %s\n',
			v[1]
				:gsub('([%(%)|%[%]])', '\27[34m%1\27[;1m')
				:gsub('(<.->)','\27[36m%1\27[;1m'),
			v[2]
		)
	end
	return str
end

local struct idelegate {
	emperor: &lib.ipc.emperor

	all: bool
	src: &lib.store.source
	srv: &lib.srv.overlord

	sid: uint64
	iname: rawstring
}
idelegate.metamethods.__methodmissing = macro(function(meth, self, ...)
	local expr = {...}
	local rt

	for _,f in pairs(lib.store.backend.entries) do
		local fn = f.field or f[1]
		local ft = f.type or f[2]
		if fn == meth then rt = ft.type.returntype break end
	end

	return quote
		var r: rt
		if self.all or (self.srv ~= nil and self.srv.sources.ct == 1)
			then r=self.srv:[meth]([expr])
			elseif self.src ~= nil then r=self.src:[meth]([expr])
			else lib.bail('no data source specified')
		end
	in r end
end)

terra idelegate:ipc_send(cmd: lib.ipc.cmd.t, operand: uint64)
	var emp = self.emperor
	var acks: lib.mem.ptr(lib.ipc.ack)
	if self.sid == 0 and self.iname == nil then
		if not self.all and emp:countpeers() > 1 then
			lib.bail('either specify the instance to control or pass --all to control all instances')
		end
		acks = emp:mallack()
		emp:decree(0,nil, cmd, operand, &acks(0)) -- TODO targeting
	else
		acks = lib.mem.heapa(lib.ipc.ack, 1)
		if not emp:decree(self.sid, self.iname, cmd, operand, &acks(0)) then
			acks:free()
		end
	end
	return acks
end

local terra gensec(sdest: rawstring)
	var dest = [&uint8](sdest)
	lib.crypt.spray(dest,64)
	for i=0,64 do dest[i] = dest[i] % (0x7e - 0x20) + 0x20 end
	dest[64] = 0
end

local terra pwset(dlg: idelegate, buf: &(int8[33]), uid: uint64, reset: bool)
	lib.dbg('generating temporary password')
	var tmppw = [&uint8](&(buf[0]))
	lib.crypt.spray(tmppw,32) tmppw[32] = 0
	for i=0,32 do
		tmppw[i] = tmppw[i] % (10 + 26*2)
		if tmppw[i] >= 36 then
			tmppw[i] = tmppw[i] + (0x61 - 36)
		elseif tmppw[i] >= 10 then
			tmppw[i] = tmppw[i] + (0x41 - 10)
		else tmppw[i] = tmppw[i] + 0x30 end
	end
	lib.dbg('assigning temporary password')
	dlg:auth_attach_pw(uid, reset,
		pstr { ptr = [rawstring](tmppw), ct = 32 },
		lib.str.plit 'temporary password');
end

local terra ipc_report(acks: lib.mem.ptr(lib.ipc.ack), rep: rawstring)
	var decbuf: int8[21]
	for i=0,acks.ct do
		var num = lib.math.decstr(acks(i).clid, &decbuf[20])
		if acks(i).success then
			lib.report('instance #',num,' reports successful ',rep)
		else
			lib.report('instance #',num,' reports failed ',rep)
		end
	end
end

local terra gen_cfstr(cfmstr: rawstring, seed: intptr)
	var confirmstrs = array(
		'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa',
		'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst'
	)
	var tdx = lib.osclock.time(nil) / 60
	cfmstr[0] = 0
	for i=0,3 do
		if i ~= 0 then lib.str.cat(cfmstr, '-') end
		lib.str.cat(cfmstr, confirmstrs[(seed ^ tdx ^ (173*i)) % [confirmstrs.type.N]])
	end
end

local emp = lib.ipc.global_emperor
local terra entry_mgtool(argc: int, argv: &rawstring): int
	if argc < 1 then lib.bail('bad invocation!') end

	lib.noise.init(2)
	[lib.init]

	var srv: lib.srv.overlord
	var dlg = idelegate {
		emperor = &emp, srv = &srv;
		src = nil, sid = 0, iname = nil, all = false;
	}

	var mode: ctloptions
	mode:parse(argc,argv) defer mode:free()
	if mode.version then version() return 0 end
	if mode.help then
		[ lib.emit(false, 1, 'usage: ', `argv[0], ' ', ctloptions.helptxt.flags, ' <cmd> [<args>…]', ctloptions.helptxt.opts, cmdhelp(ctlcmds)) ]
		return 0
	end
	if mode.quiet then lib.noise.level = 0 end
	var cnf: rawstring
	if mode.backend_file ~= nil
		then cnf = @mode.backend_file
		else cnf = lib.proc.getenv('parsav_backend_file')
	end
	if cnf == nil then cnf = [config.prefix_conf .. "/backend.conf"] end
	if mode.all then dlg.all = true else
		-- iterate through and pick the right backend, if one is indicated 
	end
	
	if mode.instance_id ~= nil and mode.instance_serial ~= nil then
		lib.bail('conflicting flags passed')
	end

	if mode.instance_id ~= nil then
		dlg.iname = @mode.instance_id
	elseif mode.instance_serial ~= nil then
		-- decode serial
	end

	if mode.arglist.ct == 0 then lib.bail('no command') return 1 end

	if lib.str.cmp(mode.arglist(0),'start') ~= 0 then
	-- hack to save us some pain around forking
		emp = lib.ipc.emperor.mk(false)
	end
	defer emp:release()

	if lib.str.cmp(mode.arglist(0),'attach') == 0 then
	elseif lib.str.cmp(mode.arglist(0),'start') == 0 then
		var smode: lib.cmdparse {
			help = {'h', 'display this list'};
			log = {'l', 'send server\'s logging output to a file', consume=1};
			pid = {'p', 'report PID of launched process'};
			instance_serial = {'s', 'report the instance serial of the launched process'};
			keep_attached = {'k', 'don\'t detach from the calling terminal until a successful startup (or ever, if passed twice)', inc=1};
		}
		smode:parse(mode.arglist.ct,mode.arglist.ptr)
		if smode.help then
			[ lib.emit(false, 1, 'usage: ', `argv[0], ' start ', smode.type.helptxt.flags, ' [<instance args>…] [-- <instance flags>…]', smode.type.helptxt.opts) ]
			return 1
		end
		var lsr = lib.ipc.listener.mk()
		var chpid = lib.proc.fork()
		if chpid == 0 then
			lsr:release()
			var chargv = lib.mem.heapa(rawstring, smode.arglist.ct + 2)
			chargv(0) = '-'
			for i = 1, chargv.ct - 1 do
				chargv(i) = smode.arglist(i-1)
			end
			chargv(chargv.ct-1) = nil
			if smode.keep_attached == 0 then
				lib.io.close(0) lib.io.close(1) lib.io.close(2)
			end
			lib.proc.exec([config.prefix_bin .. '/parsavd'], chargv.ptr)
			lib.proc.execp([config.prefix_bin .. '/parsavd'], chargv.ptr)
			lib.ipc.notify_parent(lib.ipc.signals.state_fail_find)
			lib.bail('cannot find parsav program')
			-- chargv:free()
		else
			lib.report('starting parsav daemon')
			while true do
				var sig = lsr:block()
				if sig.system then lib.dbg('got system signal') end
				if sig.from == chpid then
					if sig.system and sig.sig == lib.ipc.signals.sys_child then 
						lib.warn('parsavd failed to start')
						return 0
					elseif sig.sig == lib.ipc.signals.notify_state_change and
					       sig.event == lib.ipc.signals.state_success then
						lib.report('parsavd successfully started')
						return 0
					elseif sig.sig == lib.ipc.signals.notify_state_change and
					       sig.event == lib.ipc.signals.state_fail_find then
						lib.bail('parsavd could not be found')
					else lib.warn('got unrecognized signal, ignoring')
					end
				end
			end
			lsr:release() -- just because i feel distinctly uncomfortable leaving it out
		end
	elseif lib.str.cmp(mode.arglist(0),'stop') == 0 then
		if mode.arglist.ct ~= 1 then goto cmderr end
		var acks = dlg:ipc_send(lib.ipc.cmd.stop, 0)
		if acks:ref() then
			ipc_report(acks, 'step-down')
			acks:free()
		end
	elseif lib.str.cmp(mode.arglist(0),'ls') == 0 then
		if mode.arglist.ct ~= 1 then goto cmderr end
		if dlg.sid == 0 and dlg.iname == nil then dlg.all = true end
		var acks = dlg:ipc_send(lib.ipc.cmd.enumerate, 0)
		if acks:ref() then
			for i=0,acks.ct do
				var decbuf: int8[21]
				var num = lib.math.decstr(acks(i).clid, &decbuf[20])
				[ lib.emit(true,1, '\27[1m(', `num, ')\27[m ',`&acks(i).iname[0],' \27[1;32mactive\27[m') ]
			end
			acks:free()
		else lib.bail('no active instances') end
	else
		if lib.str.cmp(mode.arglist(0),'db') == 0 then
			var dbmode: pbasic dbmode:parse(mode.arglist.ct, &mode.arglist(0))
			if dbmode.help then
				[ lib.emit(false, 1, 'usage: ', `argv[0], ' db ', dbmode.type.helptxt.flags, ' <cmd> [<args>…]', dbmode.type.helptxt.opts, cmdhelp {
					{ 'db init <domain>', 'initialize backend databases (or a single specified database) with the necessary schema and structures for the given FQDN' };
					{ 'db vacuum', 'delete old remote content from the database' };
					{ 'db extract (<artifact>|<post>/<attachment number>)', 'extracts an attachment artifact from the database and prints it to standard out' };
					{ 'db excise (<artifact>|<post>/<attachment number>)', 'removes an undesirable artifact from the database' };
					{ 'db obliterate [<confirmation code>]', 'completely purge all parsav-related content and structure from the database, destroying all user content (requires confirmation)' };
					{ 'db insert', 'reads a file from standard in and inserts it into the attachment database, printing the resulting ID' };
				}) ]
				return 1
			end
			if dbmode.arglist.ct < 1 then goto cmderr end

			srv:setup(cnf) 
			if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then
				lib.report('initializing new database structure for domain ', dbmode.arglist(1))
				dlg:tx_enter()
				if dlg:dbsetup() then
					srv:conprep(lib.store.prepmode.conf)
					dlg:conf_set('instance-name', dbmode.arglist(1))
					dlg:conf_set('domain', dbmode.arglist(1))
					do var sec: int8[65] gensec(&sec[0])
						dlg:conf_set('server-secret', &sec[0])
						dlg:conf_set('server-secret', &sec[0])
					end
					lib.report('database setup complete; use mkroot to create an administrative user')
				else lib.bail('initialization process interrupted') end
				dlg:tx_complete()
			elseif lib.str.cmp(dbmode.arglist(0),'obliterate') == 0 then
				var cfmstr: int8[64] gen_cfstr(&cfmstr[0],0)

				if dbmode.arglist.ct == 1 then
					lib.bail('you are attempting to completely obliterate all data! make sure you have selected your target correctly. if you really want to do this, pass the confirmation string ', &cfmstr[0])
				elseif dbmode.arglist.ct == 2 then
					if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then
						lib.warn('completely obliterating all data!')
						dlg:obliterate_everything()
					elseif lib.str.cmp(dbmode.arglist(1), 'print-confirmation-string') == 0 then
						lib.io.send(1, cfmstr, lib.str.sz(cfmstr))
					else
						lib.bail('you passed an incorrect confirmation string; pass ', &cfmstr[0], ' if you really want to destroy everything')
					end
				else goto cmderr end
			else goto cmderr end
		elseif lib.str.cmp(mode.arglist(0),'be') == 0 then
			srv:setup(cnf) 
		elseif lib.str.cmp(mode.arglist(0),'conf') == 0 then
			srv:setup(cnf) 
			srv:conprep(lib.store.prepmode.conf)
			var cfmode: lib.cmdparse {
				help = {'h','display this list'};
				no_notify = {'n', "don't instruct the server to refresh its configuration cache after making changes; useful for \"transactional\" configuration changes."};
			}
			cfmode:parse(mode.arglist.ct, &mode.arglist(0))
			if cfmode.help then
				[ lib.emit(false, 1, 'usage: ', `argv[0], ' conf ', cfmode.type.helptxt.flags, ' <cmd> [<args>…]', cfmode.type.helptxt.opts, cmdhelp {
					{ 'conf set <setting> <value>', 'add or a change a server configuration parameter to the database' };
					{ 'conf get <setting>', 'report the value of a server setting' };
					{ 'conf reset <setting>', 'reset a server setting to its default value' };
					{ 'conf refresh', 'instruct an instance to refresh its configuration cache' };
					{ 'conf chsec', 'reset the server secret, invalidating all authentication cookies' };
				}) ]
				return 1
			end
			if cfmode.arglist.ct < 1 then goto cmderr end

			if cfmode.arglist.ct == 1 then
				if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then
					var sec: int8[65] gensec(&sec[0])
					dlg:conf_set('server-secret', &sec[0])
					lib.report('server secret reset')
				elseif lib.str.cmp(cfmode.arglist(0),'refresh') == 0 then
					cfmode.no_notify = false -- duh
				else goto cmderr end
			elseif cfmode.arglist.ct == 2 and
				lib.str.cmp(cfmode.arglist(0),'reset') == 0 or
				lib.str.cmp(cfmode.arglist(0),'clear') == 0 or
				lib.str.cmp(cfmode.arglist(0),'unset') == 0 then
				dlg:conf_reset(cfmode.arglist(1))
				lib.report('parameter cleared')
			elseif cfmode.arglist.ct == 3 and
				lib.str.cmp(cfmode.arglist(0),'set') == 0 then
				dlg:conf_set(cfmode.arglist(1),cfmode.arglist(2))
				lib.report('parameter set')
			else goto cmderr end

			-- successful commands fall through
			if not cfmode.no_notify then
				dlg:ipc_send(lib.ipc.cmd.cfgrefresh,0)
			end
		else
			srv:setup(cnf) 
			srv:conprep(lib.store.prepmode.full)
			if lib.str.cmp(mode.arglist(0),'mkroot') == 0 then
				var cfmode: pbasic cfmode:parse(mode.arglist.ct, &mode.arglist(0))
				if cfmode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' mkroot ', cfmode.type.helptxt.flags, ' <handle>', cfmode.type.helptxt.opts) ]
					return 1
				end
				if cfmode.arglist.ct == 1 then
					var am = dlg:conf_get('credential-store')
					var mg: bool
					if (not am) or am:cmp(lib.str.plit 'managed') then
						mg = true
					elseif am:cmp(lib.str.plit 'unmanaged') then
						lib.warn('credential store is unmanaged; you will need to create credentials for the new root user manually!')
						mg = false
					else lib.bail('unknown credential store mode "',{am.ptr,am.ct},'"; should be either "managed" or "unmanaged"') end
					var kbuf: uint8[lib.crypt.const.maxdersz]
					var root = lib.store.actor.mk(&kbuf[0])
					root.handle = cfmode.arglist(0)
					var epithets = array(
						'root', 'god', 'regional jehovah', 'titan king',
						'king of olympus', 'cyberpharaoh', 'electric ellimist',
						"rampaging c'tan", 'deathless tweetlord', 'postmaster',
						'faerie queene', 'lord of the posts', 'ruthless cybercrat',
						'general secretary', 'commissar', 'kwisatz haderach',
						'dedicated hyperturing'
						-- feel free to add more
					)
					root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])]
					root.rights.powers:fill() -- grant omnipotence
					root.rights.rank = 1
					var ruid = dlg:actor_create(&root)
					dlg:conf_set('master',root.handle)
					lib.report('created new administrator')
					if mg then
						var tmppw: int8[33]
						pwset(dlg, &tmppw, ruid, false)
						lib.report('temporary root pw: ', {&tmppw[0], 32})
					end
				else goto cmderr end
			elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then
				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
				if umode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' actor ', umode.type.helptxt.flags, ' <xid> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
						{ 'actor <xid> rank <value>', 'set an actor\'s rank to <value> (remote actors cannot exercise rank-related powers, but benefit from rank immunities)' };
						{ 'actor <xid> degrade', 'alias for `actor <xid> rank 0`' };
						{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
						{ 'actor <xid> instantiate', 'instantiate a remote actor, retrieving their profile and posts even if no one follows them' };
						{ 'actor <xid> proscribe', 'globally ban an actor from interacting with your server' };
						{ 'actor <xid> rehabilitate', 'lift a proscription on an actor' };
						{ 'actor <xid> purge-all <confirm-str>', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' };
					}) ]
					return 1
				end
				if umode.arglist.ct >= 2 then
					var degrade = lib.str.cmp(umode.arglist(1),'degrade') == 0
					var xid = umode.arglist(0)
					var usr = dlg:actor_fetch_xid(pstr {ptr=xid, ct=lib.str.sz(xid)})
					if not usr then lib.bail('no such actor') end
					if degrade or lib.str.cmp(umode.arglist(1),'rank') == 0 then
						var rank: uint16
						if degrade and umode.arglist.ct == 2 then
							rank = 0
						elseif (not degrade) and umode.arglist.ct == 3 then
							var r, ok = lib.math.decparse(pstr {
								ptr = umode.arglist(2);
								ct = lib.str.sz(umode.arglist(2));
							})
							if not ok then goto cmderr end
							rank = r
						else goto cmderr end
						usr.ptr.rights.rank = rank
						dlg:actor_save(usr.ptr)
						lib.report('set user rank')
					elseif umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(1),'bestow') == 0 then
						if umode.arglist(2)[0] == 0
							then usr.ptr.epithet = nil
							else usr.ptr.epithet = umode.arglist(2)
						end
						dlg:actor_save(usr.ptr)
						lib.report('bestowed a new epithet on ', usr.ptr.xid)
					elseif lib.str.cmp(umode.arglist(1),'purge-all') == 0 then
						var cfmstr: int8[64] gen_cfstr(&cfmstr[0],usr.ptr.id)
						if umode.arglist.ct == 2 then
							lib.bail('you are attempting to completely purge the actor ', usr.ptr.xid, ' and all related content from the database! if you really want to do this, pass the confirmation string ', &cfmstr[0])
						elseif umode.arglist.ct == 3 then
							if lib.str.ncmp(&cfmstr[0],umode.arglist(2),64) ~= 0 then
								lib.bail('you have supplied an invalid confirmation string; if you really want to purge this actor, pass ', &cfmstr[0])
							end
							lib.warn('completely purging actor ', usr.ptr.xid, ' and all related content from database')
							dlg:actor_purge_uid(usr.ptr.id)
							lib.report('actor purged')
						else goto cmderr end
					else goto cmderr end
				else goto cmderr end
			elseif lib.str.cmp(mode.arglist(0),'user') == 0 then
				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
				if umode.help then
					[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
						{ 'user <handle> create', 'add a new user' };
						{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
						{ 'user <handle> auth <type> reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' };
						{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
						{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
						{ 'user <handle> emasculate', 'strip all administrative powers and rank from a user' };
						{ 'user <handle> forgive', 'restore all default powers to a user' };
						{ 'user <handle> suspend [<timespec>]', '(e.g. \27[1muser jokester suspend 5d 6h 7m 3s\27[m to suspend "jokester" for five days, six hours, seven minutes, and three seconds) suspend a user'};
					}) ]
					return 1
				end
				var handle = umode.arglist(0)
				var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)})
				if umode.arglist.ct == 2 and lib.str.cmp(umode.arglist(1),'create')==0 then
					if usr:ref() then lib.bail('that user already exists') end
					if not lib.store.actor.handle_validate(handle) then
						lib.bail('invalid user handle') end
					var kbuf: uint8[lib.crypt.const.maxdersz]
					var na = lib.store.actor.mk(&kbuf[0])
					na.handle = handle
					dlg:actor_create(&na)
					lib.report('created new user @',na.handle,'; assign credentials to enable login')
				elseif umode.arglist.ct >= 3 then
					var grant = lib.str.cmp(umode.arglist(1),'grant') == 0
					if not usr then lib.bail('no such user') end
					if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then
						var newprivs = usr.ptr.rights.powers
						var map = array([lib.store.privmap])
						if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then
							if grant
								then newprivs:fill()
								else newprivs:clear()
							end
						else
							for i=2,umode.arglist.ct do
								var priv = umode.arglist(i)
								for j=0,[map.type.N] do
									var p = map[j]
									if p.name:cmp_raw(priv) then
										if grant then
											lib.dbg('enabling power ', {p.name.ptr,p.name.ct})
											newprivs = newprivs + p.priv
										else
											lib.dbg('disabling power ', {p.name.ptr,p.name.ct})
											newprivs = newprivs - p.priv
										end
										break
									end
								end
							end
						end

						usr.ptr.rights.powers = newprivs
						dlg:actor_save_privs(usr.ptr)
					elseif lib.str.cmp(umode.arglist(1),'auth') == 0 and umode.arglist.ct == 4 then
						var reset = lib.str.cmp(umode.arglist(3),'reset') == 0
						if reset or lib.str.cmp(umode.arglist(3),'new') == 0 then
							-- FIXME enable resetting pws for users who have
							-- not logged in yet
							if not usr then lib.bail('unknown handle') end
							if lib.str.cmp(umode.arglist(2),'pw') == 0 then
								var tmppw: int8[33]
								pwset(dlg, &tmppw, usr.ptr.id, reset)
								lib.report('new temporary password for ',usr.ptr.handle,': ', {&tmppw[0], 32})
							else lib.bail('unknown credential type') end
						elseif lib.str.cmp(umode.arglist(3),'purge') == 0 then
							var uid: uint64 = 0
							if usr:ref() then uid = usr(0).id end
							if lib.str.cmp(umode.arglist(2),'pw') == 0 then
								dlg:auth_purge_pw(uid, handle)
							elseif lib.str.cmp(umode.arglist(2),'otp') == 0 then
								dlg:auth_purge_otp(uid, handle)
							elseif lib.str.cmp(umode.arglist(2),'trust') == 0 then
								dlg:auth_purge_trust(uid, handle)
							else lib.bail('unknown credential type') end
						else goto cmderr end
					else goto cmderr end
				else goto cmderr end
			elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then
			elseif lib.str.cmp(mode.arglist(0),'tl') == 0 then
			elseif lib.str.cmp(mode.arglist(0),'serv') == 0 then
			else goto cmderr end
		end
	end

	do return 0 end
	::cmderr:: lib.bail('invalid command')
end

return entry_mgtool