Created
April 25, 2019 15:29
-
-
Save ThinkingJoules/6da18221dd7ec39058c1f9bfaff392e3 to your computer and use it in GitHub Desktop.
GunDB group permissions example. Restrict reads and/or writes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 | |
}) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 () {