/*
 * 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.
 */
package org.apache.solr.security;

import java.io.IOException;
import java.io.StringReader;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.http.auth.BasicUserPrincipal;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.Utils;
import org.apache.solr.handler.DumpRequestHandler;
import org.apache.solr.handler.ReplicationHandler;
import org.apache.solr.handler.SchemaHandler;
import org.apache.solr.handler.UpdateRequestHandler;
import org.apache.solr.handler.admin.CollectionsHandler;
import org.apache.solr.handler.admin.CoreAdminHandler;
import org.apache.solr.handler.admin.PropertiesRequestHandler;
import org.apache.solr.handler.component.SearchHandler;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.security.AuthorizationContext.CollectionRequest;
import org.apache.solr.security.AuthorizationContext.RequestType;
import org.apache.solr.common.util.CommandOperation;
import org.hamcrest.core.IsInstanceOf;
import org.hamcrest.core.IsNot;
import org.junit.Test;

import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.apache.solr.common.util.Utils.getObjectByPath;
import static org.apache.solr.common.util.Utils.makeMap;
import static org.apache.solr.common.util.CommandOperation.captureErrors;

public class TestRuleBasedAuthorizationPlugin extends SolrTestCaseJ4 {
  private static final int STATUS_OK = 200;
  private static final int FORBIDDEN = 403;
  private static final int PROMPT_FOR_CREDENTIALS = 401;

  String permissions = "{" +
      "  user-role : {" +
      "    steve: [dev,user]," +
      "    tim: [dev,admin]," +
      "    joe: [user]," +
      "    noble:[dev,user]" +
      "  }," +
      "  permissions : [" +
      "    {name:'schema-edit'," +
      "     role:admin}," +
      "    {name:'collection-admin-read'," +
      "    role:null}," +
      "    {name:collection-admin-edit ," +
      "    role:admin}," +
      "    {name:mycoll_update," +
      "      collection:mycoll," +
      "      path:'/update/*'," +
      "      role:[dev,admin]" +
      "    }," +
      "{name:read , role:dev }," +
      "{name:freeforall, path:'/foo', role:'*'}]}";



