Merge "CheckAccess: don't catch PermissionBackendException"
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 84a3d87..5905800 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -105,8 +105,7 @@
         permissionBackend.user(user).database(db).project(projectState.getNameKey());
   }
 
-  Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
-      throws PermissionBackendException {
+  Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts) {
     if (projectState.isAllUsers()) {
       refs = addUsersSelfSymref(refs);
     }
@@ -283,7 +282,10 @@
             .ref(visibleChanges.get(id).get())
             .check(RefPermission.READ_PRIVATE_CHANGES);
         return true;
-      } catch (PermissionBackendException | AuthException e) {
+      } catch (AuthException e) {
+        return false;
+      } catch (PermissionBackendException e) {
+        log.error("Failed to check permission for " + id + " in " + projectState.getName(), e);
         return false;
       }
     }
@@ -336,7 +338,7 @@
         return r.notes();
       }
     } catch (PermissionBackendException e) {
-      log.warn("Failed to check permission for " + r.id() + " in " + projectState.getName(), e);
+      log.error("Failed to check permission for " + r.id() + " in " + projectState.getName(), e);
     }
     return null;
   }
diff --git a/java/com/google/gerrit/server/schema/GroupBundle.java b/java/com/google/gerrit/server/schema/GroupBundle.java
index 302ea55..3c1c409 100644
--- a/java/com/google/gerrit/server/schema/GroupBundle.java
+++ b/java/com/google/gerrit/server/schema/GroupBundle.java
@@ -195,7 +195,7 @@
         Timestamp createdOn = rs.getTimestamp(3);
         String description = rs.getString(4);
         AccountGroup.UUID ownerGroupUuid = new AccountGroup.UUID(rs.getString(5));
-        boolean visibleToAll = rs.getBoolean(6);
+        boolean visibleToAll = "Y".equals(rs.getString(6));
 
         AccountGroup group = new AccountGroup(groupName, groupId, groupUuid, createdOn);
         group.setDescription(description);
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index ff6f2d6..20d4c43 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -42,6 +42,8 @@
     view: 'gr-plugin-list',
   }];
 
