/*
 * The MIT License
 *
 * Copyright (c) 2010-2017, Manufacture Française des Pneumatiques Michelin,
 * Thomas Maurel, Romain Seguy, Synopsys Inc., Oleg Nenashev and contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.michelin.cio.hudson.plugins.rolestrategy;

import static com.michelin.cio.hudson.plugins.rolestrategy.ValidationUtil.formatNonExistentUserGroupValidationResponse;
import static com.michelin.cio.hudson.plugins.rolestrategy.ValidationUtil.formatUserGroupValidationResponse;

import com.synopsys.arc.jenkins.plugins.rolestrategy.RoleType;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.ExtendedHierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
import hudson.Functions;
import hudson.Util;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.AbstractItem;
import hudson.model.Computer;
import hudson.model.Descriptor;
import hudson.model.Job;
import hudson.model.Node;
import hudson.security.ACL;
import hudson.security.AuthorizationStrategy;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.security.PermissionScope;
import hudson.security.SecurityRealm;
import hudson.security.SidACL;
import hudson.util.FormValidation;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
import jenkins.model.Jenkins;
import jenkins.util.SystemProperties;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.acegisecurity.acls.sid.PrincipalSid;
import org.apache.commons.lang3.StringUtils;
import org.jenkinsci.plugins.rolestrategy.AmbiguousSidsAdminMonitor;
import org.jenkinsci.plugins.rolestrategy.permissions.PermissionHelper;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest2;
import org.kohsuke.stapler.StaplerResponse2;
import org.kohsuke.stapler.interceptor.RequirePOST;
import org.kohsuke.stapler.verb.GET;
import org.kohsuke.stapler.verb.POST;
import org.springframework.security.access.AccessDeniedException;

/**
 * Role-based authorization strategy.
 *
 * @author Thomas Maurel
 */
public class RoleBasedAuthorizationStrategy extends AuthorizationStrategy {

  private static Logger LOGGER = Logger.getLogger(RoleBasedAuthorizationStrategy.class.getName());

  public static final String GLOBAL = "globalRoles";
  public static final String PROJECT = "projectRoles";
  public static final String SLAVE = "slaveRoles";
  public static final String PERMISSION_TEMPLATES = "permissionTemplates";

  public static final String MACRO_ROLE = "roleMacros";
  public static final String MACRO_USER = "userMacros";

  private final RoleMap agentRoles;
  private final RoleMap globalRoles;
  private final RoleMap itemRoles;
  private Map<String, PermissionTemplate> permissionTemplates;

  private static final boolean USE_ITEM_AND_AGENT_ROLES = SystemProperties.getBoolean(
            RoleBasedAuthorizationStrategy.class.getName() + ".useItemAndAgentRoles", false);

  /**
   * Create new RoleBasedAuthorizationStrategy.
   */
  public RoleBasedAuthorizationStrategy() {
    agentRoles = new RoleMap();
    globalRoles = new RoleMap();
    itemRoles = new RoleMap();
    permissionTemplates = new TreeMap<>();
  }

  /**
   * Creates a new {@link RoleBasedAuthorizationStrategy}.
   *
   * @param grantedRoles the roles in the strategy
   */
  public RoleBasedAuthorizationStrategy(Map<String, RoleMap> grantedRoles) {
    this(grantedRoles, null);
  }

  /**
   * Creates a new {@link RoleBasedAuthorizationStrategy}.
   *
   * @param grantedRoles the roles in the strategy
   * @param permissionTemplates the permission templates in the strategy
   */
  public RoleBasedAuthorizationStrategy(Map<String, RoleMap> grantedRoles, @CheckForNull Set<PermissionTemplate> permissionTemplates) {

    this.permissionTemplates = new TreeMap<>();
    if (permissionTemplates != null) {
      for (PermissionTemplate template : permissionTemplates) {
        this.permissionTemplates.put(template.getName(), template);
      }
    }

    RoleMap map = grantedRoles.get(SLAVE);
    agentRoles = map == null ? new RoleMap() : map;

    map = grantedRoles.get(GLOBAL);
    globalRoles = map == null ? new RoleMap() : map;

    map = grantedRoles.get(PROJECT);
    itemRoles = map == null ? new RoleMap() : map;
    refreshPermissionsFromTemplate();
  }

  public static final PermissionGroup GROUP =
          new PermissionGroup(RoleBasedAuthorizationStrategy.class, Messages._RoleBasedAuthorizationStrategy_PermissionGroupTitle());

  public static final Permission ITEM_ROLES_ADMIN = new Permission(
          GROUP,
          "ItemRoles",
          Messages._RoleBasedAuthorizationStrategy_ItemRolesAdminPermissionDescription(),
          Jenkins.ADMINISTER,
          USE_ITEM_AND_AGENT_ROLES,
          new PermissionScope[]{PermissionScope.JENKINS});

  public static final Permission AGENT_ROLES_ADMIN = new Permission(
          GROUP,
          "AgentRoles",
          Messages._RoleBasedAuthorizationStrategy_AgentRolesAdminPermissionDescription(),
          Jenkins.ADMINISTER,
          USE_ITEM_AND_AGENT_ROLES,
          new PermissionScope[]{PermissionScope.JENKINS});

  @Restricted(NoExternalUse.class) // called by jelly
  public static final Permission[] SYSTEM_READ_AND_ITEM_ROLES_ADMIN =
          new Permission[] { Jenkins.SYSTEM_READ, ITEM_ROLES_ADMIN };

  @Restricted(NoExternalUse.class) // called by jelly
  public static final Permission[] SYSTEM_READ_AND_SOME_ROLES_ADMIN =
          new Permission[] { Jenkins.SYSTEM_READ, ITEM_ROLES_ADMIN, AGENT_ROLES_ADMIN };

  @SuppressFBWarnings(value = "MS_PKGPROTECT", justification = "Used by jelly pages")
  @Restricted(NoExternalUse.class) // called by jelly
  public static final Permission[] ADMINISTER_AND_SOME_ROLES_ADMIN =
          new Permission[] { Jenkins.ADMINISTER, ITEM_ROLES_ADMIN, AGENT_ROLES_ADMIN };

  /**
   * Refresh item permissions from templates.
   */
  private void refreshPermissionsFromTemplate() {
    SortedMap<Role, Set<PermissionEntry>> roles = getGrantedRolesEntries(RoleBasedAuthorizationStrategy.PROJECT);
    for (Role role : roles.keySet()) {
      if (Util.fixEmptyAndTrim(role.getTemplateName()) != null) {
        role.refreshPermissionsFromTemplate(permissionTemplates.get(role.getTemplateName()));
      }
    }
  }

  /**
   * Get the root ACL.
   *
   * @return The global ACL
   */
  @Override
  @NonNull
  public SidACL getRootACL() {
    return globalRoles.getACL(RoleType.Global, null);
  }

  /**
   * Get the {@link RoleMap} corresponding to the {@link RoleType}.
   *
   * @param roleType the type of the role
   * @return the {@link RoleMap} corresponding to the {@code roleType}
   * @throws IllegalArgumentException for an invalid {@code roleType}
   */
  @NonNull
  @Restricted(NoExternalUse.class)
  public RoleMap getRoleMap(RoleType roleType) {
    switch (roleType) {
      case Global:
        return globalRoles;
      case Project:
        return itemRoles;
      case Slave:
        return agentRoles;
      default:
        throw new IllegalArgumentException("Unknown RoleType: " + roleType);
    }
  }

  /**
   * Get the specific ACL for projects.
   *
   * @param project The access-controlled project
   * @return The project specific ACL
   */
  @Override
  @NonNull
  public ACL getACL(@NonNull Job<?, ?> project) {
    return getACL((AbstractItem) project);
  }

  @Override
  @NonNull
  public ACL getACL(@NonNull AbstractItem project) {
    return itemRoles.newMatchingRoleMap(project.getFullName()).getACL(RoleType.Project, project).newInheritingACL(getRootACL());
  }

  @Override
  @NonNull
  public ACL getACL(@NonNull Computer computer) {
    return agentRoles.newMatchingRoleMap(computer.getName()).getACL(RoleType.Slave, computer).newInheritingACL(getRootACL());
  }

  @Override
  @NonNull
  public ACL getACL(@NonNull Node node) {
    return agentRoles.newMatchingRoleMap(node.getNodeName()).getACL(RoleType.Slave, node).newInheritingACL(getRootACL());
  }

  /**
   * Used by the container realm.
   *
   * @return All the sids referenced by the strategy
   */
  @Override
  @NonNull
  public Collection<String> getGroups() {
    Set<String> sids = new HashSet<>();

    sids.addAll(filterRoleSids(globalRoles));
    sids.addAll(filterRoleSids(itemRoles));
    sids.addAll(filterRoleSids(agentRoles));
    return sids;
  }

  private Set<String> filterRoleSids(RoleMap roleMap) {
    return roleMap.getSidEntries(false).stream().filter(entry -> entry.getType() != AuthorizationType.USER)
        .map(PermissionEntry::getSid).collect(Collectors.toSet());
  }

  /**
   * Get the roles from the global {@link RoleMap}.
   * <p>The returned sorted map is unmodifiable.
   * </p>
   *
   * @param type The object type controlled by the {@link RoleMap}
   * @return All roles from the global {@link RoleMap}.
   * @deprecated Use {@link RoleBasedAuthorizationStrategy#getGrantedRolesEntries(RoleType)}
   */
  @Nullable
  @Deprecated
  public SortedMap<Role, Set<String>> getGrantedRoles(String type) {
    return getRoleMap(RoleType.fromString(type)).getGrantedRoles();
  }

  /**
   * Get the {@link Role}s and the sids assigned to them for the given {@link RoleType}.
   *
   * @param type the type of the role
   * @return roles mapped to the set of user sids assigned to that role
   * @since 2.12
   * @deprecated use {@link #getGrantedRolesEntries(RoleType)}
   */
  @Deprecated
  public SortedMap<Role, Set<String>> getGrantedRoles(@NonNull RoleType type) {
    return getRoleMap(type).getGrantedRoles();
  }

  /**
   * Get the permission templates.
   *
   * @return set of permission templates.
   */
  public Set<PermissionTemplate> getPermissionTemplates() {
    return Set.copyOf(permissionTemplates.values());
  }

  @CheckForNull
  public PermissionTemplate getPermissionTemplate(String templateName) {
    return permissionTemplates.get(templateName);
  }

  public boolean hasPermissionTemplate(String name) {
    return permissionTemplates.containsKey(name);
  }

  /**
   * Get the {@link Role}s and the sids assigned to them for the given {@link RoleType}.
   *
   * @param type the type of the role
   * @return roles mapped to the set of user sids assigned to that role
   */
  public SortedMap<Role, Set<PermissionEntry>> getGrantedRolesEntries(@NonNull String type) {
    return getGrantedRolesEntries(RoleType.fromString(type));
  }

  /**
   * Get the {@link Role}s and the sids assigned to them for the given {@link RoleType}.
   *
   * @param type the type of the role
   * @return roles mapped to the set of user sids assigned to that role
   */
  public SortedMap<Role, Set<PermissionEntry>> getGrantedRolesEntries(@NonNull RoleType type) {
    return getRoleMap(type).getGrantedRolesEntries();
  }


  /**
   * Get all the SIDs referenced by specified {@link RoleMap} type.
   *
   * @param type The object type controlled by the {@link RoleMap}
   * @return All SIDs from the specified {@link RoleMap}.
   */
  public Set<PermissionEntry> getSidEntries(String type) {
    return getRoleMap(RoleType.fromString(type)).getSidEntries();
  }


  /**
   * Get all the SIDs referenced by specified {@link RoleMap} type.
   *
   * @param type The object type controlled by the {@link RoleMap}
   * @return All SIDs from the specified {@link RoleMap}.
   * @deprecated use {@link #getSidEntries(String)}
   */
  @Deprecated
  @CheckForNull
  @SuppressWarnings("checkstyle:AbbreviationAsWordInName")
  public Set<String> getSIDs(String type) {
    return getRoleMap(RoleType.fromString(type)).getSids();
  }

  /**
   * Returns a map associating a {@link RoleType} with each {@link RoleMap}.
   * <p>
   * This method is intended to be used for XML serialization purposes (take a look at the {@link ConverterImpl}) and, as
   * such, must remain private since it exposes all the security config.
   * </p>
   */
  @NonNull
  private Map<RoleType, RoleMap> getRoleMaps() {
    Map<RoleType, RoleMap> roleMaps = new HashMap<>();
    roleMaps.put(RoleType.Global, globalRoles);
    roleMaps.put(RoleType.Slave, agentRoles);
    roleMaps.put(RoleType.Project, itemRoles);
    return Collections.unmodifiableMap(roleMaps);
  }

  /**
   * Add the given {@link Role} to the {@link RoleMap} associated to the provided class.
   *
   * @param roleType The type of the {@link Role} to be added
   * @param role     The {@link Role} to add
   */
  private void addRole(RoleType roleType, Role role) {
    getRoleMap(roleType).addRole(role);
  }

  /**
   * Assign a role to a sid.
   *
   * @param type The type of role
   * @param role The role to assign
   * @param sid  The sid to assign to
   */
  private void assignRole(RoleType type, Role role, PermissionEntry sid) {
    RoleMap roleMap = getRoleMap(type);
    if (roleMap.hasRole(role)) {
      roleMap.assignRole(role, sid);
    }
  }

  private static void persistChanges() throws IOException {
    Jenkins j = instance();
    j.save();
    AuthorizationStrategy as = j.getAuthorizationStrategy();
    if (as instanceof RoleBasedAuthorizationStrategy) {
      RoleBasedAuthorizationStrategy rbas = (RoleBasedAuthorizationStrategy) as;
      rbas.validateConfig();
    }
  }

  private static Jenkins instance() {
    return Jenkins.get();
  }

  private static void checkAdminPerm() {
    instance().checkPermission(Jenkins.ADMINISTER);
  }

  private static void checkPerms(@NonNull Permission... permission) {
    instance().checkAnyPermission(permission);
  }

  private static boolean hasPermissionByRoleTypeForUpdates(String roleTypeAsString) {
    switch (roleTypeAsString) {
      case RoleBasedAuthorizationStrategy.GLOBAL:
        return instance().hasPermission(Jenkins.ADMINISTER);
      case RoleBasedAuthorizationStrategy.PROJECT:
        return instance().hasAnyPermission(ITEM_ROLES_ADMIN);
      case RoleBasedAuthorizationStrategy.SLAVE:
        return instance().hasAnyPermission(AGENT_ROLES_ADMIN);
      default:
        throw new IllegalArgumentException("Unknown RoleType: " + roleTypeAsString);
    }
  }



  private static void checkPermByRoleTypeForUpdates(@NonNull String roleType) {
    switch (roleType) {
      case RoleBasedAuthorizationStrategy.GLOBAL:
        checkAdminPerm();
        break;
      case RoleBasedAuthorizationStrategy.PROJECT:
        checkPerms(ITEM_ROLES_ADMIN);
        break;
      case RoleBasedAuthorizationStrategy.SLAVE:
        checkPerms(AGENT_ROLES_ADMIN);
        break;
      default:
        throw new IllegalArgumentException("Unknown RoleType: " + roleType);
    }
  }

  private static void checkPermByRoleTypeForReading(@NonNull String roleType) {
    switch (roleType) {
      case RoleBasedAuthorizationStrategy.GLOBAL:
        checkPerms(Jenkins.SYSTEM_READ);
        break;
      case RoleBasedAuthorizationStrategy.PROJECT:
        checkPerms(Jenkins.SYSTEM_READ, ITEM_ROLES_ADMIN);
        break;
      case RoleBasedAuthorizationStrategy.SLAVE:
        checkPerms(Jenkins.SYSTEM_READ, AGENT_ROLES_ADMIN);
        break;
      default:
        throw new IllegalArgumentException("Unknown RoleType: " + roleType);
    }
  }

  /**
   * API method to add a permission template.
   *
   * An existing template with the same will only be replaced when overwrite is set. Otherwise, the request will fail with status
   * <code>400</code>
   *
   * @param name The template nae
   * @param permissionIds Comma separated list of permission IDs
   * @param overwrite If an existing template should be overwritten
   */
  @POST
  @Restricted(NoExternalUse.class)
  public void doAddTemplate(@QueryParameter(required = true) String name,
                            @QueryParameter(required = true) String permissionIds,
                            @QueryParameter(required = false) boolean overwrite)
          throws IOException {
    checkPermByRoleTypeForUpdates(PROJECT);
    List<String> permissionList = Arrays.asList(permissionIds.split(","));
    Set<Permission> permissionSet = PermissionHelper.fromStrings(permissionList, true);
    PermissionTemplate template = new PermissionTemplate(permissionSet, name);
    if (!overwrite && hasPermissionTemplate(name)) {
      Stapler.getCurrentResponse2().sendError(HttpServletResponse.SC_BAD_REQUEST, "A template with name " + name + " already exists.");
      return;
    }
    permissionTemplates.put(name, template);
    refreshPermissionsFromTemplate();
    persistChanges();
  }

  /**
   * API method to remove templates.
   *
   * <p>
   * Example: {@code curl -X POST localhost:8080/role-strategy/strategy/removeTemplates --data "templates=developer,qualits"}
   *
   * @param names  comma separated list of templates to remove
   * @param force  If templates that are in use should be removed
   * @throws IOException in case saving changes fails
   */
  @POST
  @Restricted(NoExternalUse.class)
  public void doRemoveTemplates(@QueryParameter(required = true) String names,
                                @QueryParameter(required = false) boolean force) throws IOException {
    checkPermByRoleTypeForUpdates(PROJECT);
    String[] split = names.split(",");
    for (String templateName : split) {
      templateName = templateName.trim();
      PermissionTemplate pt = getPermissionTemplate(templateName);
      if (pt != null && (!pt.isUsed() || force)) {
        permissionTemplates.remove(templateName);
        RoleMap roleMap = getRoleMap(RoleType.Project);
        for (Role role : roleMap.getRoles()) {
          if (templateName.equals(role.getTemplateName())) {
            role.setTemplateName(null);
          }
        }
      }
    }
    persistChanges();
  }

  /**
   * API method to add a role.
   *
   * <p>Unknown and dangerous permissions are ignored.
   *
   * When specifying a <code>template</code> for an item role, the given permissions are ignored. The named template must exist,
   * otherwise the request fails with status <code>400</code>.
   * The <code>template</code> is ignored when adding global or agent roles.
   *
   * <p>Example:
   * {@code curl -X POST localhost:8080/role-strategy/strategy/addRole --data "type=globalRoles&amp;roleName=ADM&amp;
   * permissionIds=hudson.model.Item.Discover,hudson.model.Item.ExtendedRead&amp;overwrite=true"}
   *
   *
   * @param type          (globalRoles, projectRoles, slaveRoles)
   * @param roleName      Name of role
   * @param permissionIds Comma separated list of IDs for given roleName
   * @param overwrite     Overwrite existing role
   * @param pattern       Role pattern
   * @param template      Name of template
   * @throws IOException In case saving changes fails
   * @since 2.5.0
   */
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doAddRole(@QueryParameter(required = true) String type,
      @QueryParameter(required = true) String roleName,
      @QueryParameter(required = true) String permissionIds,
      @QueryParameter(required = true) String overwrite,
      @QueryParameter(required = false) String pattern,
      @QueryParameter(required = false) String template) throws IOException {
    checkPermByRoleTypeForUpdates(type);

    final boolean overwriteb = Boolean.parseBoolean(overwrite);
    String pttrn = ".*";
    String templateName = Util.fixEmptyAndTrim(template);

    if (!type.equals(RoleBasedAuthorizationStrategy.GLOBAL) && pattern != null) {
      pttrn = pattern;
    }
    List<String> permissionList = Arrays.asList(permissionIds.split(","));
    Set<Permission> permissionSet = PermissionHelper.fromStrings(permissionList, true);

    Role role = new Role(roleName, pttrn, permissionSet);

    if (RoleBasedAuthorizationStrategy.PROJECT.equals(type) && templateName != null) {
      if (!hasPermissionTemplate(template)) {
        Stapler.getCurrentResponse2().sendError(
                HttpServletResponse.SC_BAD_REQUEST, "A template with name " + template + " doesn't exists."
        );
        return;
      }
      role.setTemplateName(templateName);
      role.refreshPermissionsFromTemplate(getPermissionTemplate(templateName));
    }

    RoleType roleType = RoleType.fromString(type);
    if (overwriteb) {
      RoleMap roleMap = getRoleMap(roleType);
      Role role2 = roleMap.getRole(roleName);
      if (role2 != null) {
        roleMap.removeRole(role2);
      }
    }
    addRole(roleType, role);
    persistChanges();
  }

  /**
   * API method to remove roles.
   *
   * <p>
   * Example: {@code curl -X POST localhost:8080/role-strategy/strategy/removeRoles --data "type=globalRoles&amp;
   * roleNames=ADM,DEV"}
   *
   * @param type      (globalRoles, projectRoles, slaveRoles)
   * @param roleNames comma separated list of roles to remove from type
   * @throws IOException in case saving changes fails
   * @since 2.5.0
   */
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doRemoveRoles(@QueryParameter(required = true) String type, @QueryParameter(required = true) String roleNames)
      throws IOException {
    checkPermByRoleTypeForUpdates(type);

    RoleMap roleMap = getRoleMap(RoleType.fromString(type));
    String[] split = roleNames.split(",");
    for (String roleName : split) {
      Role role = roleMap.getRole(roleName);
      if (role != null) {
        roleMap.removeRole(role);
      }
    }
    persistChanges();
  }

  /**
   * API method to assign a SID of type EITHER to role.
   *
   * This method should no longer be used.
   * <p>
   * Example:
   * {@code curl -X POST localhost:8080/role-strategy/strategy/assignRole --data "type=globalRoles&amp;roleName=ADM
   * &amp;sid=username"}
   *
   * @param type     (globalRoles, projectRoles, slaveRoles)
   * @param roleName name of role (single, no list)
   * @param sid      user ID (single, no list)
   * @throws IOException in case saving changes fails
   * @since 2.5.0
   * @deprecated Use {@link #doAssignUserRole} or {@link #doAssignGroupRole} to create unambiguous entries
   */
  @Deprecated
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doAssignRole(@QueryParameter(required = true) String type,
      @QueryParameter(required = true) String roleName,
      @QueryParameter(required = true) String sid) throws IOException {
    checkPermByRoleTypeForUpdates(type);
    final RoleType roleType = RoleType.fromString(type);
    Role role = getRoleMap(roleType).getRole(roleName);
    if (role != null) {
      assignRole(roleType, role, new PermissionEntry(AuthorizationType.EITHER, sid));
    }
    persistChanges();
  }

  /**
   * API method to assign a User to role.
   *
   * <p>
   * Example:
   * {@code curl -X POST localhost:8080/role-strategy/strategy/assignUserRole --data "type=globalRoles&amp;roleName=ADM
   * &amp;user=username"}
   *
   * @param type     (globalRoles, projectRoles, slaveRoles)
   * @param roleName name of role (single, no list)
   * @param user     user ID (single, no list)
   * @throws IOException in case saving changes fails
   * @since TODO
   */
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doAssignUserRole(@QueryParameter(required = true) String type,
      @QueryParameter(required = true) String roleName,
      @QueryParameter(required = true) String user) throws IOException {
    checkPermByRoleTypeForUpdates(type);
    final RoleType roleType = RoleType.fromString(type);
    Role role = getRoleMap(roleType).getRole(roleName);
    if (role != null) {
      assignRole(roleType, role, new PermissionEntry(AuthorizationType.USER, user));
    }
    persistChanges();
  }

  /**
   * API method to assign a Group to role.
   *
   * <p>
   * Example:
   * {@code curl -X POST localhost:8080/role-strategy/strategy/assignGroupRole --data "type=globalRoles&amp;roleName=ADM
   * &amp;group=groupname"}
   *
   * @param type     (globalRoles, projectRoles, slaveRoles)
   * @param roleName name of role (single, no list)
   * @param group    group ID (single, no list)
   * @throws IOException in case saving changes fails
   * @since TODO
   */
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doAssignGroupRole(@QueryParameter(required = true) String type,
      @QueryParameter(required = true) String roleName,
      @QueryParameter(required = true) String group) throws IOException {
    checkPermByRoleTypeForUpdates(type);
    final RoleType roleType = RoleType.fromString(type);
    Role role = getRoleMap(roleType).getRole(roleName);
    if (role != null) {
      assignRole(roleType, role, new PermissionEntry(AuthorizationType.GROUP, group));
    }
    persistChanges();
  }

  /**
   * API method to delete a SID from all granted roles.
   * Only SIDS of type EITHER with the given name will be deleted.
   *
   * <p>
   * Example:
   * {@code curl -X POST localhost:8080/role-strategy/strategy/deleteSid --data "type=globalRoles&amp;sid=username"}
   *
   * @param type (globalRoles, projectRoles, slaveRoles)
   * @param sid  user/group ID to remove
   * @throws IOException in case saving changes fails
   * @since 2.4.1
   */
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doDeleteSid(@QueryParameter(required = true) String type,
      @QueryParameter(required = true) String sid) throws IOException {
    checkPermByRoleTypeForUpdates(type);
    getRoleMap(RoleType.fromString(type)).deleteSids(new PermissionEntry(AuthorizationType.EITHER, sid));
    persistChanges();
  }

  /**
   * API method to delete a user from all granted roles.
   *
   * <p>
   * Example:
   * {@code curl -X POST localhost:8080/role-strategy/strategy/deleteUser --data "type=globalRoles&amp;user=username"}
   *
   * @param type (globalRoles, projectRoles, slaveRoles)
   * @param user user ID to remove
   * @throws IOException in case saving changes fails
   * @since 2.4.1
   */
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doDeleteUser(@QueryParameter(required = true) String type,
      @QueryParameter(required = true) String user) throws IOException {
    checkPermByRoleTypeForUpdates(type);
    getRoleMap(RoleType.fromString(type)).deleteSids(new PermissionEntry(AuthorizationType.USER, user));
    persistChanges();
  }

  /**
   * API method to delete a group from all granted roles.
   *
   * <p>
   * Example:
   * {@code curl -X POST localhost:8080/role-strategy/strategy/deleteGroup --data "type=globalRoles&amp;group=groupname"}
   *
   * @param type  (globalRoles, projectRoles, slaveRoles)
   * @param group group ID to remove
   * @throws IOException in case saving changes fails
   * @since 2.4.1
   */
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doDeleteGroup(@QueryParameter(required = true) String type,
      @QueryParameter(required = true) String group) throws IOException {
    checkPermByRoleTypeForUpdates(type);
    getRoleMap(RoleType.fromString(type)).deleteSids(new PermissionEntry(AuthorizationType.GROUP, group));
    persistChanges();
  }