  public void testBasicPermissions() {
    checkRules(makeMap("resource", "/update/json/docs",
        "httpMethod", "POST",
        "userPrincipal", "unknownuser",
        "collectionRequests", "freeforall",
        "handler", new UpdateRequestHandler())
        , STATUS_OK);

    checkRules(makeMap("resource", "/update/json/docs",
        "httpMethod", "POST",
        "userPrincipal", "tim",
        "collectionRequests", "mycoll",
        "handler", new UpdateRequestHandler())
        , STATUS_OK);


    checkRules(makeMap("resource", "/update/json/docs",
        "httpMethod", "POST",
        "collectionRequests", "mycoll",
        "handler", new UpdateRequestHandler())
        , PROMPT_FOR_CREDENTIALS);

    checkRules(makeMap("resource", "/schema",
        "userPrincipal", "somebody",
        "collectionRequests", "mycoll",
        "httpMethod", "POST",
        "handler", new SchemaHandler())
        , FORBIDDEN);

    checkRules(makeMap("resource", "/schema",
        "userPrincipal", "somebody",
        "collectionRequests", "mycoll",
        "httpMethod", "GET",
        "handler", new SchemaHandler()
    )
        , STATUS_OK);

    checkRules(makeMap("resource", "/schema/fields",
        "userPrincipal", "somebody",
        "collectionRequests", "mycoll",
        "httpMethod", "GET",
        "handler", new SchemaHandler())
        , STATUS_OK);

    checkRules(makeMap("resource", "/schema",
        "userPrincipal", "somebody",
        "collectionRequests", "mycoll",
        "httpMethod", "POST",
        "handler", new SchemaHandler())
        , FORBIDDEN);

    checkRules(makeMap("resource", "/admin/collections",
        "userPrincipal", "tim",
        "requestType", RequestType.ADMIN,
        "collectionRequests", null,
        "httpMethod", "GET",
        "handler", new CollectionsHandler(),
        "params", new MapSolrParams(singletonMap("action", "LIST")))
        , STATUS_OK);

    checkRules(makeMap("resource", "/admin/collections",
        "userPrincipal", null,
        "requestType", RequestType.ADMIN,
        "collectionRequests", null,
        "httpMethod", "GET",
        "handler", new CollectionsHandler(),
        "params", new MapSolrParams(singletonMap("action", "LIST")))
        , STATUS_OK);

    checkRules(makeMap("resource", "/admin/collections",
        "userPrincipal", null,
        "requestType", RequestType.ADMIN,
        "collectionRequests", null,
        "handler", new CollectionsHandler(),
        "params", new MapSolrParams(singletonMap("action", "CREATE")))
        , PROMPT_FOR_CREDENTIALS);

    checkRules(makeMap("resource", "/admin/collections",
        "userPrincipal", null,
        "requestType", RequestType.ADMIN,
        "collectionRequests", null,
        "handler", new CollectionsHandler(),
        "params", new MapSolrParams(singletonMap("action", "RELOAD")))
        , PROMPT_FOR_CREDENTIALS);


    checkRules(makeMap("resource", "/admin/collections",
        "userPrincipal", "somebody",
        "requestType", RequestType.ADMIN,
        "collectionRequests", null,
        "handler", new CollectionsHandler(),
        "params", new MapSolrParams(singletonMap("action", "CREATE")))
        , FORBIDDEN);

    checkRules(makeMap("resource", "/admin/collections",
        "userPrincipal", "tim",
        "requestType", RequestType.ADMIN,
        "collectionRequests", null,
        "handler", new CollectionsHandler(),
        "params", new MapSolrParams(singletonMap("action", "CREATE")))
        , STATUS_OK);

    checkRules(makeMap("resource", "/select",
        "httpMethod", "GET",
        "handler", new SearchHandler(),
        "collectionRequests", singletonList(new CollectionRequest("mycoll")),
        "userPrincipal", "joe")
        , FORBIDDEN);


    Map rules = (Map) Utils.fromJSONString(permissions);
    ((Map)rules.get("user-role")).put("cio","su");
    ((List)rules.get("permissions")).add( makeMap("name", "all", "role", "su"));

    checkRules(makeMap("resource", ReplicationHandler.PATH,
        "httpMethod", "POST",
        "userPrincipal", "tim",
        "handler", new ReplicationHandler(),
        "collectionRequests", singletonList(new CollectionRequest("mycoll")) )
        , FORBIDDEN, rules);

    checkRules(makeMap("resource", ReplicationHandler.PATH,
        "httpMethod", "POST",
        "userPrincipal", "cio",
        "handler", new ReplicationHandler(),
        "collectionRequests", singletonList(new CollectionRequest("mycoll")) )
        , STATUS_OK, rules);

    checkRules(makeMap("resource", "/admin/collections",
        "userPrincipal", "tim",
        "requestType", AuthorizationContext.RequestType.ADMIN,
        "collectionRequests", null,
        "handler", new CollectionsHandler(),
        "params", new MapSolrParams(singletonMap("action", "CREATE")))
        , STATUS_OK, rules);

    rules = (Map) Utils.fromJSONString(permissions);
    ((List)rules.get("permissions")).add( makeMap("name", "core-admin-edit", "role", "su"));
    ((List)rules.get("permissions")).add( makeMap("name", "core-admin-read", "role", "user"));
    ((Map)rules.get("user-role")).put("cio","su");
    ((List)rules.get("permissions")).add( makeMap("name", "all", "role", "su"));
    permissions = Utils.toJSONString(rules);

    checkRules(makeMap("resource", "/admin/cores",
        "userPrincipal", null,
        "requestType", RequestType.ADMIN,
        "collectionRequests", null,
        "handler", new CoreAdminHandler(null),
        "params", new MapSolrParams(singletonMap("action", "CREATE")))
        , PROMPT_FOR_CREDENTIALS);

    checkRules(makeMap("resource", "/admin/cores",
        "userPrincipal", "joe",
        "requestType", RequestType.ADMIN,
        "collectionRequests", null,
        "handler", new CoreAdminHandler(null),
        "params", new MapSolrParams(singletonMap("action", "CREATE")))
        , FORBIDDEN);

  checkRules(makeMap("resource", "/admin/cores",
        "userPrincipal", "joe",
        "requestType", RequestType.ADMIN,
        "collectionRequests", null,
        "handler", new CoreAdminHandler(null),
        "params", new MapSolrParams(singletonMap("action", "STATUS")))
        , STATUS_OK);

    checkRules(makeMap("resource", "/admin/cores",
        "userPrincipal", "cio",
        "requestType", RequestType.ADMIN,
        "collectionRequests", null,
        "handler", new CoreAdminHandler(null),
        "params", new MapSolrParams(singletonMap("action", "CREATE")))
        ,STATUS_OK );

    rules = (Map) Utils.fromJSONString(permissions);
    List permissions = (List) rules.get("permissions");
    permissions.remove(permissions.size() -1);//remove the 'all' permission
    permissions.add(makeMap("name", "test-params", "role", "admin", "path", "/x", "params",
        makeMap("key", Arrays.asList("REGEX:(?i)val1", "VAL2"))));
    this.permissions = Utils.toJSONString(rules);

    checkRules(makeMap("resource", "/x",
        "userPrincipal", null,
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", new DumpRequestHandler(),
        "params", new MapSolrParams(singletonMap("key", "VAL1")))
        , PROMPT_FOR_CREDENTIALS);

    checkRules(makeMap("resource", "/x",
        "userPrincipal", null,
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", new DumpRequestHandler(),
        "params", new MapSolrParams(singletonMap("key", "Val1")))
        , PROMPT_FOR_CREDENTIALS);

    checkRules(makeMap("resource", "/x",
        "userPrincipal", null,
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", new DumpRequestHandler(),
        "params", new MapSolrParams(singletonMap("key", "Val1")))
        , PROMPT_FOR_CREDENTIALS);
    checkRules(makeMap("resource", "/x",
        "userPrincipal", "joe",
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", new DumpRequestHandler(),
        "params", new MapSolrParams(singletonMap("key", "Val1")))
        , FORBIDDEN);

    checkRules(makeMap("resource", "/x",
        "userPrincipal", "joe",
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", new DumpRequestHandler(),
        "params", new MapSolrParams(singletonMap("key", "Val2")))
        , STATUS_OK);
    checkRules(makeMap("resource", "/x",
        "userPrincipal", "joe",
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", new DumpRequestHandler(),
        "params", new MapSolrParams(singletonMap("key", "VAL2")))
        , FORBIDDEN);

    checkRules(makeMap("resource", "/update",
        "userPrincipal", "solr",
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", new UpdateRequestHandler(),
        "params", new MapSolrParams(singletonMap("key", "VAL2")))
        , FORBIDDEN, (Map<String, Object>) Utils.fromJSONString( "{user-role:{" +
        "      admin:[admin_role]," +
        "      update:[update_role]," +
        "      solr:[read_role]}," +
        "    permissions:[" +
        "      {name:update, role:[admin_role,update_role]}," +
        "      {name:read, role:[admin_role,update_role,read_role]}" +
        "]}"));
  }

  /*
   * RuleBasedAuthorizationPlugin handles requests differently based on whether the underlying handler implements
   * PermissionNameProvider or not.  If this test fails because UpdateRequestHandler stops implementing
   * PermissionNameProvider, or PropertiesRequestHandler starts to, then just change the handlers used here.
   */
  @Test
  public void testAllPermissionAllowsActionsWhenUserHasCorrectRole() {
    SolrRequestHandler handler = new UpdateRequestHandler();
    assertThat(handler, new IsInstanceOf(PermissionNameProvider.class));
    checkRules(makeMap("resource", "/update",
        "userPrincipal", "dev",
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", handler,
        "params", new MapSolrParams(singletonMap("key", "VAL2")))
        , STATUS_OK, (Map<String, Object>) Utils.fromJSONString( "{" +
            "    user-role:{" +
            "      dev:[dev_role]," +
            "      admin:[admin_role]}," +
            "    permissions:[" +
            "      {name:all, role:[dev_role, admin_role]}" +
            "]}"));

    handler = new PropertiesRequestHandler();
    assertThat(handler, new IsNot<>(new IsInstanceOf(PermissionNameProvider.class)));
    checkRules(makeMap("resource", "/admin/info/properties",
        "userPrincipal", "dev",
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", handler,
        "params", new MapSolrParams(emptyMap()))
        , STATUS_OK, (Map<String, Object>) Utils.fromJSONString( "{" +
            "    user-role:{" +
            "      dev:[dev_role]," +
            "      admin:[admin_role]}," +
            "    permissions:[" +
            "      {name:all, role:[dev_role, admin_role]}" +
            "]}"));
  }


