Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zilveer/53856847246409986939b51043e37b7f to your computer and use it in GitHub Desktop.
Save zilveer/53856847246409986939b51043e37b7f 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
})
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment