Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -83,11 +83,10 @@ $8::text, $9::smallint, $10::integer ) returning id ]]; }; - actor_auth_pw = { params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[ select a.aid, a.uid, a.name from parsav_auth as a left join parsav_actors as u on u.id = a.uid where (a.uid is null or u.handle = $1::text or ( @@ -200,10 +199,18 @@ insert into parsav_rights (actor, key, allow) values ( $1::bigint, $2::text, ($3::smallint)::integer::bool ) ]] }; + + actor_power_delete = { + params = {uint64,lib.mem.ptr(int8)}, cmd = true, sql = [[ + delete from parsav_rights where + actor = $1::bigint and + key = $2::text + ]] + }; auth_create_pw = { params = {uint64, lib.mem.ptr(uint8)}, cmd = true, sql = [[ insert into parsav_auth (uid, name, kind, cred) values ( $1::bigint, @@ -520,18 +527,11 @@ if r:null(row,3) then a.ptr.origin = 0 else a.ptr.origin = r:int(uint64,row,3) end return a end -local privmap = {} -do local struct pt { name:pstring, priv:lib.store.powerset } -for k,v in pairs(lib.store.powerset.members) do - privmap[#privmap + 1] = quote - var ps: lib.store.powerset ps:clear() - (ps.[v] << true) - in pt {name = lib.str.plit(v), priv = ps} end -end end +local privmap = lib.store.privmap local checksha = function(src, hash, origin, username, pw) local validate = function(kind, cred, credlen) return quote var r = queries.actor_auth_pw.exec( @@ -570,10 +570,53 @@ end end local schema = sqlsquash(lib.util.ingest('backend/schema/pgsql.sql')) local obliterator = sqlsquash(lib.util.ingest('backend/schema/pgsql-drop.sql')) + +local privupdate = terra( + src: &lib.store.source, + ac: &lib.store.actor +): {} + var pdef = lib.store.rights_default().powers + var map = array([privmap]) + for i=0, [map.type.N] do + var d = pdef and map[i].priv + var u = ac.rights.powers and map[i].priv + queries.actor_power_delete.exec(src, ac.id, map[i].name) + if d:sz() > 0 and u:sz() == 0 then + lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct}) + queries.actor_power_insert.exec(src, ac.id, map[i].name, 0) + elseif d:sz() == 0 and u:sz() > 0 then + lib.dbg('granting power ', {map[i].name.ptr, map[i].name.ct}) + queries.actor_power_insert.exec(src, ac.id, map[i].name, 1) + end + end +end + +local getpow = terra( + src: &lib.store.source, + uid: uint64 +): lib.store.powerset + var powers = lib.store.rights_default().powers + var map = array([privmap]) + var r = queries.actor_powers_fetch.exec(src, uid) + + for i=0, r.sz do + for j=0, [map.type.N] do + var pn = r:_string(i,0) + if map[j].name:cmp(pn) then + if r:bool(i,1) + then powers = powers + map[j].priv + else powers = powers - map[j].priv + end + end + end + end + + return powers +end local b = `lib.store.backend { id = "pgsql"; open = [terra(src: &lib.store.source): &opaque lib.report('connecting to postgres database: ', src.string.ptr) @@ -655,10 +698,11 @@ var r = queries.actor_fetch_uid.exec(src, uid) if r.sz == 0 then return [lib.mem.ptr(lib.store.actor)] { ct = 0, ptr = nil } else defer r:free() var a = row_to_actor(&r, 0) + a.ptr.rights.powers = getpow(src, uid) a.ptr.source = src return a end end]; @@ -666,10 +710,11 @@ var r = queries.actor_fetch_xid.exec(src, xid) if r.sz == 0 then return [lib.mem.ptr(lib.store.actor)] { ct = 0, ptr = nil } else defer r:free() var a = row_to_actor(&r, 0) + a.ptr.rights.powers = getpow(src, a.ptr.id) a.ptr.source = src return a end end]; @@ -808,59 +853,27 @@ for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) end -- MUST FREE ALL return ret end]; - actor_powers_fetch = [terra( - src: &lib.store.source, - uid: uint64 - ): lib.store.powerset - var powers = lib.store.rights_default().powers - var map = array([privmap]) - var r = queries.actor_powers_fetch.exec(src, uid) - - for i=0, r.sz do - for j=0, [map.type.N] do - var pn = r:_string(i,0) - if map[j].name:cmp(pn) then - if r:bool(i,1) - then powers = powers + map[j].priv - else powers = powers - map[j].priv - end - end - end - end - - return powers - end]; + actor_powers_fetch = getpow; + actor_save_privs = privupdate; actor_create = [terra( src: &lib.store.source, ac: &lib.store.actor ): uint64 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) if r.sz == 0 then lib.bail('failed to create actor!') end - var uid = r:int(uint64,0,0) + ac.id = r:int(uint64,0,0) -- check against default rights, insert records for wherever powers differ lib.dbg('created new actor, establishing powers') - var pdef = lib.store.rights_default().powers - var map = array([privmap]) - for i=0, [map.type.N] do - var d = pdef and map[i].priv - var u = ac.rights.powers and map[i].priv - if d:sz() > 0 and u:sz() == 0 then - lib.dbg('blocking power ', {map[i].name.ptr, map[i].name.ct}) - queries.actor_power_insert.exec(src, uid, map[i].name, 0) - elseif d:sz() == 0 and u:sz() > 0 then - lib.dbg('granting power ', {map[i].name.ptr, map[i].name.ct}) - queries.actor_power_insert.exec(src, uid, map[i].name, 1) - end - end + privupdate(src,ac) lib.dbg('powers established') - return uid + return ac.id end]; auth_create_pw = [terra( src: &lib.store.source, uid: uint64, Index: mgtool.t ================================================================== --- mgtool.t +++ mgtool.t @@ -29,15 +29,15 @@ { 'db obliterate', 'completely purge all parsav-related content and structure from the database, destroying all user content (requires confirmation)' }; { 'db insert', 'reads a file from standard in and inserts it into the attachment database, printing the resulting ID' }; { 'mkroot ', 'establish a new root user with the given handle' }; { 'user auth new', '(where applicable, managed auth only) create a new authentication token of the given type for a user' }; { 'user auth reset', '(where applicable, managed auth only) delete all of a user\'s authentication tokens of the given type and issue a new one' }; - { 'user auth purge-credentials []', 'delete all credentials that would allow this user to log in (where possible)' }; + { 'user auth (|all) purge', 'delete all credentials that would allow this user to log in (where possible)' }; { 'user (grant|revoke) (|all)', 'grant or revoke a specific power to or from a user' }; { 'user emasculate', 'strip all administrative powers from a user' }; { 'user suspend []', '(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'}; - { 'actor 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)' }; + { 'actor 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)' }; { 'actor create', 'instantiate a new actor' }; { 'actor bestow ', 'bestow an epithet upon an actor' }; { 'conf set ', 'add or a change a server configuration parameter to the database' }; { 'conf get ', 'report the value of a server setting' }; { 'conf reset ', 'reset a server setting to its default value' }; @@ -83,10 +83,28 @@ var dest = [&uint8](sdest) lib.crypt.spray(dest,64) for i=0,64 do dest[i] = dest[i] % (0x7e - 0x20) + 0x20 end dest[64] = 0 end + +local terra pwset(dlg: idelegate, buf: &(int8[33]), uid: uint64, reset: bool) + lib.dbg('generating temporary password') + var tmppw = [&uint8](&(buf[0])) + lib.crypt.spray(tmppw,32) tmppw[32] = 0 + for i=0,32 do + tmppw[i] = tmppw[i] % (10 + 26*2) + if tmppw[i] >= 36 then + tmppw[i] = tmppw[i] + (0x61 - 36) + elseif tmppw[i] >= 10 then + tmppw[i] = tmppw[i] + (0x41 - 10) + else tmppw[i] = tmppw[i] + 0x30 end + end + lib.dbg('assigning temporary password') + dlg:auth_create_pw(uid, reset, pstr { + ptr = [rawstring](tmppw), ct = 32 + }) +end local terra entry_mgtool(argc: int, argv: &rawstring): int if argc < 1 then lib.bail('bad invocation!') end lib.noise_init(2) @@ -221,36 +239,74 @@ root.rights.rank = 1 var ruid = dlg:actor_create(&root) dlg:conf_set('master',root.handle) lib.report('created new administrator') if mg then - lib.dbg('generating temporary password') - var tmppw: uint8[33] - lib.crypt.spray(&tmppw[0],32) tmppw[32] = 0 - for i=0,32 do - tmppw[i] = tmppw[i] % (10 + 26*2) - if tmppw[i] >= 36 then - tmppw[i] = tmppw[i] + (0x61 - 36) - elseif tmppw[i] >= 10 then - tmppw[i] = tmppw[i] + (0x41 - 10) - else tmppw[i] = tmppw[i] + 0x30 end - end - lib.dbg('assigning temporary password') - dlg:auth_create_pw(ruid, false, pstr { - ptr = [rawstring](&tmppw[0]), ct = 32 - }) - lib.report('temporary root pw: ', {[rawstring](&tmppw[0]), 32}) + var tmppw: int8[33] + pwset(dlg, &tmppw, ruid, false) + lib.report('temporary root pw: ', {&tmppw[0], 32}) end else goto cmderr end elseif lib.str.cmp(mode.arglist(0),'user') == 0 then + var umode: pbasic umode:parse(mode.arglist.ct, &mode.arglist(0)) + if umode.help then + [ lib.emit(false, 1, 'usage: ', `argv[0], ' user ', umode.type.helptxt.flags, ' […]', umode.type.helptxt.opts) ] + return 1 + end + if umode.arglist.ct >= 3 then + var grant = lib.str.cmp(umode.arglist(1),'grant') == 0 + var handle = umode.arglist(0) + var usr = dlg:actor_fetch_xid(pstr {ptr=handle, ct=lib.str.sz(handle)}) + if not usr then lib.bail('unknown handle') end + if grant or lib.str.cmp(umode.arglist(1),'revoke') == 0 then + var newprivs = usr.ptr.rights.powers + var map = array([lib.store.privmap]) + if umode.arglist.ct == 3 and lib.str.cmp(umode.arglist(2),'all') == 0 then + if grant + then newprivs:fill() + else newprivs:clear() + end + else + for i=2,umode.arglist.ct do + var priv = umode.arglist(i) + for j=0,[map.type.N] do + var p = map[j] + if p.name:cmp_raw(priv) then + if grant then + lib.dbg('enabling power ', {p.name.ptr,p.name.ct}) + newprivs = newprivs + p.priv + else + lib.dbg('disabling power ', {p.name.ptr,p.name.ct}) + newprivs = newprivs - p.priv + end + break + end + end + end + end + + usr.ptr.rights.powers = newprivs + dlg:actor_save_privs(usr.ptr) + elseif lib.str.cmp(umode.arglist(1),'auth') == 0 and umode.arglist.ct == 4 then + var reset = lib.str.cmp(umode.arglist(3),'reset') == 0 + if reset or lib.str.cmp(umode.arglist(3),'new') == 0 then + if lib.str.cmp(umode.arglist(2),'pw') == 0 then + var tmppw: int8[33] + pwset(dlg, &tmppw, usr.ptr.id, reset) + lib.report('new temporary password for ',usr.ptr.handle,': ', {&tmppw[0], 32}) + else lib.bail('unknown credential type') end + elseif lib.str.cmp(umode.arglist(3),'purge') == 0 then + else goto cmderr end + else goto cmderr end + else goto cmderr end elseif lib.str.cmp(mode.arglist(0),'actor') == 0 then elseif lib.str.cmp(mode.arglist(0),'tl') == 0 then elseif lib.str.cmp(mode.arglist(0),'serv') == 0 then else goto cmderr end end end do return 0 end - ::cmderr:: lib.bail('invalid command') return 2 + ::cmderr:: lib.bail('invalid command') end return entry_mgtool Index: parsav.md ================================================================== --- parsav.md +++ parsav.md @@ -51,10 +51,12 @@ tweets pgsql host=420.69.dread.cloud dbname=content the form the configuration string takes depends on the specific backend. 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 `, where `` 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 ` command, where `` 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. + +if something goes awry with your administrative account, don't fret! you can get your powers themselves back with the command `parsav user grant all`, and if you're having difficulties logging in, the command `parsav user 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. 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
`, 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) ### postgresql backend Index: route.t ================================================================== --- route.t +++ route.t @@ -131,16 +131,17 @@ end return end terra http.post_compose(co: &lib.srv.convo, meth: method.t) + if not co:assertpow('post') then return end + --if co.who.rights.powers.post() == false then + --co:complain(403,'insufficient privileges','you lack the post power and cannot perform this action') + if meth == method.get then lib.render.compose(co, nil) elseif meth == method.post then - if co.who.rights.powers.post() == false then - co:complain(401,'insufficient privileges','you lack the post power and cannot perform this action') return - end var text, textlen = co:postv("post") var acl, acllen = co:postv("acl") var subj, subjlen = co:postv("subject") if text == nil or acl == nil then co:complain(405, 'invalid post', 'every post must have at least body text and an ACL') Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -181,10 +181,20 @@ }) body.title:free() body.body:free() end + +convo.methods.assertpow = macro(function(self, pow) + return quote + var ok = true + if self.aid == 0 or self.who.rights.powers.[pow:asvalue()]() == false then + ok = false + self:complain(403,'insufficient privileges',['you lack the '..pow:asvalue()..' power and cannot perform this action']) + end + in ok end +end) struct convo.page { title: pstring body: pstring class: pstring Index: store.t ================================================================== --- store.t +++ store.t @@ -30,10 +30,19 @@ }; prepmode = lib.enum { 'full','conf','admin' } } + +m.privmap = {} +do local struct pt { name:lib.mem.ptr(int8), priv:m.powerset } +for k,v in pairs(m.powerset.members) do + m.privmap[#m.privmap + 1] = quote + var ps: m.powerset ps:clear() + (ps.[v] << true) + in pt {name = lib.str.plit(v), priv = ps} end +end end terra m.powerset:affect_users() return self.purge() or self.censor() or self.suspend() or self.elevate() or self.demote() or self.cred() end @@ -206,12 +215,12 @@ conf_get: {&m.source, rawstring} -> lib.mem.ptr(int8) conf_set: {&m.source, rawstring, rawstring} -> {} conf_reset: {&m.source, rawstring} -> {} - actor_save: {&m.source, &m.actor} -> bool actor_create: {&m.source, &m.actor} -> uint64 + actor_save_privs: {&m.source, &m.actor} -> {} actor_fetch_xid: {&m.source, lib.mem.ptr(int8)} -> lib.mem.ptr(m.actor) actor_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.actor) actor_notif_fetch_uid: {&m.source, uint64} -> lib.mem.ptr(m.notif) actor_enum: {&m.source} -> lib.mem.ptr(&m.actor) actor_enum_local: {&m.source} -> lib.mem.ptr(&m.actor)