  /*
   * RuleBasedAuthorizationPlugin handles requests differently based on whether the underlying handler implements
   * PermissionNameProvider or not.  If this test fails because UpdateRequestHandler stops implementing
   * PermissionNameProvider, or PropertiesRequestHandler starts to, then just change the handlers used here.
   */
  @Test
  public void testAllPermissionAllowsActionsWhenAssociatedRoleIsWildcard() {
    SolrRequestHandler handler = new UpdateRequestHandler();
    assertThat(handler, new IsInstanceOf(PermissionNameProvider.class));
    checkRules(makeMap("resource", "/update",
        "userPrincipal", "dev",
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", new UpdateRequestHandler(),
        "params", new MapSolrParams(singletonMap("key", "VAL2")))
        , STATUS_OK, (Map<String, Object>) Utils.fromJSONString( "{" +
            "    user-role:{" +
            "      dev:[dev_role]," +
            "      admin:[admin_role]}," +
            "    permissions:[" +
            "      {name:all, role:'*'}" +
            "]}"));

    handler = new PropertiesRequestHandler();
    assertThat(handler, new IsNot<>(new IsInstanceOf(PermissionNameProvider.class)));
    checkRules(makeMap("resource", "/admin/info/properties",
        "userPrincipal", "dev",
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", handler,
        "params", new MapSolrParams(emptyMap()))
        , STATUS_OK, (Map<String, Object>) Utils.fromJSONString( "{" +
            "    user-role:{" +
            "      dev:[dev_role]," +
            "      admin:[admin_role]}," +
            "    permissions:[" +
            "      {name:all, role:'*'}" +
            "]}"));
  }

  /*
   * RuleBasedAuthorizationPlugin handles requests differently based on whether the underlying handler implements
   * PermissionNameProvider or not.  If this test fails because UpdateRequestHandler stops implementing
   * PermissionNameProvider, or PropertiesRequestHandler starts to, then just change the handlers used here.
   */
  @Test
  public void testAllPermissionDeniesActionsWhenUserIsNotCorrectRole() {
    SolrRequestHandler handler = new UpdateRequestHandler();
    assertThat(handler, new IsInstanceOf(PermissionNameProvider.class));
    checkRules(makeMap("resource", "/update",
        "userPrincipal", "dev",
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", new UpdateRequestHandler(),
        "params", new MapSolrParams(singletonMap("key", "VAL2")))
        , FORBIDDEN, (Map<String, Object>) Utils.fromJSONString( "{" +
            "    user-role:{" +
            "      dev:[dev_role]," +
            "      admin:[admin_role]}," +
            "    permissions:[" +
            "      {name:all, role:'admin_role'}" +
            "]}"));

    handler = new PropertiesRequestHandler();
    assertThat(handler, new IsNot<>(new IsInstanceOf(PermissionNameProvider.class)));
    checkRules(makeMap("resource", "/admin/info/properties",
        "userPrincipal", "dev",
        "requestType", RequestType.UNKNOWN,
        "collectionRequests", "go",
        "handler", handler,
        "params", new MapSolrParams(emptyMap()))
        , FORBIDDEN, (Map<String, Object>) Utils.fromJSONString( "{" +
            "    user-role:{" +
            "      dev:[dev_role]," +
            "      admin:[admin_role]}," +
            "    permissions:[" +
            "      {name:all, role:'admin_role'}" +
            "]}"));
  }

