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: |
a64461061f49614172eea9ab72b4f428 |
| 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