parsav  Check-in [05af79b909]

Overview
Comment:user mgmt and rt improvements
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 05af79b909dfb0c5ac50472976eed8636659f4df7fd4b64a3a3bf6906e8a5f39
User & Date: lexi on 2021-01-09 07:15:21
Other Links: manifest | tags
Context
2021-01-10
03:54
add memory pool impl, handle various little details, add beginnings of mimelib check-in: 8d35307a7f user: lexi tags: trunk
2021-01-09
07:15
user mgmt and rt improvements check-in: 05af79b909 user: lexi tags: trunk
2021-01-08
05:58
enable passwords check-in: d6024624c6 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [9c53eed84d] to [175b022aff].

   187    187   			select (pg_temp.parsavpg_translate_actor(a)).*,
   188    188   
   189    189   			       au.restrict,
   190    190   						array['post'    ] <@ au.restrict,
   191    191   						array['edit'    ] <@ au.restrict,
   192    192   						array['account' ] <@ au.restrict,
   193    193   						array['upload'  ] <@ au.restrict,
          194  +						array['artifact'] <@ au.restrict,
   194    195   						array['moderate'] <@ au.restrict,
   195         -						array['admin'   ] <@ au.restrict
          196  +						array['admin'   ] <@ au.restrict,
          197  +						array['invite'  ] <@ au.restrict
   196    198   
   197    199   			from      parsav_auth au
   198    200   			left join parsav_actors a     on au.uid = a.id
   199    201   
   200    202   			where au.aid = $1::bigint and au.blacklist = false and
   201    203   				(au.netmask is null or au.netmask >> $2::inet) and
   202    204   				($3::bigint = 0 or --slightly abusing the epoch time fmt here, but
................................................................................
   271    273   			update parsav_actors set
   272    274   				authtime = $2::bigint
   273    275   				where id = $1::bigint
   274    276   		]];
   275    277   	};
   276    278   
   277    279   	auth_create_pw = {
   278         -		params = {uint64, binblob, int64, pstring}, cmd = true, sql = [[
          280  +		params = {uint64, binblob, int64, pstring}, sql = [[
   279    281   			insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values (
   280    282   				$1::bigint,
   281    283   				(select handle from parsav_actors where id = $1::bigint),
   282    284   				'pw-sha256', $2::bytea,
   283    285   				$3::bigint, $4::text
   284         -			)
          286  +			) on conflict (name,kind,cred) do update set comment = $4::text returning aid
   285    287   		]]
   286    288   	};
          289  +
          290  +	auth_privs_clear = {
          291  +		params = {uint64}, cmd = true, sql = [[
          292  +			update parsav_auth set restrict = array[]::text[] where aid = $1::bigint
          293  +		]];
          294  +	};
          295  +
          296  +	auth_priv_install = {
          297  +		params = {uint64,pstring}, cmd = true, sql = [[
          298  +			update parsav_auth set restrict = restrict || $2::text where aid = $1::bigint
          299  +		]];
          300  +	};
   287    301   
   288    302   	auth_purge_type = {
   289    303   		params = {rawstring, uint64, rawstring}, cmd = true, sql = [[
   290    304   			delete from parsav_auth where
   291    305   				((uid = 0 and name = $1::text) or uid = $2::bigint) and
   292    306   				kind like $3::text
   293    307   		]]
   294    308   	};
          309  +
          310  +	auth_purge_aid = {
          311  +		params = {uint64}, cmd = true, sql = [[
          312  +			delete from parsav_auth where aid = $1::bigint
          313  +		]]
          314  +	};
   295    315   
   296    316   	auth_enum_uid = {
   297    317   		params = {uint64}, sql = [[
   298    318   			select aid, kind, comment, netmask, blacklist from parsav_auth where uid = $1::bigint
   299    319   		]];
   300    320   	};
   301    321   
................................................................................
   401    421   		params = {uint64, uint64, pstring}, cmd = true, sql = [[
   402    422   			delete from parsav_acts where
   403    423   				actor = $1::bigint and
   404    424   				subject = $2::bigint and
   405    425   				kind = $3::text
   406    426   		]];
   407    427   	};
          428  +
          429  +	post_react_delete = {
          430  +		params = {uint64}, cmd = true, sql = [[
          431  +			delete from parsav_acts where id = $1::bigint
          432  +		]];
          433  +	};
   408    434   
   409    435   	post_reacts_fetch_uid = {
   410    436   		params = {uint64, uint64, pstring}, sql = [[
   411    437   			select id, actor, subject, kind, body, time from parsav_acts where
   412    438   				($1::bigint = 0 or actor   = $1::bigint) and
   413    439   				($2::bigint = 0 or subject = $2::bigint) and
   414    440   				($3::text is null or kind  = $3::text  )
   415         -		]]
          441  +		]];
          442  +	};
          443  +
          444  +	post_act_fetch_notice = {
          445  +		params = {uint64}, sql = [[
          446  +			select (pg_temp.parsavpg_translate_act(a)).*
          447  +			from parsav_acts as a
          448  +				where id = $1::bigint
          449  +		]];
   416    450   	};
   417    451   
   418    452   	post_enum_author_uid = {
   419    453   		params = {uint64,uint64,uint64,uint64, uint64}, sql = [[
   420    454   			select (c.post).*
   421    455   			from pg_temp.parsavpg_known_content as c
   422    456   
................................................................................
   964    998   		a.ptr.key = r:bin(row,8)
   965    999   	end
   966   1000   	a.ptr.origin = origin
   967   1001   	if avia.buf ~= nil then avia:free() end
   968   1002   	return a
   969   1003   end
   970   1004   
   971         -local privmap = lib.store.privmap
         1005  +local privmap = lib.store.powmap
   972   1006   
   973   1007   local checksha = function(src, hash, origin, username, pw)
   974   1008   	local validate = function(kind, cred, credlen)
   975   1009   		return quote 
   976   1010   			var r = queries.actor_auth_pw.exec(
   977   1011   				[&lib.store.source](src),
   978   1012   				username,
................................................................................
  1015   1049   local privupdate = terra(
  1016   1050   	src: &lib.store.source,
  1017   1051   	ac: &lib.store.actor
  1018   1052   ): {}
  1019   1053   	var pdef: lib.store.powerset pdef:clear()
  1020   1054   	var map = array([privmap])
  1021   1055   	for i=0, [map.type.N] do
  1022         -		var d = pdef and map[i].priv
  1023         -		var u = ac.rights.powers and map[i].priv
         1056  +		var d = pdef and map[i].val
         1057  +		var u = ac.rights.powers and map[i].val
  1024   1058   		queries.actor_power_delete.exec(src, ac.id, map[i].name)
  1025   1059   		if d:sz() > 0 and u:sz() == 0 then
  1026   1060   			lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct})
  1027   1061   			queries.actor_power_insert.exec(src, ac.id, map[i].name, 0)
  1028   1062   		elseif d:sz() == 0 and u:sz() > 0 then
  1029   1063   			lib.dbg('granting power ', {map[i].name.ptr, map[i].name.ct})
  1030   1064   			queries.actor_power_insert.exec(src, ac.id, map[i].name, 1)
................................................................................
  1041   1075   	var r = queries.actor_powers_fetch.exec(src, uid)
  1042   1076   
  1043   1077   	for i=0, r.sz do
  1044   1078   		for j=0, [map.type.N] do
  1045   1079   			var pn = r:_string(i,0)
  1046   1080   			if map[j].name:cmp(pn) then
  1047   1081   				if r:bool(i,1)
  1048         -					then powers = powers + map[j].priv
  1049         -					else powers = powers - map[j].priv
         1082  +					then powers = powers + map[j].val
         1083  +					else powers = powers - map[j].val
  1050   1084   				end
  1051   1085   			end
  1052   1086   		end
  1053   1087   	end
  1054   1088   
  1055   1089   	return powers
  1056   1090   end
................................................................................
  1268   1302   
  1269   1303   			var a = row_to_actor(&r, 0)
  1270   1304   			a.ptr.source = src
  1271   1305   
  1272   1306   			var au = [lib.stat(lib.store.auth)] { ok = true }
  1273   1307   			au.val.aid = aid
  1274   1308   			au.val.uid = a.ptr.id
  1275         -			if not r:null(0,13) then -- restricted?
         1309  +			if not r:null(0,14) then -- restricted?
  1276   1310   				au.val.privs:clear()
  1277         -				(au.val.privs.post    << r:bool(0,14)) 
  1278         -				(au.val.privs.edit    << r:bool(0,15))
  1279         -				(au.val.privs.account << r:bool(0,16))
  1280         -				(au.val.privs.upload  << r:bool(0,17))
  1281         -				(au.val.privs.moderate<< r:bool(0,18))
  1282         -				(au.val.privs.admin   << r:bool(0,19))
         1311  +				(au.val.privs.post    << r:bool(0,15)) 
         1312  +				(au.val.privs.edit    << r:bool(0,16))
         1313  +				(au.val.privs.account << r:bool(0,17))
         1314  +				(au.val.privs.upload  << r:bool(0,18))
         1315  +				(au.val.privs.artifact<< r:bool(0,19))
         1316  +				(au.val.privs.moderate<< r:bool(0,20))
         1317  +				(au.val.privs.admin   << r:bool(0,21))
         1318  +				(au.val.privs.invite  << r:bool(0,22))
  1283   1319   			else au.val.privs:fill() end
  1284   1320   
  1285   1321   			return au, a
  1286   1322   		end
  1287   1323   
  1288   1324   		::fail:: return [lib.stat   (lib.store.auth) ] { ok = false        },
  1289   1325   			            [lib.mem.ptr(lib.store.actor)] { ptr = nil, ct = 0 }
................................................................................
  1321   1357   		var r = queries.post_fetch.exec(src, post)
  1322   1358   		if r.sz == 0 then return [lib.mem.ptr(lib.store.post)].null() end
  1323   1359   		var p = row_to_post(&r, 0)
  1324   1360   		p.ptr.source = src
  1325   1361   		return p
  1326   1362   	end];
  1327   1363   
         1364  +	post_act_cancel = [terra(
         1365  +		src: &lib.store.source,
         1366  +		act: uint64
         1367  +	): {} queries.post_react_delete.exec(src, act) end];
         1368  +
  1328   1369   	post_retweet = [terra(
  1329   1370   		src: &lib.store.source,
  1330   1371   		uid: uint64,
  1331   1372   		post: uint64,
  1332   1373   		undo: bool
  1333   1374   	): {}
  1334         -		var time = lib.osclock.time(nil)
  1335   1375   		if not undo then
         1376  +			var time = lib.osclock.time(nil)
  1336   1377   			queries.post_react_simple.exec(src,uid,post,"rt",time)
  1337   1378   		else
  1338   1379   			queries.post_react_cancel.exec(src,uid,post,"rt")
  1339   1380   		end
  1340   1381   	end];
  1341   1382   	post_like = [terra(
  1342   1383   		src: &lib.store.source,
................................................................................
  1528   1569   
  1529   1570   	auth_attach_pw = [terra(
  1530   1571   		src: &lib.store.source,
  1531   1572   		uid: uint64,
  1532   1573   		reset: bool,
  1533   1574   		pw: pstring,
  1534   1575   		comment: pstring
  1535         -	): {}
         1576  +	): uint64
  1536   1577   		var hash: uint8[lib.crypt.algsz.sha256]
  1537   1578   		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(lib.crypt.alg.sha256.id),
  1538   1579   			[&uint8](pw.ptr), pw.ct, &hash[0]) ~= 0 then
  1539   1580   			lib.bail('cannot hash password')
  1540   1581   		end
  1541   1582   		if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end
  1542         -		queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, lib.osclock.time(nil), comment)
         1583  +		var r = queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, lib.osclock.time(nil), comment)
         1584  +		if r.sz == 0 then return 0 end
         1585  +		var aid = r:int(uint64,0,0)
         1586  +		r:free()
         1587  +		return aid
         1588  +	end];
         1589  +
         1590  +	auth_privs_set = [terra(
         1591  +		src: &lib.store.source,
         1592  +		aid: uint64,
         1593  +		set: lib.store.privset
         1594  +	): {}
         1595  +		var map = array([lib.store.privmap])
         1596  +		queries.auth_privs_clear.exec(src,aid)
         1597  +		if set:sz() == 0 then return end
         1598  +		for i=0, [map.type.N] do
         1599  +			if (set and map[i].val):sz() > 0 then
         1600  +				queries.auth_priv_install.exec(src,aid,map[i].name)
         1601  +			end
         1602  +		end
  1543   1603   	end];
  1544   1604   
  1545   1605   	auth_purge_pw = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
  1546   1606   		queries.auth_purge_type.exec(src, handle, uid, 'pw-%')
  1547   1607   	end];
  1548   1608   
  1549   1609   	auth_purge_otp = [terra(src: &lib.store.source, uid: uint64, handle: rawstring): {}
................................................................................
  1699   1759   			post.id, post.chgcount, post.edited,
  1700   1760   			post.subject, post.acl, post.body)
  1701   1761   	end];
  1702   1762   
  1703   1763   	post_enum_parent = [terra(
  1704   1764   		src: &lib.store.source,
  1705   1765   		post: uint64
  1706         -	): lib.mem.ptr(lib.mem.ptr(lib.store.post))
         1766  +	): lib.mem.lstptr(lib.store.post)
  1707   1767   		var r = queries.post_enum_parent.exec(src,post)
  1708   1768   		if r.sz == 0 then
  1709   1769   			return [lib.mem.ptr(lib.mem.ptr(lib.store.post))].null()
  1710   1770   		end
  1711   1771   		defer r:free()
  1712   1772   		var lst = lib.mem.heapa([lib.mem.ptr(lib.store.post)], r.sz)
  1713   1773   
  1714   1774   		for i=0, r.sz do lst.ptr[i] = row_to_post(&r, i) end
  1715   1775   
  1716   1776   		return lst
  1717   1777   	end];
         1778  +
         1779  +	post_act_fetch_notice = [terra(
         1780  +		src: &lib.store.source,
         1781  +		act: uint64
         1782  +	): lib.store.notice
         1783  +		var r = queries.post_act_fetch_notice.exec(src,act)
         1784  +		if r.sz == 0 then return lib.store.notice { kind = lib.store.noticetype.none } end
         1785  +		defer r:free()
         1786  +		
         1787  +		var n: lib.store.notice
         1788  +		n.kind = r:int(uint16,0,0)
         1789  +		n.when = r:int(int64,0,1)
         1790  +		n.who = r:int(int64,0,2)
         1791  +		n.what = r:int(uint64,0,3)
         1792  +		if n.kind == lib.store.noticetype.react then
         1793  +			var react = r:_string(0,5)
         1794  +			lib.str.ncpy(n.reaction, react.ptr, lib.math.smallest(react.ct,[(`n.reaction).tree.type.N]))
         1795  +		end
         1796  +
         1797  +		return n
         1798  +	end];
  1718   1799   
  1719   1800   	thread_latest_arrival_calc = [terra(
  1720   1801   		src: &lib.store.source,
  1721   1802   		post: uint64
  1722   1803   	): lib.store.timepoint
  1723   1804   		var r = queries.thread_latest_arrival_calc.exec(src,post)
  1724   1805   		if r.sz == 0 or r:null(0,0) then return 0 end

Modified backend/schema/pgsql-views.sql from [25f9d405bc] to [c997d2f5ad].

    40     40   	kind	smallint,
    41     41   	"when"	bigint,
    42     42   	who		bigint,
    43     43   	what	bigint,
    44     44   	reply	bigint,
    45     45   	reaction text
    46     46   );
           47  +
           48  +create or replace function
           49  +pg_temp.parsavpg_translate_act(parsav_acts)
           50  +returns pg_temp.parsavpg_intern_notice as $$
           51  +	select row(
           52  +			kmap.kind::smallint,
           53  +			($1).time,
           54  +			($1).actor,
           55  +			($1).subject,
           56  +			null::bigint,
           57  +			($1).body
           58  +		)::pg_temp.parsavpg_intern_notice as notice
           59  +	from (values
           60  +		('rt',    <notice:rt>   ),
           61  +		('like',  <notice:like> ),
           62  +		('react', <notice:react>)
           63  +	) as kmap(kstr,kind) where kmap.kstr = ($1).kind
           64  +$$ language sql;
    47     65   
    48     66   create type pg_temp.parsavpg_intern_actor as (
    49     67   	id			bigint,
    50     68   	nym			text,
    51     69   	handle		text,
    52     70   	origin		bigint,
    53     71   	bio			text,
................................................................................
   156    174   --);
   157    175   
   158    176   create temp view parsavpg_notices as (
   159    177    -- TODO add mentions
   160    178   	with ntimes as (
   161    179   		select uid, value as when from parsav_actor_conf_ints where key = 'notice-clear-time'
   162    180   	), acts as (
   163         -		select row(
   164         -				kmap.kind::smallint,
   165         -				a.time,
   166         -				a.actor,
   167         -				a.subject,
   168         -				null::bigint,
   169         -				null::text
   170         -			)::pg_temp.parsavpg_intern_notice as notice,
          181  +		select
          182  +			pg_temp.parsavpg_translate_act(a) as notice,
          183  +		-- row(
          184  +		-- 		kmap.kind::smallint,
          185  +		-- 		a.time,
          186  +		-- 		a.actor,
          187  +		-- 		a.subject,
          188  +		-- 		null::bigint,
          189  +		-- 		null::text
          190  +		-- 	)::pg_temp.parsavpg_intern_notice as notice,
   171    191   			p.author as rcpt
   172    192   		from parsav_acts as a
   173    193   			inner join parsav_posts as p on a.subject = p.id
   174         -			inner join (values
   175         -				('rt',    <notice:rt>   ),
   176         -				('like',  <notice:like> ),
   177         -				('react', <notice:react>)
   178         -			) as kmap(kstr,kind) on kmap.kstr = a.kind
          194  +			-- inner join (values
          195  +			-- 	('rt',    <notice:rt>   ),
          196  +			-- 	('like',  <notice:like> ),
          197  +			-- 	('react', <notice:react>)
          198  +			-- ) as kmap(kstr,kind) on kmap.kstr = a.kind
   179    199   			left  join ntimes as nt on nt.uid = p.author
   180    200   		where a.time >= coalesce(nt.when,0)
   181    201   	), replies as (
   182    202   		select row(
   183    203   				<notice:reply>::smallint,
   184    204   				coalesce(p.posted,p.discovered),
   185    205   				p.author,

Modified mgtool.t from [4f69a41277] to [b1a646e421].

   487    487   					dlg:actor_create(&na)
   488    488   					lib.report('created new user @',na.handle,'; assign credentials to enable login')
   489    489   				elseif umode.arglist.ct >= 3 then
   490    490   					var grant = lib.str.cmp(umode.arglist(1),'grant') == 0
   491    491   					if not usr then lib.bail('no such user') end
   492    492   					if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then
   493    493   						var newprivs = usr.ptr.rights.powers
   494         -						var map = array([lib.store.privmap])
          494  +						var map = array([lib.store.powmap])
   495    495   						if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then
   496    496   							if grant
   497    497   								then newprivs:fill()
   498    498   								else newprivs:clear()
   499    499   							end
   500    500   						else
   501    501   							for i=2,umode.arglist.ct do
   502    502   								var priv = umode.arglist(i)
   503    503   								for j=0,[map.type.N] do
   504    504   									var p = map[j]
   505    505   									if p.name:cmp_raw(priv) then
   506    506   										if grant then
   507    507   											lib.dbg('enabling power ', {p.name.ptr,p.name.ct})
   508         -											newprivs = newprivs + p.priv
          508  +											newprivs = newprivs + p.val
   509    509   										else
   510    510   											lib.dbg('disabling power ', {p.name.ptr,p.name.ct})
   511         -											newprivs = newprivs - p.priv
          511  +											newprivs = newprivs - p.val
   512    512   										end
   513    513   										break
   514    514   									end
   515    515   								end
   516    516   							end
   517    517   						end
   518    518   

Modified render/conf/sec.t from [7f83a40056] to [f6e2d18341].

     9      9   	lib.osclock.ctime_r(&time, &tstr[0])
    10     10   	var body = data.view.conf_sec {
    11     11   		lastreset = pstr {
    12     12   			ptr = &tstr[0], ct = lib.str.sz(&tstr[0])
    13     13   		}
    14     14   	}
    15     15   	
           16  +	var a: lib.str.acc a:init(768) defer a:free()
           17  +
    16     18   	if co.srv.cfg.credmgd then
    17     19   		var new = co:pgetv('new')
    18         -		var a: lib.str.acc a:init(768)
    19     20   		if not new then
    20     21   			body:append(&a)
    21     22   			var credmgr = data.view.conf_sec_credmg {
    22     23   				credlist = pstr{'',0};
    23     24   			}
    24     25   			var creds = co.srv:auth_enum_uid(uid)
    25     26   			if creds.ct > 0 then defer creds:free()
................................................................................
    33     34   						cl:lpush('</option>')
    34     35   					end
    35     36   				end
    36     37   				credmgr.credlist = cl:finalize()
    37     38   			end
    38     39   			credmgr:append(&a)
    39     40   			if credmgr.credlist.ct > 0 then credmgr.credlist:free() end
    40         -		elseif new:cmp(lib.str.plit'pw') then
    41         -			var d: data.view.conf_sec_pwnew
    42         -			var time = lib.osclock.time(nil)
    43         -			var timestr: int8[26] lib.osclock.ctime_r(&time, &timestr[0])
    44         -			var cmt: lib.str.acc
    45         -			cmt:init(48):lpush('enrolled over http on '):push(&timestr[0],0)
    46         -			d.comment = cmt:finalize()
           41  +		else
           42  +			if new:cmp(lib.str.plit'pw') then
           43  +				var d: data.view.conf_sec_pwnew
           44  +				var time = lib.osclock.time(nil)
           45  +				var timestr: int8[26] lib.osclock.ctime_r(&time, &timestr[0])
           46  +				var cmt: lib.str.acc
           47  +				cmt:init(48):lpush('enrolled over http on '):push(&timestr[0],0)
           48  +				d.comment = cmt:finalize()
           49  +
           50  +				var st = d:tostr()
           51  +				d.comment:free()
           52  +				return st
           53  +			elseif new:cmp(lib.str.plit'challenge') then
           54  +			-- we're going to break the rules a bit and do database munging from
           55  +			-- the rendering code, because doing otherwise in this case would be
           56  +			-- genuinely nightmarish
           57  +			elseif new:cmp(lib.str.plit'otp') then
           58  +			elseif new:cmp(lib.str.plit'api') then
           59  +			else return pstr.null() end
           60  +		end
           61  +	else body:append(&a) end
    47     62   
    48         -			var st = d:tostr()
    49         -			d.comment:free()
    50         -			return st
    51         -		elseif new:cmp(lib.str.plit'challenge') then
    52         -		-- we're going to break the rules a bit and do database munging from
    53         -		-- the rendering code, because doing otherwise in this case would be
    54         -		-- genuinely nightmarish
    55         -		elseif new:cmp(lib.str.plit'otp') then
    56         -		elseif new:cmp(lib.str.plit'api') then
    57         -		else return pstr.null() end
    58         -		return a:finalize()
    59         -	else return body:tostr() end
           63  +	return a:finalize()
    60     64   end
    61     65   
    62     66   terra lib.render.conf.sec_overlay
    63     67   (co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
    64     68    -- render the credential panel for the current user, allowing
    65     69    -- it to be reused in the administration UI
    66     70   	return render_conf_sec(co,co.who.id)
    67     71   end
    68     72   
    69     73   return render_conf_sec

Modified render/conf/users.t from [41f55e0682] to [59cb9f4fac].

   227    227   				push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null())
   228    228   			end
   229    229   
   230    230   			cinp:lpush('</div></div>')
   231    231   
   232    232   			if (co.who.rights.powers.elevate() or
   233    233   			   co.who.rights.powers.demote()) and user.ptr.id ~= co.who.id then
   234         -				var map = array([lib.store.privmap])
          234  +				var map = array([lib.store.powmap])
   235    235   				cinp:lpush('<details><summary>powers</summary><div class="pick-list">')
   236    236   					for i=0, [map.type.N] do
   237         -						if (co.who.rights.powers and map[i].priv):sz() > 0 then
   238         -							var on = (user.ptr.rights.powers and map[i].priv):sz() > 0
          237  +						if (co.who.rights.powers and map[i].val):sz() > 0 then
          238  +							var on = (user.ptr.rights.powers and map[i].val):sz() > 0
   239    239   							var enabled = (     on  and co.who.rights.powers.demote() ) or
   240    240   										  ((not on) and co.who.rights.powers.elevate())
   241    241   							var namea: lib.str.acc namea:compose('power-', map[i].name)
   242    242   							var name = namea:finalize()
   243    243   							push_pickbox(&cinp, name, pstr.null(), map[i].name, on, enabled, pstr.null())
   244    244   							name:free()
   245    245   						end

Modified render/tweet-page.t from [57c7b287c5] to [c0b864229b].

    39     39   		pg:lpush('<form class="action-bar" method="post">')
    40     40   		if not co.srv:post_liked_uid(co.who.id, p.id)
    41     41   			then pg:lpush('<button class="pos" name="act" accesskey="l" value="like">like</button>')
    42     42   			else pg:lpush('<button class="neg" name="act" accesskey="l" value="dislike">dislike</button>')
    43     43   		end
    44     44   		pg:lpush('<button class="pos" name="act" accesskey="r" value="rt">retweet</button>')
    45     45   		if p.author == co.who.id then
    46         -			pg:lpush('<a class="button" accesskey="e" href="/post/'):rpush(path(1)):lpush('/edit">edit</a><a class="neg button" accesskey="d" href="/post/'):rpush(path(1)):lpush('/del">delete</a>')
           46  +			if co.who.rights.powers.edit() then
           47  +				pg:lpush('<a class="button" accesskey="e" href="/post/'):rpush(path(1)):lpush('/edit">edit</a>')
           48  +			end
           49  +			pg:lpush('<a class="neg button" accesskey="d" href="/post/'):rpush(path(1)):lpush('/del">delete</a>')
           50  +		elseif co.who.rights.powers.snitch() then
           51  +			pg:lpush('<a class="neg button" accesskey="s" href="/post/'):rpush(path(1)):lpush('/report">report</a>')
    47     52   		end
    48     53   		-- TODO list user's chosen reaction emoji
    49     54   		pg:lpush('</form>')
    50     55   
    51     56   	end
    52     57   	pg:lpush('<div id="convo" data-live="10">')
    53     58   	render_tweet_replies(co, &pg, p.id)

Modified route.t from [b4f49ac3f6] to [a8702e1420].

   197    197   terra http.tweet_page(co: &lib.srv.convo, path: hpath, meth: method.t)
   198    198   	var pid, ok = lib.math.shorthand.parse(path(1).ptr, path(1).ct)
   199    199   	if not ok then
   200    200   		co:complain(400, 'bad post ID', 'that post ID is not valid')
   201    201   		return
   202    202   	end
   203    203   	var post = co.srv:post_fetch(pid)
          204  +	var rt: lib.store.notice
   204    205   	if not post then
   205         -		co:complain(404, 'post not found', 'no such post is known to this server')
   206         -		return
          206  +		rt = co.srv:post_act_fetch_notice(pid)
          207  +		if rt.kind ~= lib.store.noticetype.rt then
          208  +			co:complain(404, 'post not found', 'no such post is known to this server')
          209  +			return
          210  +		elseif rt.who ~= co.who.id then
          211  +			co:complain(403, 'forbidden', 'you cannot cancel other people\'s retweets')
          212  +			return
          213  +		end
   207    214   	end
   208         -	defer post:free()
          215  +	defer post:free() -- NOP on null
   209    216   
   210    217   	if path.ct == 3 then
   211    218   		var lnk: lib.str.acc lnk:compose('/post/', path(1))
   212    219   		var lnkp = lnk:finalize() defer lnkp:free()
   213         -		if post(0).author ~= co.who.id then
          220  +		if post:ref() and post(0).author ~= co.who.id then
   214    221   			co:complain(403, 'forbidden', 'you cannot alter other people\'s posts')
   215    222   			return
   216         -		elseif path(2):cmp(lib.str.lit 'edit') then
          223  +		elseif post:ref() and path(2):cmp(lib.str.lit 'edit') then
          224  +			if not co:assertpow('edit') then return end
   217    225   			if meth_get(meth) then
   218    226   				lib.render.compose(co, post.ptr, nil)
   219    227   				return
   220    228   			elseif meth == method.post then
   221    229   				var newbody = co:postv('post')._0
   222    230   				var newacl = co:postv('acl')._0
   223    231   				var newsubj = co:postv('subject')._0
................................................................................
   226    234   				if newsubj ~= nil then post(0).subject = newsubj end
   227    235   				post(0):save(true)
   228    236   				co:reroute(lnkp.ptr)
   229    237   			end
   230    238   			return
   231    239   		elseif path(2):cmp(lib.str.lit 'del') then
   232    240   			if meth_get(meth) then
   233         -				var conf = data.view.confirm {
   234         -					title = lib.str.plit 'delete post';
   235         -					query = lib.str.plit 'are you sure you want to delete this post?';
   236         -					cancel = lnkp
   237         -				}
          241  +				var conf: data.view.confirm
          242  +				if post:ref() then
          243  +					conf = data.view.confirm {
          244  +						title = lib.str.plit 'delete post';
          245  +						query = lib.str.plit 'are you sure you want to delete this post?';
          246  +						cancel = lnkp
          247  +					}
          248  +				else
          249  +					conf = data.view.confirm {
          250  +						title = lib.str.plit 'cancel retweet';
          251  +						query = lib.str.plit 'are you sure you want to undo this retweet?';
          252  +						cancel = lib.str.plit'/';
          253  +					}
          254  +				end
   238    255   				var body = conf:tostr() defer body:free()
   239    256   				co:stdpage([lib.srv.convo.page] {
   240    257   					title = lib.str.plit 'post :: delete';
   241    258   					class = lib.str.plit 'query';
   242    259   					body = body; cache = false;
   243    260   				})
   244    261   				return
   245    262   			elseif meth == method.post then
   246    263   				var act = co:ppostv('act')
   247    264   				if act:cmp(lib.str.plit 'confirm') then
   248         -					post(0).source:post_destroy(post(0).id)
          265  +					if post:ref() then
          266  +						post(0).source:post_destroy(post(0).id)
          267  +					elseif rt.kind ~= 0 then
          268  +						co.srv:post_act_cancel(pid)
          269  +					end
   249    270   					co:reroute('/') -- TODO maybe return to parent or conversation if possible
   250    271   					return
   251    272   				else goto badop end
   252    273   			end
   253    274   		else goto badurl end
   254    275   	end
   255    276   
   256         -	if meth == method.post then
          277  +	if post:ref() and meth == method.post then
   257    278   		if co.aid == 0 then goto noauth end
   258    279   		var act = co:ppostv('act')
   259    280   		if act:cmp(lib.str.plit 'like') and not co.srv:post_liked_uid(co.who.id,pid) then
   260    281   			co.srv:post_like(co.who.id, pid, false)
   261    282   			post.ptr.likes = post.ptr.likes + 1
   262    283   		elseif act:cmp(lib.str.plit 'dislike') and co.srv:post_liked_uid(co.who.id,pid) then
   263    284   			co.srv:post_like(co.who.id, pid, true)
................................................................................
   276    297   				author = co.who.id, parent = pid;
   277    298   				subject = subj.ptr, acl = acl.ptr, body = replytext.ptr;
   278    299   			}
   279    300   
   280    301   			reply:publish(co.srv)
   281    302   		else goto badop end
   282    303   	end
          304  +
          305  +	if not post then goto badurl end
   283    306   
   284    307   	lib.render.tweet_page(co, path, post.ptr)
   285    308   	do return end
   286    309   
   287    310   	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
   288    311   	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
   289    312   	::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end
   290    313   end
   291    314   
   292    315   local terra 
   293    316   credsec_for_uid(co: &lib.srv.convo, uid: uint64)
   294    317   	var act = co:ppostv('act')
          318  +	lib.dbg('showing credentials')
   295    319   	if act:cmp(lib.str.plit 'invalidate') then
   296    320   		lib.dbg('setting user\'s cookie validation time to now')
   297    321   		co.who.source:auth_sigtime_user_alter(uid, lib.osclock.time(nil))
   298    322   		-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
   299    323   		co:installkey('/conf/sec',co.aid)
   300    324   		return
   301    325   	elseif act:cmp(lib.str.plit 'newcred') then
   302    326   		var cmt = co:ppostv('comment')
   303    327   		var pw = co:ppostv('newpw')
          328  +		var aid: uint64 = 0
   304    329   		if pw:ref() then
   305    330   			var cpw = co:ppostv('rptpw')
   306    331   			if not pw:cmp(cpw) then
   307    332   				co:complain(400,'enrollment failure','the passwords you supplied do not match')
   308    333   				return
   309    334   			end
   310         -			co.srv:auth_attach_pw(uid, false, pw, cmt)
   311         -			co:reroute('?')
   312         -			return
          335  +			aid = co.srv:auth_attach_pw(uid, false, pw, cmt)
   313    336   		else
   314    337   			var key = co:ppostv('newkey')
   315    338   			if key:ref() then
   316    339   
   317    340   			end
   318    341   		end
          342  +		if aid ~= 0 then
          343  +			lib.dbg('setting credential restrictions')
          344  +			var privs = [(function()
          345  +				local check = quote end
          346  +				local me = symbol(lib.store.privset)
          347  +				for i,v in ipairs(lib.store.privset.members) do
          348  +					check = quote [check]
          349  +						var val = co:pgetv(['allow-' .. v])
          350  +						if val:ref() and val:cmp(lib.str.plit'on')
          351  +							then ([me].[v] << true)
          352  +							else ([me].[v] << false)
          353  +						end
          354  +					end
          355  +				end
          356  +				return quote
          357  +					var [me]
          358  +					[check]
          359  +				in [me] end
          360  +			end)()]
          361  +			privs:dump()
          362  +			if privs:sz() > 0 then
          363  +				lib.dbg('installing credential restrictions')
          364  +				lib.io.fmt('on priv %llu\n',aid)
          365  +				co.srv:auth_privs_set(aid, privs)
          366  +			end
          367  +		end
          368  +		co:reroute('?')
          369  +		return
   319    370   	end
   320    371   	co:complain(400,'bad request','the operation you have requested is not meaningful in this context')
   321    372   end
   322    373   
   323    374   terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
   324    375   	var msg = pstring.null()
   325    376   	-- first things first, do priv checks
   326         -	if path.ct >= 1 then
          377  +	if path.ct >= 2 then
   327    378   		if not co.who.rights.powers.config() and (
   328    379   			path(1):cmp(lib.str.lit 'srv')   or
   329    380   			path(1):cmp(lib.str.lit 'badge') or
   330    381   			path(1):cmp(lib.str.lit 'emoji')
   331    382   		) then goto nopriv
   332    383   
   333    384   		elseif not co.who.rights.powers.rebrand() and (
   334    385   			path(1):cmp(lib.str.lit 'brand')
   335    386   		) then goto nopriv
   336    387   
   337    388   		elseif not co.who.rights.powers.account() and (
   338    389   			path(1):cmp(lib.str.lit 'profile') or
   339         -			path(1):cmp(lib.str.lit 'acct')
          390  +			path(1):cmp(lib.str.lit 'sec') or
          391  +			path(1):cmp(lib.str.lit 'avi') or
          392  +			path(1):cmp(lib.str.lit 'ui')
   340    393   		) then goto nopriv
   341    394   
   342    395   		elseif not co.who.rights.powers:affect_users() and (
   343    396   			path(1):cmp(lib.str.lit 'users')
   344    397   		) then goto nopriv end
   345    398   	end
   346    399   
................................................................................
   384    437   		elseif path(1):cmp(lib.str.lit 'users') then
   385    438   			if path.ct >= 3 then
   386    439   				var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
   387    440   				if ok then
   388    441   					var usr = co.srv:actor_fetch_uid(userid)
   389    442   					if usr:ref() then defer usr:free()
   390    443   						if not co.who:overpowers(usr.ptr) then goto nopriv end
          444  +					end
          445  +					if path.ct == 4 then
          446  +						if path(3):cmp(lib.str.lit 'cred') then
          447  +							credsec_for_uid(co, userid)
          448  +						end
   391    449   					end
   392    450   				end
   393    451   			elseif path.ct == 2 and meth == method.post then
   394    452   				var act = co:ppostv('act')
   395    453   				if act:cmp(lib.str.plit'create') then
   396    454   					var newname = co:ppostv('handle')
   397    455   					if not newname or not lib.store.actor.handle_validate(newname.ptr) then

Modified srv.t from [56e1fe84a6] to [80adbc5ad3].

   516    516   					end ::nocookie::;
   517    517   				end
   518    518   
   519    519   				if co.aid ~= 0 then
   520    520   					var sess, usr = co.srv:actor_session_fetch(co.aid, peer, co.aid_issue)
   521    521   					if sess.ok == false then co.aid = 0 co.aid_issue = 0 else
   522    522   						co.who = usr.ptr
   523         -						co.who.rights.powers = server:actor_powers_fetch(co.who.id)
          523  +						var pows = server:actor_powers_fetch(co.who.id)
          524  +						var privs = sess.val.privs
          525  +						if not privs.post()     then (pows.post     << false) end
          526  +						if not privs.edit()     then (pows.edit     << false) end
          527  +						if not privs.account()  then (pows.account  << false) end
          528  +						if not privs.artifact() then (pows.artifact << false) end
          529  +						if not privs.invite()   then (pows.invite   << false) end
          530  +						if not privs.moderate() then
          531  +							(pows.censor     << false)
          532  +							(pows.discipline << false)
          533  +							(pows.vacate     << false)
          534  +							(pows.crier      << false)
          535  +						end
          536  +						if not privs.admin() then
          537  +							(pows.cred    << false)
          538  +							(pows.elevate << false)
          539  +							(pows.demote  << false)
          540  +							(pows.rebrand << false)
          541  +							(pows.herald  << false)
          542  +							(pows.config  << false)
          543  +							(pows.purge   << false)
          544  +						end
          545  +						co.who.rights.powers = pows
   524    546   						var userhue, hueok = server:actor_conf_int_get(co.who.id, 'ui-accent')
   525    547   						if hueok then co.ui_hue = userhue end
   526    548   					end
   527    549   				end
   528    550   
   529    551   				var livelast_p = lib.http.findheader(msg, 'X-Live-Last-Arrival')
   530    552   				if livelast_p ~= nil and livelast_p.ptr ~= nil then

Modified static/live.js from [76c21b64a1] to [c1f304b23e].

    59     59   				if (event.key == 'f') { // fave
    60     60   					postReq(root, 'like', post.querySelector('.stats>.like'))
    61     61   				} else if (event.key == 'r') { // rt
    62     62   					postReq(root, 'rt', post.querySelector('.stats>.rt'))
    63     63   				} else if (event.key == 'Enter') { // nav
    64     64   					window.location = root;
    65     65   					return;
           66  +				} else if (event.key == 'u' && root != cururl) {
           67  +					window.location = cururl; // detweet TODO don't try to delete other's retweets
    66     68   				} else if (post.attributes.getNamedItem('data-own')) {
    67     69   					if      (event.key == 'd') { window.location = root + '/del';  }
    68     70   					else if (event.key == 'e') { window.location = root + '/edit'; }
    69         -					else if (event.key == 'u' && root != cururl) { window.location = cururl; } // detweet
    70     71   				}
    71     72   			}
    72     73   			if (nexturl != null) {
    73     74   				if (cururl != null) {
    74     75   					let cur = window._liveTweetMap.map.get(cururl);
    75     76   					cur.me.classList.remove('live-selected')
    76     77   				}

Modified store.t from [b8bbc4e0ec] to [0ebd27b207].

    20     20   		'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
    21     21   		'exclude', -- own posts will not be visible to this user
    22     22   	};
    23     23   	credset = lib.set {
    24     24   		'pw', 'otp', 'challenge', 'trust'
    25     25   	};
    26     26   	privset = lib.set {
    27         -		'post', 'edit', 'account', 'upload', 'moderate', 'admin', 'invite'
           27  +		'post', 'edit', 'account', 'upload', 'artifact', 'moderate', 'admin', 'invite'
    28     28   	};
    29     29   	powerset = lib.set {
    30     30   		-- user powers -- default on
    31     31   		'login', -- not locked out
    32     32   		'visible', -- account & posts can be seen by others
    33     33   		'post', -- can do poasts
    34     34   		'shout', -- posts show up on local timeline
................................................................................
    52     52   		'invite' -- *unlimited* invites
    53     53   	};
    54     54   	prepmode = lib.enum {
    55     55   		'full','conf','admin'
    56     56   	}
    57     57   }
    58     58   
    59         -m.privmap = {}
    60         -do local struct pt { name:lib.mem.ptr(int8), priv:m.powerset }
    61         -for k,v in pairs(m.powerset.members) do
    62         -	m.privmap[#m.privmap + 1] = quote
    63         -		var ps: m.powerset ps:clear()
    64         -		(ps.[v] << true)
    65         -	in pt {name = lib.str.plit(v), priv = ps} end
    66         -end end
           59  +local function setmap(set)
           60  +	local map = {}
           61  +	local struct pt { name:lib.mem.ptr(int8), val:set }
           62  +	for k,v in pairs(set.members) do
           63  +		map[#map + 1] = quote
           64  +			var ps: set ps:clear()
           65  +			(ps.[v] << true)
           66  +		in pt {name = lib.str.plit(v), val = ps} end
           67  +	end
           68  +	return map
           69  +end
           70  +m.powmap = setmap(m.powerset)
           71  +m.privmap = setmap(m.privset)
    67     72   
    68     73   terra m.powerset:affect_users()
    69     74   	return self.purge() or self.discipline() or self.herald() or
    70     75   	       self.elevate() or self.demote() or self.cred()
    71     76   end
    72     77   
    73     78   local str = rawstring
................................................................................
   408    413   	actor_notice_enum: {&m.source, uint64} -> lib.mem.ptr(m.notice)
   409    414   	actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
   410    415   	actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
   411    416   	actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship
   412    417   
   413    418   	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
   414    419   	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
   415         -	auth_attach_pw:  {&m.source, uint64, bool, pstr, pstr} -> {}
          420  +	auth_attach_pw:  {&m.source, uint64, bool, pstr, pstr} -> uint64
   416    421   	auth_attach_key: {&m.source, uint64, bool, pstr, pstr} -> {}
   417    422   		-- uid: uint64
   418    423   		-- reset: bool (delete other passwords?)
   419    424   		-- pw: pstring
   420    425   		-- comment: pstring
          426  +	auth_privs_set: {&m.source, uint64, m.privset} -> {}
   421    427   	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
   422    428   	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
   423    429   	auth_purge_trust: {&m.source, uint64, rawstring} -> {}
   424    430   	auth_sigtime_user_fetch: {&m.source, uint64} -> m.timepoint
   425    431   		-- authentication tokens and accounts have a property that controls
   426    432   		-- whether auth cookies dated to a certain point are valid. cookies
   427    433   		-- that are generated before the timepoint are considered invalid.
................................................................................
   443    449   			-- artifact id: uint64
   444    450   			-- detach: bool
   445    451   	post_retweet: {&m.source, uint64, uint64, bool} -> {}
   446    452   	post_like: {&m.source, uint64, uint64, bool} -> {}
   447    453   			-- undo: bool
   448    454   	post_react: {&m.source, uint64, uint64, pstring} -> {}
   449    455   			-- emoji: pstring (null to delete previous reaction, otherwise adds/changes)
          456  +	post_act_cancel: {&m.source, uint64} -> {}
   450    457   	post_liked_uid: {&m.source, uint64, uint64} -> bool
   451    458   	post_reacted_uid: {&m.source, uint64, uint64} -> bool
          459  +	post_act_fetch_notice: {&m.source, uint64} -> m.notice
   452    460   
   453    461   	thread_latest_arrival_calc: {&m.source, uint64} -> m.timepoint
   454    462   
   455    463   	artifact_instantiate: {&m.source, lib.mem.ptr(uint8), lib.mem.ptr(int8)} -> uint64
   456    464   		-- instantiate an artifact in the database, either installing a new
   457    465   		-- artifact or returning the id of an existing artifact with the same hash
   458    466   			-- artifact: bytea

Modified view/conf-sec-credmg.tpl from [6f4f9fa693] to [c30c9309b5].

    11     11   </form>
    12     12   <hr>
    13     13   <form method="get">
    14     14   	<p>you can associate extra credentials with this account. you can also limit how much of this account’s authority these credentials can be used to exercise &mdash; for instance, it might be useful to create API keys that can read the account timeline, but not post as the account owner or access any of his administrative powers. if you don't select a capability set, the credential will be able to wield the full scope of the associated account‘s powers.</p>
    15     15   	<div class="check-panel">
    16     16   		<label><input type="checkbox" name="allow-post"> post</label>
    17     17   		<label><input type="checkbox" name="allow-edit"> edit</label>
    18         -		<label><input type="checkbox" name="allow-acct"> manage account</label>
           18  +		<label><input type="checkbox" name="allow-account"> manage account settings</label>
    19     19   		<label><input type="checkbox" name="allow-upload"> upload artifacts</label>
    20         -		<label><input type="checkbox" name="allow-censor"> moderation</label>
           20  +		<label><input type="checkbox" name="allow-artifact"> edit and delete artifacts</label>
           21  +		<label><input type="checkbox" name="allow-moderate"> moderation</label>
    21     22   		<label><input type="checkbox" name="allow-admin"> other admin powers</label>
    22     23   		<label><input type="checkbox" name="allow-invite"> invite</label>
    23     24   	</div>
    24     25   	<p>you can also specify an IP address range in CIDR format to associate with this credential. if you do so, this credential will only be usable when connecting from an IP address in that range. otherwise, it will be valid when connecting from anywhere on the internet.</p>
    25     26   	<div class="elem">
    26     27   		<label for="netmask">netmask</label>
    27     28   		<input type="text" name="netmask" id="netmask" placeholder="10.0.0.0/8">