diff --git a/client/components/import/import.controller.js b/client/components/import/import.controller.js index f031ec9..42b78b4 100644 --- a/client/components/import/import.controller.js +++ b/client/components/import/import.controller.js @@ -1,36 +1,36 @@ 'use strict'; angular.module('manticoreApp') .controller('ImportCtrl', function ($scope, $rootScope, FileUploader, Auth) { var uploader = new FileUploader({ - url: '/api/documents/upload', + url: '/api/documents/', headers: { 'Authorization': 'Bearer ' + Auth.getToken() }, removeAfterUpload: false, autoUpload: true, onCompleteAll: function () { // Wait a little before firing this event, as the upload may not // be accessible from MongoDB immediately window.setTimeout(function () { $rootScope.$broadcast('documentsUploaded'); uploader.clearQueue(); }, 1000); } }); uploader.filters.push({ name: 'sizeFilter', fn: function (item) { return item.size <= 10485760; // 10 Megabytes } }); uploader.filters.push({ name: 'typeFilter', fn: function (item) { return item.type === 'application/vnd.oasis.opendocument.text'; } }); $scope.uploader = uploader; }); diff --git a/client/components/wodo/adaptor.service.js b/client/components/wodo/adaptor.service.js index 63c5bcb..9dc207f 100644 --- a/client/components/wodo/adaptor.service.js +++ b/client/components/wodo/adaptor.service.js @@ -1,298 +1,298 @@ /*jslint unparam: true*/ /*global runtime, core, ops, io*/ 'use strict'; angular.module('manticoreApp') .factory('Adaptor', function () { var OperationRouter = function (socket, odfContainer, errorCb) { var EVENT_BEFORESAVETOFILE = 'beforeSaveToFile', EVENT_SAVEDTOFILE = 'savedToFile', EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED = 'hasLocalUnsyncedOperationsChanged', EVENT_HASSESSIONHOSTCONNECTIONCHANGED = 'hasSessionHostConnectionChanged', EVENT_MEMBERADDED = 'memberAdded', EVENT_MEMBERCHANGED = 'memberChanged', EVENT_MEMBERREMOVED = 'memberRemoved', eventNotifier = new core.EventNotifier([ EVENT_BEFORESAVETOFILE, EVENT_SAVEDTOFILE, EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, EVENT_HASSESSIONHOSTCONNECTIONCHANGED, EVENT_MEMBERADDED, EVENT_MEMBERCHANGED, EVENT_MEMBERREMOVED, ops.OperationRouter.signalProcessingBatchStart, ops.OperationRouter.signalProcessingBatchEnd ]), operationFactory, playbackFunction, lastServerSyncHeadId = 0, sendClientOpspecsLock = false, sendClientOpspecsTask, hasSessionHostConnection = true, unplayedServerOpSpecQueue = [], unsyncedClientOpSpecQueue = [], operationTransformer = new ops.OperationTransformer(), /**@const*/sendClientOpspecsDelay = 300; function playbackOpspecs(opspecs) { var op, i; if (!opspecs.length) { return; } eventNotifier.emit(ops.OperationRouter.signalProcessingBatchStart, {}); for (i = 0; i < opspecs.length; i += 1) { op = operationFactory.create(opspecs[i]); if (op !== null) { if (!playbackFunction(op)) { eventNotifier.emit(ops.OperationRouter.signalProcessingBatchEnd, {}); errorCb('opExecutionFailure'); return; } } else { eventNotifier.emit(ops.OperationRouter.signalProcessingBatchEnd, {}); errorCb('Unknown opspec: ' + runtime.toJson(opspecs[i])); return; } } eventNotifier.emit(ops.OperationRouter.signalProcessingBatchEnd, {}); } function handleNewServerOpsWithUnsyncedClientOps(serverOps) { var transformResult = operationTransformer.transform(unsyncedClientOpSpecQueue, serverOps); if (!transformResult) { errorCb('Has unresolvable conflict.'); return false; } unsyncedClientOpSpecQueue = transformResult.opSpecsA; unplayedServerOpSpecQueue = unplayedServerOpSpecQueue.concat(transformResult.opSpecsB); return true; } function handleNewClientOpsWithUnplayedServerOps(clientOps) { var transformResult = operationTransformer.transform(clientOps, unplayedServerOpSpecQueue); if (!transformResult) { errorCb('Has unresolvable conflict.'); return false; } unsyncedClientOpSpecQueue = unsyncedClientOpSpecQueue.concat(transformResult.opSpecsA); unplayedServerOpSpecQueue = transformResult.opSpecsB; return true; } function receiveServerOpspecs(headId, serverOpspecs) { if (unsyncedClientOpSpecQueue.length > 0) { handleNewServerOpsWithUnsyncedClientOps(serverOpspecs); // could happen that ops from server make client ops obsolete if (unsyncedClientOpSpecQueue.length === 0) { eventNotifier.emit(EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, false); } } else { // apply directly playbackOpspecs(serverOpspecs); } lastServerSyncHeadId = headId; } function sendClientOpspecs() { var originalUnsyncedLength = unsyncedClientOpSpecQueue.length; if (originalUnsyncedLength) { sendClientOpspecsLock = true; socket.emit('commit_ops', { head: lastServerSyncHeadId, ops: unsyncedClientOpSpecQueue }, function (response) { if (response.conflict === true) { sendClientOpspecs(); } else { lastServerSyncHeadId = response.head; // on success no other server ops should have sneaked in meanwhile, so no need to check // got no other client ops meanwhile? if (unsyncedClientOpSpecQueue.length === originalUnsyncedLength) { unsyncedClientOpSpecQueue.length = 0; // finally apply all server ops collected while waiting for sync playbackOpspecs(unplayedServerOpSpecQueue); unplayedServerOpSpecQueue.length = 0; eventNotifier.emit(EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, false); sendClientOpspecsLock = false; } else { // send off the new client ops directly unsyncedClientOpSpecQueue.splice(0, originalUnsyncedLength); sendClientOpspecs(); } } }); } } this.setOperationFactory = function (f) { operationFactory = f; }; this.setPlaybackFunction = function (f) { playbackFunction = f; }; this.push = function (operations) { var clientOpspecs = [], now = Date.now(), hasLocalUnsyncedOpsBefore = (unsyncedClientOpSpecQueue.length !== 0), hasLocalUnsyncedOpsNow; operations.forEach(function(op) { var opspec = op.spec(); opspec.timestamp = now; clientOpspecs.push(opspec); }); playbackOpspecs(clientOpspecs); if (unplayedServerOpSpecQueue.length > 0) { handleNewClientOpsWithUnplayedServerOps(clientOpspecs); } else { unsyncedClientOpSpecQueue = unsyncedClientOpSpecQueue.concat(clientOpspecs); } hasLocalUnsyncedOpsNow = (unsyncedClientOpSpecQueue.length !== 0); if (hasLocalUnsyncedOpsNow !== hasLocalUnsyncedOpsBefore) { eventNotifier.emit(EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, hasLocalUnsyncedOpsNow); } sendClientOpspecsTask.trigger(); }; this.requestReplay = function (cb) { var cbOnce = function () { eventNotifier.unsubscribe(ops.OperationRouter.signalProcessingBatchEnd, cbOnce); cb(); }; // hack: relies on at least addmember op being added for ourselves and being executed eventNotifier.subscribe(ops.OperationRouter.signalProcessingBatchEnd, cbOnce); socket.emit('replay', {}); }; this.close = function (cb) { cb(); }; this.subscribe = function (eventId, cb) { eventNotifier.subscribe(eventId, cb); }; this.unsubscribe = function (eventId, cb) { eventNotifier.unsubscribe(eventId, cb); }; this.hasLocalUnsyncedOps = function () { return unsyncedClientOpSpecQueue.length !== 0; }; this.hasSessionHostConnection = function () { return hasSessionHostConnection; }; function init() { var replayed = false, followupHead, followupOps = []; sendClientOpspecsTask = core.Task.createTimeoutTask(function () { if (!sendClientOpspecsLock) { sendClientOpspecs(); } }, sendClientOpspecsDelay); socket.on('replay', function (data) { receiveServerOpspecs(data.head, data.ops); replayed = true; if (followupHead && followupHead > data.head) { receiveServerOpspecs(followupHead, followupOps); } }); socket.on('new_ops', function (data) { if (replayed) { receiveServerOpspecs(data.head, data.ops); } else { followupHead = data.head; followupOps = followupOps.concat(data.ops); } }); } init(); }; var ClientAdaptor = function (documentId, authToken, connectedCb, kickedCb, disconnectedCb) { var self = this, memberId, genesisUrl, socket; this.getMemberId = function () { return memberId; }; this.getGenesisUrl = function () { return genesisUrl; }; this.createOperationRouter = function (odfContainer, errorCb) { runtime.assert(Boolean(memberId), 'You must be connected to a session before creating an operation router'); return new OperationRouter(socket, odfContainer, errorCb); }; this.joinSession = function (cb) { socket.on('join_success', function handleJoinSuccess(data) { socket.removeListener('join_success', handleJoinSuccess); memberId = data.memberId; genesisUrl = data.genesisUrl; - cb(memberId); + cb(memberId, data.permission); }); socket.emit('join', { documentId: documentId }); }; this.leaveSession = function (cb) { socket.emit('leave', {}, cb); socket.removeAllListeners(); }; this.getSocket = function () { return socket; }; this.destroy = function () { socket.disconnect(); }; function init() { socket = io({ query: 'token=' + authToken, forceNew: true }); socket.on('connect', connectedCb); socket.on('kick', kickedCb); socket.on('disconnect', disconnectedCb); } init(); }; return ClientAdaptor; }); diff --git a/client/components/wodo/editor.controller.js b/client/components/wodo/editor.controller.js index 9b90b1a..42c0d0e 100644 --- a/client/components/wodo/editor.controller.js +++ b/client/components/wodo/editor.controller.js @@ -1,92 +1,101 @@ 'use strict'; /*global Wodo*/ angular.module('manticoreApp') .controller('WodoCtrl', function ($scope, Auth, Adaptor) { var editorInstance, clientAdaptor, editorOptions = { collabEditingEnabled: true, - allFeaturesEnabled: true + unstableFeaturesEnabled: true, + imageEditingEnabled: false, + hyperlinkEditingEnabled: false }, onConnectCalled = false; function closeEditing() { editorInstance.leaveSession(function () { $scope.$apply(function () { $scope.joined = false; }); clientAdaptor.leaveSession(function () { console.log('Closed editing, left session.'); }); }); } function handleEditingError(error) { alert('Something went wrong!\n' + error); console.log(error); closeEditing(); } - function openEditor() { + function openEditor(permission) { + if (permission === 'write') { + editorOptions.allFeaturesEnabled = true; + editorOptions.reviewModeEnabled = false; + } else { + editorOptions.reviewModeEnabled = true; + } + Wodo.createCollabTextEditor('wodoContainer', editorOptions, function (err, editor) { editorInstance = editor; $scope.editor = editor; editorInstance.clientAdaptor = clientAdaptor; editorInstance.addEventListener(Wodo.EVENT_UNKNOWNERROR, handleEditingError); editorInstance.joinSession(clientAdaptor, function () { $scope.$apply(function () { $scope.joined = true; }); }); }); } function boot() { clientAdaptor = new Adaptor( $scope.document._id, Auth.getToken(), function onConnect() { console.log('onConnect'); if (onConnectCalled) { console.log('Reconnecting not yet supported'); return; } onConnectCalled = true; - clientAdaptor.joinSession(function (memberId) { + clientAdaptor.joinSession(function (memberId, permission) { if (!memberId) { console.log('Could not join; memberId not received'); } else { console.log('Joined with memberId ' + memberId); - openEditor(); + openEditor(permission); } }); }, function onKick() { console.log('onKick'); closeEditing(); }, function onDisconnect() { console.log('onDisconnect'); } ); } function destroy (cb) { if (editorInstance) { closeEditing(); editorInstance.destroy(cb); } else { if (clientAdaptor) { clientAdaptor.leaveSession(); clientAdaptor.destroy(); cb(); } } } this.boot = boot; this.destroy = destroy; }); diff --git a/package.json b/package.json index 0cc58af..309705b 100644 --- a/package.json +++ b/package.json @@ -1,100 +1,100 @@ { "name": "manticore", "version": "0.0.0", "main": "server/app.js", "dependencies": { "express": "~4.0.0", "morgan": "~1.0.0", "body-parser": "~1.5.0", "method-override": "~1.0.0", "serve-favicon": "~2.0.1", "cookie-parser": "~1.0.1", "express-session": "~1.0.2", "errorhandler": "~1.0.0", "compression": "~1.0.1", "lodash": "~2.4.1", "jade": "~1.2.0", - "mongoose": "~3.8.8", + "mongoose": "~4.1.12", "gridfs-stream": "1.1.1", - "jsonwebtoken": "^0.3.0", - "express-jwt": "^0.1.3", + "jsonwebtoken": "~5.4.0", + "express-jwt": "~3.1.0", "passport": "~0.2.0", "passport-local": "~0.1.6", "composable-middleware": "^0.3.0", "connect-mongo": "^0.4.1", "multer": "0.1.8", "socket.io": "1.3.5", - "socketio-jwt": "4.2.0", + "socketio-jwt": "~4.3.2", "async": "~1.3.0", "phantom": "~0.7.2", "dav": "~1.7.6", "request": "~2.60.0", "ldapjs": "mcavage/node-ldapjs", "passport-ldapauth": "0.3.0" }, "devDependencies": { - "grunt": "~0.4.4", + "grunt": "~0.4.5", "grunt-autoprefixer": "~0.7.2", "grunt-wiredep": "~1.8.0", "grunt-concurrent": "~0.5.0", "grunt-contrib-clean": "~0.5.0", "grunt-contrib-concat": "~0.4.0", "grunt-contrib-copy": "~0.5.0", "grunt-contrib-cssmin": "~0.9.0", "grunt-contrib-htmlmin": "~0.2.0", "grunt-contrib-imagemin": "~0.7.1", "grunt-contrib-jshint": "~0.10.0", "grunt-contrib-uglify": "~0.4.0", "grunt-contrib-watch": "~0.6.1", "grunt-contrib-jade": "^0.11.0", "grunt-google-cdn": "~0.4.0", "grunt-newer": "~0.7.0", "grunt-ng-annotate": "^0.2.3", "grunt-filerev": "~2.2.0", "grunt-svgmin": "~0.4.0", "grunt-usemin": "~3.0.0", "grunt-env": "~0.4.1", "grunt-node-inspector": "~0.1.5", "grunt-nodemon": "~0.2.0", "grunt-angular-templates": "^0.5.4", "grunt-dom-munger": "^3.4.0", "grunt-protractor-runner": "^1.1.0", "grunt-asset-injector": "^0.1.0", "grunt-karma": "~0.8.2", "grunt-build-control": "DaftMonk/grunt-build-control", "grunt-mocha-test": "~0.10.2", "grunt-contrib-stylus": "latest", "jit-grunt": "^0.5.0", "time-grunt": "~0.3.1", "grunt-express-server": "~0.4.17", "grunt-open": "~0.2.3", "open": "~0.0.4", "jshint-stylish": "~0.1.5", "connect-livereload": "~0.4.0", "karma-ng-scenario": "~0.1.0", "karma-firefox-launcher": "~0.1.3", "karma-script-launcher": "~0.1.0", "karma-html2js-preprocessor": "~0.1.0", "karma-ng-jade2js-preprocessor": "^0.1.2", "karma-jasmine": "~0.1.5", "karma-chrome-launcher": "~0.1.3", "requirejs": "~2.1.11", "karma-requirejs": "~0.2.1", "karma-coffee-preprocessor": "~0.2.1", "karma-jade-preprocessor": "0.0.11", "karma-phantomjs-launcher": "~0.1.4", "karma": "~0.12.9", "karma-ng-html2js-preprocessor": "~0.1.0", "supertest": "~0.11.0", "should": "~3.3.1" }, "engines": { "node": ">=0.10.0" }, "scripts": { "start": "node server/app.js", "test": "grunt test", "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" }, "private": true } diff --git a/server/api/document/document.controller.js b/server/api/document/document.controller.js index 45b003b..eafda75 100644 --- a/server/api/document/document.controller.js +++ b/server/api/document/document.controller.js @@ -1,89 +1,110 @@ /** * Using Rails-like standard naming convention for endpoints. * GET /documents -> index * POST /documents -> create * GET /documents/:id -> show * PUT /documents/:id -> update * DELETE /documents/:id -> destroy */ 'use strict'; var _ = require('lodash'); var mongoose = require('mongoose'); var Grid = require('gridfs-stream'); var multer = require('multer'); var storage = require('./storage'); var DocumentChunk = require('./document.model').DocumentChunk; var Document = require('./document.model').Document; var Template = require('../template/template.model'); var gfs = Grid(mongoose.connection.db, mongoose.mongo); // Get list of documents exports.index = storage.index; // Get a single document exports.show = storage.show; // Middleware for handling file uploads exports.upload = storage.upload; exports.acknowledgeUpload = function (req, res) { - return res.send(200); + return res.send(201); }; exports.showSnapshot = function(req, res) { var snapshotId = req.params.id; gfs.findOne({_id: snapshotId}, function (err, file) { if (err) { return handleError(res, err); } if (!file) { return res.send(404); } var download = gfs.createReadStream({ _id: snapshotId }); download.on('error', function (err) { return handleError(res, err); }); res.set('Content-Type', file.contentType); res.attachment(file.filename) download.pipe(res); }); }; exports.createFromTemplate = storage.createFromTemplate; exports.createChunkFromSnapshot = storage.createChunkFromSnapshot; -// Updates an existing document in the DB. -exports.update = function(req, res) { - if(req.body._id) { delete req.body._id; } +exports.overwrite = storage.overwrite || function (req, res) { return res.send(405); }; + +exports.getAccess = function (req, res) { Document.findById(req.params.id, function (err, document) { if (err) { return handleError(res, err); } if(!document) { return res.send(404); } - var updated = _.merge(document, req.body); - updated.save(function (err) { - if (err) { return handleError(res, err); } - return res.json(200, document); - }); + var objectCache = req.app.get('objectCache'), + access = (objectCache.isTracked(document) ? + objectCache.getTrackedObject(document).access : document.access); + + return res.json(200, access); + }); +} + +// Updates an existing access list in the DB. +exports.updateAccess = function(req, res) { + Document.findById(req.params.id, function (err, document) { + if (err) { return handleError(res, err); } + if(!document) { return res.send(404); } + var objectCache = req.app.get('objectCache'); + if (objectCache.isTracked(document)) { + var trackedDoc = objectCache.getTrackedObject(document); + trackedDoc.access = req.body; + trackedDoc.markModified('access'); + return res.send(200); + } else { + document.access = req.body; + document.save(function (err) { + if (err) { return handleError(res, err); } + return res.send(200); + }); + } }); }; // Deletes a document from the DB. exports.destroy = function(req, res) { Document.findById(req.params.id, function (err, document) { if(err) { return handleError(res, err); } if(!document) { return res.send(404); } document.remove(function(err) { if(err) { return handleError(res, err); } return res.send(204); }); }); }; function handleError(res, err) { return res.send(500, err); } diff --git a/server/api/document/document.model.js b/server/api/document/document.model.js index 197ca07..762ed4c 100644 --- a/server/api/document/document.model.js +++ b/server/api/document/document.model.js @@ -1,39 +1,62 @@ 'use strict'; -var mongoose = require('mongoose'), +var _ = require('lodash'), + mongoose = require('mongoose'), Schema = mongoose.Schema; -var storageTypes = ['webdav']; +var config = require('../../config/environment'); + +var storageTypes = ['webdav', 'chwala']; /* * Each DocumentChunk has an associated ODF snapshot file within * GridFS, a list of operations required to bring the snapshot into * a workable initial state for the chunk, and a list of operations * that signifies the edit history after the aforementioned document * state */ var DocumentChunk = new Schema({ sequence: { type: Number, default: 0 }, snapshot: { fileId: { type: Schema.Types.ObjectId, required: true }, operations: { type: Array, default: [] } }, operations: { type: Array, default: [] } }); var DocumentSchema = new Schema({ + _id: { type: Schema.Types.String, default: mongoose.Types.ObjectId, unique: true }, title: String, created: { type: Date, default: Date.now, required: true }, date: { type: Date, default: Date.now }, creator: { type: Schema.Types.ObjectId, ref: 'User' }, editors: { type: [{type: Schema.Types.ObjectId, ref: 'User'}], default: [] }, chunks: { type: [{type: Schema.Types.ObjectId, ref: 'DocumentChunk'}], default: [] }, live: { type: Boolean, default: false }, provider: String, - webdav: {} + webdav: {}, + chwala: {}, + access: { type: [{ identity: String, permission: { type: String, enum: ['read', 'write', 'deny' ]}}], default: [] } }); +DocumentSchema.methods = { + /** Potentially unsafe */ + getAccessType: function (identity) { + return _.result(_.find(this.access, function (accessItem) { + return _.isEqual(accessItem.identity, identity); + }), 'permission') || config.defaultAccess || 'deny'; + } +}; + +// DocumentSchema +// .path('access') +// .validate(function(access) { +// return _.find(this.access, function (accessItem) { +// return _.includes(['read, write', 'deny']permission !== 'read' +// }); +// }, 'Invalid access control info'); + module.exports = { DocumentChunk: mongoose.model('DocumentChunk', DocumentChunk), Document: mongoose.model('Document', DocumentSchema) }; diff --git a/server/api/document/index.js b/server/api/document/index.js index 2e4c75a..2db2299 100644 --- a/server/api/document/index.js +++ b/server/api/document/index.js @@ -1,20 +1,21 @@ 'use strict'; var express = require('express'); var controller = require('./document.controller'); var auth = require('../../auth/auth.service'); var router = express.Router(); router.get('/', auth.isAuthenticated(), controller.index); router.get('/:id', auth.isAuthenticated(), controller.show); router.get('/snapshot/:id', controller.showSnapshot) router.get('/fromTemplate/:id', auth.isAuthenticated(), controller.createFromTemplate); -router.post('/upload', auth.isAuthenticated(), controller.upload, +router.post('/', auth.isAuthenticated(), controller.upload, controller.acknowledgeUpload); -router.put('/:id', auth.isAuthenticated(), controller.update); -router.patch('/:id', auth.isAuthenticated(), controller.update); +router.put('/:id', auth.isAuthenticated(), controller.overwrite); +router.get('/:id/access', auth.isAuthenticated(), controller.getAccess); +router.put('/:id/access', auth.isAuthenticated(), controller.updateAccess); router.delete('/:id', auth.isAuthenticated(), controller.destroy); module.exports = router; diff --git a/server/api/document/storage/chwala/index.js b/server/api/document/storage/chwala/index.js new file mode 100644 index 0000000..1a6af58 --- /dev/null +++ b/server/api/document/storage/chwala/index.js @@ -0,0 +1,209 @@ +'use strict'; + +var _ = require('lodash'); +var async = require('async'); +var mongoose = require('mongoose'); +var https = require('https'); +var fs = require('fs'); +var url = require('url'); +var path = require('path'); +var Grid = require('gridfs-stream'); +var multer = require('multer'); +var request = require('request'); +var querystring = require('querystring'); +var crypto = require('crypto'); + +var config = require('../../../../config/environment'); + +var User = require('../../../user/user.model'); +var Document = require('../../document.model').Document; +var DocumentChunk = require('../../document.model').DocumentChunk; +var Template = require('../../../template/template.model'); + +var gfs = Grid(mongoose.connection.db, mongoose.mongo); + +var serverUrl = config.storage.chwala.server; + +function decrypt(password) { + var decipher = crypto.createDecipher('aes-256-cbc', config.auth.ldap.key); + return decipher.update(password, 'base64', 'utf8') + decipher.final('utf8'); +} + +function downloadToGridFS(user, uuid, fileId, cb) { + var file = gfs.createWriteStream({ + _id: fileId, + filename: uuid, + mode: 'w', + chunkSize: 1024 * 4, + content_type: 'application/vnd.oasis.opendocument.text', + root: 'fs' + }); + + request.get({ + url: serverUrl + '/' + uuid, + auth: { + user: user.ldap.username, + pass: decrypt(user.ldap.password) + } + }) + .on('error', function (err) { + cb(err); + }) + .pipe(file); + + file.on('finish', cb); +} + +function uploadToServer(user, uuid, readStream, cb) { + readStream.pipe(request.put({ + url: serverUrl + '/' + uuid, + auth: { + user: user.ldap.username, + pass: decrypt(user.ldap.password) + }, + headers: { + 'Content-Type': 'application/vnd.oasis.opendocument.text' + } + }, cb)); +} + +function createFirstChunk(user, uuid, cb) { + var chunkId = new mongoose.Types.ObjectId(), + fileId = new mongoose.Types.ObjectId(); + + var firstChunk = new DocumentChunk({ + _id: chunkId, + snapshot: { + fileId: fileId + } + }); + + downloadToGridFS(user, uuid, fileId, function(err) { + if (err) { return cb(err); } + firstChunk.save(function (err) { + cb(err, firstChunk); + }); + }); +} + +exports.index = function (req, res) { + var userId = req.user._id; + + Document.find({ + '$or': [ + { 'creator': userId }, + { 'editors': { '$in': [userId] } } + ] + }) + .populate('creator', 'name email') + .populate('editors', 'name email') + .exec(function (err, documents) { + if(err) { return handleError(res, err); } + return res.json(200, documents); + }); +}; + +exports.show = function(req, res) { + Document.findById(req.params.id) + .populate('creator', 'name email') + .populate('editors', 'name email') + .exec(function (err, document) { + if(err) { return handleError(res, err); } + if(!document) { return res.send(404); } + return res.json(document); + }); +}; + +exports.createFromTemplate = function (req, res) { + return res.send(405); +}; + +exports.upload = function (req, res, next) { + var id = req.body.id, + title = req.body.title, + access = req.body.access; + + Document.findById(id, function (err, document) { + if (err) { return handleError(res, err); } + if (document) { return res.json(422, document); } + + createFirstChunk(req.user, id, function (err, firstChunk) { + if (err) { return handleError(res, err); } + Document.create({ + _id: id, + title: title, + creator: req.user._id, + chunks: [firstChunk._id], + access: access + }, function (err, document) { + if (err) { return handleError(res, err); } + next(); + }); + }); + }); +}; + +exports.overwrite = function (req, res) { + Document.findById(req.params.id, function (err, document) { + if (err) { return handleError(res, err); } + if (!document) { return res.json(404, document); } + + createFirstChunk(req.user, req.params.id, function (err, newFirstChunk) { + if (err) { return handleError(res, err); } + document.date = Date.now(); + document.chunks = [newFirstChunk] + document.markModified('chunks'); + document.save(function (err, document) { + if (err) { return handleError(res, err); } + return res.send(200); + }); + }); + }); +} + +exports.createChunkFromSnapshot = function (document, snapshot, cb) { + var chunkId = new mongoose.Types.ObjectId(), + fileId = new mongoose.Types.ObjectId(); + + var writeStream = gfs.createWriteStream({ + _id: fileId, + filename: document.title + '_' + Date.now(), + mode: 'w', + chunkSize: 1024 * 4, + content_type: 'application/vnd.oasis.opendocument.text', + root: 'fs' + }); + + writeStream.end(new Buffer(snapshot.data), function () { + User.findById(document.creator._id, function (err, user) { + if (err) { return cb(err); } + uploadToServer( + user, + document._id, + gfs.createReadStream({ _id: fileId }), + true, + function (err, response) { + if (err) { return cb(err); } + var chunk = new DocumentChunk({ + _id: chunkId, + sequence: snapshot.sequence, + snapshot: { + fileId: fileId, + operations: snapshot.operations + } + }); + chunk.save(function (err) { + if (err) { return cb(err); } + document.chunks.push(chunkId); + document.markModified('chunks'); + cb(null, chunk); + }); + }); + }); + }); +}; + +function handleError(res, err) { + console.log(err); + return res.send(500, err); +} diff --git a/server/api/user/user.model.js b/server/api/user/user.model.js index bd8a72e..c3955c4 100644 --- a/server/api/user/user.model.js +++ b/server/api/user/user.model.js @@ -1,146 +1,147 @@ 'use strict'; var mongoose = require('mongoose'); var Schema = mongoose.Schema; var crypto = require('crypto'); var authTypes = ['webdav', 'ldap']; var UserSchema = new Schema({ name: String, email: { type: String, lowercase: true }, role: { type: String, default: 'user' }, hashedPassword: String, provider: String, webdav: {}, + ldap: {}, salt: String }); /** * Virtuals */ UserSchema .virtual('password') .set(function(password) { this._password = password; this.salt = this.makeSalt(); this.hashedPassword = this.encryptPassword(password); }) .get(function() { return this._password; }); // Public profile information UserSchema .virtual('profile') .get(function() { return { 'name': this.name, 'role': this.role }; }); // Non-sensitive info we'll be putting in the token UserSchema .virtual('token') .get(function() { return { '_id': this._id, 'role': this.role }; }); /** * Validations */ // Validate empty email UserSchema .path('email') .validate(function(email) { return email.length; }, 'Email cannot be blank'); // Validate empty password UserSchema .path('hashedPassword') .validate(function(hashedPassword) { if (authTypes.indexOf(this.provider) !== -1) return true; return hashedPassword.length; }, 'Password cannot be blank'); // Validate email is not taken UserSchema .path('email') .validate(function(value, respond) { var self = this; this.constructor.findOne({email: value}, function(err, user) { if(err) throw err; if(user) { if(self.id === user.id) return respond(true); return respond(false); } respond(true); }); }, 'The specified email address is already in use.'); var validatePresenceOf = function(value) { return value && value.length; }; /** * Pre-save hook */ UserSchema .pre('save', function(next) { if (!this.isNew) return next(); if (!validatePresenceOf(this.hashedPassword) && authTypes.indexOf(this.provider) === -1) next(new Error('Invalid password')); else next(); }); /** * Methods */ UserSchema.methods = { /** * Authenticate - check if the passwords are the same * * @param {String} plainText * @return {Boolean} * @api public */ authenticate: function(plainText) { return this.encryptPassword(plainText) === this.hashedPassword; }, /** * Make salt * * @return {String} * @api public */ makeSalt: function() { return crypto.randomBytes(16).toString('base64'); }, /** * Encrypt password * * @param {String} password * @return {String} * @api public */ encryptPassword: function(password) { if (!password || !this.salt) return ''; var salt = new Buffer(this.salt, 'base64'); return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64'); } }; module.exports = mongoose.model('User', UserSchema); diff --git a/server/auth/ldap/passport.js b/server/auth/ldap/passport.js index cd857ba..cfddbba 100644 --- a/server/auth/ldap/passport.js +++ b/server/auth/ldap/passport.js @@ -1,61 +1,65 @@ var passport = require('passport'); var LdapStrategy = require('passport-ldapauth'); var crypto = require('crypto'); exports.setup = function (User, config) { function encrypt(password) { var cipher = crypto.createCipher('aes-256-cbc', config.storage.webdav.key); return cipher.update(password, 'utf8', 'base64') + cipher.final('base64') } passport.use(new LdapStrategy({ server: { url: config.auth.ldap.server, searchBase: config.auth.ldap.base, searchFilter: config.auth.ldap.filter, bindDn: config.auth.ldap.bindDn, bindCredentials: config.auth.ldap.bindPw, searchAttributes: ['cn'] }, usernameField: 'email', passwordField: 'password', passReqToCallback: true, badRequestMessage: true, invalidCredentials: true, userNotFound: true, constraintViolation: true }, function(req, ldapUser, done) { var email = req.body.email, password = req.body.password, fullName = ldapUser.cn; User.findOne({ email: email }, function (err, user) { if (err) { return done(err); } if (!user) { var newUser = new User({ name: fullName, email: email, provider: 'ldap', role: 'user', webdav: (config.storage.type === 'webdav') ? { username: email, password: encrypt(password) + } : undefined, + ldap: (config.storage.type === 'chwala') ? { + username: email, + password: encrypt(password) } : undefined }); newUser.save(function (err, user) { if (err) { return done(err); } if (!err) { return done(null, user); } }); } else { return done(null, user); } }); } )); }; diff --git a/server/components/adaptor/index.js b/server/components/adaptor/index.js index dfb4f43..192f5ad 100644 --- a/server/components/adaptor/index.js +++ b/server/components/adaptor/index.js @@ -1,100 +1,108 @@ /* * Copyright (C) 2015 KO GmbH * * @licstart * This file is part of Kotype. * * Kotype is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License (GNU AGPL) * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * Kotype is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Kotype. If not, see . * @licend * * @source: https://github.com/kogmbh/Kotype/ */ "use strict"; /*jslint nomen: true, unparam: true */ /*global require, console, setInterval, module */ var async = require("async"), Room = require("./room"), Document = require("../../api/document/document.model").Document, User = require("../../api/user/user.model"); // Maintains an in-memory cache of users, documents, and sessions. // And writes/reads them from the DB on demand. var ServerAdaptor = function (app, socketServer, objectCache) { var rooms = {}; function addToRoom(documentId, socket) { var room = rooms[documentId]; if (!room) { Document.findById(documentId).populate('creator', 'name email') .exec(function (err, doc) { if (err) { return console.log(err); } if (!doc) { return console.log("documentId unknown:"+documentId); } + if (doc.getAccessType(socket.user.email) === 'deny') { + return console.log("Access denied."); + } + var document = objectCache.getTrackedObject(doc); document.live = true; rooms[documentId] = room = new Room(app, document, objectCache, function () { rooms[documentId] = room; room.attachSocket(socket); }); var interval = setInterval(function () { if (!document.live) { clearInterval(interval); objectCache.forgetTrackedObject(document); room.destroy(function () { delete rooms[documentId]; }); } }, 500); }); } else if (room.isAvailable()) { + var document = objectCache.getTrackedObject({ _id: documentId }); + if (document.getAccessType(socket.user.email) === 'deny') { + return console.log("Access denied."); + } room.attachSocket(socket); } else { console.log("Room currently unavailable, disconnecting client.") socket.disconnect(); } } this.destroy = function (callback) { async.each(Object.keys(rooms), function (documentId, cb) { rooms[documentId].destroy(cb); }, function () { rooms = {}; callback() }); }; function init() { socketServer.on("connection", function (socket) { User.findById(socket.decoded_token._id, function (err, user) { if (err) { return console.log(err); } socket.user = user; socket.on("join", function (data) { var documentId = data.documentId; if (documentId) { - console.log("Authorized user " + user.name + " for document " + documentId); + console.log("Request by user " + user.name + " for document " + documentId); addToRoom(documentId, socket); } else { console.log("Error: Client did not specify a document ID"); } }); }); }); } init(); }; module.exports = ServerAdaptor; diff --git a/server/components/adaptor/room.js b/server/components/adaptor/room.js index d565b08..f8ecc42 100644 --- a/server/components/adaptor/room.js +++ b/server/components/adaptor/room.js @@ -1,478 +1,479 @@ /* * Copyright (C) 2015 KO GmbH * * @licstart * This file is part of Kotype. * * Kotype is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License (GNU AGPL) * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * Kotype is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Kotype. If not, see . * @licend * * @source: https://github.com/kogmbh/Kotype/ */ var async = require("async"); var _ = require("lodash"); var RColor = require('../colors'); var DocumentChunk = require("../../api/document/document.model").DocumentChunk; var DocumentController = require("../../api/document/document.controller"); var Recorder = require('./recorder'); var Room = function (app, document, objectCache, cb) { var ChunkManager = function (seedChunk) { var serverSeq, chunks = []; function getOperationsAfter(seq) { var ops = []; for (var i = chunks.length - 1; i >= 0; i--) { if (chunks[i].sequence >= seq) { ops = chunks[i].operations.concat(ops); } else { var basedOn = seq - chunks[i].sequence; ops = chunks[i].operations.slice(basedOn).concat(ops); break; } } return ops; } this.getOperationsAfter = getOperationsAfter; function appendOperations(ops) { var lastChunk = getLastChunk(); lastChunk.operations = lastChunk.operations.concat(ops); serverSeq += ops.length; } this.appendOperations = appendOperations; function appendChunk(chunk) { var trackedChunk = objectCache.getTrackedObject(chunk); chunks.push(trackedChunk); serverSeq = trackedChunk.sequence + trackedChunk.operations.length; } this.appendChunk = appendChunk; function getLastChunk() { return _.last(chunks); } this.getLastChunk = getLastChunk; this.getServerSequence = function () { return serverSeq; }; appendChunk(seedChunk); }; var chunkManager, recorder, hasCursor = {}, sockets = [], userColorMap = {}, randomColor = new RColor(), saveInProgress = false, isAvailable = false; function trackTitle(ops) { var newTitle, i; for (i = 0; i < ops.length; i += 1) { if (ops[i].optype === "UpdateMetadata" && ops[i].setProperties["dc:title"] !== undefined) { newTitle = ops[i].setProperties["dc:title"]; } } if (newTitle !== undefined) { if (newTitle.length === 0) { newTitle = "Untitled Document"; } } if (newTitle) { document.title = newTitle; } } function trackEditors() { // TODO: rather track by ops, to decouple from socket implementation sockets.forEach(function (socket) { var _id = socket.user._id; if (document.editors.indexOf(_id) === -1) { document.editors.push(_id); } }); } function trackCursors(ops) { var i; for (i = 0; i < ops.length; i += 1) { if (ops[i].optype === "AddCursor") { hasCursor[ops[i].memberid] = true; } if (ops[i].optype === "RemoveCursor") { hasCursor[ops[i].memberid] = false; } } } // Removes all cursors and members in the correct order within the last chunk function sanitizeDocument() { var chunk = chunkManager.getLastChunk(), ops = chunk.snapshot.operations.concat(chunk.operations), unbalancedCursors = {}, unbalancedMembers = {}, lastAccessDate = document.date, newOps = [], i; for (i = 0; i < ops.length; i += 1) { if (ops[i].optype === "AddCursor") { unbalancedCursors[ops[i].memberid] = true; } else if (ops[i].optype === "RemoveCursor") { unbalancedCursors[ops[i].memberid] = false; } else if (ops[i].optype === "AddMember") { unbalancedMembers[ops[i].memberid] = true; } else if (ops[i].optype === "RemoveMember") { unbalancedMembers[ops[i].memberid] = false; } } Object.keys(unbalancedCursors).forEach(function (memberId) { if (unbalancedCursors[memberId]) { newOps.push({ optype: "RemoveCursor", memberid: memberId, timestamp: lastAccessDate }); } }); Object.keys(unbalancedMembers).forEach(function (memberId) { if (unbalancedMembers[memberId]) { newOps.push({ optype: "RemoveMember", memberid: memberId, timestamp: lastAccessDate }); } }); if (newOps.length) { // Update op stack chunkManager.appendOperations(newOps); } } function broadcastMessage(message, data) { sockets.forEach(function (peerSocket) { peerSocket.emit(message, data) }); } function sendOpsToMember(socket, ops) { socket.emit("new_ops", { head: chunkManager.getServerSequence(), ops: ops }); } function setupMemberSnapshot(socket, snapshot) { socket.emit("replay", { head: chunkManager.getServerSequence(), ops: snapshot.operations.concat(chunkManager.getOperationsAfter(snapshot.sequence)) }); } function broadcastOpsByMember(socket, ops) { if (!ops.length) { return; } sockets.forEach(function (peerSocket) { if (peerSocket.memberId !== socket.memberId) { sendOpsToMember(peerSocket, ops); } }); } function writeOpsToDocument(ops, cb) { if (!ops.length || !document.live) { cb(); } recorder.push(ops, function () { trackTitle(ops); trackEditors(); // Update op stack chunkManager.appendOperations(ops); // Update modified date document.date = new Date(); cb(); }); } function addMember(user, cb) { var memberId, op, timestamp = Date.now(), color = userColorMap[user._id]; memberId = user.name + "_" + timestamp.toString(); // Let user colors persist in a Room even after they've // left and joined. if (!color) { userColorMap[user._id] = color = randomColor.get(true, 0.7); } op = { optype: "AddMember", memberid: memberId, timestamp: timestamp, setProperties: { fullName: user.name, color: color } }; writeOpsToDocument([op], function () { cb(memberId, [op]); }); } function removeMember(memberId, cb) { var ops = [], timestamp = Date.now(); if (hasCursor[memberId]) { ops.push({ optype: "RemoveCursor", memberid: memberId, timestamp: timestamp }); } ops.push({ optype: "RemoveMember", memberid: memberId, timestamp: timestamp }); writeOpsToDocument(ops, function () { cb(ops); }); } this.socketCount = function () { return sockets.length; }; this.attachSocket = function (socket) { // Add the socket to the room and give the // client it's unique memberId addMember(socket.user, function (memberId, ops) { socket.memberId = memberId; sockets.push(socket); broadcastOpsByMember(socket, ops); // Generate genesis URL with the latest document version's snapshot // We use a two-time URL because WebODF makes two identical GET requests (!) var genesisUrl = '/genesis/' + Date.now().toString(), usages = 0; recorder.getSnapshot(function (snapshot) { var buffer = new Buffer(snapshot.data); app.get(genesisUrl, function (req, res) { usages++; res.set('Content-Type', 'application/vnd.oasis.opendocument.text'); res.attachment(document.title); res.send(buffer); var routes = app._router.stack; if (usages === 2) { buffer = null; for (var i = 0; i < routes.length; i++) { if (routes[i].path === genesisUrl) { routes.splice(i, 1); break; } } } }); socket.emit("join_success", { memberId: memberId, - genesisUrl: genesisUrl + genesisUrl: genesisUrl, + permission: document.getAccessType(socket.user.email) }); // Service replay requests socket.on("replay", function () { setupMemberSnapshot(socket, snapshot); }); // Store, analyze, and broadcast incoming commits socket.on("commit_ops", function (data, cb) { var clientSeq = data.head, ops = data.ops; if (clientSeq === chunkManager.getServerSequence()) { writeOpsToDocument(ops, function () { cb({ conflict: false, head: chunkManager.getServerSequence() }); trackCursors(ops); broadcastOpsByMember(socket, data.ops); }); } else { cb({ conflict: true }); } }); // Service save requests. A save is a commit + socket.on("save", function (cb) { // Saves are blocking inside the phantomjs process, and they affect everyone, // therefore use a lock. if (saveInProgress) { var checkIfSaved = setInterval(function () { if (!saveInProgress) { clearInterval(checkIfSaved); cb(); } }, 1000); } else { saveInProgress = true; recorder.getSnapshot(function (snapshot) { DocumentController.createChunkFromSnapshot(document, snapshot, function (err, chunk) { saveInProgress = false; if (err) { return cb(err); } chunkManager.appendChunk(chunk); cb(); }); }); } }); // Service various requests socket.on("access_get", function (data, cb) { cb({ access: document.isPublic ? "public" : "normal" }); }); if (socket.user.identity !== "guest") { socket.on("access_change", function (data) { document.isPublic = data.access === "public"; broadcastMessage("access_changed", { access: data.access === "public" ? "public" : "normal" }); if (data.access !== "public") { sockets.forEach(function (peerSocket) { if (peerSocket.user.identity === "guest") { console.log(peerSocket.user.name); removeSocket(peerSocket); } }); } }); } }); // Handle dropout events socket.on("leave", function () { removeSocket(socket); }); socket.on("disconnect", function () { removeSocket(socket); }); }); }; function detachSocket(socket, callback) { removeMember(socket.memberId, function (ops) { broadcastOpsByMember(socket, ops); socket.removeAllListeners(); function lastCB() { socket.removeAllListeners(); if (callback) { callback(); } } // If a socket that is already connected is being // removed, this means that this is a deliberate // kicking-out, and not a natural event that could // result in a reconnection later. Therefore, clean // up. if (socket.connected) { console.log(socket.user.name + " is connected, removing"); socket.on('disconnect', lastCB); socket.emit("kick"); socket.emit("disconnect"); } else { console.log(socket.user.name + " is not connected, removing"); lastCB(); } }); } function removeSocket(socket) { var index = sockets.indexOf(socket); detachSocket(socket); if (index !== -1) { sockets.splice(index, 1); } } this.getDocument = function () { return document; }; this.isAvailable = function () { return isAvailable; }; this.destroy = function (callback) { async.each(sockets, function (socket, cb) { detachSocket(socket, cb); }, function () { //objectCache.forgetTrackedObject(chunk); document.live = false; sockets.length = 0; recorder.destroy(callback); }); }; function init() { // Setup caching DocumentChunk.findById(_.last(document.chunks), function (err, lastChunk) { chunkManager = new ChunkManager(lastChunk); // Sanitize leftovers from previous session, if any sanitizeDocument(); recorder = new Recorder(chunkManager.getLastChunk(), function () { isAvailable = true; cb(); }); }); } init(); }; module.exports = Room; diff --git a/server/config/environment/index.js b/server/config/environment/index.js index e25996a..9c277a1 100644 --- a/server/config/environment/index.js +++ b/server/config/environment/index.js @@ -1,75 +1,80 @@ 'use strict'; var path = require('path'); var _ = require('lodash'); function requiredProcessEnv(name) { if(!process.env[name]) { throw new Error('You must set the ' + name + ' environment variable'); } return process.env[name]; } // All configurations will extend these options // ============================================ var all = { env: process.env.NODE_ENV, // Root path of server root: path.normalize(__dirname + '/../../..'), // Server port port: process.env.PORT || 9000, // Should we populate the DB with sample data? seedDB: true, // Secret for session, you will want to change this and make it an environment variable secrets: { session: 'manticore-secret' }, // List of user roles userRoles: ['guest', 'user', 'admin'], // MongoDB connection options mongo: { options: { db: { safe: true } } }, + defaultAccess: process.env.DEFAULT_ACCESS, conversionHost: process.env.LOCODOC_SERVER, auth: { type: process.env.AUTH || 'local', 'webdav': { server: process.env.WEBDAV_SERVER, path: process.env.WEBDAV_PATH, - key: process.env.WEBDAV_ENCRYPTION_KEY + key: process.env.AUTH_ENCRYPTION_KEY }, 'ldap': { server: process.env.LDAP_SERVER, base: process.env.LDAP_BASE, filter: process.env.LDAP_FILTER, bindDn: process.env.LDAP_BIND_DN, - bindPw: process.env.LDAP_BIND_PW + bindPw: process.env.LDAP_BIND_PW, + key: process.env.AUTH_ENCRYPTION_KEY } }, storage: { type: process.env.STORAGE || 'local', 'webdav': { server: process.env.WEBDAV_SERVER, path: process.env.WEBDAV_PATH, - key: process.env.WEBDAV_ENCRYPTION_KEY + key: process.env.AUTH_ENCRYPTION_KEY + }, + 'chwala': { + server: process.env.CHWALA_SERVER } } }; // Export the config object based on the NODE_ENV // ============================================== module.exports = _.merge( all, require('./' + process.env.NODE_ENV + '.js') || {}); diff --git a/server/config/local.env.sample.js b/server/config/local.env.sample.js index c9a4bde..e78d7b3 100644 --- a/server/config/local.env.sample.js +++ b/server/config/local.env.sample.js @@ -1,49 +1,67 @@ 'use strict'; // Use local.env.js for environment variables that grunt will set when the server starts locally. // Use for your api keys, secrets, etc. This file should not be tracked by git. // // You will need to set these on the server you deploy to. module.exports = { DOMAIN: 'http://localhost:9000', SESSION_SECRET: 'manticore-secret', // Control debug level for modules using visionmedia/debug DEBUG: '', + /* + * Default access permissions for documents. + * If a user has a link to a session and tries to open it, this represents the + * access type they have if no permission has been explicitly set for them. + * Possible values: 'write', 'read', 'deny'. + * By default, is set to 'write' for testing purposes. + * If completely outsourcing access control to a third party service (like Kolab), set it to 'deny'. + * If left blank, defaults to 'deny'. + */ + DEFAULT_ACCESS: 'deny', + /* * Supported authentication strategies. * 1. 'local' for using Manticore's built-in accounts system. Allow signups. * 2. 'webdav' for authenticating against a WebDAV server. Only login, no signups. * 3. 'ldap' for authenticating against an LDAP service. Only login, no signups. */ AUTH: 'local', /* * Supported storage backends. * 1. 'local' for storing everything in Mongo/GridFS. The fastest and most reliable way. * Can be used with any AUTH strategy. * 2. 'webdav' for two-way synchronizing of documents with a WebDAV server. - * Can be used if AUTH is 'ldap' or 'webdav'; those credentials are used to talk to the webdav server. + * Can be used if AUTH is 'ldap' or 'webdav'; those credentials are used to talk to the storage server. + * 3. 'chwala' can be used for integrating with Kolab. */ STORAGE: 'local', /* * WebDAV server config, only if AUTH or STORAGE is 'webdav'. - * Make sure you provide an encryption key to protect users' webdav credentials. */ - WEBDAV_SERVER: 'https://kolabmachine', - WEBDAV_PATH: '/iRony/files/Files', - WEBDAV_ENCRYPTION_KEY: 'your-AES-encryption-key', + WEBDAV_SERVER: 'https://demo.owncloud.org', + WEBDAV_PATH: '/remote.php/webdav', + + CHWALA_SERVER: 'http://172.17.0.12', + + /* + * Make sure you provide an encryption key to protect users' auth credentials. + * This is necessary because the storage server may not support authentication tokens. + */ + AUTH_ENCRYPTION_KEY: 'suchauth123muchkey456', - // LDAP server config, required iff AUTH is 'ldap'. {{username}} will be replaced with users' logins - LDAP_SERVER: 'ldaps://kolabmachine', - LDAP_BASE: 'ou=People,dc=test,dc=example,dc=org', + // LDAP server config, only if AUTH is 'ldap' + LDAP_SERVER: 'ldap://172.17.0.12', + LDAP_BASE: 'ou=People,dc=example,dc=org', LDAP_FILTER: '(&(objectclass=person)(|(uid={{username}})(mail={{username}})))', - LDAP_BIND_DN: 'uid=kolab-service,ou=Special Users,dc=test,dc=example,dc=org', - LDAP_BIND_PW: 'kolab-service-pass', + LDAP_BIND_DN: 'uid=binderservice,ou=Special Users,dc=example,dc=org', + LDAP_BIND_PW: 'binderpass', - // locodoc (document format conversion) server + // locodoc Server config LOCODOC_SERVER: 'http://localhost:3030' };