/*
 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 = [];
  $scope.filteredPredefinedPermissions = [];

  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);
  }

  /**
   * Check if user's roles are compatible with permission's roles
   * @param roles list of roles for a permission, where at least one is required
   * @param rolesForUser list of roles for user
   * @return true if user has one of the required roles, or permission has a wildcard role
   */
  function roleMatch(roles, rolesForUser) {
    if (roles.includes("*"))
      return true
    for (let 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);
  };

  // TODO: Use new permissions.js
  $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", "health", "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.wrapSchemeCmd = function(cmd, data) {
    var schemeCmd = {};
    schemeCmd[cmd] = $scope.multiAuthWithBasic ? { "basic": data } : data;
    return schemeCmd;
  };

  $scope.findEditableAuthz = function(data) {
    var authz = data.authorization;
    if (!authz) {
      return null;
    }
    
    var authzClass = authz["class"];
    if (authzClass.endsWith(".RuleBasedAuthorizationPlugin")) {
      return authz;
    }

    // go dig around in the schemes list looking for the editable one ...
    if (authzClass.endsWith(".MultiAuthRuleBasedAuthorizationPlugin")) {
      if ("schemes" in authz) {
        for (var i in authz.schemes) {
          if (authz.schemes[i]["class"].endsWith(".RuleBasedAuthorizationPlugin")) {
            return authz.schemes[i];
          }
        }
      }
    }

    return null;
  };

  $scope.validatePermConfig = function() {
    $scope.hasPermWarnings = false;
    $scope.permWarnings = [];

    var namedPerms = $scope.permissionsTable.filter(p => $scope.predefinedPermissions.includes(p.name));
    // look for named perms with -edit but w/o a -read
    for (var n in namedPerms) {
      var perm = namedPerms[n];
      if (perm.name.endsWith("-edit")) {
        // see if there is a "-read" too
        var pfx = perm.name.substring(0, perm.name.indexOf("-edit"));
        const readPermName = pfx + "-read";
        var readPerm = namedPerms.find(p => p.name === readPermName);
        if (!readPerm) {
          $scope.permWarnings.push(readPermName+" is not protected! In general, if you protect "+perm.name+", you should also protect "+readPermName);
        }
      }
    }

    var hasAll = namedPerms.find(p => p.name === "all");
    if (hasAll) {
      if ($scope.permissionsTable[$scope.permissionsTable.length-1].name !== "all") {
        $scope.permWarnings.push("The 'all' permission should always be the last permission in your config so that more specific permissions are applied first.");
      }
    } else {
      $scope.permWarnings.push("The 'all' permission is not configured! In general, you should assign the 'all' permission to an admin role and list it as the last permission in your config.");
    }

    $scope.hasPermWarnings = $scope.permWarnings.length > 0;
  };

  $scope.refresh = function () {
    $scope.hideAll();

    $scope.tls = false;
    $scope.blockUnknown = "false"; // default setting
    $scope.realmName = "solr";
    $scope.forwardCredentials = "false";
    $scope.multiAuthWithBasic = 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) {
      $scope.tls = data.security ? data.security["tls"] : false;
      $scope.authenticationPlugin = data.security ? data.security["authenticationPlugin"] : null;
      $scope.authorizationPlugin = data.security ? data.security["authorizationPlugin"] : null;
      $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 === 401 || 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.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" ||
         $scope.authorizationPlugin === "org.apache.solr.security.MultiAuthRuleBasedAuthorizationPlugin");

    // don't allow CRUD on roles if using external
    $scope.manageUserRolesEnabled = false;

    Security.get({path: "authorization"}, function (data) {
      //console.log(">> authorization: "+JSON.stringify(data));
      
      if (!data.authorization) {
        $scope.isSecurityAdminEnabled = false;
        $scope.hasSecurityEditPerm = false;
        return;
      }

      var authz = $scope.findEditableAuthz(data);
      //console.log(">> authz: "+JSON.stringify(authz));
      
      if (authz) {
        $scope.manageUserRolesEnabled = true;
      }

      if ($scope.manageUserRolesEnabled) {
        $scope.userRoles = authz["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.permissionsConfig = data.authorization["permissions"];
      $scope.permissionsTable = [];
      for (p in $scope.permissionsConfig) {
        $scope.permissionsTable.push(permRow($scope.permissionsConfig[p], parseInt(p)+1));
      }
      $scope.filteredPerms = $scope.permissionsTable;

      // check for issues with perm config
      $scope.validatePermConfig();

      // use the current user's roles (obtained from System.get) to check if they have the security permissions
      // Note: the backend will check too so this is only for display purposes
      $scope.hasSecurityEditPerm = $scope.isPermitted(permissions.SECURITY_EDIT_PERM);
      $scope.hasSecurityReadPerm = $scope.hasSecurityEditPerm || $scope.isPermitted(permissions.SECURITY_READ_PERM);

      // authentication
      if ($scope.authenticationPlugin === "org.apache.solr.security.BasicAuthPlugin" || $scope.authenticationPlugin === "org.apache.solr.security.MultiAuthPlugin") {
        $scope.manageUsersEnabled = true;

        Security.get({path: "authentication"}, function (data) {
          // console.log(">> authentication: "+JSON.stringify(data));
          
          if (!data.authentication) {
            $scope.manageUsersEnabled = false;
            $scope.users = [];
            $scope.filteredUsers = $scope.users;
            return;
          }

          // 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;
              }
            }
          }

          //console.log(">> authn: "+JSON.stringify(authn));

          $scope.blockUnknown = authn["blockUnknown"] === true ? "true" : "false";
          $scope.forwardCredentials = authn["forwardCredentials"] === true ? "true" : "false";

          if ("realm" in authn) {
            $scope.realmName = authn["realm"];
          }

          var users = [];
          if (authn.credentials) {
            for (var u in authn.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.length < 15 && !password.match(strongPasswordRegex)) {
      $scope.validationError = "Password not strong enough! Must have length >= 15 or 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;
    var cmdJson = $scope.wrapSchemeCmd("set-user-role", setUserRoles);
    Security.post({path: "authorization"}, cmdJson, 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();
      var cmdJson = $scope.wrapSchemeCmd("set-user", setUserJson);
      Security.post({path: "authentication"}, cmdJson, function (data) {
        var errorCause = checkError(data);
        if (errorCause != null) {
          $scope.securityAPIError = "create user "+username+" failed due to: "+errorCause;
          $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) {
          $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;
      var cmdJson = $scope.wrapSchemeCmd("set-user-role", removeRoles);
      Security.post({path: "authorization"}, cmdJson, function (data) {
        var deleteUserCmd = $scope.wrapSchemeCmd("delete-user", [$scope.upsertUser.username]);
        Security.post({path: "authentication"}, deleteUserCmd, 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.permissionsConfig.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.permissionsConfig.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.permissionsConfig.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();
          // avoids a weird race with not getting the latest config after an update
          Security.get({path: "authorization"}, function (ignore) {
            $scope.refreshSecurityPanel();
          });
        });
      });
    } else {
      var action = isAdd ? "set-permission" : "update-permission";
      var postBody = {};
      postBody[action] = setPermJson;

      // if they have the "all" permission in the last position, then keep it there when adding a new permission
      if (!setPermJson["before"] && !setPermJson["index"] && $scope.permissionsTable && $scope.permissionsTable.length > 0 && $scope.permissionsTable[$scope.permissionsTable.length-1].name === "all") {
        setPermJson["before"] = $scope.permissionsTable.length;
      }

      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();
        // avoids a weird race with not getting the latest config after an update
        Security.get({path: "authorization"}, function (ignore) {
          $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.permissionsConfig.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.permissionsConfig.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.permissionsConfig.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 authz = $scope.findEditableAuthz(data);
      if (!authz) {
        $scope.validationError = "User roles not editable via the UI!";
        return;
      }

      var userRoles = authz["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;
      }

      var cmdJson = $scope.wrapSchemeCmd("set-user-role", setUserRoles);
      Security.post({path: "authorization"}, cmdJson, 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.permissionsConfig.map(p => p.name)))).sort();
    $scope.toggleRoleDialog();
  };

  $scope.onBlockUnknownChange = function() {
    var cmdJson = $scope.wrapSchemeCmd("set-property", { "blockUnknown": $scope.blockUnknown === "true" });
    Security.post({path: "authentication"}, cmdJson, function (data) {
      $scope.refreshSecurityPanel();
    });
  };

  $scope.onForwardCredsChange = function() {
    var cmdJson = $scope.wrapSchemeCmd("set-property", { "forwardCredentials": $scope.forwardCredentials === "true" });
    Security.post({path: "authentication"}, cmdJson, 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();
})