  public void testEditRules() throws IOException {
    Perms perms =  new Perms();
    perms.runCmd("{set-permission : {name: config-edit, role: admin } }", true);
    assertEquals("config-edit",  getObjectByPath(perms.conf, false, "permissions[0]/name"));
    assertEquals(1 , perms.getVal("permissions[0]/index"));
    assertEquals("admin", perms.getVal("permissions[0]/role"));
    perms.runCmd("{set-permission : {name: config-edit, role: [admin, dev], index:2 } }", false);
    perms.runCmd("{set-permission : {name: config-edit, role: [admin, dev], index:1}}", true);
    Collection roles = (Collection) perms.getVal("permissions[0]/role");
    assertEquals(2, roles.size());
    assertTrue(roles.contains("admin"));
    assertTrue(roles.contains("dev"));
    perms.runCmd("{set-permission : {role: [admin, dev], collection: x , path: '/a/b' , method :[GET, POST] }}", true);
    assertNotNull(perms.getVal("permissions[1]"));
    assertEquals("x", perms.getVal("permissions[1]/collection"));
    assertEquals("/a/b", perms.getVal("permissions[1]/path"));
    perms.runCmd("{update-permission : {index : 2, method : POST }}", true);
    assertEquals("POST", perms.getVal("permissions[1]/method"));
    perms.runCmd("{set-permission : {name : read, collection : y, role:[guest, dev] ,  before :2}}", true);
    assertNotNull(perms.getVal("permissions[2]"));
    assertEquals("y", perms.getVal("permissions[1]/collection"));
    assertEquals("read", perms.getVal("permissions[1]/name"));
    perms.runCmd("{delete-permission : 3}", true);
    assertTrue(captureErrors(perms.parsedCommands).isEmpty());
    assertEquals("y", perms.getVal("permissions[1]/collection"));
  }

  static class  Perms {
    Map conf =  new HashMap<>();
    ConfigEditablePlugin plugin = new RuleBasedAuthorizationPlugin();
    List<CommandOperation> parsedCommands;

    public void runCmd(String cmds, boolean failOnError) throws IOException {
      parsedCommands = CommandOperation.parse(new StringReader(cmds));
      LinkedList ll = new LinkedList();
      Map<String, Object> edited = plugin.edit(conf, parsedCommands);
      if(edited!= null) conf = edited;
      List<Map> maps = captureErrors(parsedCommands);
      if(failOnError){
        assertTrue("unexpected error ,"+maps , maps.isEmpty());
      } else {
        assertFalse("expected error", maps.isEmpty());
      }
    }
    public Object getVal(String path){
      return getObjectByPath(conf,false, path);
    }
  }

  private void checkRules(Map<String, Object> values, int expected) {
    checkRules(values,expected,(Map) Utils.fromJSONString(permissions));
  }

  private void checkRules(Map<String, Object> values, int expected, Map<String ,Object> permissions) {
    AuthorizationContext context = new MockAuthorizationContext(values);
    try (RuleBasedAuthorizationPlugin plugin = new RuleBasedAuthorizationPlugin()) {
      plugin.init(permissions);
      AuthorizationResponse authResp = plugin.authorize(context);
      assertEquals(expected, authResp.statusCode);
    } catch (IOException e) {
      ; // swallow error, otherwise a you have to add a _lot_ of exceptions to methods.
    }
  }

  private static class MockAuthorizationContext extends AuthorizationContext {
    private final Map<String,Object> values;

    private MockAuthorizationContext(Map<String, Object> values) {
      this.values = values;
    }

    @Override
    public SolrParams getParams() {
      SolrParams params = (SolrParams) values.get("params");
      return params == null ?  new MapSolrParams(new HashMap<String, String>()) : params;
    }

    @Override
    public Principal getUserPrincipal() {
      Object userPrincipal = values.get("userPrincipal");
      return userPrincipal == null ? null : new BasicUserPrincipal(String.valueOf(userPrincipal));
    }

    @Override
    public String getHttpHeader(String header) {
      return null;
    }

    @Override
    public Enumeration getHeaderNames() {
      return null;
    }

    @Override
    public String getRemoteAddr() {
      return null;
    }

    @Override
    public String getRemoteHost() {
      return null;
    }

    @Override
    public List<CollectionRequest> getCollectionRequests() {
      Object collectionRequests = values.get("collectionRequests");
      if (collectionRequests instanceof String) {
        return singletonList(new CollectionRequest((String)collectionRequests));
      }
      return (List<CollectionRequest>) collectionRequests;
    }

    @Override
    public RequestType getRequestType() {
      return (RequestType) values.get("requestType");
    }

    @Override
    public String getHttpMethod() {
      return (String) values.get("httpMethod");
    }

    @Override
    public Object getHandler() {
      Object handler = values.get("handler");
      return handler instanceof String ? (PermissionNameProvider) request -> PermissionNameProvider.Name.get((String) handler) : handler;
    }

    @Override
    public String getResource() {
      return (String) values.get("resource");
    }
  }
}
