Skip to content

Instantly share code, notes, and snippets.

@ThinkingJoules
Created April 25, 2019 15:29
Show Gist options
  • Save ThinkingJoules/6da18221dd7ec39058c1f9bfaff392e3 to your computer and use it in GitHub Desktop.
Save ThinkingJoules/6da18221dd7ec39058c1f9bfaff392e3 to your computer and use it in GitHub Desktop.
GunDB group permissions example. Restrict reads and/or writes.
//CLIENT
Gun.on('opt', function (ctx) {
if (ctx.once) {
return
}
this.to.next(ctx)
ctx.on('auth', function(msg){
let to = this.to
clientAuth(ctx)
function clientAuth(ctx){
let root = ctx.root
let msg = {}
msg.creds = Object.assign({},root.user.is)//add pubkey info to message
msg['#'] = Gun.text.random(9) //generate random msg ID
Gun.SEA.sign(msg['#'],root.opt.creds,function(sig){
/*
Sign this message ID.
this is the 'proof' it is us who sent this auth msg.
if we were sending data, then that should have been signed
*/
Gun.SEA.encrypt(sig,root.opt.pid,function(data){
/*
encrypt?
Just extra security.
Uses peerID as passphrase
If random, basically no one will know it (but the super peer)
-THIS ENCRYPTION METHOD IS NOT SECURE IF YOU REUSE YOUR PEERID, OR IF OTHERS KNOW IT
-SHOULD BE RANDOM (which is the GUN default)
*/
msg.authConn = data//add msg prop that we are looking for on superpeer
root.on('out',msg)//send wire message to super peer(s)
})
})
}
to.next(msg)
})
})
//SERVER
let authdConns = {} //our auth'd connection list
Gun.on('opt', function (ctx) {
if (ctx.once) {
return
}
this.to.next(ctx)
ctx.on('in', function (msg) {//any message on the 'in' wire
var to = this.to //deref 'this' so we can pass 'to' around
if(msg.authConn){//if this is an 'authorization message'
verifyClientConn(ctx,msg)
function verifyClientConn(ctx,msg){//inverse of clientAuth
let root = ctx.root
let ack = {'@':msg['#']} //create acknowledgement message
let{authConn,creds} = msg //pull data out of msg
let pid = msg._ && msg._.via && msg._.via.id || false
if(!pid){console.log('No PID'); return;}
Gun.SEA.decrypt(authConn,pid,function(data){
if(data){//decryption worked
Gun.SEA.verify(data,creds.pub,function(sig){//use decrypted data and pub key on msg
if(sig !== undefined && sig === msg['#']){//if the sig verified and it matches message ID
//success
authdConns[pid] = creds.pub //add PeerID to auth'd list so we can skip verifying all messages from them
root.on('in',ack) //acknowledge message (avoid 'err: No ack received')
//we do not pass msg to next (to.next(msg)) since we don't want to broadcast this to all peers (other clients)
}else{
ack.err = 'Could not verify signature'
root.on('in', ack) //ack with err, should log to console through gun.
//failure
}
})
}else{
console.log('decrypting failed')
}
})
}
}
if(msg._ && msg._.via && msg._.via.id){//any message that has an 'id' of which peer sent it.
verifyPermissions(ctx,msg,to)//see pseudo code below
}else{ // pass to next middleware, no verification
to.next(msg)
}
})
ctx.on('bye', function(msg){//remove auth'd peer from list
let to = this.to
clientLeft(msg)
to.next(msg)
})
})
//EXAMPLE VERIFY
/*
You can get a lot of messages on the 'in' wire.
So the goal is to fire `to.next(msg)` as quickly as possible.
Any extra calls/logic will impact performance so code carefully!
*/
//first figure out if the message really needs further verification
function verifyPermissions(ctx,msg,to){
if(msg.get && msg.get['#']){// get, only if it has a soul in it
verifyOp(ctx,msg,to,'get')
}else if (msg.put && Object.keys(msg.put).length){// put, only if it has a soul in it
verifyOp(ctx,msg,to,'put')
}else{//pass it along, no verification
to.next(msg)
}
}
//next level of verification, figure out if this soul is protected or not (up to your custom logic)
function verifyOp(ctx,msg,to,op){
let root = ctx.root
let pobj = {msg,to,op}
pobj.pub = false
pobj.verified = false
pobj.soul = (op === 'put') ? Object.keys(msg.put)[0] : msg.get['#']
pobj.prop = (op === 'put') ? msg.put[pobj.soul] : msg.get['.']
pobj.who = msg._ && msg._.via && msg._.via.id || false //is this from an outside peer?
let isProtected = isRestricted(pobj.soul,pobj.op)
function isRestricted(soul,op){
let getWhiteList = [/~/,/permissions/] //'~' is for gun.user() souls
/*
Will want to whitelist as many 'reads' as possible off of a pattern on your soul schema
1. Figure out if it is in your namespace whitelist
2. If not, figure out if current soul fits your schema (does it belong to your type of namespacing)
2. If so, figure out if it is in your namespace whitelist
3. Apply a default fallback for things outside of your namespace
*/
if(op === 'get'){
for (const r of getWhiteList) {
let p = r.test(soul)
if(p){//if on whitelist, it is not restricted
//This mean people who are not logged in can 'read'
return false
}
}
let isNameSpace = /\/t\d+/g.test(soul) //test a pattern that will match your soul schema
if(isNameSpace)return true//if not on whitelist, but part of namespace it is restricted
return false //default everything else to read w/o auth
}else{//puts
if(/~/.test(soul))return false //allow user puts
/*
Anything added here will act just like normal Gun
ANYONE can write to these souls.
*/
return true //default all other puts to needing permission
}
}
if(!isProtected){//no auth needed
//move message along without doing any more checks.
to.next(msg)
return
}
let authdPub = authdConns[pobj.who]//is this peerID auth'd and verified?
if(pobj.who && authdPub){
//console.log('Authd and verified Connection!')
pobj.verified = true
pobj.pub = authdConns[pobj.who]
testRequest(root,pobj,pobj.soul)
}else{//not logged in, could potentially have permissions?
testRequest(root,pobj,pobj.soul)
}
}
/*testRequest is going to be custom based on your soul schema
Below is some *pseudo code* to help get you started.
This will simply hardcode 'admins' as an array of pubkeys that have full read/write
*/
let permCache = {} //this is to cache reads, so we don't do extra network requests
let admins = ['pubKey1', 'pubKey2', 'pubKey3'] //who can override any permission settings
function testRequest(root, request){
let {pub,msg,to,verified,soul,prop,op} = request
//Most of the logic in this will be around doing dynamic 'groups' of pubkeys
if(soul.includes('isProtected')){
/*
The 'permissions' nodes are whitelisted, so we don't end up stacking up request to be verified
This entire thing hinges around how you do your soul schema so you can quickly and easily identify
who has access to what with minimal logic. If you want to do dynamic groups/etc it adds a lot of logic
to 'testRequst', but it can be done.
*/
getSoul(soul+'/permissions',false,function(val){//attempt to look for it in cache, else request it from gun
if(val){
let whichGroup = (op === 'put') ? val.write : val.read
/*
The permissions node is entirely up to how you want to build your permission system.
I did my entirely based on groups of pubkeys, and the ability that group has.
Can they read, can they write, or do both (yes, you can do write only with this method)
If you can read from disk directly (check in gun gitter...) then you can have a 'create' ability as well
For now assume 'val' is:
{read: 'Group1', write: 'Group1'} Pubkeys in [GroupName] have [key] ability.
Let's also assume the message came from someone that is an admin (pubkey is NOT in 'Group1')
*/
getSoul('someNameSpaceScheme/'+ whichGroup,false,function(grp){
if(grp && grp[pub]){
//if this is true, then send the message on!
to.next(msg)
}else{
isAdmin()
}
})
}else{
isAdmin()
}
})
}
function isAdmin(){
let err = 'PERMISSION DENIED'
if(!verified){//they are not logged in
console.log(err)
root.on('in',{'@': msg['#']||msg['@'], err: err})
return
}
if(admins.includes(pub) && verified){
//connection is auth'd and that pubkey is in the admin array
to.next(msg)
}else{
root.on('in',{'@': msg['#']||msg['@'], err: err})
}
}
function getSoul(soul,cb){
if(!(cb instanceof Function)) cb = function(){}
if(permCache[soul] !== undefined){//null if node does not exist, but has been queried and sub is set
//console.log('cache')
cb.call(this, permCache[soul])
}else{
//console.log('gun')
gun.get(soul).get(function(messg,eve){//check existence
eve.off()
cb.call(this,messg.put)
permCache[soul] = messg.put || null //non-undefined in case no data, but still falsy
})
gun.get(soul).on(function(data){//setup sub to keep cache accurate
permCache[soul] = data
})
}
}
}
@trokster
Copy link

