parsav  Diff

Differences From Artifact [54eca1a845]:

To Artifact [c954b701da]:


    18     18   local subcmds = {
    19     19   }
    20     20   
    21     21   local ctlcmds = {
    22     22   	{ 'start', 'start a new instance of the server' };
    23     23   	{ 'stop', 'stop a running instance' };
    24     24   	{ 'attach', 'capture log output from a running instance' };
    25         -	{ 'db init <domain>', 'initialize backend databases (or a single specified database) with the necessary schema and structures for the given FQDN' };
    26         -	{ 'db vacuum', 'delete old remote content from the database' };
    27         -	{ 'db extract (<artifact>|<post>/<attachment number>)', 'extracts an attachment artifact from the database and prints it to standard out' };
    28         -	{ 'db excise <artifact>', 'extracts an attachment artifact from the database and prints it to standard out' };
    29         -	{ 'db obliterate', 'completely purge all parsav-related content and structure from the database, destroying all user content (requires confirmation)' };
    30         -	{ 'db insert', 'reads a file from standard in and inserts it into the attachment database, printing the resulting ID' };
           25  +	{ 'db', 'set up and manage the database' };
           26  +	{ 'user', 'manage users, privileges, and credentials'};
    31     27   	{ 'mkroot <handle>', 'establish a new root user with the given handle' };
    32         -	{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
    33         -	{ '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' };
    34         -	{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
    35         -	{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
    36         -	{ 'user <handle> emasculate', 'strip all administrative powers from a user' };
    37         -	{ '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'};
    38     28   	{ 'actor <xid> purge-all', '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)' };
    39     29   	{ 'actor <xid> create', 'instantiate a new actor' };
    40     30   	{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
    41         -	{ 'conf set <setting> <value>', 'add or a change a server configuration parameter to the database' };
    42         -	{ 'conf get <setting>', 'report the value of a server setting' };
    43         -	{ 'conf reset <setting>', 'reset a server setting to its default value' };
    44         -	{ 'conf refresh', 'instruct an instance to refresh its configuration cache' };
    45         -	{ 'conf chsec', 'reset the server secret, invalidating all authentication cookies' };
           31  +	{ 'conf', 'manage the server configuration'};
    46     32   	{ 'serv dl', 'initiate an update cycle over foreign actors' };
    47     33   	{ 'tl', 'print the current local timeline to standard out' };
    48     34   	{ 'be pgsql setup-auth (managed|unmanaged)', '(PGSQL backends) select the authentication strategy to use' };
    49     35   }
    50     36   
    51         -local ctlcmdhelp = 'commands:\n'
    52         -for _, v in ipairs(ctlcmds) do
    53         -	ctlcmdhelp = ctlcmdhelp .. string.format (
    54         -		'    \27[1m%s\27[m: %s\n', v[1]:gsub('(<%w+>)','\27[36m%1\27[;1m'), v[2]
    55         -	)
           37  +local cmdhelp = function(tbl)
           38  +	local str = '\ncommands:\n'
           39  +	for _, v in ipairs(tbl) do
           40  +		str = str .. string.format (
           41  +			'    \27[1m%s\27[m: %s\n',
           42  +			v[1]
           43  +				:gsub('([%(%)|%[%]])', '\27[34m%1\27[;1m')
           44  +				:gsub('(<.->)','\27[36m%1\27[;1m'),
           45  +			v[2]
           46  +		)
           47  +	end
           48  +	return str
    56     49   end
    57     50   
    58     51   local struct idelegate {
    59     52   	all: bool
    60     53   	src: &lib.store.source
    61     54   	srv: &lib.srv.overlord
    62     55   }
................................................................................
   100     93   	end
   101     94   	lib.dbg('assigning temporary password')
   102     95   	dlg:auth_create_pw(uid, reset, pstr {
   103     96   		ptr = [rawstring](tmppw), ct = 32
   104     97   	})
   105     98   end
   106     99   
          100  +local emp = lib.ipc.global_emperor
   107    101   local terra entry_mgtool(argc: int, argv: &rawstring): int
   108    102   	if argc < 1 then lib.bail('bad invocation!') end
   109    103   
   110         -	lib.noise_init(2)
          104  +	lib.noise.init(2)
   111    105   	[lib.init]
   112    106   
   113    107   	var srv: lib.srv.overlord
   114    108   	var dlg = idelegate { srv = &srv, src = nil }
   115    109   
   116    110   	var mode: ctloptions
   117    111   	mode:parse(argc,argv) defer mode:free()
   118    112   	if mode.version then version() return 0 end
   119    113   	if mode.help then
   120         -		[ lib.emit(false, 1, 'usage: ', `argv[0], ' ', ctloptions.helptxt.flags, ' <cmd> [<args>…]', ctloptions.helptxt.opts, ctlcmdhelp) ]
          114  +		[ lib.emit(false, 1, 'usage: ', `argv[0], ' ', ctloptions.helptxt.flags, ' <cmd> [<args>…]', ctloptions.helptxt.opts, cmdhelp(ctlcmds)) ]
   121    115   		return 0
   122    116   	end
          117  +	if mode.quiet then lib.noise.level = 0 end
   123    118   	var cnf: rawstring
   124    119   	if mode.backend_file ~= nil
   125    120   		then cnf = @mode.backend_file
   126    121   		else cnf = lib.proc.getenv('parsav_backend_file')
   127    122   	end
   128         -	if cnf == nil then cnf = "backend.conf" end
          123  +	if cnf == nil then cnf = [config.prefix_conf .. "/backend.conf"] end
   129    124   	if mode.all then dlg.all = true else
   130    125   		-- iterate through and pick the right backend
   131    126   	end
   132    127   
   133    128   	if mode.arglist.ct == 0 then lib.bail('no command') return 1 end
          129  +
          130  +	if lib.str.cmp(mode.arglist(0),'start') ~= 0 then
          131  +	-- hack to save us some pain around forking
          132  +		emp = lib.ipc.emperor.mk(false)
          133  +	end
          134  +	defer emp:release()
          135  +
   134    136   	if lib.str.cmp(mode.arglist(0),'attach') == 0 then
   135    137   	elseif lib.str.cmp(mode.arglist(0),'start') == 0 then
          138  +		mode.arglist(0) = "-";
          139  +		var chargv = mode.arglist.ptr
          140  +		var lsr = lib.ipc.listener.mk()
          141  +		var chpid = lib.proc.fork()
          142  +		if chpid == 0 then
          143  +			lsr:release()
          144  +			--lib.proc.daemonize(1,0)
          145  +			lib.io.close(0) lib.io.close(1) lib.io.close(2)
          146  +			lib.proc.exec([config.prefix_bin .. '/parsavd'], chargv)
          147  +			lib.proc.execp([config.prefix_bin .. '/parsavd'], chargv)
          148  +			lib.ipc.notify_parent(lib.ipc.signals.state_fail_find)
          149  +			lib.bail('cannot find parsav program')
          150  +		else
          151  +			lib.report('starting parsav daemon')
          152  +			while true do
          153  +				var sig = lsr:block()
          154  +				if sig.system then lib.dbg('got system signal') end
          155  +				if sig.from == chpid then
          156  +					if sig.system and sig.sig == lib.ipc.signals.sys_child then 
          157  +						lib.warn('parsavd failed to start')
          158  +						return 0
          159  +					elseif sig.sig == lib.ipc.signals.notify_state_change and
          160  +					       sig.event == lib.ipc.signals.state_success then
          161  +						lib.report('parsavd successfully started')
          162  +						return 0
          163  +					elseif sig.sig == lib.ipc.signals.notify_state_change and
          164  +					       sig.event == lib.ipc.signals.state_fail_find then
          165  +						lib.bail('parsavd could not be found')
          166  +					else lib.warn('got unrecognized signal, ignoring')
          167  +					end
          168  +				end
          169  +			end
          170  +			lsr:release() -- just because i feel distinctly uncomfortable leaving it out
          171  +		end
   136    172   	elseif lib.str.cmp(mode.arglist(0),'stop') == 0 then
          173  +		var acks = emp:mallack()
          174  +		emp:decree(0,nil, lib.ipc.cmd.stop, 0, &acks(0)) -- TODO targeting
          175  +		for i=0,acks.ct do
          176  +			if acks(i).success then
          177  +				lib.io.fmt('instance %llu successfully stepped down\n', acks(i).clid)
          178  +			else
          179  +				lib.io.fmt('instance %llu reports failure to halt\n', acks(i).clid)
          180  +			end
          181  +		end
          182  +		acks:free()
   137    183   	else
   138    184   		if lib.str.cmp(mode.arglist(0),'db') == 0 then
   139    185   			var dbmode: pbasic dbmode:parse(mode.arglist.ct, &mode.arglist(0))
   140    186   			if dbmode.help then
   141         -				[ lib.emit(false, 1, 'usage: ', `argv[0], ' db ', dbmode.type.helptxt.flags, ' <cmd> [<args>…]', dbmode.type.helptxt.opts) ]
          187  +				[ lib.emit(false, 1, 'usage: ', `argv[0], ' db ', dbmode.type.helptxt.flags, ' <cmd> [<args>…]', dbmode.type.helptxt.opts, cmdhelp {
          188  +					{ 'db init <domain>', 'initialize backend databases (or a single specified database) with the necessary schema and structures for the given FQDN' };
          189  +					{ 'db vacuum', 'delete old remote content from the database' };
          190  +					{ 'db extract (<artifact>|<post>/<attachment number>)', 'extracts an attachment artifact from the database and prints it to standard out' };
          191  +					{ 'db excise (<artifact>|<post>/<attachment number>)', 'removes an undesirable artifact from the database' };
          192  +					{ 'db obliterate [<confirmation code>]', 'completely purge all parsav-related content and structure from the database, destroying all user content (requires confirmation)' };
          193  +					{ 'db insert', 'reads a file from standard in and inserts it into the attachment database, printing the resulting ID' };
          194  +				}) ]
   142    195   				return 1
   143    196   			end
   144    197   			if dbmode.arglist.ct < 1 then goto cmderr end
   145    198   
   146    199   			srv:setup(cnf) 
   147    200   			if lib.str.cmp(dbmode.arglist(0),'init') == 0 and dbmode.arglist.ct == 2 then
   148    201   				lib.report('initializing new database structure for domain ', dbmode.arglist(1))
................................................................................
   157    210   				var confirmstrs = array(
   158    211   					'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa'
   159    212   				)
   160    213   				var cfmstr: int8[64] cfmstr[0] = 0
   161    214   				var tdx = lib.osclock.time(nil) / 60
   162    215   				for i=0,3 do
   163    216   					if i ~= 0 then lib.str.cat(&cfmstr[0], '-') end
   164         -					lib.str.cat(&cfmstr[0], confirmstrs[(tdx + 49*i) % [confirmstrs.type.N]])
          217  +					lib.str.cat(&cfmstr[0], confirmstrs[(tdx ^ (173*i)) % [confirmstrs.type.N]])
   165    218   				end
   166    219   
   167    220   				if dbmode.arglist.ct == 1 then
   168    221   					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])
   169    222   				elseif dbmode.arglist.ct == 2 then
   170    223   					if lib.str.cmp(dbmode.arglist(1), cfmstr) == 0 then
   171    224   						lib.warn('completely obliterating all data!')
................................................................................
   182    235   			srv:conprep(lib.store.prepmode.conf)
   183    236   			var cfmode: lib.cmdparse {
   184    237   				help = {'h','display this list'};
   185    238   				no_notify = {'n', "don't instruct the server to refresh its configuration cache after making changes; useful for \"transactional\" configuration changes."};
   186    239   			}
   187    240   			cfmode:parse(mode.arglist.ct, &mode.arglist(0))
   188    241   			if cfmode.help then
   189         -				[ lib.emit(false, 1, 'usage: ', `argv[0], ' conf ', cfmode.type.helptxt.flags, ' <cmd> [<args>…]', cfmode.type.helptxt.opts) ]
          242  +				[ lib.emit(false, 1, 'usage: ', `argv[0], ' conf ', cfmode.type.helptxt.flags, ' <cmd> [<args>…]', cfmode.type.helptxt.opts, cmdhelp {
          243  +					{ 'conf set <setting> <value>', 'add or a change a server configuration parameter to the database' };
          244  +					{ 'conf get <setting>', 'report the value of a server setting' };
          245  +					{ 'conf reset <setting>', 'reset a server setting to its default value' };
          246  +					{ 'conf refresh', 'instruct an instance to refresh its configuration cache' };
          247  +					{ 'conf chsec', 'reset the server secret, invalidating all authentication cookies' };
          248  +				}) ]
   190    249   				return 1
   191    250   			end
   192    251   			if cfmode.arglist.ct < 1 then goto cmderr end
   193    252   
   194    253   			if cfmode.arglist.ct == 1 then
   195    254   				if lib.str.cmp(cfmode.arglist(0),'chsec') == 0 then
   196    255   					var sec: int8[65] gensec(&sec[0])
................................................................................
   245    304   						pwset(dlg, &tmppw, ruid, false)
   246    305   						lib.report('temporary root pw: ', {&tmppw[0], 32})
   247    306   					end
   248    307   				else goto cmderr end
   249    308   			elseif lib.str.cmp(mode.arglist(0),'user') == 0 then
   250    309   				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
   251    310   				if umode.help then
   252         -					[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts) ]
          311  +					[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts, cmdhelp {
          312  +						{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
          313  +						{ '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' };
          314  +						{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
          315  +						{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
          316  +						{ 'user <handle> emasculate', 'strip all administrative powers from a user' };
          317  +						{ 'user <handle> forgive', 'restore all default powers to a user' };
          318  +						{ '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'};
          319  +					}) ]
   253    320   					return 1
   254    321   				end
   255    322   				if umode.arglist.ct >= 3 then
   256    323   					var grant = lib.str.cmp(umode.arglist(1),'grant') == 0
   257    324   					var handle = umode.arglist(0)
   258    325   					var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)})
   259         -					if not usr then lib.bail('unknown handle') end
   260    326   					if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then
          327  +						if not usr then lib.bail('unknown handle') end
   261    328   						var newprivs = usr.ptr.rights.powers
   262    329   						var map = array([lib.store.privmap])
   263    330   						if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then
   264    331   							if grant
   265    332   								then newprivs:fill()
   266    333   								else newprivs:clear()
   267    334   							end
................................................................................
   285    352   						end
   286    353   
   287    354   						usr.ptr.rights.powers = newprivs
   288    355   						dlg:actor_save_privs(usr.ptr)
   289    356   					elseif lib.str.cmp(umode.arglist(1),'auth') == 0 and umode.arglist.ct == 4 then
   290    357   						var reset = lib.str.cmp(umode.arglist(3),'reset') == 0
   291    358   						if reset or lib.str.cmp(umode.arglist(3),'new') == 0 then
          359  +							-- FIXME enable resetting pws for users who have
          360  +							-- not logged in yet
          361  +							if not usr then lib.bail('unknown handle') end
   292    362   							if lib.str.cmp(umode.arglist(2),'pw') == 0 then
   293    363   								var tmppw: int8[33]
   294    364   								pwset(dlg, &tmppw, usr.ptr.id, reset)
   295    365   								lib.report('new temporary password for ',usr.ptr.handle,': ', {&tmppw[0], 32})
   296    366   							else lib.bail('unknown credential type') end
   297    367   						elseif lib.str.cmp(umode.arglist(3),'purge') == 0 then
          368  +							var uid: uint64 = 0
          369  +							if usr:ref() then uid = usr(0).id end
          370  +							if lib.str.cmp(umode.arglist(2),'pw') == 0 then
          371  +								dlg:auth_purge_pw(uid, handle)
          372  +							elseif lib.str.cmp(umode.arglist(2),'otp') == 0 then
          373  +								dlg:auth_purge_otp(uid, handle)
          374  +							elseif lib.str.cmp(umode.arglist(2),'trust') == 0 then
          375  +								dlg:auth_purge_trust(uid, handle)
          376  +							else lib.bail('unknown credential type') end
   298    377   						else goto cmderr end
   299    378   					else goto cmderr end
   300    379   				else goto cmderr end
   301    380   			elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then
   302    381   			elseif lib.str.cmp(mode.arglist(0),'tl') == 0 then
   303    382   			elseif lib.str.cmp(mode.arglist(0),'serv') == 0 then
   304    383   			else goto cmderr end