Repository: hackmdio/codimd (JavaScript)

Issue:
Add an option to delete the permission that guests can view
hi,
There are `allowAnonymous` and `allowAnonymousEdits` for changing the authority of the guest, but there is no setting to prohibit guest viewing.

I suppose I can create a patch.  (`allowAnonymousViews=false`)
* hide `New guest note` button
* hide `freely`, `editable` and `locked` permission

Is it possible to add this patch? Thanks.


Discussion:
| allowAnonymous | allowAnonymousEdits | allowAnonymousViews | `New guest note` button | `freely` permission | `editable/locked` permission | 
| -------- | -------- | -------- | -------- | -------- | -------- |
| true | - | - | active | active | active |
| false | - | false | inactive | inactive | inactive |
| false | false | true | inactive | inactive | active |
| false | true | true | inactive | active | active |
Sorry for reaching out so late, I was quite sure to have already answered that. Had some busy week. The proposal looks good, I would like to see such implementation.

On the other hand, we may rework the entire permission this year, so if you want to work on the bigger step, then I guess this would be a way to go for you! Of course, before we can get our hands on the permissions, we probably need to work on the account system. But depending on your preferences, every helping hand is very welcome!



Patch:
diff --git a/app.js b/app.js
index 50dae472a7..4093bd3b9c 100644
--- a/app.js
+++ b/app.js
@@ -180,6 +180,7 @@ app.locals.serverURL = config.serverURL
 app.locals.sourceURL = config.sourceURL
 app.locals.allowAnonymous = config.allowAnonymous
 app.locals.allowAnonymousEdits = config.allowAnonymousEdits