trokster commented Mar 24, 2020

Just wanted to say: Thanks ! This was very helpful.

To limit cached data if one needs to cache more than just permissions :

let getSoul = (function () {

  let cached = {};
  const max_stack_size = 1000;

  return function (soul, cb) {

    if (!(cb instanceof Function)) cb = function () {};
    if (cached[soul]) { //null if node does not exist, but has been queried and sub is set
      cached[soul].called++;
      cb.call(this, cached[soul].item);
    } else {
      let ff = function (data) {
        cached[soul].item = data;
      };
      //console.log('gun')
      gun.get(soul).get(function (messg, eve) { //check existence
        eve.off();
        cached[soul] = {
          called: 0,
          change: ff,
          item: messg.put || null //non-undefined in case no data, but still falsy
        }
        cb.call(this, messg.put);

        // Do a random removes until we reach max_stack_size-5
        let keys = Object.keys(cached);
        if (keys.length > max_stack_size) {
          // keys.sort(function(a,b){return a.called > b.called ? -1 : a.called < b.called ? 1 : 0;});
          while (Object.keys(cached).length > (max_stack_size - 5)) {
            let key = Object.keys(cached)[Math.floor(Math.random(Object.keys.length))];
            gun.get(key).off(cached[key].change);
            delete(cached[key]);
          }
        }
      });

      gun.get(soul).on(ff);
    }
  }
})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment