text: /*
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at
  
       http://www.apache.org/licenses/LICENSE-2.0
  
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
  */
  
  solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cookies, $window, Constants, System, Security) {
    $scope.resetMenu("security", Constants.IS_ROOT_PAGE);
  
    $scope.params = [];
  
    var strongPasswordRegex = /^(?=.*[0-9])(?=.*[!@#$%^&*\-_()[\]])[a-zA-Z0-9!@#$%^&*\-_()[\]]{8,30}$/;
  
    function toList(str) {
      if (Array.isArray(str)) {
        return str; // already a list
      }
      return str.trim().split(",").map(s => s.trim()).filter(s => s !== "");
    }
  
    function asList(listOrStr) {
      return Array.isArray(listOrStr) ? listOrStr : (listOrStr ? [listOrStr] : []);
    }
  
    function transposeUserRoles(userRoles) {
      var roleUsers = {};
      for (var u in userRoles) {
        var roleList = asList(userRoles[u]);
        for (var i in roleList) {
          var role = roleList[i];
          if (!roleUsers[role]) roleUsers[role] = []
          roleUsers[role].push(u);
        }
      }
  
      var roles = [];
      for (var r in roleUsers) {
        roles.push({"name":r, "users":Array.from(new Set(roleUsers[r]))});
      }
      return roles.sort((a, b) => (a.name > b.name) ? 1 : -1);
    }
  
    function roleMatch(roles, rolesForUser) {
      for (r in rolesForUser) {
        if (roles.includes(rolesForUser[r]))
          return true;
      }
      return false;
    }
  
    function permRow(perm, i) {
      var roles = asList(perm.role);
      var paths = asList(perm.path);
  
      var collectionNames = "";
      var collections = [];
      if ("collection" in perm) {
        if (perm["collection"] == null) {
          collectionNames = "null";
        } else {
          collections = asList(perm.collection);
          collectionNames = collections.sort().join(", ");
        }
      } else {
        // no collection property on the perm, so the default "*" applies
        collectionNames = "";
        collections.push("*");
      }
  
      var method = asList(perm.method);
  
      // perms don't always have an index ?!?
      var index = "index" in perm ? perm["index"] : ""+i;
  
      return { "index": index, "name": perm.name, "collectionNames": collectionNames, "collections": collections,
        "roles": roles, "paths": paths, "method": method, "params": perm.params };
    }
  
    function checkError(data) {
      var cause = null;
      if ("errorMessages" in data && Array.isArray(data["errorMessages"]) && data["errorMessages"].length > 0) {
        cause = "?";
        if ("errorMessages" in data["errorMessages"][0] && Array.isArray(data["errorMessages"][0]["errorMessages"]) && data["errorMessages"][0]["errorMessages"].length > 0) {
          cause = data["errorMessages"][0]["errorMessages"][0];
        }
      }
      return cause;
    }
  
    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);
      if (str.length <= maxLen + varLen) {
        return str;
      }
  
      var total = str.split(delim).length;
      var at = str.indexOf(delim, maxLen - varLen);
      str = (at !== -1 && at < maxLen + varLen) ? str.substring(0, at) : str.substring(0, maxLen);
      var trimmed = str.split(delim).length;
      var diff = total - trimmed;
      str += " ... "+(diff > 1 ? "(+"+diff+" more)" : "");
      return str;
    }
  
    $scope.closeErrorDialog = function () {
      delete $scope.securityAPIError;
      delete $scope.securityAPIErrorDetails;
    };
  
    $scope.displayList = function(listOrStr) {
      if (!listOrStr) return "";
      var str = Array.isArray(listOrStr) ? listOrStr.sort().join(", ") : (""+listOrStr).trim();
      return truncateTo(str, 160, ", ");
    };
  
    $scope.displayParams = function(obj) {
      if (!obj) return "";
      if (Array.isArray(obj)) return obj.sort().join(", ");
  
      var display = "";
      for (const [key, value] of Object.entries(obj)) {
        if (display.length > 0) display += "; ";
        display += (key + "=" + (Array.isArray(value)?value.sort().join(","):value+""));
      }
      return truncateTo(display, 160, "; ");
    };
  
    $scope.displayRoles = function(obj) {
      return (!obj || (Array.isArray(obj) && obj.length === 0)) ? "null" : $scope.displayList(obj);
    };
  
    $scope.predefinedPermissions = ["collection-admin-edit", "collection-admin-read", "core-admin-read", "core-admin-edit", "zk-read",
      "read", "update", "all", "config-edit", "config-read", "schema-read", "schema-edit", "security-edit", "security-read",
      "metrics-read", "filestore-read", "filestore-write", "package-edit", "package-read"].sort();
  
    $scope.predefinedPermissionCollection = {"read":"*", "update":"*", "config-edit":"*", "config-read":"*", "schema-edit":"*", "schema-read":"*"};
  
    $scope.errorHandler = function (e) {
      var error = e.data && e.data.error ? e.data.error : null;
      if (error && error.msg) {
        $scope.securityAPIError = error.msg;
        $scope.securityAPIErrorDetails = e.data.errorDetails;
      } else if (e.data && e.data.message) {
        $scope.securityAPIError = e.data.message;
        $scope.securityAPIErrorDetails = JSON.stringify(e.data);
      }
    };
  
    $scope.showHelp = function (id) {
      if ($scope.helpId && ($scope.helpId === id || id === '')) {
        delete $scope.helpId;
      } else {
        $scope.helpId = id;
      }
    };
  
    $scope.refresh = function () {
      $scope.hideAll();
  
      $scope.blockUnknown = "false"; // default setting
      $scope.realmName = "solr";
      $scope.forwardCredentials = "false";
  
      $scope.currentUser = sessionStorage.getItem("auth.username");
  
      $scope.userFilter = "";
      $scope.userFilterOption = "";
      $scope.userFilterText = "";
      $scope.userFilterOptions = [];
  
      $scope.permFilter = "";
      $scope.permFilterOption = "";
      $scope.permFilterOptions = [];
      $scope.permFilterTypes = ["", "name", "role", "path", "collection"];
  
      System.get(function(data) {
        // console.log(">> system: "+JSON.stringify(data));
        $scope.authenticationPlugin = data.security ? data.security["authenticationPlugin"] : null;
        $scope.authorizationPlugin = data.security ? data.security["authorizationPlugin"] : null;
        $scope.myRoles = data.security ? data.security["roles"] : [];
        $scope.isSecurityAdminEnabled = $scope.authenticationPlugin != null;
        $scope.isCloudMode = data.mode.match( /solrcloud/i ) != null;
        $scope.zkHost = $scope.isCloudMode ? data["zkHost"] : "";
        $scope.solrHome = data["solr_home"];
        $scope.refreshSecurityPanel();
      }, function(e) {
        if (e.status === 403) {
          $scope.isSecurityAdminEnabled = true;
          $scope.hasSecurityEditPerm = false;
          $scope.hideAll();
        }
      });
    };
  
    $scope.hideAll = function () {
      // add more dialogs here
      delete $scope.validationError;
      $scope.showUserDialog = false;
      $scope.showPermDialog = false;
      delete $scope.helpId;
    };
  
    $scope.getCurrentUserRoles = function() {
      if ($scope.manageUserRolesEnabled) {
        return Array.isArray($scope.userRoles[$scope.currentUser]) ? $scope.userRoles[$scope.currentUser] : [$scope.userRoles[$scope.currentUser]];
      } else {
        return $scope.myRoles;
      }
    };
  
    $scope.hasPermission = function(permissionName) {
      var rolesForPermission = $scope.permissionsTable.filter(p => permissionName === p.name).flatMap(p => p.roles);
      return (rolesForPermission.length > 0 && roleMatch(rolesForPermission, $scope.getCurrentUserRoles()));
    };
  
    $scope.refreshSecurityPanel = function() {
  
      // determine if the authorization plugin supports CRUD permissions
      $scope.managePermissionsEnabled =
          ($scope.authorizationPlugin === "org.apache.solr.security.RuleBasedAuthorizationPlugin" ||
              $scope.authorizationPlugin === "org.apache.solr.security.ExternalRoleRuleBasedAuthorizationPlugin");
  
      // don't allow CRUD on roles if using external
      $scope.manageUserRolesEnabled = $scope.authorizationPlugin === "org.apache.solr.security.RuleBasedAuthorizationPlugin";
  
      Security.get({path: "authorization"}, function (data) {
        if (!data.authorization) {
          $scope.isSecurityAdminEnabled = false;
          $scope.hasSecurityEditPerm = false;
          return;
        }
  
        if ($scope.manageUserRolesEnabled) {
          $scope.userRoles = data.authorization["user-role"];
          $scope.roles = transposeUserRoles($scope.userRoles);
          $scope.filteredRoles = $scope.roles;
          $scope.roleNames = $scope.roles.map(r => r.name).sort();
          $scope.roleNamesWithWildcard = ["*"].concat($scope.roleNames);
          if (!$scope.permFilterTypes.includes("user")) {
            $scope.permFilterTypes.push("user"); // can only filter perms by user if we have a role to user mapping
          }
        } else {
          $scope.userRoles = {};
          $scope.roles = [];
          $scope.filteredRoles = [];
          $scope.roleNames = [];
        }
  
        $scope.permissions = data.authorization["permissions"];
        $scope.permissionsTable = [];
        for (p in $scope.permissions) {
          $scope.permissionsTable.push(permRow($scope.permissions[p], parseInt(p)+1));
        }
        $scope.filteredPerms = $scope.permissionsTable;
  
        $scope.hasSecurityEditPerm = $scope.hasPermission("security-edit");
        $scope.hasSecurityReadPerm = $scope.hasSecurityEditPerm || $scope.hasPermission("security-read");
  
        if ($scope.authenticationPlugin === "org.apache.solr.security.BasicAuthPlugin") {
          $scope.manageUsersEnabled = true;
  
          Security.get({path: "authentication"}, function (data) {
            if (!data.authentication) {
              // TODO: error msg
              $scope.manageUsersEnabled = false;
            }
  
            $scope.blockUnknown = data.authentication["blockUnknown"] === true ? "true" : "false";
            $scope.forwardCredentials = data.authentication["forwardCredentials"] === true ? "true" : "false";
  
            if ("realm" in data.authentication) {
              $scope.realmName = data.authentication["realm"];
            }
  
            var users = [];
            if (data.authentication.credentials) {
              for (var u in data.authentication.credentials) {
                var roles = $scope.userRoles[u];
                if (!roles) roles = [];
                users.push({"username":u, "roles":roles});
              }
            }
            $scope.users = users.sort((a, b) => (a.username > b.username) ? 1 : -1);
            $scope.filteredUsers = $scope.users.slice(0,100); // only display first 100
          }, $scope.errorHandler);
        } else {
          $scope.users = [];
          $scope.filteredUsers = $scope.users;
          $scope.manageUsersEnabled = false;
        }
      }, $scope.errorHandler);
    };
  
    $scope.validatePassword = function() {
      var password = $scope.upsertUser.password.trim();
      var password2 = $scope.upsertUser.password2 ? $scope.upsertUser.password2.trim() : "";
      if (password !== password2) {
        $scope.validationError = "Passwords do not match!";
        return false;
      }
  
      if (!password.match(strongPasswordRegex)) {
        $scope.validationError = "Password not strong enough! Must contain at least one lowercase letter, one uppercase letter, one digit, and one of these special characters: !@#$%^&*_-[]()";
        return false;
      }
  
      return true;
    };
  
    $scope.updateUserRoles = function() {
      var setUserRoles = {};
      var roles = [];
      if ($scope.upsertUser.selectedRoles) {
        roles = roles.concat($scope.upsertUser.selectedRoles);
      }
      if ($scope.upsertUser.newRole && $scope.upsertUser.newRole.trim() !== "") {
        var newRole = $scope.upsertUser.newRole.trim();
        if (newRole !== "null" && newRole !== "*" && newRole.length <= 30) {
          roles.push(newRole);
        } // else, no new role for you!
      }
      var userRoles = Array.from(new Set(roles));
      setUserRoles[$scope.upsertUser.username] = userRoles.length > 0 ? userRoles : null;
      Security.post({path: "authorization"}, { "set-user-role": setUserRoles }, function (data) {
        $scope.toggleUserDialog();
        $scope.refreshSecurityPanel();
      });
    };
  
    $scope.doUpsertUser = function() {
      if (!$scope.upsertUser) {
        delete $scope.validationError;
        $scope.showUserDialog = false;
        return;
      }
  
      if (!$scope.upsertUser.username || $scope.upsertUser.username.trim() === "") {
        $scope.validationError = "Username is required!";
        return;
      }
  
      // keep username to a reasonable length? but allow for email addresses
      var username = $scope.upsertUser.username.trim();
      if (username.length > 30) {
        $scope.validationError = "Username must be 30 characters or less!";
        return;
      }
  
      var doSetUser = false;
      if ($scope.userDialogMode === 'add') {
        if ($scope.users) {
          var existing = $scope.users.find(u => u.username === username);
          if (existing) {
            $scope.validationError = "User '"+username+"' already exists!";
            return;
          }
        }
  
        if (!$scope.upsertUser.password) {
          $scope.validationError = "Password is required!";
          return;
        }
  
        if (!$scope.validatePassword()) {
          return;
        }
        doSetUser = true;
      } else {
        if ($scope.upsertUser.password) {
          if ($scope.validatePassword()) {
            doSetUser = true;
          } else {
            return; // update to password is invalid
          }
        } // else no update to password
      }
  
      if ($scope.upsertUser.newRole && $scope.upsertUser.newRole.trim() !== "") {
        var newRole = $scope.upsertUser.newRole.trim();
        if (newRole === "null" || newRole === "*" || newRole.length > 30) {
          $scope.validationError = "Invalid new role: "+newRole;
          return;
        }
      }
  
      delete $scope.validationError;
  
      if (doSetUser) {
        var setUserJson = {};
        setUserJson[username] = $scope.upsertUser.password.trim();
        Security.post({path: "authentication"}, { "set-user": setUserJson }, function (data) {
  
          var errorCause = checkError(data);
          if (errorCause != null) {
            $scope.securityAPIError = "create user "+username+" failed due to: "+errorCause;
            $scope.securityAPIErrorDetails = JSON.stringify(data);
            return;
          }
  
          $scope.updateUserRoles();
        });
      } else {
        $scope.updateUserRoles();
      }
    };
  
    $scope.confirmDeleteUser = function() {
      if (window.confirm("Confirm delete the '"+$scope.upsertUser.username+"' user?")) {
        // remove all roles for the user and the delete the user
        var removeRoles = {};
        removeRoles[$scope.upsertUser.username] = null;
        Security.post({path: "authorization"}, { "set-user-role": removeRoles }, function (data) {
          Security.post({path: "authentication"}, {"delete-user": [$scope.upsertUser.username]}, function (data2) {
            $scope.toggleUserDialog();
            $scope.refreshSecurityPanel();
          });
        });
      }
    };
  
    $scope.showAddUserDialog = function() {
      $scope.userDialogMode = "add";
      $scope.userDialogHeader = "Add New User";
      $scope.userDialogAction = "Add User";
      $scope.upsertUser = {};
      $scope.toggleUserDialog();
    };
  
    $scope.toggleUserDialog = function() {
      if ($scope.showUserDialog) {
        delete $scope.upsertUser;
        delete $scope.validationError;
        $scope.showUserDialog = false;
        return;
      }
  
      $scope.hideAll();
      $('#user-dialog').css({left: 132, top: 132});
      $scope.showUserDialog = true;
    };
  
    $scope.onPredefinedChanged = function() {
      if (!$scope.upsertPerm) {
        return;
      }
  
      if ($scope.upsertPerm.name && $scope.upsertPerm.name.trim() !== "") {
        delete $scope.selectedPredefinedPermission;
      } else {
        $scope.upsertPerm.name = "";
      }
  
      if ($scope.selectedPredefinedPermission && $scope.selectedPredefinedPermission in $scope.predefinedPermissionCollection) {
        $scope.upsertPerm.collection = $scope.predefinedPermissionCollection[$scope.selectedPredefinedPermission];
      }
  
      $scope.isPermFieldDisabled = ($scope.upsertPerm.name === "" && $scope.selectedPredefinedPermission);
    };
  
    $scope.showAddPermDialog = function() {
      $scope.permDialogMode = "add";
      $scope.permDialogHeader = "Add New Permission";
      $scope.permDialogAction = "Add Permission";
      $scope.upsertPerm = {};
      $scope.upsertPerm.name = "";
      $scope.upsertPerm.index = "";
      $scope.upsertPerm["method"] = {"get":"true", "post":"true", "put":"true", "delete":"true"};
      $scope.isPermFieldDisabled = false;
      delete $scope.selectedPredefinedPermission;
  
      $scope.params = [{"name":"", "value":""}];
  
      var permissionNames = $scope.permissions.map(p => p.name);
      $scope.filteredPredefinedPermissions = $scope.predefinedPermissions.filter(p => !permissionNames.includes(p));
  
      $scope.togglePermDialog();
    };
  
    $scope.togglePermDialog = function() {
      if ($scope.showPermDialog) {
        delete $scope.upsertPerm;
        delete $scope.validationError;
        $scope.showPermDialog = false;
        $scope.isPermFieldDisabled = false;
        delete $scope.selectedPredefinedPermission;
        return;
      }
  
      $scope.hideAll();
  
      var leftPos = $scope.permDialogMode === "add" ? 500 : 100;
      var topPos = $('#permissions').offset().top - 320;
      if (topPos < 0) topPos = 0;
      $('#add-permission-dialog').css({left: leftPos, top: topPos});
  
      $scope.showPermDialog = true;
    };
  
    $scope.getMethods = function() {
      var methods = [];
      if ($scope.upsertPerm.method.get === "true") {
        methods.push("GET");
      }
      if ($scope.upsertPerm.method.put === "true") {
        methods.push("PUT");
      }
      if ($scope.upsertPerm.method.post === "true") {
        methods.push("POST");
      }
      if ($scope.upsertPerm.method.delete === "true") {
        methods.push("DELETE");
      }
      return methods;
    };
  
    $scope.confirmDeletePerm = function() {
      var permName = $scope.selectedPredefinedPermission ? $scope.selectedPredefinedPermission : $scope.upsertPerm.name.trim();
      if (window.confirm("Confirm delete the '"+permName+"' permission?")) {
        var index = parseInt($scope.upsertPerm.index);
        Security.post({path: "authorization"}, { "delete-permission": index }, function (data) {
          $scope.togglePermDialog();
          $scope.refreshSecurityPanel();
        });
      }
    };
  
    $scope.doUpsertPermission = function() {
      if (!$scope.upsertPerm) {
        $scope.upsertPerm = {};
      }
  
      var isAdd = $scope.permDialogMode === "add";
      var name = $scope.selectedPredefinedPermission ? $scope.selectedPredefinedPermission : $scope.upsertPerm.name.trim();
  
      if (isAdd) {
        if (!name) {
          $scope.validationError = "Either select a predefined permission or provide a name for a custom permission";
          return;
        }
        var permissionNames = $scope.permissions.map(p => p.name);
        if (permissionNames.includes(name)) {
          $scope.validationError = "Permission '"+name+"' already exists!";
          return;
        }
  
        if (name === "*") {
          $scope.validationError = "Invalid permission name!";
          return;
        }
      }
  
      var role = null;
      if ($scope.manageUserRolesEnabled) {
        role = $scope.upsertPerm.selectedRoles;
        if (!role || role.length === 0) {
          role = null;
        } else if (role.includes("*")) {
          role = ["*"];
        }
      } else if ($scope.upsertPerm.manualRoles && $scope.upsertPerm.manualRoles.trim() !== "") {
        var manualRoles = $scope.upsertPerm.manualRoles.trim();
        role = (manualRoles === "null") ? null : toList(manualRoles);
      }
  
      var setPermJson = {"name": name, "role": role };
  
      if ($scope.selectedPredefinedPermission) {
        $scope.params = [{"name":"","value":""}];
      } else {
        // collection
        var coll = null;
        if ($scope.upsertPerm.collection != null && $scope.upsertPerm.collection !== "null") {
          if ($scope.upsertPerm.collection === "*") {
            coll = "*";
          } else {
            coll = $scope.upsertPerm.collection && $scope.upsertPerm.collection.trim() !== "" ? toList($scope.upsertPerm.collection) : "";
          }
        }
        setPermJson["collection"] = coll;
  
        // path
        if (!$scope.upsertPerm.path || (Array.isArray($scope.upsertPerm.path) && $scope.upsertPerm.path.length === 0)) {
          $scope.validationError = "Path is required for custom permissions!";
          return;
        }
  
        setPermJson["path"] = toList($scope.upsertPerm.path);
  
        if ($scope.upsertPerm.method) {
          var methods = $scope.getMethods();
          if (methods.length === 0) {
            $scope.validationError = "Must specify at least one request method for a custom permission!";
            return;
          }
  
          if (methods.length < 4) {
            setPermJson["method"] = methods;
          } // else no need to specify, rule applies to all methods
        }
  
        // params
        var params = {};
        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);
              } else {
                params[name] = [p.value];
              }
            }
          }
        }
        setPermJson["params"] = params;
      }
  
      var indexUpdated = false;
      if ($scope.upsertPerm.index) {
        var indexOrBefore = isAdd ? "before" : "index";
        var indexInt = parseInt($scope.upsertPerm.index);
        if (indexInt < 1) indexInt = 1;
        if (indexInt >= $scope.permissions.length) indexInt = null;
        if (indexInt != null) {
          setPermJson[indexOrBefore] = indexInt;
        }
        indexUpdated = (!isAdd && indexInt !== parseInt($scope.upsertPerm.originalIndex));
      }
  
      if (indexUpdated) {
        // changing position is a delete + re-add in new position
        Security.post({path: "authorization"}, { "delete-permission": parseInt($scope.upsertPerm.originalIndex) }, function (remData) {
          if (setPermJson.index) {
            var before = setPermJson.index;
            delete setPermJson.index;
            setPermJson["before"] = before;
          }
  
          // add perm back in new position
          Security.post({path: "authorization"}, { "set-permission": setPermJson }, function (data) {
            var errorCause = checkError(data);
            if (errorCause != null) {
              $scope.securityAPIError = "set-permission "+name+" failed due to: "+errorCause;
              $scope.securityAPIErrorDetails = JSON.stringify(data);
              return;
            }
            $scope.togglePermDialog();
            $scope.refreshSecurityPanel();
          });
        });
      } else {
        var action = isAdd ? "set-permission" : "update-permission";
        var postBody = {};
        postBody[action] = setPermJson;
        Security.post({path: "authorization"}, postBody, function (data) {
          var errorCause = checkError(data);
          if (errorCause != null) {
            $scope.securityAPIError = action+" "+name+" failed due to: "+errorCause;
            $scope.securityAPIErrorDetails = JSON.stringify(data);
            return;
          }
  
          $scope.togglePermDialog();
          $scope.refreshSecurityPanel();
        });
      }
    };
  
    $scope.applyUserFilter = function() {
      $scope.userFilterText = "";
      $scope.userFilterOption = "";
      $scope.userFilterOptions = [];
      $scope.filteredUsers = $scope.users; // reset the filtered when the filter type changes
  
      if ($scope.userFilter === "name" || $scope.userFilter === "path") {
        // no-op: filter is text input
      } else if ($scope.userFilter === "role") {
        $scope.userFilterOptions = $scope.roleNames;
      } else if ($scope.userFilter === "perm") {
        $scope.userFilterOptions = $scope.permissions.map(p => p.name).sort();
      } else {
        $scope.userFilter = "";
      }
    };
  
    $scope.onUserFilterTextChanged = function() {
      // don't fire until we have at least 2 chars ...
      if ($scope.userFilterText && $scope.userFilterText.trim().length >= 2) {
        $scope.userFilterOption = $scope.userFilterText.toLowerCase();
        $scope.onUserFilterOptionChanged();
      } else {
        $scope.filteredUsers = $scope.users;
      }
    };
  
    function pathMatch(paths, filter) {
      for (p in paths) {
        if (paths[p].includes(filter)) {
          return true;
        }
      }
      return false;
    }
  
    $scope.onUserFilterOptionChanged = function() {
      var filter = $scope.userFilterOption ? $scope.userFilterOption.trim() : "";
      if (filter.length === 0) {
        $scope.filteredUsers = $scope.users;
        return;
      }
  
      if ($scope.userFilter === "name") {
        $scope.filteredUsers = $scope.users.filter(u => u.username.toLowerCase().includes(filter));
      } else if ($scope.userFilter === "role") {
        $scope.filteredUsers = $scope.users.filter(u => u.roles.includes(filter));
      } else if ($scope.userFilter === "path") {
        var rolesForPath = Array.from(new Set($scope.permissionsTable.filter(p => p.roles && pathMatch(p.paths, filter)).flatMap(p => p.roles)));
        var usersForPath = Array.from(new Set($scope.roles.filter(r => r.users && r.users.length > 0 && rolesForPath.includes(r.name)).flatMap(r => r.users)));
        $scope.filteredUsers = $scope.users.filter(u => usersForPath.includes(u.username));
      } else if ($scope.userFilter === "perm") {
        var rolesForPerm = Array.from(new Set($scope.permissionsTable.filter(p => p.name === filter).flatMap(p => p.roles)));
        var usersForPerm = Array.from(new Set($scope.roles.filter(r => r.users && r.users.length > 0 && rolesForPerm.includes(r.name)).flatMap(r => r.users)));
        $scope.filteredUsers = $scope.users.filter(u => usersForPerm.includes(u.username));
      } else {
        // reset
        $scope.userFilter = "";
        $scope.userFilterOption = "";
        $scope.userFilterText = "";
        $scope.filteredUsers = $scope.users;
      }
    };
  
    $scope.applyPermFilter = function() {
      $scope.permFilterText = "";
      $scope.permFilterOption = "";
      $scope.permFilterOptions = [];
      $scope.filteredPerms = $scope.permissionsTable;
  
      if ($scope.permFilter === "name" || $scope.permFilter === "path") {
        // no-op: filter is text input
      } else if ($scope.permFilter === "role") {
        var roles = $scope.manageUserRolesEnabled ? $scope.roleNames : Array.from(new Set($scope.permissionsTable.flatMap(p => p.roles))).sort();
        $scope.permFilterOptions = ["*", "null"].concat(roles);
      } else if ($scope.permFilter === "user") {
        $scope.permFilterOptions = Array.from(new Set($scope.roles.flatMap(r => r.users))).sort();
      } else if ($scope.permFilter === "collection") {
        $scope.permFilterOptions = Array.from(new Set($scope.permissionsTable.flatMap(p => p.collections))).sort();
        $scope.permFilterOptions.push("null");
      } else {
        // no perm filtering
        $scope.permFilter = "";
      }
    };
  
    $scope.onPermFilterTextChanged = function() {
      // don't fire until we have at least 2 chars ...
      if ($scope.permFilterText && $scope.permFilterText.trim().length >= 2) {
        $scope.permFilterOption = $scope.permFilterText.trim().toLowerCase();
        $scope.onPermFilterOptionChanged();
      } else {
        $scope.filteredPerms = $scope.permissionsTable;
      }
    };
  
    $scope.onPermFilterOptionChanged = function() {
      var filterCriteria = $scope.permFilterOption ? $scope.permFilterOption.trim() : "";
      if (filterCriteria.length === 0) {
        $scope.filteredPerms = $scope.permissionsTable;
        return;
      }
  
      if ($scope.permFilter === "name") {
        $scope.filteredPerms = $scope.permissionsTable.filter(p => p.name.toLowerCase().includes(filterCriteria));
      } else if ($scope.permFilter === "role") {
        if (filterCriteria === "null") {
          $scope.filteredPerms = $scope.permissionsTable.filter(p => p.roles.length === 0);
        } else {
          $scope.filteredPerms = $scope.permissionsTable.filter(p => p.roles.includes(filterCriteria));
        }
      } else if ($scope.permFilter === "path") {
        $scope.filteredPerms = $scope.permissionsTable.filter(p => pathMatch(p.paths, filterCriteria));
      } else if ($scope.permFilter === "user") {
        // get the user's roles and then find all the permissions mapped to each role
        var rolesForUser = $scope.roles.filter(r => r.users.includes(filterCriteria)).map(r => r.name);
        $scope.filteredPerms = $scope.permissionsTable.filter(p => p.roles.length > 0 && roleMatch(p.roles, rolesForUser));
      } else if ($scope.permFilter === "collection") {
        function collectionMatch(collNames, colls, filter) {
          return (filter === "null") ?collNames === "null" : colls.includes(filter);
        }
        $scope.filteredPerms = $scope.permissionsTable.filter(p => collectionMatch(p.collectionNames, p.collections, filterCriteria));
      } else {
        // reset
        $scope.permFilter = "";
        $scope.permFilterOption = "";
        $scope.permFilterText = "";
        $scope.filteredPerms = $scope.permissionsTable;
      }
    };
  
    $scope.editUser = function(row) {
      if (!row || !$scope.hasSecurityEditPerm) {
        return;
      }
  
      var userId = row.username;
      $scope.userDialogMode = "edit";
      $scope.userDialogHeader = "Edit User: "+userId;
      $scope.userDialogAction = "Update";
      var userRoles = userId in $scope.userRoles ? $scope.userRoles[userId] : [];
      if (!Array.isArray(userRoles)) {
        userRoles = [userRoles];
      }
  
      $scope.upsertUser = { username: userId, selectedRoles: userRoles };
      $scope.toggleUserDialog();
    };
  
    function buildMethods(m) {
      return {"get":""+m.includes("GET"), "post":""+m.includes("POST"), "put":""+m.includes("PUT"), "delete":""+m.includes("DELETE")};
    }
  
    $scope.editPerm = function(row) {
      if (!$scope.managePermissionsEnabled || !$scope.hasSecurityEditPerm || !row) {
        return;
      }
  
      var name = row.name;
      $scope.permDialogMode = "edit";
      $scope.permDialogHeader = "Edit Permission: "+name;
      $scope.permDialogAction = "Update";
  
      var perm = $scope.permissionsTable.find(p => p.name === name);
      var isPredefined = $scope.predefinedPermissions.includes(name);
      if (isPredefined) {
        $scope.selectedPredefinedPermission = name;
        $scope.upsertPerm = { };
        $scope.filteredPredefinedPermissions = [];
        $scope.filteredPredefinedPermissions.push(name);
        if ($scope.selectedPredefinedPermission && $scope.selectedPredefinedPermission in $scope.predefinedPermissionCollection) {
          $scope.upsertPerm.collection = $scope.predefinedPermissionCollection[$scope.selectedPredefinedPermission];
        }
        $scope.isPermFieldDisabled = true;
      } else {
        $scope.upsertPerm = { name: name, collection: perm.collectionNames, path: perm.paths };
        $scope.params = [];
        if (perm.params) {
          for (const [key, value] of Object.entries(perm.params)) {
            if (Array.isArray(value)) {
              for (i in value) {
                $scope.params.push({"name":key, "value":value[i]});
              }
            } else {
              $scope.params.push({"name":key, "value":value});
            }
          }
        }
        if ($scope.params.length === 0) {
          $scope.params = [{"name":"","value":""}];
        }
  
        $scope.upsertPerm["method"] = perm.method.length === 0 ? {"get":"true", "post":"true", "put":"true", "delete":"true"} : buildMethods(perm.method);
        $scope.isPermFieldDisabled = false;
        delete $scope.selectedPredefinedPermission;
      }
  
      $scope.upsertPerm.index = perm["index"];
      $scope.upsertPerm.originalIndex = perm["index"];
  
      // roles depending on authz plugin support
      if ($scope.manageUserRolesEnabled) {
        $scope.upsertPerm["selectedRoles"] = asList(perm.roles);
      } else {
        $scope.upsertPerm["manualRoles"] = asList(perm.roles).sort().join(", ");
      }
  
      $scope.togglePermDialog();
    };
  
    $scope.applyRoleFilter = function() {
      $scope.roleFilterText = "";
      $scope.roleFilterOption = "";
      $scope.roleFilterOptions = [];
      $scope.filteredRoles = $scope.roles; // reset the filtered when the filter type changes
  
      if ($scope.roleFilter === "name" || $scope.roleFilter === "path") {
        // no-op: filter is text input
      } else if ($scope.roleFilter === "user") {
        $scope.roleFilterOptions = Array.from(new Set($scope.roles.flatMap(r => r.users))).sort();
      } else if ($scope.roleFilter === "perm") {
        $scope.roleFilterOptions = $scope.permissions.map(p => p.name).sort();
      } else {
        $scope.roleFilter = "";
      }
    };
  
    $scope.onRoleFilterTextChanged = function() {
      // don't fire until we have at least 2 chars ...
      if ($scope.roleFilterText && $scope.roleFilterText.trim().length >= 2) {
        $scope.roleFilterOption = $scope.roleFilterText.toLowerCase();
        $scope.onRoleFilterOptionChanged();
      } else {
        $scope.filteredRoles = $scope.roles;
      }
    };
  
    $scope.onRoleFilterOptionChanged = function() {
      var filter = $scope.roleFilterOption ? $scope.roleFilterOption.trim() : "";
      if (filter.length === 0) {
        $scope.filteredRoles = $scope.roles;
        return;
      }
  
      if ($scope.roleFilter === "name") {
        $scope.filteredRoles = $scope.roles.filter(r => r.name.toLowerCase().includes(filter));
      } else if ($scope.roleFilter === "user") {
        $scope.filteredRoles = $scope.roles.filter(r => r.users.includes(filter));
      } else if ($scope.roleFilter === "path") {
        var rolesForPath = Array.from(new Set($scope.permissionsTable.filter(p => p.roles && pathMatch(p.paths, filter)).flatMap(p => p.roles)));
        $scope.filteredRoles = $scope.roles.filter(r => rolesForPath.includes(r.name));
      } else if ($scope.roleFilter === "perm") {
        var rolesForPerm = Array.from(new Set($scope.permissionsTable.filter(p => p.name === filter).flatMap(p => p.roles)));
        $scope.filteredRoles = $scope.roles.filter(r => rolesForPerm.includes(r.name));
      } else {
        // reset
        $scope.roleFilter = "";
        $scope.roleFilterOption = "";
        $scope.roleFilterText = "";
        $scope.filteredRoles = $scope.roles;
      }
    };
  
    $scope.showAddRoleDialog = function() {
      $scope.roleDialogMode = "add";
      $scope.roleDialogHeader = "Add New Role";
      $scope.roleDialogAction = "Add Role";
      $scope.upsertRole = {};
      $scope.userNames = $scope.users.map(u => u.username);
      $scope.grantPermissionNames = Array.from(new Set($scope.predefinedPermissions.concat($scope.permissions.map(p => p.name)))).sort();
      $scope.toggleRoleDialog();
    };
  
    $scope.toggleRoleDialog = function() {
      if ($scope.showRoleDialog) {
        delete $scope.upsertRole;
        delete $scope.validationError;
        delete $scope.userNames;
        $scope.showRoleDialog = false;
        return;
      }
      $scope.hideAll();
      $('#role-dialog').css({left: 680, top: 139});
      $scope.showRoleDialog = true;
    };
  
    $scope.doUpsertRole = function() {
      if (!$scope.upsertRole) {
        delete $scope.validationError;
        $scope.showRoleDialog = false;
        return;
      }
  
      if (!$scope.upsertRole.name || $scope.upsertRole.name.trim() === "") {
        $scope.validationError = "Role name is required!";
        return;
      }
  
      // keep role name to a reasonable length? but allow for email addresses
      var name = $scope.upsertRole.name.trim();
      if (name.length > 30) {
        $scope.validationError = "Role name must be 30 characters or less!";
        return;
      }
  
      if (name === "null" || name === "*") {
        $scope.validationError = "Role name '"+name+"' is invalid!";
        return;
      }
  
      if ($scope.roleDialogMode === "add") {
        if ($scope.roleNames.includes(name)) {
          $scope.validationError = "Role '"+name+"' already exists!";
          return;
        }
      }
  
      var usersForRole = [];
      if ($scope.upsertRole.selectedUsers && $scope.upsertRole.selectedUsers.length > 0) {
        usersForRole = usersForRole.concat($scope.upsertRole.selectedUsers);
      }
      usersForRole = Array.from(new Set(usersForRole));
      if (usersForRole.length === 0) {
        $scope.validationError = "Must assign new role '"+name+"' to at least one user.";
        return;
      }
  
      var perms = [];
      if ($scope.upsertRole.grantedPerms && Array.isArray($scope.upsertRole.grantedPerms) && $scope.upsertRole.grantedPerms.length > 0) {
        perms = $scope.upsertRole.grantedPerms;
      }
  
      // go get the latest role mappings ...
      Security.get({path: "authorization"}, function (data) {
        var userRoles = data.authorization["user-role"];
        var setUserRoles = {};
        for (u in usersForRole) {
          var user = usersForRole[u];
          var currentRoles = user in userRoles ? asList(userRoles[user]) : [];
          // add the new role for this user if needed
          if (!currentRoles.includes(name)) {
            currentRoles.push(name);
          }
          setUserRoles[user] = currentRoles;
        }
  
        Security.post({path: "authorization"}, { "set-user-role": setUserRoles }, function (data2) {
  
          var errorCause = checkError(data2);
          if (errorCause != null) {
            $scope.securityAPIError = "set-user-role for "+username+" failed due to: "+errorCause;
            $scope.securityAPIErrorDetails = JSON.stringify(data2);
            return;
          }
  
          if (perms.length === 0) {
            // close dialog and refresh the tables ...
            $scope.toggleRoleDialog();
            $scope.refreshSecurityPanel();
            return;
          }
  
          var currentPerms = data.authorization["permissions"];
          for (i in perms) {
            var permName = perms[i];
            var existingPerm = currentPerms.find(p => p.name === permName);
  
            if (existingPerm) {
              var roleList = [];
              if (existingPerm.role) {
                if (Array.isArray(existingPerm.role)) {
                  roleList = existingPerm.role;
                } else {
                  roleList.push(existingPerm.role);
                }
              }
              if (!roleList.includes(name)) {
                roleList.push(name);
              }
              existingPerm.role = roleList;
              Security.post({path: "authorization"}, { "update-permission": existingPerm }, function (data3) {
                $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();
                });
              } // else ignore it
            }
          }
          $scope.toggleRoleDialog();
        });
      });
  
    };
  
    $scope.editRole = function(row) {
      if (!row || !$scope.hasSecurityEditPerm) {
        return;
      }
  
      var roleName = row.name;
      $scope.roleDialogMode = "edit";
      $scope.roleDialogHeader = "Edit Role: "+roleName;
      $scope.roleDialogAction = "Update";
      var role = $scope.roles.find(r => r.name === roleName);
      var perms = $scope.permissionsTable.filter(p => p.roles.includes(roleName)).map(p => p.name);
      $scope.upsertRole = { name: roleName, selectedUsers: role.users, grantedPerms: perms };
      $scope.userNames = $scope.users.map(u => u.username);
      $scope.grantPermissionNames = Array.from(new Set($scope.predefinedPermissions.concat($scope.permissions.map(p => p.name)))).sort();
      $scope.toggleRoleDialog();
    };
  
    $scope.onBlockUnknownChange = function() {
      Security.post({path: "authentication"}, { "set-property": { "blockUnknown": $scope.blockUnknown === "true" } }, function (data) {
        $scope.refreshSecurityPanel();
      });
    };
  
    $scope.onForwardCredsChange = function() {
      Security.post({path: "authentication"}, { "set-property": { "forwardCredentials": $scope.forwardCredentials === "true" } }, function (data) {
        $scope.refreshSecurityPanel();
      });
    };
  
    $scope.removeParam= function(index) {
      if ($scope.params.length === 1) {
        $scope.params = [{"name":"","value":""}];
      } else {
        $scope.params.splice(index, 1);
      }
    };
    
    $scope.addParam = function(index) {
      $scope.params.splice(index+1, 0, {"name":"","value":""});
    };
  
    $scope.refresh();
  })
  