diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/config/default.js | 40 | ||||
-rw-r--r-- | lib/config/defaultSSL.js | 2 | ||||
-rw-r--r-- | lib/config/dockerSecret.js | 7 | ||||
-rw-r--r-- | lib/config/environment.js | 44 | ||||
-rw-r--r-- | lib/config/index.js | 17 | ||||
-rw-r--r-- | lib/config/utils.js | 7 | ||||
-rw-r--r-- | lib/csp.js | 80 | ||||
-rw-r--r-- | lib/migrations/20171009121200-longtext-for-mysql.js | 16 | ||||
-rw-r--r-- | lib/models/note.js | 2 | ||||
-rw-r--r-- | lib/models/revision.js | 18 | ||||
-rw-r--r-- | lib/models/user.js | 26 | ||||
-rw-r--r-- | lib/realtime.js | 8 | ||||
-rw-r--r--[-rwxr-xr-x] | lib/response.js | 23 | ||||
-rw-r--r-- | lib/web/auth/google/index.js | 6 | ||||
-rw-r--r-- | lib/web/auth/index.js | 2 | ||||
-rw-r--r-- | lib/web/auth/ldap/index.js | 11 | ||||
-rw-r--r-- | lib/web/auth/mattermost/index.js | 49 | ||||
-rw-r--r-- | lib/web/auth/saml/index.js | 95 | ||||
-rw-r--r-- | lib/web/noteRouter.js | 4 | ||||
-rw-r--r-- | lib/web/utils.js | 7 |
20 files changed, 425 insertions, 39 deletions
diff --git a/lib/config/default.js b/lib/config/default.js index a9689974..28f4490c 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -7,9 +7,23 @@ module.exports = { urladdport: false, alloworigin: ['localhost'], usessl: false, + hsts: { + enable: true, + maxAgeSeconds: 31536000, + includeSubdomains: true, + preload: true + }, + csp: { + enable: true, + directives: { + }, + addDefaults: true, + upgradeInsecureRequests: 'auto' + }, protocolusessl: false, usecdn: true, allowanonymous: true, + allowanonymousedits: false, allowfreeurl: false, defaultpermission: 'editable', dburl: '', @@ -75,10 +89,16 @@ module.exports = { clientSecret: undefined, scope: undefined }, - dropbox: { + mattermost: { + baseURL: undefined, clientID: undefined, clientSecret: undefined }, + dropbox: { + clientID: undefined, + clientSecret: undefined, + appKey: undefined + }, google: { clientID: undefined, clientSecret: undefined @@ -92,8 +112,24 @@ module.exports = { searchBase: undefined, searchFilter: undefined, searchAttributes: undefined, + usernameField: undefined, tlsca: undefined }, + saml: { + idpSsoUrl: undefined, + idpCert: undefined, + issuer: undefined, + identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + groupAttribute: undefined, + externalGroups: [], + requiredGroups: [], + attribute: { + id: undefined, + username: undefined, + email: undefined + } + }, email: true, - allowemailregister: true + allowemailregister: true, + allowpdfexport: true } diff --git a/lib/config/defaultSSL.js b/lib/config/defaultSSL.js index 1f1d5590..362c62a1 100644 --- a/lib/config/defaultSSL.js +++ b/lib/config/defaultSSL.js @@ -12,6 +12,6 @@ function getFile (path) { module.exports = { sslkeypath: getFile('/run/secrets/key.pem'), sslcertpath: getFile('/run/secrets/cert.pem'), - sslcapath: getFile('/run/secrets/ca.pem'), + sslcapath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [], dhparampath: getFile('/run/secrets/dhparam.pem') } diff --git a/lib/config/dockerSecret.js b/lib/config/dockerSecret.js index eea2fafd..b9116cd3 100644 --- a/lib/config/dockerSecret.js +++ b/lib/config/dockerSecret.js @@ -38,9 +38,14 @@ if (fs.existsSync(basePath)) { clientID: getSecret('gitlab_clientID'), clientSecret: getSecret('gitlab_clientSecret') }, + mattermost: { + clientID: getSecret('mattermost_clientID'), + clientSecret: getSecret('mattermost_clientSecret') + }, dropbox: { clientID: getSecret('dropbox_clientID'), - clientSecret: getSecret('dropbox_clientSecret') + clientSecret: getSecret('dropbox_clientSecret'), + appKey: getSecret('dropbox_appKey') }, google: { clientID: getSecret('google_clientID'), diff --git a/lib/config/environment.js b/lib/config/environment.js index 49e44cad..932363da 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -1,17 +1,27 @@ 'use strict' -const {toBooleanConfig} = require('./utils') +const {toBooleanConfig, toArrayConfig} = require('./utils') module.exports = { domain: process.env.HMD_DOMAIN, urlpath: process.env.HMD_URL_PATH, port: process.env.HMD_PORT, - urladdport: process.env.HMD_URL_ADDPORT, + urladdport: toBooleanConfig(process.env.HMD_URL_ADDPORT), usessl: toBooleanConfig(process.env.HMD_USESSL), + hsts: { + enable: toBooleanConfig(process.env.HMD_HSTS_ENABLE), + maxAgeSeconds: process.env.HMD_HSTS_MAX_AGE, + includeSubdomains: toBooleanConfig(process.env.HMD_HSTS_INCLUDE_SUBDOMAINS), + preload: toBooleanConfig(process.env.HMD_HSTS_PRELOAD) + }, + csp: { + enable: toBooleanConfig(process.env.HMD_CSP_ENABLE) + }, protocolusessl: toBooleanConfig(process.env.HMD_PROTOCOL_USESSL), - alloworigin: process.env.HMD_ALLOW_ORIGIN ? process.env.HMD_ALLOW_ORIGIN.split(',') : undefined, + alloworigin: toArrayConfig(process.env.HMD_ALLOW_ORIGIN), usecdn: toBooleanConfig(process.env.HMD_USECDN), allowanonymous: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS), + allowanonymousedits: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS_EDITS), allowfreeurl: toBooleanConfig(process.env.HMD_ALLOW_FREEURL), defaultpermission: process.env.HMD_DEFAULT_PERMISSION, dburl: process.env.HMD_DB_URL, @@ -50,9 +60,15 @@ module.exports = { clientSecret: process.env.HMD_GITLAB_CLIENTSECRET, scope: process.env.HMD_GITLAB_SCOPE }, + mattermost: { + baseURL: process.env.HMD_MATTERMOST_BASEURL, + clientID: process.env.HMD_MATTERMOST_CLIENTID, + clientSecret: process.env.HMD_MATTERMOST_CLIENTSECRET + }, dropbox: { clientID: process.env.HMD_DROPBOX_CLIENTID, - clientSecret: process.env.HMD_DROPBOX_CLIENTSECRET + clientSecret: process.env.HMD_DROPBOX_CLIENTSECRET, + appKey: process.env.HMD_DROPBOX_APPKEY }, google: { clientID: process.env.HMD_GOOGLE_CLIENTID, @@ -66,9 +82,25 @@ module.exports = { tokenSecret: process.env.HMD_LDAP_TOKENSECRET, searchBase: process.env.HMD_LDAP_SEARCHBASE, searchFilter: process.env.HMD_LDAP_SEARCHFILTER, - searchAttributes: process.env.HMD_LDAP_SEARCHATTRIBUTES, + searchAttributes: toArrayConfig(process.env.HMD_LDAP_SEARCHATTRIBUTES), + usernameField: process.env.HMD_LDAP_USERNAMEFIELD, tlsca: process.env.HMD_LDAP_TLS_CA }, + saml: { + idpSsoUrl: process.env.HMD_SAML_IDPSSOURL, + idpCert: process.env.HMD_SAML_IDPCERT, + issuer: process.env.HMD_SAML_ISSUER, + identifierFormat: process.env.HMD_SAML_IDENTIFIERFORMAT, + groupAttribute: process.env.HMD_SAML_GROUPATTRIBUTE, + externalGroups: toArrayConfig(process.env.HMD_SAML_EXTERNALGROUPS, '|', []), + requiredGroups: toArrayConfig(process.env.HMD_SAML_REQUIREDGROUPS, '|', []), + attribute: { + id: process.env.HMD_SAML_ATTRIBUTE_ID, + username: process.env.HMD_SAML_ATTRIBUTE_USERNAME, + email: process.env.HMD_SAML_ATTRIBUTE_EMAIL + } + }, email: toBooleanConfig(process.env.HMD_EMAIL), - allowemailregister: toBooleanConfig(process.env.HMD_ALLOW_EMAIL_REGISTER) + allowemailregister: toBooleanConfig(process.env.HMD_ALLOW_EMAIL_REGISTER), + allowpdfexport: toBooleanConfig(process.env.HMD_ALLOW_PDF_EXPORT) } diff --git a/lib/config/index.js b/lib/config/index.js index bea5a6af..3d22c3c3 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -1,3 +1,4 @@ + 'use strict' const fs = require('fs') @@ -12,8 +13,10 @@ const debugConfig = { debug: (env === Environment.development) } +const {version} = require(path.join(appRootPath, 'package.json')) + const packageConfig = { - version: '0.5.1', + version: version, minimumCompatibleVersion: '0.5.0' } @@ -46,7 +49,7 @@ if (config.ldap.tlsca) { // Permission config.permission = Permission -if (!config.allowanonymous) { +if (!config.allowanonymous && !config.allowanonymousedits) { delete config.permission.freely } if (!(config.defaultpermission in config.permission)) { @@ -89,10 +92,16 @@ config.isTwitterEnable = config.twitter.consumerKey && config.twitter.consumerSe config.isEmailEnable = config.email config.isGitHubEnable = config.github.clientID && config.github.clientSecret config.isGitLabEnable = config.gitlab.clientID && config.gitlab.clientSecret +config.isMattermostEnable = config.mattermost.clientID && config.mattermost.clientSecret config.isLDAPEnable = config.ldap.url +config.isSAMLEnable = config.saml.idpSsoUrl +config.isPDFExportEnable = config.allowpdfexport // generate correct path -config.sslcapath = path.join(appRootPath, config.sslcapath) +config.sslcapath.forEach(function (capath, i, array) { + array[i] = path.resolve(appRootPath, capath) +}) + config.sslcertpath = path.join(appRootPath, config.sslcertpath) config.sslkeypath = path.join(appRootPath, config.sslkeypath) config.dhparampath = path.join(appRootPath, config.dhparampath) @@ -106,7 +115,7 @@ config.errorpath = path.join(appRootPath, config.errorpath) config.prettypath = path.join(appRootPath, config.prettypath) config.slidepath = path.join(appRootPath, config.slidepath) -// maek config readonly +// make config readonly config = deepFreeze(config) module.exports = config diff --git a/lib/config/utils.js b/lib/config/utils.js index 11bbd8cb..9ff2f96d 100644 --- a/lib/config/utils.js +++ b/lib/config/utils.js @@ -6,3 +6,10 @@ exports.toBooleanConfig = function toBooleanConfig (configValue) { } return configValue } + +exports.toArrayConfig = function toArrayConfig (configValue, separator = ',', fallback) { + if (configValue && typeof configValue === 'string') { + return (configValue.split(separator).map(arrayItem => arrayItem.trim())) + } + return fallback +} diff --git a/lib/csp.js b/lib/csp.js new file mode 100644 index 00000000..509bc530 --- /dev/null +++ b/lib/csp.js @@ -0,0 +1,80 @@ +var config = require('./config') +var uuid = require('uuid') + +var CspStrategy = {} + +var defaultDirectives = { + defaultSrc: ['\'self\''], + scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', 'https://*.disqus.com', '\'unsafe-eval\''], + // ^ TODO: Remove unsafe-eval - webpack script-loader issues https://github.com/hackmdio/hackmd/issues/594 + imgSrc: ['*'], + styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://assets-cdn.github.com'], // unsafe-inline is required for some libs, plus used in views + fontSrc: ['\'self\'', 'https://public.slidesharecdn.com'], + objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/ + childSrc: ['*'], + connectSrc: ['*'] +} + +var cdnDirectives = { + scriptSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.mathjax.org'], + styleSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.googleapis.com'], + fontSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.gstatic.com'] +} + +CspStrategy.computeDirectives = function () { + var directives = {} + mergeDirectives(directives, config.csp.directives) + mergeDirectivesIf(config.csp.addDefaults, directives, defaultDirectives) + mergeDirectivesIf(config.usecdn, directives, cdnDirectives) + if (!areAllInlineScriptsAllowed(directives)) { + addInlineScriptExceptions(directives) + } + addUpgradeUnsafeRequestsOptionTo(directives) + return directives +} + +function mergeDirectives (existingDirectives, newDirectives) { + for (var propertyName in newDirectives) { + var newDirective = newDirectives[propertyName] + if (newDirective) { + var existingDirective = existingDirectives[propertyName] || [] + existingDirectives[propertyName] = existingDirective.concat(newDirective) + } + } +} + +function mergeDirectivesIf (condition, existingDirectives, newDirectives) { + if (condition) { + mergeDirectives(existingDirectives, newDirectives) + } +} + +function areAllInlineScriptsAllowed (directives) { + return directives.scriptSrc.indexOf('\'unsafe-inline\'') !== -1 +} + +function addInlineScriptExceptions (directives) { + directives.scriptSrc.push(getCspNonce) + // TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html + // Any more clean solution appreciated. + directives.scriptSrc.push('\'sha256-EtvSSxRwce5cLeFBZbvZvDrTiRoyoXbWWwvEVciM5Ag=\'') +} + +function getCspNonce (req, res) { + return "'nonce-" + res.locals.nonce + "'" +} + +function addUpgradeUnsafeRequestsOptionTo (directives) { + if (config.csp.upgradeInsecureRequests === 'auto' && config.usessl) { + directives.upgradeInsecureRequests = true + } else if (config.csp.upgradeInsecureRequests === true) { + directives.upgradeInsecureRequests = true + } +} + +CspStrategy.addNonceToLocals = function (req, res, next) { + res.locals.nonce = uuid.v4() + next() +} + +module.exports = CspStrategy diff --git a/lib/migrations/20171009121200-longtext-for-mysql.js b/lib/migrations/20171009121200-longtext-for-mysql.js new file mode 100644 index 00000000..61b409ca --- /dev/null +++ b/lib/migrations/20171009121200-longtext-for-mysql.js @@ -0,0 +1,16 @@ +'use strict' +module.exports = { + up: function (queryInterface, Sequelize) { + queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT('long')}) + queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT('long')}) + queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT('long')}) + queryInterface.changeColumn('Revisions', 'latContent', {type: Sequelize.TEXT('long')}) + }, + + down: function (queryInterface, Sequelize) { + queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT}) + queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT}) + queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT}) + queryInterface.changeColumn('Revisions', 'latContent', {type: Sequelize.TEXT}) + } +} diff --git a/lib/models/note.js b/lib/models/note.js index c0ef1374..33dde80d 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -60,7 +60,7 @@ module.exports = function (sequelize, DataTypes) { } }, content: { - type: DataTypes.TEXT, + type: DataTypes.TEXT('long'), get: function () { return sequelize.processData(this.getDataValue('content'), '') }, diff --git a/lib/models/revision.js b/lib/models/revision.js index 6f3a746f..170931b8 100644 --- a/lib/models/revision.js +++ b/lib/models/revision.js @@ -58,7 +58,7 @@ module.exports = function (sequelize, DataTypes) { defaultValue: Sequelize.UUIDV4 }, patch: { - type: DataTypes.TEXT, + type: DataTypes.TEXT('long'), get: function () { return sequelize.processData(this.getDataValue('patch'), '') }, @@ -67,7 +67,7 @@ module.exports = function (sequelize, DataTypes) { } }, lastContent: { - type: DataTypes.TEXT, + type: DataTypes.TEXT('long'), get: function () { return sequelize.processData(this.getDataValue('lastContent'), '') }, @@ -76,7 +76,7 @@ module.exports = function (sequelize, DataTypes) { } }, content: { - type: DataTypes.TEXT, + type: DataTypes.TEXT('long'), get: function () { return sequelize.processData(this.getDataValue('content'), '') }, @@ -110,7 +110,7 @@ module.exports = function (sequelize, DataTypes) { where: { noteId: note.id }, - order: '"createdAt" DESC' + order: [['createdAt', 'DESC']] }).then(function (revisions) { var data = [] for (var i = 0, l = revisions.length; i < l; i++) { @@ -131,7 +131,7 @@ module.exports = function (sequelize, DataTypes) { where: { noteId: note.id }, - order: '"createdAt" DESC' + order: [['createdAt', 'DESC']] }).then(function (revisions) { if (revisions.length <= 0) return callback(null, null) // measure target revision position @@ -142,7 +142,7 @@ module.exports = function (sequelize, DataTypes) { $gte: time } }, - order: '"createdAt" DESC' + order: [['createdAt', 'DESC']] }).then(function (count) { if (count <= 0) return callback(null, null) sendDmpWorker({ @@ -231,14 +231,14 @@ module.exports = function (sequelize, DataTypes) { where: { noteId: note.id }, - order: '"createdAt" DESC' + order: [['createdAt', 'DESC']] }).then(function (revisions) { if (revisions.length <= 0) { // if no revision available Revision.create({ noteId: note.id, - lastContent: note.content, - length: note.content.length, + lastContent: note.content ? note.content : '', + length: note.content ? note.content.length : 0, authorship: note.authorship }).then(function (revision) { Revision.finishSaveNoteRevision(note, revision, callback) diff --git a/lib/models/user.js b/lib/models/user.js index 14c30bc3..f421fe43 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -104,8 +104,21 @@ module.exports = function (sequelize, DataTypes) { break case 'gitlab': photo = profile.avatarUrl - if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') - else photo = photo.replace(/(\?s=)\d*$/i, '$196') + if (photo) { + if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') + else photo = photo.replace(/(\?s=)\d*$/i, '$196') + } else { + photo = letterAvatars(profile.username) + } + break + case 'mattermost': + photo = profile.avatarUrl + if (photo) { + if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400') + else photo = photo.replace(/(\?s=)\d*$/i, '$196') + } else { + photo = letterAvatars(profile.username) + } break case 'dropbox': // no image api provided, use gravatar @@ -130,6 +143,15 @@ module.exports = function (sequelize, DataTypes) { photo = letterAvatars(profile.username) } break + case 'saml': + if (profile.emails[0]) { + photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0]) + if (bigger) photo += '?s=400' + else photo += '?s=96' + } else { + photo = letterAvatars(profile.username) + } + break } return photo }, diff --git a/lib/realtime.js b/lib/realtime.js index 361bbf09..c731e5b0 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -709,7 +709,7 @@ function connection (socket) { return failConnection(404, 'note id not found', socket) } - if (isDuplicatedInSocketQueue(socket, connectionSocketQueue)) return + if (isDuplicatedInSocketQueue(connectionSocketQueue, socket)) return // store noteId in this socket session socket.noteId = noteId @@ -723,8 +723,8 @@ function connection (socket) { var maxrandomcount = 10 var found = false do { - Object.keys(notes[noteId].users).forEach(function (user) { - if (user.color === color) { + Object.keys(notes[noteId].users).forEach(function (userId) { + if (notes[noteId].users[userId].color === color) { found = true } }) @@ -781,7 +781,7 @@ function connection (socket) { var note = notes[noteId] // Only owner can change permission if (note.owner && note.owner === socket.request.user.id) { - if (permission === 'freely' && !config.allowanonymous) return + if (permission === 'freely' && !config.allowanonymous && !config.allowanonymousedits) return note.permission = permission models.Note.update({ permission: permission diff --git a/lib/response.js b/lib/response.js index a22d1e70..445aa0d7 100755..100644 --- a/lib/response.js +++ b/lib/response.js @@ -60,15 +60,19 @@ function showIndex (req, res, next) { url: config.serverurl, useCDN: config.usecdn, allowAnonymous: config.allowanonymous, + allowAnonymousEdits: config.allowanonymousedits, facebook: config.isFacebookEnable, twitter: config.isTwitterEnable, github: config.isGitHubEnable, gitlab: config.isGitLabEnable, + mattermost: config.isMattermostEnable, dropbox: config.isDropboxEnable, google: config.isGoogleEnable, ldap: config.isLDAPEnable, + saml: config.isSAMLEnable, email: config.isEmailEnable, allowemailregister: config.allowemailregister, + allowpdfexport: config.allowpdfexport, signin: req.isAuthenticated(), infoMessage: req.flash('info'), errorMessage: req.flash('error') @@ -90,15 +94,19 @@ function responseHackMD (res, note) { title: title, useCDN: config.usecdn, allowAnonymous: config.allowanonymous, + allowAnonymousEdits: config.allowanonymousedits, facebook: config.isFacebookEnable, twitter: config.isTwitterEnable, github: config.isGitHubEnable, gitlab: config.isGitLabEnable, + mattermost: config.isMattermostEnable, dropbox: config.isDropboxEnable, google: config.isGoogleEnable, ldap: config.isLDAPEnable, + saml: config.isSAMLEnable, email: config.isEmailEnable, - allowemailregister: config.allowemailregister + allowemailregister: config.allowemailregister, + allowpdfexport: config.allowpdfexport }) } @@ -111,7 +119,8 @@ function newNote (req, res, next) { } models.Note.create({ ownerId: owner, - alias: req.alias ? req.alias : null + alias: req.alias ? req.alias : null, + content: req.body ? req.body : '' }).then(function (note) { return res.redirect(config.serverurl + '/' + LZString.compressToBase64(note.id)) }).catch(function (err) { @@ -382,7 +391,12 @@ function noteActions (req, res, next) { actionInfo(req, res, note) break case 'pdf': - actionPDF(req, res, note) + if (config.allowpdfexport) { + actionPDF(req, res, note) + } else { + logger.error('PDF export failed: Disabled by config. Set "allowpdfexport: true" to enable. Check the documentation for details') + response.errorForbidden(res) + } break case 'gist': actionGist(req, res, note) @@ -584,7 +598,8 @@ function showPublishSlide (req, res, next) { lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, robots: meta.robots || false, // default allow robots GA: meta.GA, - disqus: meta.disqus + disqus: meta.disqus, + cspNonce: res.locals.nonce } return renderPublishSlide(data, res) }).catch(function (err) { diff --git a/lib/web/auth/google/index.js b/lib/web/auth/google/index.js index bf2a260f..609c69cf 100644 --- a/lib/web/auth/google/index.js +++ b/lib/web/auth/google/index.js @@ -6,7 +6,7 @@ var GoogleStrategy = require('passport-google-oauth20').Strategy const config = require('../../../config') const {setReturnToFromReferer, passportGeneralCallback} = require('../utils') -let facebookAuth = module.exports = Router() +let googleAuth = module.exports = Router() passport.use(new GoogleStrategy({ clientID: config.google.clientID, @@ -14,12 +14,12 @@ passport.use(new GoogleStrategy({ callbackURL: config.serverurl + '/auth/google/callback' }, passportGeneralCallback)) -facebookAuth.get('/auth/google', function (req, res, next) { +googleAuth.get('/auth/google', function (req, res, next) { setReturnToFromReferer(req) passport.authenticate('google', { scope: ['profile'] })(req, res, next) }) // google auth callback -facebookAuth.get('/auth/google/callback', +googleAuth.get('/auth/google/callback', passport.authenticate('google', { successReturnToOrRedirect: config.serverurl + '/', failureRedirect: config.serverurl + '/' diff --git a/lib/web/auth/index.js b/lib/web/auth/index.js index b5ca8434..db5ff11d 100644 --- a/lib/web/auth/index.js +++ b/lib/web/auth/index.js @@ -33,9 +33,11 @@ if (config.isFacebookEnable) authRouter.use(require('./facebook')) if (config.isTwitterEnable) authRouter.use(require('./twitter')) if (config.isGitHubEnable) authRouter.use(require('./github')) if (config.isGitLabEnable) authRouter.use(require('./gitlab')) +if (config.isMattermostEnable) authRouter.use(require('./mattermost')) if (config.isDropboxEnable) authRouter.use(require('./dropbox')) if (config.isGoogleEnable) authRouter.use(require('./google')) if (config.isLDAPEnable) authRouter.use(require('./ldap')) +if (config.isSAMLEnable) authRouter.use(require('./saml')) if (config.isEmailEnable) authRouter.use(require('./email')) // logout diff --git a/lib/web/auth/ldap/index.js b/lib/web/auth/ldap/index.js index 766c5cbc..cc0d29ad 100644 --- a/lib/web/auth/ldap/index.js +++ b/lib/web/auth/ldap/index.js @@ -23,9 +23,16 @@ passport.use(new LDAPStrategy({ tlsOptions: config.ldap.tlsOptions || null } }, function (user, done) { + var uuid = user.uidNumber || user.uid || user.sAMAccountName + var username = uuid + + if (config.ldap.usernameField && user[config.ldap.usernameField]) { + username = user[config.ldap.usernameField] + } + var profile = { - id: 'LDAP-' + user.uidNumber, - username: user.uid, + id: 'LDAP-' + uuid, + username: username, displayName: user.displayName, emails: user.mail ? [user.mail] : [], avatarUrl: null, diff --git a/lib/web/auth/mattermost/index.js b/lib/web/auth/mattermost/index.js new file mode 100644 index 00000000..9ccf3de5 --- /dev/null +++ b/lib/web/auth/mattermost/index.js @@ -0,0 +1,49 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +const Mattermost = require('mattermost') +const OAuthStrategy = require('passport-oauth2').Strategy +const config = require('../../../config') +const {setReturnToFromReferer, passportGeneralCallback} = require('../utils') + +const mattermost = new Mattermost.Client() + +let mattermostAuth = module.exports = Router() + +let mattermostStrategy = new OAuthStrategy({ + authorizationURL: config.mattermost.baseURL + '/oauth/authorize', + tokenURL: config.mattermost.baseURL + '/oauth/access_token', + clientID: config.mattermost.clientID, + clientSecret: config.mattermost.clientSecret, + callbackURL: config.serverurl + '/auth/mattermost/callback' +}, passportGeneralCallback) + +mattermostStrategy.userProfile = (accessToken, done) => { + mattermost.setUrl(config.mattermost.baseURL) + mattermost.token = accessToken + mattermost.useHeaderToken() + mattermost.getMe( + (data) => { + done(null, data) + }, + (err) => { + done(err) + } + ) +} + +passport.use(mattermostStrategy) + +mattermostAuth.get('/auth/mattermost', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('oauth2')(req, res, next) +}) + +// mattermost auth callback +mattermostAuth.get('/auth/mattermost/callback', + passport.authenticate('oauth2', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + }) +) diff --git a/lib/web/auth/saml/index.js b/lib/web/auth/saml/index.js new file mode 100644 index 00000000..386293ae --- /dev/null +++ b/lib/web/auth/saml/index.js @@ -0,0 +1,95 @@ +'use strict' + +const Router = require('express').Router +const passport = require('passport') +const SamlStrategy = require('passport-saml').Strategy +const config = require('../../../config') +const models = require('../../../models') +const logger = require('../../../logger') +const {urlencodedParser} = require('../../utils') +const fs = require('fs') +const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) } + +let samlAuth = module.exports = Router() + +passport.use(new SamlStrategy({ + callbackUrl: config.serverurl + '/auth/saml/callback', + entryPoint: config.saml.idpSsoUrl, + issuer: config.saml.issuer || config.serverurl, + cert: fs.readFileSync(config.saml.idpCert, 'utf-8'), + identifierFormat: config.saml.identifierFormat +}, function (user, done) { + // check authorization if needed + if (config.saml.externalGroups && config.saml.grouptAttribute) { + var externalGroups = intersection(config.saml.externalGroups, user[config.saml.groupAttribute]) + if (externalGroups.length > 0) { + logger.error('saml permission denied: ' + externalGroups.join(', ')) + return done('Permission denied', null) + } + } + if (config.saml.requiredGroups && config.saml.grouptAttribute) { + if (intersection(config.saml.requiredGroups, user[config.saml.groupAttribute]).length === 0) { + logger.error('saml permission denied') + return done('Permission denied', null) + } + } + // user creation + var uuid = user[config.saml.attribute.id] || user.nameID + var profile = { + provider: 'saml', + id: 'SAML-' + uuid, + username: user[config.saml.attribute.username] || user.nameID, + emails: user[config.saml.attribute.email] ? [user[config.saml.attribute.email]] : [] + } + if (profile.emails.length === 0 && config.saml.identifierFormat === 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress') { + profile.emails.push(user.nameID) + } + var stringifiedProfile = JSON.stringify(profile) + models.User.findOrCreate({ + where: { + profileid: profile.id.toString() + }, + defaults: { + profile: stringifiedProfile + } + }).spread(function (user, created) { + if (user) { + var needSave = false + if (user.profile !== stringifiedProfile) { + user.profile = stringifiedProfile + needSave = true + } + if (needSave) { + user.save().then(function () { + if (config.debug) { logger.debug('user login: ' + user.id) } + return done(null, user) + }) + } else { + if (config.debug) { logger.debug('user login: ' + user.id) } + return done(null, user) + } + } + }).catch(function (err) { + logger.error('saml auth failed: ' + err) + return done(err, null) + }) +})) + +samlAuth.get('/auth/saml', + passport.authenticate('saml', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + }) +) + +samlAuth.post('/auth/saml/callback', urlencodedParser, + passport.authenticate('saml', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + }) +) + +samlAuth.get('/auth/saml/metadata', function (req, res) { + res.type('application/xml') + res.send(passport._strategy('saml').generateServiceProviderMetadata()) +}) diff --git a/lib/web/noteRouter.js b/lib/web/noteRouter.js index 007c02c2..41bf5f73 100644 --- a/lib/web/noteRouter.js +++ b/lib/web/noteRouter.js @@ -4,10 +4,14 @@ const Router = require('express').Router const response = require('../response') +const {markdownParser} = require('./utils') + const noteRouter = module.exports = Router() // get new note noteRouter.get('/new', response.newNote) +// post new note with content +noteRouter.post('/new', markdownParser, response.newNote) // get publish note noteRouter.get('/s/:shortid', response.showPublishNote) // publish note actions diff --git a/lib/web/utils.js b/lib/web/utils.js index c9016523..d58294ad 100644 --- a/lib/web/utils.js +++ b/lib/web/utils.js @@ -7,3 +7,10 @@ exports.urlencodedParser = bodyParser.urlencoded({ extended: false, limit: 1024 * 1024 * 10 // 10 mb }) + +// create text/markdown parser +exports.markdownParser = bodyParser.text({ + inflate: true, + type: ['text/plain', 'text/markdown'], + limit: 1024 * 1024 * 10 // 10 mb +}) |