parsav  mgtool.t at [050ce7d4fc]

File mgtool.t artifact 63607cdb2b part of check-in 050ce7d4fc


-- 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 },
		'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)

					do var newkp = lib.crypt.genkp()
					 -- generate server privkey
						var kbuf: uint8[lib.crypt.const.maxdersz]
						var derkey = lib.crypt.der(false,&newkp, &kbuf[0])
						if not derkey then
							lib.bail('could not write out DER form of server pubkey!')
						end
						dlg:server_setup_self(dbmode.arglist(1), derkey)
					end

					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])
					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('managed') then
						mg = true
					elseif am:cmp('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', 'grand inquisitor', 'reverend mother',
						'cyberpope', 'verified®', 'patent pending'
						-- 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.powmap])
						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.val
										else
											lib.dbg('disabling power ', {p.name.ptr,p.name.ct})
											newprivs = newprivs - p.val
										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