+  const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
+
   const ACCOUNT_CAPABILITIES = ['createProject', 'createGroup', 'viewPlugins'];
 
   Polymer({
@@ -58,6 +60,7 @@
         type: Number,
         observer: '_computeGroupName',
       },
+      _groupIsInternal: Boolean,
       _groupName: String,
       _groupOwner: {
         type: Boolean,
@@ -160,16 +163,17 @@
             name: this._groupName,
             view: Gerrit.Nav.View.GROUP,
             url: Gerrit.Nav.getUrlForGroup(this._groupId),
-            children: [
-              {
-                name: 'Members',
-                detailType: Gerrit.Nav.GroupDetailView.MEMBERS,
-                view: Gerrit.Nav.View.GROUP,
-                url: Gerrit.Nav.getUrlForGroupMembers(this._groupId),
-              },
-            ],
+            children: [],
           };
-          if (this._isAdmin || this._groupOwner) {
+          if (this._groupIsInternal) {
+            linkCopy.subsection.children.push({
+              name: 'Members',
+              detailType: Gerrit.Nav.GroupDetailView.MEMBERS,
+              view: Gerrit.Nav.View.GROUP,
+              url: Gerrit.Nav.getUrlForGroupMembers(this._groupId),
+            });
+          }
+          if (this._groupIsInternal && (this._isAdmin || this._groupOwner)) {
             linkCopy.subsection.children.push(
                 {
                   name: 'Audit Log',
@@ -295,6 +299,7 @@
         if (!group || !group.name) { return; }
 
         this._groupName = group.name;
+        this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
         this.reload();
 
         promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index a488245..4cdf8c8 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -312,6 +312,7 @@
           sandbox.stub(element.$.restAPI, 'getGroupConfig')
               .returns(Promise.resolve({
                 name: 'foo',
+                id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9',
               }));
           sandbox.stub(element.$.restAPI, 'getIsGroupOwner')
               .returns(Promise.resolve(true));
@@ -329,7 +330,7 @@
           assert.equal(selected.textContent.trim(), 'Groups');
         });
 
-        test('group', () => {
+        test('internal group', () => {
           element.params = {
             view: Gerrit.Nav.View.GROUP,
             groupId: 1234,
@@ -337,6 +338,34 @@
           element._groupName = 'foo';
           return element.reload().then(() => {
             flushAsynchronousOperations();
+            const subsectionItems = Polymer.dom(element.root)
+                .querySelectorAll('.subsectionItem');
+            assert.equal(subsectionItems.length, 2);
+            assert.isTrue(element._groupIsInternal);
+            const selected = element.$$('gr-page-nav .selected');
+            assert.isOk(selected);
+            assert.equal(selected.textContent.trim(), 'foo');
+          });
+        });
+
+        test('external group', () => {
+          element.$.restAPI.getGroupConfig.restore();
+          sandbox.stub(element.$.restAPI, 'getGroupConfig')
+              .returns(Promise.resolve({
+                name: 'foo',
+                id: 'external-id',
+              }));
+          element.params = {
+            view: Gerrit.Nav.View.GROUP,
+            groupId: 1234,
+          };
+          element._groupName = 'foo';
+          return element.reload().then(() => {
+            flushAsynchronousOperations();
+            const subsectionItems = Polymer.dom(element.root)
+                .querySelectorAll('.subsectionItem');
+            assert.equal(subsectionItems.length, 0);
+            assert.isFalse(element._groupIsInternal);
             const selected = element.$$('gr-page-nav .selected');
             assert.isOk(selected);
             assert.equal(selected.textContent.trim(), 'foo');
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
index 21978d5..e9b7aa3 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
@@ -72,9 +72,9 @@
                 <gr-autocomplete
                     id="groupNameInput"
                     text="{{_groupConfig.name}}"
-                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]"></gr-autocomplete>
+                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></gr-autocomplete>
               </span>
-              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
                 <gr-button
                     id="inputUpdateNameBtn"
                     on-tap="_handleSaveName"
@@ -90,10 +90,10 @@
                 <gr-autocomplete
                     text="{{_groupConfig.owner}}"
                     query="[[_query]]"
-                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
                 </gr-autocomplete>
               </span>
-              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
                 <gr-button
                     on-tap="_handleSaveOwner"
                     disabled="[[!_owner]]">
@@ -109,9 +109,9 @@
                     class="description"
                     autocomplete="on"
                     bind-value="{{_groupConfig.description}}"
-                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]"></iron-autogrow-textarea>
+                    disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></iron-autogrow-textarea>
               </div>
-              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
                 <gr-button
                     on-tap="_handleSaveDescription"
                     disabled="[[!_description]]">
@@ -131,7 +131,7 @@
                   <gr-select
                       id="visibleToAll"
                       bind-value="{{_groupConfig.options.visible_to_all}}">
-                    <select disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+                    <select disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
                       <template is="dom-repeat" items="[[_submitTypes]]">
                         <option value="[[item.value]]">[[item.label]]</option>
                       </template>
@@ -139,7 +139,7 @@
                   </gr-select>
                 </span>
               </section>
-              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin)]]">
+              <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]">
                 <gr-button
                     on-tap="_handleSaveOptions"
                     disabled="[[!_options]]">
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
index 1e16720..5c02691 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -17,6 +17,8 @@
 (function() {
   'use strict';
 
+  const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
+
   const OPTIONS = {
     submitFalse: {
       value: false,
@@ -43,6 +45,7 @@
         type: Boolean,
         value: false,
       },
+      _groupIsInternal: Boolean,
       _description: {
         type: Boolean,
         value: false,
@@ -109,6 +112,7 @@
             if (!config || !config.name) { return Promise.resolve(); }
 
             this._groupName = config.name;
+            this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
 
             promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
               this._isAdmin = isAdmin ? true : false;
@@ -149,7 +153,8 @@
           .then(config => {
             if (config.status === 200) {
               this._groupName = this._groupConfig.name;
-              this.fire('name-changed', {name: this._groupConfig.name});
+              this.fire('name-changed', {name: this._groupConfig.name,
+                external: this._groupIsExtenral});
               this._rename = false;
             }
           });
@@ -222,8 +227,8 @@
           });
     },
 
-    _computeGroupDisabled(owner, admin) {
-      return admin || owner ? false : true;
+    _computeGroupDisabled(owner, admin, groupIsInternal) {
+      return groupIsInternal && (admin || owner) ? false : true;
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
index 4ee7414a..a0fc71a 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
@@ -40,8 +40,7 @@
     const group = {
       id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
       url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
-      options: {
-      },
+      options: {},
       description: 'Gerrit Site Administrators',
       group_id: 1,
       owner: 'Administrators',
@@ -72,12 +71,31 @@
           .display === 'none');
     });
 
-    test('default values are populated', done => {
+    test('default values are populated with internal group', done => {
       sandbox.stub(element.$.restAPI, 'getIsGroupOwner', () => {
         return Promise.resolve(true);
       });
       element.groupId = 1;
       element._loadGroup().then(() => {
+        assert.isTrue(element._groupIsInternal);
+        assert.isFalse(element.$.visibleToAll.bindValue);
+        done();
+      });
+    });
+
+    test('default values with external group', done => {
+      const groupExternal = Object.assign({}, group);
+      groupExternal.id = 'external-group-id';
+      groupStub.restore();
+      groupStub = sandbox.stub(element.$.restAPI, 'getGroupConfig', () => {
+        return Promise.resolve(groupExternal);
+      });
+      sandbox.stub(element.$.restAPI, 'getIsGroupOwner', () => {
+        return Promise.resolve(true);
+      });
+      element.groupId = 1;
+      element._loadGroup().then(() => {
+        assert.isFalse(element._groupIsInternal);
         assert.isFalse(element.$.visibleToAll.bindValue);
         done();
       });
@@ -161,28 +179,32 @@
           });
     });
 
-    test('_computeGroupDisabled return false for admin', () => {
-      const admin = true;
-      const owner = false;
-      assert.equal(element._computeGroupDisabled(owner, admin), false);
-    });
+    test('_computeGroupDisabled', () => {
+      let admin = true;
+      let owner = false;
+      let groupIsInternal = true;
+      assert.equal(element._computeGroupDisabled(owner, admin,
+          groupIsInternal), false);
 
-    test('_computeGroupDisabled return true for admin', () => {
-      const admin = false;
-      const owner = false;
-      assert.equal(element._computeGroupDisabled(owner, admin), true);
-    });
+      admin = false;
+      assert.equal(element._computeGroupDisabled(owner, admin,
+          groupIsInternal), true);
 
-    test('_computeGroupDisabled return false for owner', () => {
-      const admin = false;
-      const owner = true;
-      assert.equal(element._computeGroupDisabled(owner, admin), false);
-    });
+      owner = true;
+      assert.equal(element._computeGroupDisabled(owner, admin,
+          groupIsInternal), false);
 
-    test('_computeGroupDisabled return true for owner', () => {
-      const admin = false;
-      const owner = false;
-      assert.equal(element._computeGroupDisabled(owner, admin), true);
+      owner = false;
+      assert.equal(element._computeGroupDisabled(owner, admin,
+          groupIsInternal), true);
+
+      groupIsInternal = false;
+      assert.equal(element._computeGroupDisabled(owner, admin,
+          groupIsInternal), true);
+
+      admin = true;
+      assert.equal(element._computeGroupDisabled(owner, admin,
+          groupIsInternal), true);
     });
 
     test('_computeLoadingClass', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 42404aa..ff4562b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -611,6 +611,7 @@
           on-send="_handleReplySent"
           on-cancel="_handleReplyCancel"
           on-autogrow="_handleReplyAutogrow"
+          on-send-disabled-changed="_resetReplyOverlayFocusStops"
           hidden$="[[!_loggedIn]]">
       </gr-reply-dialog>
     </gr-overlay>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 9be7db4..5c6859d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -999,7 +999,7 @@
      */
     _openReplyDialog(opt_section) {
       this.$.replyOverlay.open().then(() => {
-        this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+        this._resetReplyOverlayFocusStops();
         this.$.replyDialog.open(opt_section);
         Polymer.dom.flush();
         this.$.replyOverlay.center();
@@ -1554,5 +1554,9 @@
     _handleStopEditTap() {
       Gerrit.Nav.navigateToChange(this._change, this._patchRange.patchNum);
     },
+
+    _resetReplyOverlayFocusStops() {
+      this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index 0cfae94..af3acc4 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -115,7 +115,7 @@
       sandbox.restore();
     });
 
-    test('send blocked when invalid email is supplied to ccs', () => {
+    test('_submit blocked when invalid email is supplied to ccs', () => {
       const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
       // Stub the below function to avoid side effects from the send promise
       // resolving.
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 088875f..8225dac 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -64,6 +64,8 @@
         display: flex;
         justify-content: space-between;
         position: sticky;
+        /* @see Issue 8602 */
+        z-index: 1;
       }
       .actions .right gr-button {
         margin-left: 1em;
@@ -291,9 +293,10 @@
               class="action cancel"
               on-tap="_cancelTapHandler">Cancel</gr-button>
           <gr-button
+              id="sendButton"
               link
               primary
-              disabled="[[_computeSendButtonDisabled(_sendButtonLabel, diffDrafts, draft, _reviewersMutated, _labelsChanged, _includeComments)]]"
+              disabled="[[_sendDisabled]]"
               class="action send"
               has-tooltip
               title$="[[_computeSendButtonTooltip(canBeStarted)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 3875e25..e8c720a 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -52,6 +52,8 @@
   // googlesource.com.
   const START_REVIEW_MESSAGE = 'This change is ready for review.';
 
+  const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.';
+
   Polymer({
     is: 'gr-reply-dialog',
 
@@ -87,6 +89,12 @@
      * @event comment-refresh
      */
 
+     /**
+      * Fires when the state of the send button (enabled/disabled) changes.
+      *
+      * @event send-disabled-changed
+      */
+
     properties: {
       /**
        * @type {{ _number: number, removable_reviewers: Array }}
@@ -206,6 +214,12 @@
         type: String,
         value: '',
       },
+      _sendDisabled: {
+        type: Boolean,
+        computed: '_computeSendButtonDisabled(_sendButtonLabel, diffDrafts, ' +
+            'draft, _reviewersMutated, _labelsChanged, _includeComments)',
+        observer: '_sendDisabledChanged',
+      },
     },
 
     FocusTarget,
@@ -273,9 +287,10 @@
     },
 
     getFocusStops() {
+      const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton;
       return {
         start: this.$.reviewers.focusStart,
-        end: this.$.sendButton,
+        end,
       };
     },
 
@@ -726,6 +741,13 @@
         // the text field of the CC entry.
         return;
       }
+      if (this._sendDisabled) {
+        this.dispatchEvent(new CustomEvent('show-alert', {
+          bubbles: true,
+          detail: {message: EMPTY_REPLY_MESSAGE},
+        }));
+        return;
+      }
       return this.send(this._includeComments, this.canBeStarted)
           .then(keepReviewers => {
             this._purgeReviewersPendingRemove(false, keepReviewers);
@@ -856,5 +878,9 @@
     setPluginMessage(message) {
       this._pluginMessage = message;
     },
+
+    _sendDisabledChanged(sendDisabled) {
+      this.dispatchEvent(new CustomEvent('send-disabled-changed'));
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index b9eebdc..41d7b49 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -1100,6 +1100,33 @@
       assert.isFalse(fn('Send', {}, '', false, true, false));
     });
 
+    test('_submit blocked when no mutations exist', () => {
+      const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve());
+      // Stub the below function to avoid side effects from the send promise
+      // resolving.
+      sandbox.stub(element, '_purgeReviewersPendingRemove');
+      element.diffDrafts = {};
+      flushAsynchronousOperations();
+
+      MockInteractions.tap(element.$$('gr-button.send'));
+      assert.isFalse(sendStub.called);
+
+      element.diffDrafts = {test: true};
+      flushAsynchronousOperations();
+
+      MockInteractions.tap(element.$$('gr-button.send'));
+      assert.isTrue(sendStub.called);
+    });
+
+    test('getFocusStops', () => {
+      // Setting diffDrafts to an empty object causes _sendDisabled to be
+      // computed to false.
+      element.diffDrafts = {};
+      assert.equal(element.getFocusStops().end, element.$.cancelButton);
+      element.diffDrafts = {test: true};
+      assert.equal(element.getFocusStops().end, element.$.sendButton);
+    });
+
     test('setPluginMessage', () => {
       element.setPluginMessage('foo');
       assert.equal(element.$.pluginMessage.textContent, 'foo');