+app.locals.permission = config.permission
 app.locals.allowPDFExport = config.allowPDFExport
 app.locals.authProviders = {
   facebook: config.isFacebookEnable,
diff --git a/lib/config/default.js b/lib/config/default.js
index 3fc11fcc04..93de259999 100644
--- a/lib/config/default.js
+++ b/lib/config/default.js
@@ -31,6 +31,7 @@ module.exports = {
   useCDN: true,
   allowAnonymous: true,
   allowAnonymousEdits: false,
+  allowAnonymousViews: true,
   allowFreeURL: false,
   forbiddenNoteIDs: ['robots.txt', 'favicon.ico', 'api'],
   defaultPermission: 'editable',
diff --git a/lib/config/environment.js b/lib/config/environment.js
index 09a27e14e7..de573e1847 100644
--- a/lib/config/environment.js
+++ b/lib/config/environment.js
@@ -27,6 +27,7 @@ module.exports = {
   useCDN: toBooleanConfig(process.env.CMD_USECDN),
   allowAnonymous: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS),
   allowAnonymousEdits: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_EDITS),
+  allowAnonymousViews: toBooleanConfig(process.env.CMD_ALLOW_ANONYMOUS_VIEWS),
   allowFreeURL: toBooleanConfig(process.env.CMD_ALLOW_FREEURL),
   forbiddenNoteIDs: toArrayConfig(process.env.CMD_FORBIDDEN_NOTE_IDS),
   defaultPermission: process.env.CMD_DEFAULT_PERMISSION,
diff --git a/lib/config/index.js b/lib/config/index.js
index 1dd9fbe7ee..22e7ec47dc 100644
--- a/lib/config/index.js
+++ b/lib/config/index.js
@@ -68,11 +68,17 @@ if (config.ldap.tlsca) {
 
 // Permission
 config.permission = Permission
-if (!config.allowAnonymous && !config.allowAnonymousEdits) {
+let defaultPermission = config.permission.editable
+if (!config.allowAnonymous && !config.allowAnonymousViews) {
+  delete config.permission.freely
+  delete config.permission.editable
+  delete config.permission.locked
+  defaultPermission = config.permission.limited
+} else if (!config.allowAnonymous && !config.allowAnonymousEdits) {
   delete config.permission.freely
 }
 if (!(config.defaultPermission in config.permission)) {
-  config.defaultPermission = config.permission.editable
+  config.defaultPermission = defaultPermission
 }
 
 // cache result, cannot change config in runtime!!!
diff --git a/lib/realtimeClientConnection.js b/lib/realtimeClientConnection.js
index b8aaa0543f..b14907910e 100644
--- a/lib/realtimeClientConnection.js
+++ b/lib/realtimeClientConnection.js
@@ -57,6 +57,19 @@ class RealtimeClientConnection {
     return config.allowAnonymous || config.allowAnonymousEdits
   }
 
+  getAvailablePermissions () {
+    // TODO: move this method to config module
+    const availablePermission = Object.assign({}, config.permission)
+    if (!config.allowAnonymous && !config.allowAnonymousViews) {
+      delete availablePermission.freely
+      delete availablePermission.editable
+      delete availablePermission.locked
+    } else if (!config.allowAnonymous && !config.allowAnonymousEdits) {
+      delete availablePermission.freely
+    }
+    return availablePermission
+  }
+
   getCurrentUser () {
     if (!this.socket.id) return
     return this.realtime.getUserFromUserPool(this.socket.id)
@@ -211,7 +224,7 @@ class RealtimeClientConnection {
     const note = this.getCurrentNote()
     // Only owner can change permission
     if (!this.isNoteOwner()) return
-    if (!this.isAnonymousEnable() && permission === 'freely') return
+    if (!(permission in this.getAvailablePermissions())) return
 
     this.changeNotePermission(permission)
       .then(() => {
diff --git a/public/views/codimd/body.ejs b/public/views/codimd/body.ejs
index dc111909ec..b7b730f2c2 100644
--- a/public/views/codimd/body.ejs
+++ b/public/views/codimd/body.ejs
@@ -15,12 +15,12 @@
                     <a id="permissionLabel" class="ui-permission-label text-uppercase" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
                     </a>
                     <ul class="dropdown-menu" aria-labelledby="permissionLabel">
-                        <li class="ui-permission-freely"<% if(!allowAnonymous && !allowAnonymousEdits) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
-                        <li class="ui-permission-editable"><a><i class="fa fa-shield fa-fw"></i> Editable - Signed-in people can edit</a></li>
-                        <li class="ui-permission-limited"><a><i class="fa fa-id-card fa-fw"></i> Limited - Signed-in people can edit (forbid guests)</a></li>
-                        <li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
-                        <li class="ui-permission-protected"><a><i class="fa fa-umbrella fa-fw"></i> Protected - Only owner can edit (forbid guests)</a></li>
-                        <li class="ui-permission-private"><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view &amp; edit</a></li>
+                        <li class="ui-permission-freely"<% if(!('freely' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
+                        <li class="ui-permission-editable"<% if(!('editable' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-shield fa-fw"></i> Editable - Signed-in people can edit</a></li>
+                        <li class="ui-permission-limited"<% if(!('limited' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-id-card fa-fw"></i> Limited - Signed-in people can edit (forbid guests)</a></li>
+                        <li class="ui-permission-locked"<% if(!('locked' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
+                        <li class="ui-permission-protected"<% if(!('protected' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-umbrella fa-fw"></i> Protected - Only owner can edit (forbid guests)</a></li>
+                        <li class="ui-permission-private"<% if(!('private' in permission)) { %> style="display: none;"<% } %>><a><i class="fa fa-hand-stop-o fa-fw"></i> Private - Only owner can view &amp; edit</a></li>
                         <li class="divider"></li>
                         <li class="ui-delete-note"><a><i class="fa fa-trash-o fa-fw"></i> Delete this note</a></li>
                     </ul>


Test patch:
diff --git a/test/realtime/socket-events.test.js b/test/realtime/socket-events.test.js
index 0a6aa7af13..0015d179fb 100644
--- a/test/realtime/socket-events.test.js
+++ b/test/realtime/socket-events.test.js
@@ -56,7 +56,15 @@ describe('realtime#socket event', function () {
     })
     configMock = {
       fullversion: '1.5.0',
-      minimumCompatibleVersion: '1.0.0'
+      minimumCompatibleVersion: '1.0.0',
+      permission: {
+        freely: 'freely',
+        editable: 'editable',
+        limited: 'limited',
+        locked: 'locked',
+        protected: 'protected',
+        private: 'private'
+      }
     }
     mock('../../lib/logger', {
       error: () => {
@@ -522,9 +530,10 @@ describe('realtime#socket event', function () {
       }, 5)
     })
 
-    it('should change permission to freely when config allowAnonymous and allowAnonymousEdits are true', function (done) {
+    it('should change permission to freely when config allowAnonymous, allowAnonymousEdits and allowAnonymousViews are true', function (done) {
       configMock.allowAnonymous = true
       configMock.allowAnonymousEdits = true
+      configMock.allowAnonymousViews = true
       realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
 
       permissionFunc('freely')
@@ -539,9 +548,10 @@ describe('realtime#socket event', function () {
       }, 5)
     })
 
-    it('should not change permission to freely when config allowAnonymous and allowAnonymousEdits are false', function (done) {
+    it('should not change permission to freely when config allowAnonymous, allowAnonymousEdits and allowAnonymousViews are false', function (done) {
       configMock.allowAnonymous = false
       configMock.allowAnonymousEdits = false
+      configMock.allowAnonymousViews = false
       realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
 
       permissionFunc('freely')
@@ -556,6 +566,7 @@ describe('realtime#socket event', function () {
     it('should change permission to freely when config allowAnonymous is true', function (done) {
       configMock.allowAnonymous = true
       configMock.allowAnonymousEdits = false
+      configMock.allowAnonymousViews = false
       realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
 
       permissionFunc('freely')
@@ -570,13 +581,80 @@ describe('realtime#socket event', function () {
       }, 5)
     })
 
-    it('should change permission to freely when config allowAnonymousEdits is true', function (done) {
+    it('should not change permission to freely when config allowAnonymousEdits is true', function (done) {
       configMock.allowAnonymous = false
       configMock.allowAnonymousEdits = true
+      configMock.allowAnonymousViews = false
       realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
 
       permissionFunc('freely')
 
+      setTimeout(() => {
+        assert(modelsMock.Note.update.called === false)
+        assert(checkViewPermissionSpy.called === false)
+        done()
+      }, 5)
+    })
+
+    it('should not change permission to freely when config allowAnonymousViews is true', function (done) {
+      configMock.allowAnonymous = false
+      configMock.allowAnonymousEdits = false
+      configMock.allowAnonymousViews = true
+      realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
+
+      permissionFunc('freely')
+
+      setTimeout(() => {
+        assert(modelsMock.Note.update.called === false)
+        assert(checkViewPermissionSpy.called === false)
+        done()
+      }, 5)
+    })
+
+    it('should change permission to editable when config allowAnonymousViews is true', function (done) {
+      configMock.allowAnonymous = false
+      configMock.allowAnonymousEdits = false
+      configMock.allowAnonymousViews = true
+      realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
+
+      permissionFunc('editable')
+
+      setTimeout(() => {
+        assert(checkViewPermissionSpy.callCount === 2)
+        assert(otherClient.emit.called === false)
+        assert(otherClient.disconnect.called === false)
+        assert(clientSocket.emit.called === false)
+        assert(clientSocket.disconnect.called === false)
+        done()
+      }, 5)
+    })
+
+    it('should change permission to freely when config allowAnonymousEdits and allowAnonymousViews are false true', function (done) {
+      configMock.allowAnonymous = false
+      configMock.allowAnonymousEdits = true
+      configMock.allowAnonymousViews = true
+      realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
+
+      permissionFunc('freely')
+
+      setTimeout(() => {
+        assert(checkViewPermissionSpy.callCount === 2)
+        assert(otherClient.emit.called === false)
+        assert(otherClient.disconnect.called === false)
+        assert(clientSocket.emit.called === false)
+        assert(clientSocket.disconnect.called === false)
+        done()
+      }, 5)
+    })
+
+    it('should change permission to editable when config allowAnonymousEdits and allowAnonymousViews are false true', function (done) {
+      configMock.allowAnonymous = false
+      configMock.allowAnonymousEdits = true
+      configMock.allowAnonymousViews = true
+      realtime.notes[noteId].socks = [clientSocket, undefined, otherClient]
+
+      permissionFunc('editable')
+
       setTimeout(() => {
         assert(checkViewPermissionSpy.callCount === 2)
         assert(otherClient.emit.called === false)