SyncGroup permisions and lazy trip SGs
Change-Id: I8b31c18a88c29b465462dd413e5ad598476b7466
diff --git a/mocks/syncbase-wrapper.js b/mocks/syncbase-wrapper.js
index a8368a1..a3dfd6c 100644
--- a/mocks/syncbase-wrapper.js
+++ b/mocks/syncbase-wrapper.js
@@ -4,6 +4,8 @@
require('es6-shim');
+var verror = require('vanadium').verror;
+
var $ = require('../src/util/jquery');
var defineClass = require('../src/util/define-class');
@@ -185,30 +187,56 @@
syncGroup: function(sgAdmin, name) {
var repo = this.repo;
+ var sgp;
- var sgp = {
+ function sgFactory(spec) {
+ return function() {
+ var sg = new Set();
+ sg.prefixes = spec;
+ return sg;
+ };
+ }
+
+ function errNoExist() {
+ return new verror.NoExistError(null, 'SyncGroup does not exist.');
+ }
+
+ function getSg() {
+ return syncgroups[sgAdmin + '$' + name];
+ }
+
+ function joinSg(factory) {
+ var sgKey = sgAdmin + '$' + name;
+ var sg = syncgroups[sgKey];
+ if (!sg) {
+ sg = syncgroups[sgKey] = factory();
+ }
+ sg.add(repo);
+
+ return Promise.resolve(sgp);
+ }
+
+ sgp = {
buildSpec: function(prefixes) {
return prefixes;
},
+ changeSpec: function(){
+ return getSg()? Promise.resolve() : Promise.reject(errNoExist());
+ },
+
join: function() {
- var sgKey = sgAdmin + '$' + name;
- var sg = syncgroups[sgKey];
- sg.add(repo);
- return Promise.resolve(sgp);
+ return joinSg(function() {
+ throw errNoExist();
+ });
+ },
+
+ createOrJoin: function(spec) {
+ return joinSg(sgFactory(spec));
},
joinOrCreate: function(spec) {
- var sgKey = sgAdmin + '$' + name;
- var sg = syncgroups[sgKey];
- if (!sg) {
- sg = syncgroups[sgKey] = new Set();
- }
-
- sg.prefixes = spec;
- sg.add(repo);
-
- return Promise.resolve(sgp);
+ return joinSg(sgFactory(spec));
}
};
diff --git a/src/identity.js b/src/identity.js
index b13e25e..1c9c832 100644
--- a/src/identity.js
+++ b/src/identity.js
@@ -18,6 +18,10 @@
this.entityName = this.username + '/' + this.deviceName;
}
+Identity.blessingForUsername = function(username) {
+ return 'dev.v.io/u/' + username;
+};
+
function autoUsername() {
return uuid.v4();
}
diff --git a/src/invitation-manager.js b/src/invitation-manager.js
index 86399db..f6e4cb7 100644
--- a/src/invitation-manager.js
+++ b/src/invitation-manager.js
@@ -4,6 +4,8 @@
require('es6-shim');
+var verror = require('vanadium').verror;
+
var $ = require('./util/jquery');
var defineClass = require('./util/define-class');
var debug = require('./debug');
@@ -41,16 +43,23 @@
];
}
+function tripSgName(tripId) {
+ return 'trip-' + tripId;
+}
+
var InvitationManager = defineClass({
publics: {
invite: function(recipient, owner, tripId) {
var self = this;
- return this.groupManagerPromise.then(function(gm) {
- return gm.joinSyncGroup(recipient, 'invitations').then(function() {
- return gm.syncbaseWrapper.put(invitationKey(recipient, owner, tripId),
- self.username);
- });
+ return this.sgmPromise.then(function(sgm) {
+ return Promise.all([
+ self.addTripCollaborator(owner, tripId, recipient),
+ sgm.joinSyncGroup(recipient, 'invitations')
+ ]).then(function() {
+ return sgm.syncbaseWrapper.put(
+ invitationKey(recipient, owner, tripId), self.username);
+ });
});
},
@@ -62,6 +71,17 @@
privates: {
invitation: defineClass.innerClass({
publics: {
+ accept: function() {
+ return this.outer.joinTripSyncGroup(this.owner, this.tripId)
+ .then(this.delete);
+ },
+
+ decline: function() {
+ return this.delete();
+ }
+ },
+
+ privates: {
delete: function() {
var self = this;
@@ -87,12 +107,61 @@
}
}),
+ createTripSyncGroup: function(tripId, initialCollaborators) {
+ return this.sgmPromise.then(function(sgm) {
+ return sgm.createSyncGroup(tripSgName(tripId), [['trips', tripId]],
+ [sgm.identity.username].concat(initialCollaborators));
+ });
+ },
+
+ joinTripSyncGroup: function(owner, tripId) {
+ return this.sgmPromise.then(function(sgm) {
+ return sgm.joinSyncGroup(owner, tripSgName(tripId));
+ });
+ },
+
+ addTripCollaborator: function(owner, tripId, collaborator) {
+ var self = this;
+
+ return this.sgmPromise.then(function(sgm) {
+ return sgm.addCollaborator(owner, tripSgName(tripId), collaborator)
+ .catch(function(err) {
+ if (err instanceof verror.NoExistError &&
+ owner === self.username) {
+ return self.createTripSyncGroup(tripId, collaborator);
+ } else {
+ throw err;
+ }
+ });
+ });
+ },
+
+ manageTripSyncGroups: function(trips) {
+ var self = this;
+
+ //TODO(rosswang): maybe make this more intelligent, and handle ejection
+ if (trips) {
+ $.each(trips, function(tripId, trip) {
+ if (trip.owner) {
+ self.joinTripSyncGroup(trip.owner, tripId)
+ .catch(function(err) {
+ if (!(err instanceof verror.NoExistError)) {
+ throw err;
+ }
+ }).catch(self.onError);
+ }
+ });
+ }
+ },
+
processUpdates: function(data) {
var self = this;
- var toMe;
- if (data.invitations &&
- (toMe = data.invitations[escapeUsername(this.username)])) {
+ this.manageTripSyncGroups(data.trips);
+
+ var toMe = data.invitations &&
+ data.invitations[escapeUsername(this.username)];
+ if (toMe) {
$.each(toMe, function(owner, ownerRecords) {
var ownerInvites = self.invitations[owner];
if (!ownerInvites) {
@@ -150,25 +219,21 @@
onError: 'memory'
},
- init: function(usernamePromise, groupManagerPromise) {
+ init: function(sgmPromise) {
var self = this;
- this.syncbasePromise = groupManagerPromise.then(function(gm) {
- gm.syncbaseWrapper.onUpdate.add(self.processUpdates);
- return gm.syncbaseWrapper;
+ this.syncbasePromise = sgmPromise.then(function(sgm) {
+ self.username = sgm.identity.username;
+ sgm.syncbaseWrapper.onUpdate.add(self.processUpdates);
+ return sgm.syncbaseWrapper;
});
- this.groupManagerPromise = groupManagerPromise;
+ this.sgmPromise = sgmPromise;
this.invitations = {};
- usernamePromise.then(function(username) {
- //this will have been set prior to groupManagerPromise completing
- self.username = username;
- });
-
- groupManagerPromise.then(function(gm) {
- gm.createSyncGroup('invitations',
- [['invitations', escapeUsername(self.username)]])
+ sgmPromise.then(function(sgm) {
+ sgm.createSyncGroup('invitations',
+ [['invitations', escapeUsername(self.username)]], ['...'])
.catch(self.onError);
});
}
diff --git a/src/sync-util/trip-manager.js b/src/sync-util/trip-manager.js
index 91ed6f8..3549b86 100644
--- a/src/sync-util/trip-manager.js
+++ b/src/sync-util/trip-manager.js
@@ -21,17 +21,6 @@
},
publics: {
- createTripSyncGroup: function(groupManager, tripId) {
- return groupManager.createSyncGroup('trip-' + tripId,
- [['trips', tripId]]);
- },
-
- joinTripSyncGroup: function(owner, tripId) {
- return this.startSyncgroupManager.then(function(gm) {
- return gm.joinSyncGroup(owner, 'trip-' + tripId);
- });
- },
-
/**
* Sets the active trip to the given trip ID after it is available.
*/
@@ -95,8 +84,6 @@
processTrips: function(userTripMetadata, trips) {
var self = this;
- this.manageTripSyncGroups(trips);
-
var trip;
if (this.awaitedTripId) {
@@ -149,17 +136,10 @@
} else {
var tripId = this.activeTripId = uuid.v4();
debug.log('Creating new trip ' + tripId);
- trip = {}; //don't initialize owner until the syncgroup is ready
- this.startSyncgroupManager.then(function(gm) {
- return Promise.all([
- self.createTripSyncGroup(gm, tripId),
- self.usernamePromise
- ]).then(function(args) {
- return gm.syncbaseWrapper.put(['trips', tripId, 'owner'],
- args[1]).then(function() {
- return args[0];
- });
- })
+ trip = {};
+ this.startSyncgroupManager.then(function(sgm) {
+ return sgm.syncbaseWrapper.put(['trips', tripId, 'owner'],
+ sgm.identity.username)
.catch(self.onError);
});
}
@@ -203,29 +183,12 @@
isNascent: function(trip) {
return this.getTripLength(trip) <= 1;
- },
-
- manageTripSyncGroups: function(trips) {
- var self = this;
-
- //TODO(rosswang): maybe make this more intelligent, and handle ejection
- if (trips) {
- $.each(trips, function(tripId, trip) {
- /* Join is idempotent, but repeatedly joining might be causing major,
- * fatal sluggishness. TODO(rosswang): if this is not the case, maybe
- * go ahead and poll. */
- if (!self.joinedTrips.has(tripId) && trip.owner) {
- self.joinedTrips.add(tripId);
- self.joinTripSyncGroup(trip.owner, tripId).catch(self.onError);
- }
- });
- }
}
},
init: function(usernamePromise, deferredSyncbaseWrapper,
startSyncgroupManager) {
- this.usernamePromise = usernamePromise;
+ this.usernamePromise = usernamePromise;
this.sbw = deferredSyncbaseWrapper;
this.startSyncgroupManager = startSyncgroupManager;
this.joinedTrips = new Set();
diff --git a/src/syncgroup-manager.js b/src/syncgroup-manager.js
index e471537..f0562fd 100644
--- a/src/syncgroup-manager.js
+++ b/src/syncgroup-manager.js
@@ -7,21 +7,23 @@
var vanadium = require('vanadium');
var defineClass = require('./util/define-class');
+var Identity = require('./identity');
var naming = require('./naming');
var SyncgroupManager = defineClass({
publics: {
- createSyncGroup: function(name, prefixes) {
+ createSyncGroup: function(name, prefixes, initialCollaborators) {
var self = this;
- var sg = self.syncbaseWrapper.syncGroup(self.sgAdmin, name);
+ var sg = this.syncbaseWrapper.syncGroup(self.sgAdmin, name);
- var mgmt = vanadium.naming.join(self.mountNames.app, 'sgmt');
- var spec = sg.buildSpec(prefixes, [mgmt]);
+ var mgmt = vanadium.naming.join(this.mountNames.app, 'sgmt');
+ var spec = sg.buildSpec(prefixes, [mgmt], this.identity.account,
+ initialCollaborators.map(function(username) {
+ return username === '...'?
+ username : Identity.blessingForUsername(username);
+ }));
- /* TODO(rosswang): Right now, duplicate Syncbase creates on
- * different Syncbase instances results in siloed SyncGroups.
- * Revisit this logic once it merges properly. */
return sg.joinOrCreate(spec).then(function() {
// TODO(rosswang): this is a hack to make the SyncGroup joinable
return self.vanadiumWrapper.setPermissions(mgmt, new Map([
@@ -39,13 +41,26 @@
},
joinSyncGroup: function(owner, name) {
- var sg = this.syncbaseWrapper.syncGroup(
- vanadium.naming.join(naming.appMount(owner), 'sgadmin'), name);
- return sg.join();
+ return this.getForeignSyncGroup(owner, name).join();
+ },
+
+ addCollaborator: function(owner, sgName, username) {
+ var blessing = Identity.blessingForUsername(username);
+ return this.getForeignSyncGroup(owner, sgName)
+ .changeSpec(function(spec) {
+ ['Read', 'Write', 'Resolve'].forEach(function(perm) {
+ spec.perms.get(perm).in.push(blessing);
+ });
+ });
}
},
privates: {
+ getForeignSyncGroup: function(owner, name) {
+ return this.syncbaseWrapper.syncGroup(
+ vanadium.naming.join(naming.appMount(owner), 'sgadmin'), name);
+ },
+
advertise: function() {
var basicPerms = new Map([
['Admin', {in: [this.identity.account]}],
@@ -63,7 +78,7 @@
}
},
- constants: [ 'sgAdmin', 'syncbaseWrapper' ],
+ constants: [ 'identity', 'sgAdmin', 'syncbaseWrapper' ],
events: {
onError: 'memory'
diff --git a/src/travel.js b/src/travel.js
index facf963..8699200 100644
--- a/src/travel.js
+++ b/src/travel.js
@@ -341,16 +341,14 @@
});
message.$.find('a[name=accept]').click(function() {
- self.sync.joinTripSyncGroup(owner, tripId)
- .then(invitation.delete)
- .then(function() {
- self.sync.watchForTrip(tripId);
- return strings.invitationAccepted(sender, owner);
- }).then(async.resolve, async.reject);
+ invitation.accept().then(function() {
+ self.sync.watchForTrip(tripId);
+ return strings.invitationAccepted(sender, owner);
+ }).then(async.resolve, async.reject);
return false;
});
message.$.find('a[name=decline]').click(function() {
- invitation.delete().then(function() {
+ invitation.decline().then(function() {
return strings.invitationDeclined(sender, owner);
}).then(async.resolve, async.reject);
return false;
diff --git a/src/travelsync.js b/src/travelsync.js
index 9e0d635..3c00064 100644
--- a/src/travelsync.js
+++ b/src/travelsync.js
@@ -113,16 +113,17 @@
return gm;
},
- createPrimarySyncGroup: function(groupManager) {
+ createPrimarySyncGroup: function(syncgroupManager) {
var self = this;
this.status.userSyncGroup = 'creating';
- return groupManager.createSyncGroup('user', [[]])
+ return syncgroupManager.createSyncGroup('user', [[]],
+ [syncgroupManager.identity.username])
.then(function(sg) {
self.status.userSyncGroup = 'created';
return sg;
}, function(err) {
- self.status.usersSyncGroup = 'failed';
+ self.status.userSyncGroup = 'failed';
throw err;
});
}
@@ -208,8 +209,7 @@
};
});
- this.invitationManager = new InvitationManager(usernamePromise,
- this.startSyncgroupManager);
+ this.invitationManager = new InvitationManager(this.startSyncgroupManager);
this.invitationManager.onError.add(this.onError);
}
});
diff --git a/src/vanadium-wrapper/index.js b/src/vanadium-wrapper/index.js
index 06962dd..45b4c0d 100644
--- a/src/vanadium-wrapper/index.js
+++ b/src/vanadium-wrapper/index.js
@@ -57,7 +57,8 @@
return mount(true);
}
}, function(err) {
- if (err.id === 'v.io/v23/naming.nameDoesntExist') {
+ // TODO(rosswang): does this work?
+ if (err instanceof vanadiumDefault.naming.ErrNoSuchName) {
return mount(true);
} else {
throw err;
diff --git a/src/vanadium-wrapper/syncbase-wrapper.js b/src/vanadium-wrapper/syncbase-wrapper.js
index 643c471..972d290 100644
--- a/src/vanadium-wrapper/syncbase-wrapper.js
+++ b/src/vanadium-wrapper/syncbase-wrapper.js
@@ -7,6 +7,7 @@
var promisify = require('es6-promisify');
var syncbase = require('syncbase');
var vanadium = require('vanadium');
+var verror = vanadium.verror;
var defineClass = require('../util/define-class');
@@ -17,12 +18,10 @@
*/
function setUp(context, app, db) {
function nonfatals(err) {
- switch (err.id) {
- case 'v.io/v23/verror.Exist':
- console.info(err.msg);
- return;
- default:
- throw err;
+ if (err instanceof verror.ExistError) {
+ console.info(err.msg);
+ } else {
+ throw err;
}
}
@@ -91,7 +90,7 @@
publics: {
/**
- * @param seq a function executing the batch operations, receiving as its
+ * @param fn a function executing the batch operations, receiving as its
* `this` context and first parameter the batch operation methods
* (put, delete), each of which returns a promise. The callback must return
* the overarching promise.
@@ -204,12 +203,20 @@
});
var join = promisify(function(cb) {
- debug.log('Syncbase: join syncgroup ' + name);
sg.join(self.context, SG_MEMBER_INFO, chainable(cb));
});
- var setSpec = promisify(function(spec, cb) {
- sg.setSpec(self.context, spec, '', chainable(cb));
+ var getSpec = promisify(function(cb) {
+ sg.getSpec(self.context, function(err, spec, version) {
+ cb(err, {
+ spec: spec,
+ version: version
+ });
+ });
+ });
+
+ var setSpec = promisify(function(spec, version, cb) {
+ sg.setSpec(self.context, spec, version, chainable(cb));
});
/* Be explicit about arg lists because promisify is sensitive to extra
@@ -217,14 +224,14 @@
* they're made by promisify, wrap them in a fn that actually takes 0
* args. */
sgp = {
- buildSpec: function(prefixes, mountTables) {
+ buildSpec: function(prefixes, mountTables, admin, initialPermissions) {
return new syncbase.nosql.SyncGroupSpec({
perms: new Map([
- ['Admin', {in: ['...']}],
- ['Read', {in: ['...']}],
- ['Write', {in: ['...']}],
- ['Resolve', {in: ['...']}],
- ['Debug', {in: ['...']}]
+ ['Admin', {in: [admin]}],
+ ['Read', {in: initialPermissions}],
+ ['Write', {in: initialPermissions}],
+ ['Resolve', {in: initialPermissions}],
+ ['Debug', {in: [admin]}]
]),
prefixes: prefixes.map(function(p) { return 't:' + joinKey(p); }),
mountTables: mountTables
@@ -234,17 +241,28 @@
create: function(spec) { return create(spec); },
destroy: function() { return destroy(); },
join: function() { return join(); },
- setSpec: function(spec) { return setSpec(spec); },
+ getSpec: function() { return getSpec(); },
+ setSpec: function(spec, version) { return setSpec(spec, version); },
+ changeSpec: function(fn) {
+ return sgp.getSpec().then(function(versionedSpec) {
+ var spec = versionedSpec.spec;
+ return sgp.setSpec(fn(spec) || spec, versionedSpec.version)
+ .catch(function(err) {
+ if (err instanceof verror.VersionError) {
+ return sgp.changeSpec(fn);
+ } else {
+ throw err;
+ }
+ });
+ });
+ },
createOrJoin: function(spec) {
return sgp.create(spec)
.catch(function(err) {
- if (err.id === 'v.io/v23/verror.Exist') {
+ if (err instanceof verror.ExistError) {
debug.log('Syncbase: syncgroup ' + name + ' already exists.');
- return sgp.join()
- .then(function() {
- return sgp.setSpec(spec);
- });
+ return sgp.join();
} else {
throw err;
}
@@ -253,10 +271,8 @@
joinOrCreate: function(spec) {
return sgp.join()
- .then(function() {
- return sgp.setSpec(spec);
- }, function(err) {
- if (err.id === 'v.io/v23/verror.NoExist') {
+ .catch(function(err) {
+ if (err instanceof verror.NoExistError) {
debug.log('Syncbase: syncgroup ' + name + ' does not exist.');
return sgp.createOrJoin(spec);
} else {
@@ -349,7 +365,7 @@
}
}).on('error', reject);
}).catch(function(err) {
- if (err.id === 'v.io/v23/verror.Internal') {
+ if (err instanceof verror.InternalError) {
console.error(err);
} else {
throw err;