parsav  Check-in [a64461061f]

Overview
Comment:add privilege control verbs
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: a64461061f49614172eea9ab72b4f428a5af9d34d6d4602125f343f3cf885bb3
User & Date: lexi on 2020-12-29 00:57:00
Other Links: manifest | tags
Context
2020-12-29
14:35
add ipc backbone check-in: 87731d4007 user: lexi tags: trunk
00:57
add privilege control verbs check-in: a64461061f user: lexi tags: trunk
2020-12-28
23:42
vastly improve the setup process check-in: d228cd7fcb user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [0fdc39456b] to [f0f9593494].

    81     81   				to_timestamp($4::bigint),
    82     82   				$5::bigint, $6::bigint, $7::bytea,
    83     83   				$8::text, $9::smallint, $10::integer
    84     84   			) returning id
    85     85   		]];
    86     86   	};
    87     87   
    88         -
    89     88   	actor_auth_pw = {
    90     89   		params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
    91     90   			select a.aid, a.uid, a.name from parsav_auth as a
    92     91   				left join parsav_actors as u on u.id = a.uid
    93     92   			where (a.uid is null or u.handle = $1::text or (
    94     93   					a.uid = 0 and a.name = $1::text
    95     94   				)) and
................................................................................
   198    197   	actor_power_insert = {
   199    198   		params = {uint64,lib.mem.ptr(int8),uint16}, cmd = true, sql = [[
   200    199   			insert into parsav_rights (actor, key, allow) values (
   201    200   				$1::bigint, $2::text, ($3::smallint)::integer::bool
   202    201   			)
   203    202   		]]
   204    203   	};
          204  +
          205  +	actor_power_delete = {
          206  +		params = {uint64,lib.mem.ptr(int8)}, cmd = true, sql = [[
          207  +			delete from parsav_rights where
          208  +				actor = $1::bigint and
          209  +				key = $2::text
          210  +		]]
          211  +	};
   205    212   
   206    213   	auth_create_pw = {
   207    214   		params = {uint64, lib.mem.ptr(uint8)}, cmd = true, sql = [[
   208    215   			insert into parsav_auth (uid, name, kind, cred) values (
   209    216   				$1::bigint,
   210    217   				(select handle from parsav_actors where id = $1::bigint),
   211    218   				'pw-sha256', $2::bytea
................................................................................
   518    525   		a.ptr.key = r:bin(row,8)
   519    526   	end
   520    527   	if r:null(row,3) then a.ptr.origin = 0
   521    528   	else a.ptr.origin = r:int(uint64,row,3) end
   522    529   	return a
   523    530   end
   524    531   
   525         -local privmap = {}
   526         -do local struct pt { name:pstring, priv:lib.store.powerset }
   527         -for k,v in pairs(lib.store.powerset.members) do
   528         -	privmap[#privmap + 1] = quote
   529         -		var ps: lib.store.powerset ps:clear()
   530         -		(ps.[v] << true)
   531         -	in pt {name = lib.str.plit(v), priv = ps} end
   532         -end end
          532  +local privmap = lib.store.privmap
   533    533   
   534    534   local checksha = function(src, hash, origin, username, pw)
   535    535   	local validate = function(kind, cred, credlen)
   536    536   		return quote 
   537    537   			var r = queries.actor_auth_pw.exec(
   538    538   				[&lib.store.source](src),
   539    539   				username,
................................................................................
   568    568   		[vdrs]
   569    569   		lib.dbg(['could not find password hash'])
   570    570   	end
   571    571   end
   572    572   
   573    573   local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql'))
   574    574   local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql'))
          575  +
          576  +local privupdate = terra(
          577  +	src: &lib.store.source,
          578  +	ac: &lib.store.actor
          579  +): {}
          580  +	var pdef = lib.store.rights_default().powers
          581  +	var map = array([privmap])
          582  +	for i=0, [map.type.N] do
          583  +		var d = pdef and map[i].priv
          584  +		var u = ac.rights.powers and map[i].priv
          585  +		queries.actor_power_delete.exec(src, ac.id, map[i].name)
          586  +		if d:sz() > 0 and u:sz() == 0 then
          587  +			lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct})
          588  +			queries.actor_power_insert.exec(src, ac.id, map[i].name, 0)
          589  +		elseif d:sz() == 0 and u:sz() > 0 then
          590  +			lib.dbg('granting power ', {map[i].name.ptr, map[i].name.ct})
          591  +			queries.actor_power_insert.exec(src, ac.id, map[i].name, 1)
          592  +		end
          593  +	end
          594  +end
          595  +
          596  +local getpow = terra(
          597  +	src: &lib.store.source,
          598  +	uid: uint64
          599  +): lib.store.powerset
          600  +	var powers = lib.store.rights_default().powers
          601  +	var map = array([privmap])
          602  +	var r = queries.actor_powers_fetch.exec(src, uid)
          603  +
          604  +	for i=0, r.sz do
          605  +		for j=0, [map.type.N] do
          606  +			var pn = r:_string(i,0)
          607  +			if map[j].name:cmp(pn) then
          608  +				if r:bool(i,1)
          609  +					then powers = powers + map[j].priv
          610  +					else powers = powers - map[j].priv
          611  +				end
          612  +			end
          613  +		end
          614  +	end
          615  +
          616  +	return powers
          617  +end
   575    618   
   576    619   local b = `lib.store.backend {
   577    620   	id = "pgsql";
   578    621   	open = [terra(src: &lib.store.source): &opaque
   579    622   		lib.report('connecting to postgres database: ', src.string.ptr)
   580    623   		var [con] = lib.pq.PQconnectdb(src.string.ptr)
   581    624   		if lib.pq.PQstatus(con) ~= lib.pq.CONNECTION_OK then
................................................................................
   653    696   	
   654    697   	actor_fetch_uid = [terra(src: &lib.store.source, uid: uint64)
   655    698   		var r = queries.actor_fetch_uid.exec(src, uid)
   656    699   		if r.sz == 0 then
   657    700   			return [lib.mem.ptr(lib.store.actor)] { ct = 0, ptr = nil }
   658    701   		else defer r:free()
   659    702   			var a = row_to_actor(&r, 0)
          703  +			a.ptr.rights.powers = getpow(src, uid)
   660    704   			a.ptr.source = src
   661    705   			return a
   662    706   		end
   663    707   	end];
   664    708   
   665    709   	actor_fetch_xid = [terra(src: &lib.store.source, xid: lib.mem.ptr(int8))
   666    710   		var r = queries.actor_fetch_xid.exec(src, xid)
   667    711   		if r.sz == 0 then
   668    712   			return [lib.mem.ptr(lib.store.actor)] { ct = 0, ptr = nil }
   669    713   		else defer r:free()
   670    714   			var a = row_to_actor(&r, 0)
          715  +			a.ptr.rights.powers = getpow(src, a.ptr.id)
   671    716   			a.ptr.source = src
   672    717   			return a
   673    718   		end
   674    719   	end];
   675    720   
   676    721   	actor_enum = [terra(src: &lib.store.source)
   677    722   		var r = queries.actor_enum.exec(src)
................................................................................
   806    851   		
   807    852   		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
   808    853   		for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) end -- MUST FREE ALL
   809    854   
   810    855   		return ret
   811    856   	end];
   812    857   
   813         -	actor_powers_fetch = [terra(
   814         -		src: &lib.store.source,
   815         -		uid: uint64
   816         -	): lib.store.powerset
   817         -		var powers = lib.store.rights_default().powers
   818         -		var map = array([privmap])
   819         -		var r = queries.actor_powers_fetch.exec(src, uid)
   820         -
   821         -		for i=0, r.sz do
   822         -			for j=0, [map.type.N] do
   823         -				var pn = r:_string(i,0)
   824         -				if map[j].name:cmp(pn) then
   825         -					if r:bool(i,1)
   826         -						then powers = powers + map[j].priv
   827         -						else powers = powers - map[j].priv
   828         -					end
   829         -				end
   830         -			end
   831         -		end
   832         -
   833         -		return powers
   834         -	end];
          858  +	actor_powers_fetch = getpow;
          859  +	actor_save_privs = privupdate;
   835    860   
   836    861   	actor_create = [terra(
   837    862   		src: &lib.store.source,
   838    863   		ac: &lib.store.actor
   839    864   	): uint64
   840    865   		var r = queries.actor_create.exec(src,ac.nym, ac.handle, ac.origin, ac.knownsince, ac.bio, ac.avatar, ac.key, ac.epithet, ac.rights.rank, ac.rights.quota)
   841    866   		if r.sz == 0 then lib.bail('failed to create actor!') end
   842         -		var uid = r:int(uint64,0,0)
          867  +		ac.id = r:int(uint64,0,0)
   843    868   
   844    869   		-- check against default rights, insert records for wherever powers differ
   845    870   		lib.dbg('created new actor, establishing powers')
   846         -		var pdef = lib.store.rights_default().powers
   847         -		var map = array([privmap])
   848         -		for i=0, [map.type.N] do
   849         -			var d = pdef and map[i].priv
   850         -			var u = ac.rights.powers and map[i].priv
   851         -			if d:sz() > 0 and u:sz() == 0 then
   852         -				lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct})
   853         -				queries.actor_power_insert.exec(src, uid, map[i].name, 0)
   854         -			elseif d:sz() == 0 and u:sz() > 0 then
   855         -				lib.dbg('granting power ', {map[i].name.ptr, map[i].name.ct})
   856         -				queries.actor_power_insert.exec(src, uid, map[i].name, 1)
   857         -			end
   858         -		end
          871  +		privupdate(src,ac)
   859    872   
   860    873   		lib.dbg('powers established')
   861         -		return uid
          874  +		return ac.id
   862    875   	end];
   863    876   
   864    877   	auth_create_pw = [terra(
   865    878   		src: &lib.store.source,
   866    879   		uid: uint64,
   867    880   		reset: bool,
   868    881   		pw: lib.mem.ptr(int8)

Modified mgtool.t from [293667feb7] to [54eca1a845].

    27     27   	{ 'db extract (<artifact>|<post>/<attachment number>)', 'extracts an attachment artifact from the database and prints it to standard out' };
    28     28   	{ 'db excise <artifact>', 'extracts an attachment artifact from the database and prints it to standard out' };
    29     29   	{ 'db obliterate', 'completely purge all parsav-related content and structure from the database, destroying all user content (requires confirmation)' };
    30     30   	{ 'db insert', 'reads a file from standard in and inserts it into the attachment database, printing the resulting ID' };
    31     31   	{ 'mkroot <handle>', 'establish a new root user with the given handle' };
    32     32   	{ 'user <handle> auth <type> new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' };
    33     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 purge-credentials [<type>]', 'delete all credentials that would allow this user to log in (where possible)' };
           34  +	{ 'user <handle> auth (<type>|all) purge', 'delete all credentials that would allow this user to log in (where possible)' };
    35     35   	{ 'user <handle> (grant|revoke) (<priv>|all)', 'grant or revoke a specific power to or from a user' };
    36     36   	{ 'user <handle> emasculate', 'strip all administrative powers from a user' };
    37     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         -	{ 'actor <xid> purge-all', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth purge-credentials\27[m to prevent a user from accessing the instance)' };
           38  +	{ '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     39   	{ 'actor <xid> create', 'instantiate a new actor' };
    40     40   	{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
    41     41   	{ 'conf set <setting> <value>', 'add or a change a server configuration parameter to the database' };
    42     42   	{ 'conf get <setting>', 'report the value of a server setting' };
    43     43   	{ 'conf reset <setting>', 'reset a server setting to its default value' };
    44     44   	{ 'conf refresh', 'instruct an instance to refresh its configuration cache' };
    45     45   	{ 'conf chsec', 'reset the server secret, invalidating all authentication cookies' };
................................................................................
    81     81   
    82     82   local terra gensec(sdest: rawstring)
    83     83   	var dest = [&uint8](sdest)
    84     84   	lib.crypt.spray(dest,64)
    85     85   	for i=0,64 do dest[i] = dest[i] % (0x7e - 0x20) + 0x20 end
    86     86   	dest[64] = 0
    87     87   end
           88  +
           89  +local terra pwset(dlg: idelegate, buf: &(int8[33]), uid: uint64, reset: bool)
           90  +	lib.dbg('generating temporary password')
           91  +	var tmppw = [&uint8](&(buf[0]))
           92  +	lib.crypt.spray(tmppw,32) tmppw[32] = 0
           93  +	for i=0,32 do
           94  +		tmppw[i] = tmppw[i] % (10 + 26*2)
           95  +		if tmppw[i] >= 36 then
           96  +			tmppw[i] = tmppw[i] + (0x61 - 36)
           97  +		elseif tmppw[i] >= 10 then
           98  +			tmppw[i] = tmppw[i] + (0x41 - 10)
           99  +		else tmppw[i] = tmppw[i] + 0x30 end
          100  +	end
          101  +	lib.dbg('assigning temporary password')
          102  +	dlg:auth_create_pw(uid, reset, pstr {
          103  +		ptr = [rawstring](tmppw), ct = 32
          104  +	})
          105  +end
    88    106   
    89    107   local terra entry_mgtool(argc: int, argv: &rawstring): int
    90    108   	if argc < 1 then lib.bail('bad invocation!') end
    91    109   
    92    110   	lib.noise_init(2)
    93    111   	[lib.init]
    94    112   
................................................................................
   219    237   					root.epithet = epithets[lib.crypt.random(intptr,0,[epithets.type.N])]
   220    238   					root.rights.powers:fill() -- grant omnipotence
   221    239   					root.rights.rank = 1
   222    240   					var ruid = dlg:actor_create(&root)
   223    241   					dlg:conf_set('master',root.handle)
   224    242   					lib.report('created new administrator')
   225    243   					if mg then
   226         -						lib.dbg('generating temporary password')
   227         -						var tmppw: uint8[33]
   228         -						lib.crypt.spray(&tmppw[0],32) tmppw[32] = 0
   229         -						for i=0,32 do
   230         -							tmppw[i] = tmppw[i] % (10 + 26*2)
   231         -							if tmppw[i] >= 36 then
   232         -								tmppw[i] = tmppw[i] + (0x61 - 36)
   233         -							elseif tmppw[i] >= 10 then
   234         -								tmppw[i] = tmppw[i] + (0x41 - 10)
   235         -							else tmppw[i] = tmppw[i] + 0x30 end
   236         -						end
   237         -						lib.dbg('assigning temporary password')
   238         -						dlg:auth_create_pw(ruid, false, pstr {
   239         -							ptr = [rawstring](&tmppw[0]), ct = 32
   240         -						})
   241         -						lib.report('temporary root pw: ', {[rawstring](&tmppw[0]), 32})
          244  +						var tmppw: int8[33]
          245  +						pwset(dlg, &tmppw, ruid, false)
          246  +						lib.report('temporary root pw: ', {&tmppw[0], 32})
   242    247   					end
   243    248   				else goto cmderr end
   244    249   			elseif lib.str.cmp(mode.arglist(0),'user') == 0 then
          250  +				var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0))
          251  +				if umode.help then
          252  +					[ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' <handle> <cmd> [<args>…]', umode.type.helptxt.opts) ]
          253  +					return 1
          254  +				end
          255  +				if umode.arglist.ct >= 3 then
          256  +					var grant = lib.str.cmp(umode.arglist(1),'grant') == 0
          257  +					var handle = umode.arglist(0)
          258  +					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  +					if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then
          261  +						var newprivs = usr.ptr.rights.powers
          262  +						var map = array([lib.store.privmap])
          263  +						if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then
          264  +							if grant
          265  +								then newprivs:fill()
          266  +								else newprivs:clear()
          267  +							end
          268  +						else
          269  +							for i=2,umode.arglist.ct do
          270  +								var priv = umode.arglist(i)
          271  +								for j=0,[map.type.N] do
          272  +									var p = map[j]
          273  +									if p.name:cmp_raw(priv) then
          274  +										if grant then
          275  +											lib.dbg('enabling power ', {p.name.ptr,p.name.ct})
          276  +											newprivs = newprivs + p.priv
          277  +										else
          278  +											lib.dbg('disabling power ', {p.name.ptr,p.name.ct})
          279  +											newprivs = newprivs - p.priv
          280  +										end
          281  +										break
          282  +									end
          283  +								end
          284  +							end
          285  +						end
          286  +
          287  +						usr.ptr.rights.powers = newprivs
          288  +						dlg:actor_save_privs(usr.ptr)
          289  +					elseif lib.str.cmp(umode.arglist(1),'auth') == 0 and umode.arglist.ct == 4 then
          290  +						var reset = lib.str.cmp(umode.arglist(3),'reset') == 0
          291  +						if reset or lib.str.cmp(umode.arglist(3),'new') == 0 then
          292  +							if lib.str.cmp(umode.arglist(2),'pw') == 0 then
          293  +								var tmppw: int8[33]
          294  +								pwset(dlg, &tmppw, usr.ptr.id, reset)
          295  +								lib.report('new temporary password for ',usr.ptr.handle,': ', {&tmppw[0], 32})
          296  +							else lib.bail('unknown credential type') end
          297  +						elseif lib.str.cmp(umode.arglist(3),'purge') == 0 then
          298  +						else goto cmderr end
          299  +					else goto cmderr end
          300  +				else goto cmderr end
   245    301   			elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then
   246    302   			elseif lib.str.cmp(mode.arglist(0),'tl') == 0 then
   247    303   			elseif lib.str.cmp(mode.arglist(0),'serv') == 0 then
   248    304   			else goto cmderr end
   249    305   		end
   250    306   	end
   251    307   
   252    308   	do return 0 end
   253         -	::cmderr:: lib.bail('invalid command') return 2
          309  +	::cmderr:: lib.bail('invalid command')
   254    310   end
   255    311   
   256    312   return entry_mgtool

Modified parsav.md from [4b27db126a] to [409d9b6b60].

    49     49   
    50     50       master   pgsql   host=localhost dbname=parsav
    51     51   	tweets   pgsql   host=420.69.dread.cloud dbname=content
    52     52   
    53     53   the form the configuration string takes depends on the specific backend.
    54     54   
    55     55   once you've set up a backend and confirmed parsav can connect succesfully to it, you can initialize the database with the command `parsav db init <domain>`, where `<domain>` is the name of the domain name you will be hosting `parsav` from. this will install all necessary structures and functions in the target and create all necessary files. it will not, however, create any users. you can create an initial administrative user with the `parsav mkroot <handle>` command, where `<handle>` is the handle you want to use on the server. this will also assign a temporary password for the user if possible. you should now be able to log in and administer the server.
           56  +
           57  +if something goes awry with your administrative account, don't fret! you can get your powers themselves back with the command `parsav user <handle> grant all`, and if you're having difficulties logging in, the command `parsav user <handle> auth pw reset` will give you a fresh password. if all else fails, you can always run `mkroot` again to create a new root account, and try to repair the damage from there.
    56     58   
    57     59   by default, parsav binds to [::1]:10917. if you want to change this (to run it on a different port, or make it directly accessible to other servers on the network), you can use the command `parsav conf set bind <address>`, where `address` is a binding specification like `0.0.0.0:80`. it is recommended, however, that `parsavd` be kept accessible only from localhost, and that connections be forwarded to it from nginx, haproxy, or a similar reverse proxy. (this can also be changed with the online configuration UI)
    58     60   
    59     61   ### postgresql backend
    60     62   
    61     63   a database will need to be created for `parsav`'s use before `parsav db init` will work. this can be accomplished with a command like `$ createdb parsav`. you'll also of course need to set up some way for `parsavd` to authenticate itself to `postgres`. peer auth is the most secure option, and this is what you should use if postgres and `parsavd` are running on the same box. specify the database name to the backend the usual way, with a clause like `dbname=parsav` in your connection string.
    62     64   

Modified route.t from [4fbd6ed0a5] to [4ebb6db558].

   129    129   	else
   130    130   		::wrongmeth:: co:complain(405, 'method not allowed', 'that method is not meaningful for this endpoint') do return end
   131    131   	end
   132    132   	return
   133    133   end
   134    134   
   135    135   terra http.post_compose(co: &lib.srv.convo, meth: method.t)
          136  +	if not co:assertpow('post') then return end
          137  +	--if co.who.rights.powers.post() == false then
          138  +		--co:complain(403,'insufficient privileges','you lack the <strong>post</strong> power and cannot perform this action')
          139  +
   136    140   	if meth == method.get then
   137    141   		lib.render.compose(co, nil)
   138    142   	elseif meth == method.post then
   139         -		if co.who.rights.powers.post() == false then
   140         -			co:complain(401,'insufficient privileges','you lack the <strong>post</strong> power and cannot perform this action') return
   141         -		end
   142    143   		var text, textlen = co:postv("post")
   143    144   		var acl, acllen = co:postv("acl")
   144    145   		var subj, subjlen = co:postv("subject")
   145    146   		if text == nil or acl == nil then
   146    147   			co:complain(405, 'invalid post', 'every post must have at least body text and an ACL')
   147    148   			return
   148    149   		end

Modified srv.t from [7234d58b59] to [7c204f248d].

   179    179   	body:send(self.con, code, [lib.mem.ptr(lib.http.header)] {
   180    180   		ptr = &hdrs[0], ct = [hdrs.type.N]
   181    181   	})
   182    182   
   183    183   	body.title:free()
   184    184   	body.body:free()
   185    185   end
          186  +
          187  +convo.methods.assertpow = macro(function(self, pow)
          188  +	return quote
          189  +		var ok = true
          190  +		if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then
          191  +			ok = false
          192  +			self:complain(403,'insufficient privileges',['you lack the <strong>'..pow:asvalue()..'</strong> power and cannot perform this action'])
          193  +		end
          194  +	in ok end
          195  +end)
   186    196   
   187    197   struct convo.page {
   188    198   	title: pstring
   189    199   	body: pstring
   190    200   	class: pstring
   191    201   }
   192    202   

Modified store.t from [71684bc451] to [3a79c99b1d].

    28     28   		'cred', 'elevate', 'demote', 'rebrand', -- modify site's brand identity
    29     29   		'herald' -- grant serverwide epithets
    30     30   	};
    31     31   	prepmode = lib.enum {
    32     32   		'full','conf','admin'
    33     33   	}
    34     34   }
           35  +
           36  +m.privmap = {}
           37  +do local struct pt { name:lib.mem.ptr(int8), priv:m.powerset }
           38  +for k,v in pairs(m.powerset.members) do
           39  +	m.privmap[#m.privmap + 1] = quote
           40  +		var ps: m.powerset ps:clear()
           41  +		(ps.[v] << true)
           42  +	in pt {name = lib.str.plit(v), priv = ps} end
           43  +end end
    35     44   
    36     45   terra m.powerset:affect_users()
    37     46   	return self.purge() or self.censor() or self.suspend() or
    38     47   	       self.elevate() or self.demote() or self.cred()
    39     48   end
    40     49   
    41     50   local str = rawstring
................................................................................
   204    213   	conprep: {&m.source, m.prepmode.t} -> {} -- prepares queries and similar tasks that require the schema to already be in place
   205    214   	obliterate_everything: &m.source -> bool -- wipes everything parsav-related out of the database
   206    215   
   207    216   	conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8)
   208    217   	conf_set: {&m.source, rawstring, rawstring} -> {}
   209    218   	conf_reset: {&m.source, rawstring} -> {}
   210    219   
   211         -	actor_save: {&m.source, &m.actor} -> bool
   212    220   	actor_create: {&m.source, &m.actor} -> uint64
          221  +	actor_save_privs: {&m.source, &m.actor} -> {}
   213    222   	actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor)
   214    223   	actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor)
   215    224   	actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif)
   216    225   	actor_enum: {&m.source} -> lib.mem.ptr(&m.actor)
   217    226   	actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)
   218    227   	actor_stats: {&m.source, uint64} -> m.actor_stats
   219    228