This is an automated email from the ASF dual-hosted git repository.

janhoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 9be9ffe4484 SOLR-8474: Fix Security screen stale refresh after config 
updates (flaky AdminUiSecurityAuthTest) (#4800)
9be9ffe4484 is described below

commit 9be9ffe44840d668d764eb7bcee4de6a06bcb0ef
Author: Jan Høydahl <[email protected]>
AuthorDate: Mon Aug 24 10:15:16 2026 +0200

    SOLR-8474: Fix Security screen stale refresh after config updates (flaky 
AdminUiSecurityAuthTest) (#4800)
---
 .../solr/webapp/AdminUiSecurityAuthTest.java       |   9 +-
 solr/webapp/web/js/angular/controllers/security.js | 159 +++++++++++++++------
 2 files changed, 119 insertions(+), 49 deletions(-)

diff --git 
a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java 
b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java
index db53010a6a6..8ac87a18af5 100644
--- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java
+++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSecurityAuthTest.java
@@ -26,7 +26,6 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 import org.openqa.selenium.By;
 import org.openqa.selenium.JavascriptExecutor;
-import org.openqa.selenium.WebElement;
 
 /**
  * Tests the Admin UI with BasicAuth enabled: the login screen flow and the 
Security screen,
@@ -61,12 +60,8 @@ public class AdminUiSecurityAuthTest extends AdminUiTestBase 
{
   public void testLoginAndSecurityScreen() throws Exception {
     // an unauthenticated visit is redirected to the login screen
     openPage("", By.id("login"));
-    WebElement username = waitFor(By.id("username"));
-    username.clear();
-    username.sendKeys(USER);
-    WebElement password = waitFor(By.id("password"));
-    password.clear();
-    password.sendKeys(PASS);
+    setText(By.id("username"), USER);
+    setText(By.id("password"), PASS);
     click(By.xpath("//div[@id='login']//button[@type='submit']"));
 
     // after login the dashboard loads and shows the authenticated security 
info
diff --git a/solr/webapp/web/js/angular/controllers/security.js 
b/solr/webapp/web/js/angular/controllers/security.js
index 776116eedd5..9dec02f8d02 100644
--- a/solr/webapp/web/js/angular/controllers/security.js
+++ b/solr/webapp/web/js/angular/controllers/security.js
@@ -107,6 +107,54 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
     return cause;
   }
 
+  /**
+   * Security config updates are persisted to ZooKeeper and reloaded by the 
nodes
+   * asynchronously, so a GET issued right after an update may still return the
+   * previous config. Polls the given endpoint until check(data) sees the 
update
+   * (giving up after ~10s), then invokes done.
+   */
+  function whenReflected(path, check, done) {
+    var attemptsLeft = 40;
+    function poll() {
+      Security.get({path: path}, function (data) {
+        if (--attemptsLeft <= 0 || check(data)) {
+          done();
+        } else {
+          $timeout(poll, 250);
+        }
+      }, function (e) {
+        $scope.errorHandler(e);
+        done();
+      });
+    }
+    poll();
+  }
+
+  /** Returns the basic-auth section of an authentication response, unwrapping 
multi-auth schemes. */
+  function findBasicAuthn(data) {
+    var authn = data.authentication;
+    if (authn && "schemes" in authn) {
+      for (var a in authn.schemes) {
+        if (authn.schemes[a]["scheme"] === "basic") {
+          return authn.schemes[a];
+        }
+      }
+    }
+    return authn;
+  }
+
+  function hasCredential(data, username) {
+    var authn = findBasicAuthn(data);
+    return authn != null && authn.credentials != null && username in 
authn.credentials;
+  }
+
+  /** Returns the roles of the named permission as a list, or null when the 
permission is absent. */
+  function permissionRoles(data, permName) {
+    var perms = data.authorization ? asList(data.authorization["permissions"]) 
: [];
+    var perm = perms.find(p => p.name === permName);
+    return perm ? asList(perm.role) : null;
+  }
+
   function truncateTo(str, maxLen, delim) {
     // allow for a little on either side of maxLen for better display
     var varLen = Math.min(Math.round(maxLen * 0.1), 15);
@@ -375,16 +423,8 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
           }
 
           // find the "basic" scheme if using multi-auth
-          var authn = data.authentication;
-          if ("schemes" in data.authentication) {
-            for (var a in data.authentication.schemes) {
-              if (data.authentication.schemes[a]["scheme"] === "basic") {
-                authn = data.authentication.schemes[a];
-                $scope.multiAuthWithBasic = true;
-                break;
-              }
-            }
-          }
+          var authn = findBasicAuthn(data);
+          $scope.multiAuthWithBasic = authn !== data.authentication;
 
           //console.log(">> authn: "+JSON.stringify(authn));
 
@@ -445,11 +485,17 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
       } // else, no new role for you!
     }
     var userRoles = Array.from(new Set(roles));