  /**
   * API method to remove a SID from a role.
   * Only entries of type EITHER will be removed.
   *
   * use {@link #doUnassignUserRole(String, String, String)} or {@link #doUnassignGroupRole(String, String, String)} to unassign a
   * User or a Group.
   *
   * <p>
   * Example:
   * {@code curl -X POST localhost:8080/role-strategy/strategy/unassignRole --data "type=globalRoles&amp;roleName=AMD&amp;sid=username"}
   *
   * @param type     (globalRoles, projectRoles, slaveRoles)
   * @param roleName unassign role with sid
   * @param sid      user ID to remove
   * @throws IOException in case saving changes fails
   *
   * @since 2.6.0
   */
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doUnassignRole(@QueryParameter(required = true) String type,
      @QueryParameter(required = true) String roleName,
      @QueryParameter(required = true) String sid) throws IOException {
    checkPermByRoleTypeForUpdates(type);
    RoleMap roleMap = getRoleMap(RoleType.fromString(type));
    Role role = roleMap.getRole(roleName);
    if (role != null) {
      roleMap.deleteRoleSid(new PermissionEntry(AuthorizationType.EITHER, sid), role.getName());
    }
    persistChanges();
  }

  /**
   * API method to remove a user from a role.
   *
   * <p>
   * Example:
   * {@code curl -X POST localhost:8080/role-strategy/strategy/unassignUserRole --data
   *   "type=globalRoles&amp;roleName=AMD&amp;user=username"}
   *
   * @param type     (globalRoles, projectRoles, slaveRoles)
   * @param roleName unassign role with sid
   * @param user     user ID to remove
   * @throws IOException in case saving changes fails
   * @since TODO
   */
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doUnassignUserRole(@QueryParameter(required = true) String type,
      @QueryParameter(required = true) String roleName,
      @QueryParameter(required = true) String user) throws IOException {
    checkPermByRoleTypeForUpdates(type);
    RoleMap roleMap = getRoleMap(RoleType.fromString(type));
    Role role = roleMap.getRole(roleName);
    if (role != null) {
      roleMap.deleteRoleSid(new PermissionEntry(AuthorizationType.USER, user), role.getName());
    }
    persistChanges();
  }

  /**
   * API method to remove a user from a role.
   *
   * <p>
   * Example:
   * {@code curl -X POST localhost:8080/role-strategy/strategy/unassignGroupRole --data
   *   "type=globalRoles&amp;roleName=AMD&amp;user=username"}
   *
   * @param type     (globalRoles, projectRoles, slaveRoles)
   * @param roleName unassign role with sid
   * @param group    user ID to remove
   * @throws IOException in case saving changes fails
   * @since TODO
   */
  @RequirePOST
  @Restricted(NoExternalUse.class)
  public void doUnassignGroupRole(@QueryParameter(required = true) String type,
      @QueryParameter(required = true) String roleName,
      @QueryParameter(required = true) String group) throws IOException {
    checkPermByRoleTypeForUpdates(type);
    RoleMap roleMap = getRoleMap(RoleType.fromString(type));
    Role role = roleMap.getRole(roleName);
    if (role != null) {
      roleMap.deleteRoleSid(new PermissionEntry(AuthorizationType.GROUP, group), role.getName());
    }
    persistChanges();
  }

  /**
   * API method to get the granted permissions of a template and if the template is used.
   *
   * <p>
   * Example: {@code curl -XGET 'http://localhost:8080/jenkins/role-strategy/strategy/getTemplate?name=developer'}
   *
   * <p>
   * Returns json with granted permissions and assigned sids.<br>
   * Example:
   *
   * <pre>{@code
   *   {
   *     "permissionIds": {
   *         "hudson.model.Item.Read":true,
   *         "hudson.model.Item.Build":true,