-    setUserRoles[$scope.upsertUser.username] = userRoles.length > 0 ? 
userRoles : null;
+    var username = $scope.upsertUser.username;
+    setUserRoles[username] = userRoles.length > 0 ? userRoles : null;
     var cmdJson = $scope.wrapSchemeCmd("set-user-role", setUserRoles);
     Security.post({path: "authorization"}, cmdJson, function (data) {
       $scope.toggleUserDialog();
-      $scope.refreshSecurityPanel();
+      whenReflected("authorization", function (data2) {
+        var authz = $scope.findEditableAuthz(data2);
+        if (!authz) return true;
+        var current = asList(authz["user-role"][username]);
+        return current.length === userRoles.length && userRoles.every(r => 
current.includes(r));
+      }, $scope.refreshSecurityPanel);
     });
   };
 
@@ -522,9 +568,9 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
           $scope.securityAPIErrorDetails = JSON.stringify(data);
           return;
         }
-        // TODO: shouldn't need this extra GET, but sometimes the config back 
from the server doesn't have our new user
-        // and doing this seems to avoid what looks like a race?
-        Security.get({path: "authentication"}, function (data2) {
+        whenReflected("authentication", function (data2) {
+          return hasCredential(data2, username);
+        }, function () {
           $scope.updateUserRoles();
         });
       });
@@ -534,16 +580,19 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
   };
 
   $scope.confirmDeleteUser = function() {
-    if (window.confirm("Confirm delete the '"+$scope.upsertUser.username+"' 
user?")) {
+    var username = $scope.upsertUser.username;
+    if (window.confirm("Confirm delete the '"+username+"' user?")) {
       // remove all roles for the user and the delete the user
       var removeRoles = {};
-      removeRoles[$scope.upsertUser.username] = null;
+      removeRoles[username] = null;
       var cmdJson = $scope.wrapSchemeCmd("set-user-role", removeRoles);
       Security.post({path: "authorization"}, cmdJson, function (data) {
-        var deleteUserCmd = $scope.wrapSchemeCmd("delete-user", 
[$scope.upsertUser.username]);
+        var deleteUserCmd = $scope.wrapSchemeCmd("delete-user", [username]);
         Security.post({path: "authentication"}, deleteUserCmd, function 
(data2) {
           $scope.toggleUserDialog();
-          $scope.refreshSecurityPanel();
+          whenReflected("authentication", function (data3) {
+            return !hasCredential(data3, username);
+          }, $scope.refreshSecurityPanel);
         });
       });
     }
@@ -650,7 +699,9 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
       var index = parseInt($scope.upsertPerm.index);
       Security.post({path: "authorization"}, { "delete-permission": index }, 
function (data) {
         $scope.togglePermDialog();
-        $scope.refreshSecurityPanel();
+        whenReflected("authorization", function (data2) {
+          return permissionRoles(data2, permName) == null;
+        }, $scope.refreshSecurityPanel);
       });
     }
   };
@@ -734,12 +785,12 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
       if ($scope.params && $scope.params.length > 0) {
         for (i in $scope.params) {
           var p = $scope.params[i];
-          var name = p.name.trim();
-          if (name !== "" && p.value) {
-            if (name in params) {
-              params[name].push(p.value);
+          var paramName = p.name.trim();
+          if (paramName !== "" && p.value) {
+            if (paramName in params) {
+              params[paramName].push(p.value);
             } else {
-              params[name] = [p.value];
+              params[paramName] = [p.value];
             }
           }
         }
@@ -777,10 +828,11 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
             return;
           }
           $scope.togglePermDialog();
-          // avoids a weird race with not getting the latest config after an 
update
-          Security.get({path: "authorization"}, function (ignore) {
-            $scope.refreshSecurityPanel();
-          });
+          whenReflected("authorization", function (data2) {
+            var have = permissionRoles(data2, setPermJson.name);
+            var want = asList(setPermJson.role);
+            return have != null && have.length === want.length && want.every(r 
=> have.includes(r));
+          }, $scope.refreshSecurityPanel);
         });
       });
     } else {
@@ -802,10 +854,11 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
         }
 
         $scope.togglePermDialog();
-        // avoids a weird race with not getting the latest config after an 
update
-        Security.get({path: "authorization"}, function (ignore) {
-          $scope.refreshSecurityPanel();
-        });
+        whenReflected("authorization", function (data2) {
+          var have = permissionRoles(data2, setPermJson.name);
+          var want = asList(setPermJson.role);
+          return have != null && have.length === want.length && want.every(r 
=> have.includes(r));
+        }, $scope.refreshSecurityPanel);
       });
     }
   };
@@ -1167,21 +1220,27 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
 
         var errorCause = checkError(data2);
         if (errorCause != null) {
-          $scope.securityAPIError = "set-user-role for "+username+" failed due 
to: "+errorCause;
+          $scope.securityAPIError = "set-user-role for role "+name+" failed 
due to: "+errorCause;
           $scope.securityAPIErrorDetails = JSON.stringify(data2);
           return;
         }
 
+        function roleReflected(data3) {
+          var authz3 = $scope.findEditableAuthz(data3);
+          if (!authz3) return true;
+          return usersForRole.every(u => 
asList(authz3["user-role"][u]).includes(name));
+        }
+
         if (perms.length === 0) {
           // close dialog and refresh the tables ...
           $scope.toggleRoleDialog();
-          $scope.refreshSecurityPanel();
+          whenReflected("authorization", roleReflected, 
$scope.refreshSecurityPanel);
           return;
         }
 
         var currentPerms = data.authorization["permissions"];
         for (i in perms) {
-          var permName = perms[i];
+          let permName = perms[i];
           var existingPerm = currentPerms.find(p => p.name === permName);
 
           if (existingPerm) {
@@ -1198,14 +1257,20 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
             }
             existingPerm.role = roleList;
             Security.post({path: "authorization"}, { "update-permission": 
existingPerm }, function (data3) {
-              $scope.refreshSecurityPanel();
+              whenReflected("authorization", function (data4) {
+                var have = permissionRoles(data4, permName);
+                return roleReflected(data4) && have != null && 
have.includes(name);
+              }, $scope.refreshSecurityPanel);
             });
           } else {
             // new perm ... must be a predefined ...
             if ($scope.predefinedPermissions.includes(permName)) {
               var setPermission = {name: permName, role:[name]};
               Security.post({path: "authorization"}, { "set-permission": 
setPermission }, function (data3) {
-                $scope.refreshSecurityPanel();
+                whenReflected("authorization", function (data4) {
+                  var have = permissionRoles(data4, permName);
+                  return roleReflected(data4) && have != null && 
have.includes(name);
+                }, $scope.refreshSecurityPanel);
               });
             } // else ignore it
           }
@@ -1234,16 +1299,26 @@ solrAdminApp.controller('SecurityController', function 
($scope, $timeout, $cooki
   };
 
   $scope.onBlockUnknownChange = function() {
-    var cmdJson = $scope.wrapSchemeCmd("set-property", { "blockUnknown": 
$scope.blockUnknown === "true" });
+    var blockUnknown = $scope.blockUnknown === "true";
+    var cmdJson = $scope.wrapSchemeCmd("set-property", { "blockUnknown": 
blockUnknown });
     Security.post({path: "authentication"}, cmdJson, function (data) {
-      $scope.refreshSecurityPanel();
+      whenReflected("authentication", function (data2) {
+        var authn = findBasicAuthn(data2);
+        if (authn == null) return true;
+        var v = authn["blockUnknown"];
+        return (v !== false && v !== "false") === blockUnknown;
+      }, $scope.refreshSecurityPanel);
     });
   };
 
   $scope.onForwardCredsChange = function() {
-    var cmdJson = $scope.wrapSchemeCmd("set-property", { "forwardCredentials": 
$scope.forwardCredentials === "true" });
+    var forwardCredentials = $scope.forwardCredentials === "true";
+    var cmdJson = $scope.wrapSchemeCmd("set-property", { "forwardCredentials": 
forwardCredentials });
     Security.post({path: "authentication"}, cmdJson, function (data) {
-      $scope.refreshSecurityPanel();
+      whenReflected("authentication", function (data2) {
+        var authn = findBasicAuthn(data2);
+        return authn != null && (authn["forwardCredentials"] === true) === 
forwardCredentials;
+      }, $scope.refreshSecurityPanel);
     });
   };
 

Reply via email to