/*
 * 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.geode.management.internal.security;

import java.io.ObjectInputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.Descriptor;
import javax.management.InstanceAlreadyExistsException;
import javax.management.InstanceNotFoundException;
import javax.management.IntrospectionException;
import javax.management.InvalidAttributeValueException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanFeatureInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.NotCompliantMBeanException;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.OperationsException;
import javax.management.Query;
import javax.management.QueryExp;
import javax.management.ReflectionException;
import javax.management.loading.ClassLoaderRepository;
import javax.management.remote.MBeanServerForwarder;

import org.apache.commons.lang3.StringUtils;

import org.apache.geode.annotations.Immutable;
import org.apache.geode.internal.security.SecurityService;
import org.apache.geode.management.internal.ManagementConstants;
import org.apache.geode.security.GemFireSecurityException;
import org.apache.geode.security.ResourcePermission;
import org.apache.geode.security.ResourcePermission.Operation;
import org.apache.geode.security.ResourcePermission.Resource;
import org.apache.geode.security.ResourcePermission.Target;

/**
 * This class intercepts all MBean requests for GemFire MBeans and passed it to
 * ManagementInterceptor for authorization
 *
 * @since Geode 1.0
 */
public class MBeanServerWrapper implements MBeanServerForwarder {

  private MBeanServer mbs;

  private final SecurityService securityService;

  public MBeanServerWrapper(SecurityService securityService) {
    this.securityService = securityService;
  }

  private void checkDomain(ObjectName name) {
    if (ManagementConstants.OBJECTNAME__DEFAULTDOMAIN.equals(name.getDomain())) {
      throw new SecurityException(ResourceConstants.ACCESS_DENIED_MESSAGE);
    }
  }

  @Override
  public ObjectInstance createMBean(String className, ObjectName name) throws ReflectionException,
      InstanceAlreadyExistsException, MBeanException, NotCompliantMBeanException {
    checkDomain(name);
    return mbs.createMBean(className, name);
  }

  @Override
  public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName)
      throws ReflectionException, InstanceAlreadyExistsException, MBeanException,
      NotCompliantMBeanException, InstanceNotFoundException {
    checkDomain(name);
    return mbs.createMBean(className, name, loaderName);
  }

  @Override
  public ObjectInstance createMBean(String className, ObjectName name, Object[] params,
      String[] signature) throws ReflectionException, InstanceAlreadyExistsException,
      MBeanException, NotCompliantMBeanException {
    checkDomain(name);
    return mbs.createMBean(className, name, params, signature);
  }

  @Override
  public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName,
      Object[] params, String[] signature)
      throws ReflectionException, InstanceAlreadyExistsException, MBeanException,
      NotCompliantMBeanException, InstanceNotFoundException {
    checkDomain(name);
    return mbs.createMBean(className, name, loaderName, params, signature);
  }

  @Override
  public ObjectInstance registerMBean(Object object, ObjectName name)
      throws InstanceAlreadyExistsException, MBeanRegistrationException,
      NotCompliantMBeanException {
    checkDomain(name);
    return mbs.registerMBean(object, name);
  }

  @Override
  public void unregisterMBean(ObjectName name)
      throws InstanceNotFoundException, MBeanRegistrationException {
    checkDomain(name);
    mbs.unregisterMBean(name);
  }

  @Override
  public ObjectInstance getObjectInstance(ObjectName name) throws InstanceNotFoundException {
    return mbs.getObjectInstance(name);
  }

  @Immutable
  private static final QueryExp notAccessControlMBean =
      Query.not(Query.isInstanceOf(Query.value(AccessControlMXBean.class.getName())));

  @Override
  public Set<ObjectInstance> queryMBeans(ObjectName name, QueryExp query) {
    // We need to filter out the AccessControlMXBean so that the clients wouldn't see it
    if (query != null) {
      return mbs.queryMBeans(name, Query.and(query, notAccessControlMBean));
    } else {
      return mbs.queryMBeans(name, notAccessControlMBean);
    }
  }

  @Override
  public Set<ObjectName> queryNames(ObjectName name, QueryExp query) {
    if (query != null) {
      return mbs.queryNames(name, Query.and(query, notAccessControlMBean));
    } else {
      return mbs.queryNames(name, notAccessControlMBean);
    }
  }

  @Override
  public boolean isRegistered(ObjectName name) {
    return mbs.isRegistered(name);
  }

  @Override
  public Integer getMBeanCount() {
    return mbs.getMBeanCount();
  }

  @Override
  public Object getAttribute(ObjectName name, String attribute)
      throws MBeanException, InstanceNotFoundException, ReflectionException {
    ResourcePermission ctx = getOperationContext(name, attribute, false);
    securityService.authorize(ctx);
    Object result;
    try {
      result = mbs.getAttribute(name, attribute);
    } catch (AttributeNotFoundException nex) {
      return null;
    }
    return result;
  }

  @Override
  public AttributeList getAttributes(ObjectName name, String[] attributes)
      throws InstanceNotFoundException, ReflectionException {
    AttributeList results;
    checkAuthorization(name, attributes);
    try {
      results = mbs.getAttributes(name, attributes);
    } catch (Exception e) {
      throw new GemFireSecurityException(
          "error getting values of attributes :" + Arrays.toString(attributes) + " from " + name,
          e);
    }
    return results;
  }

  void checkAuthorization(ObjectName name, String[] attributes)
      throws InstanceNotFoundException, ReflectionException {
    Set<ResourcePermission> contextSet = new HashSet<>();
    for (String attribute : attributes) {
      ResourcePermission ctx = getOperationContext(name, attribute, false);
      if (ctx != null) {
        if (contextSet.add(ctx)) {
          securityService.authorize(ctx);
        }
      }
    }
  }

  @Override
  public void setAttribute(ObjectName name, Attribute attribute)
      throws InstanceNotFoundException, AttributeNotFoundException, InvalidAttributeValueException,
      MBeanException, ReflectionException {
    ResourcePermission ctx = getOperationContext(name, attribute.getName(), false);
    securityService.authorize(ctx);
    mbs.setAttribute(name, attribute);
  }

  @Override
  public AttributeList setAttributes(ObjectName name, AttributeList attributes)
      throws InstanceNotFoundException, ReflectionException {
    // call setAttribute instead to use the authorization logic
    checkAuthorization(name,
        (String[]) attributes.parallelStream().map(attribute -> ((Attribute) attribute).getName())
            .toArray());
    try {
      mbs.setAttributes(name, attributes);
    } catch (Exception e) {
      throw new GemFireSecurityException("error setting attributes :" + attributes + " of " + name,
          e);
    }
    return attributes;
  }

  @Override
  public Object invoke(ObjectName name, String operationName, Object[] params, String[] signature)
      throws InstanceNotFoundException, MBeanException, ReflectionException {

    ResourcePermission ctx = getOperationContext(name, operationName, true);
    securityService.authorize(ctx);

    return mbs.invoke(name, operationName, params, signature);
  }

  // TODO: cache this
  private ResourcePermission getOperationContext(ObjectName objectName, String featureName,
      boolean isOp) throws InstanceNotFoundException, ReflectionException {
    MBeanInfo beanInfo;
    try {
      beanInfo = mbs.getMBeanInfo(objectName);
    } catch (IntrospectionException e) {
      throw new GemFireSecurityException("error getting beanInfo of " + objectName, e);
    }
    // If there is no annotation defined either in the class level or method level, we should
    // consider this operation/attribute freely accessible
    ResourcePermission result = null;

    // find the context in the beanInfo if defined in the class level
    result = getOperationContext(beanInfo.getDescriptor(), result);

    MBeanFeatureInfo[] featureInfos;
    if (isOp) {
      featureInfos = beanInfo.getOperations();
    } else {
      featureInfos = beanInfo.getAttributes();
    }
    // still look into the attributes/operations to see if it's defined in the method level
    for (MBeanFeatureInfo info : featureInfos) {
      if (info.getName().equals(featureName)) {
        // found the featureInfo of this method on the bean
        result = getOperationContext(info.getDescriptor(), result);
        break;
      }
    }
    return result;
  }

  private ResourcePermission getOperationContext(Descriptor descriptor,
      ResourcePermission defaultValue) {
    String resource = (String) descriptor.getFieldValue("resource");
    String operationCode = (String) descriptor.getFieldValue("operation");
    String targetCode = (String) descriptor.getFieldValue("target");
    if (resource != null && operationCode != null) {
      if (StringUtils.isBlank(targetCode)) {
        return new ResourcePermission(Resource.valueOf(resource), Operation.valueOf(operationCode));
      } else {
        return new ResourcePermission(Resource.valueOf(resource), Operation.valueOf(operationCode),
            Target.valueOf(targetCode).getName());
      }
    }
    return defaultValue;
  }


  @Override
  public String getDefaultDomain() {
    return mbs.getDefaultDomain();
  }

  @Override
  public String[] getDomains() {
    return mbs.getDomains();
  }

  @Override
  public void addNotificationListener(ObjectName name, NotificationListener listener,
      NotificationFilter filter, Object handback) throws InstanceNotFoundException {
    mbs.addNotificationListener(name, listener, filter, handback);
  }

  @Override
  public void addNotificationListener(ObjectName name, ObjectName listener,
      NotificationFilter filter, Object handback) throws InstanceNotFoundException {
    mbs.addNotificationListener(name, listener, filter, handback);
  }

  @Override
  public void removeNotificationListener(ObjectName name, ObjectName listener)
      throws InstanceNotFoundException, ListenerNotFoundException {
    mbs.removeNotificationListener(name, listener);
  }

  @Override
  public void removeNotificationListener(ObjectName name, ObjectName listener,
      NotificationFilter filter, Object handback)
      throws InstanceNotFoundException, ListenerNotFoundException {
    mbs.removeNotificationListener(name, listener, filter, handback);

  }

  @Override
  public void removeNotificationListener(ObjectName name, NotificationListener listener)
      throws InstanceNotFoundException, ListenerNotFoundException {
    mbs.removeNotificationListener(name, listener);
  }

  @Override
  public void removeNotificationListener(ObjectName name, NotificationListener listener,
      NotificationFilter filter, Object handback)
      throws InstanceNotFoundException, ListenerNotFoundException {
    mbs.removeNotificationListener(name, listener, filter, handback);
  }

  @Override
  public MBeanInfo getMBeanInfo(ObjectName name)
      throws InstanceNotFoundException, IntrospectionException, ReflectionException {
    return mbs.getMBeanInfo(name);
  }

  @Override
  public boolean isInstanceOf(ObjectName name, String className) throws InstanceNotFoundException {
    return mbs.isInstanceOf(name, className);
  }

  @Override
  public Object instantiate(String className) throws ReflectionException, MBeanException {
    return mbs.instantiate(className);
  }

  @Override
  public Object instantiate(String className, ObjectName loaderName)
      throws ReflectionException, MBeanException, InstanceNotFoundException {
    return mbs.instantiate(className, loaderName);
  }

  @Override
  public Object instantiate(String className, Object[] params, String[] signature)
      throws ReflectionException, MBeanException {
    return mbs.instantiate(className, params, signature);
  }

  @Override
  public Object instantiate(String className, ObjectName loaderName, Object[] params,
      String[] signature) throws ReflectionException, MBeanException, InstanceNotFoundException {
    return mbs.instantiate(className, params, signature);
  }

  @SuppressWarnings("deprecation")
  @Override
  public ObjectInputStream deserialize(ObjectName name, byte[] data) throws OperationsException {
    return mbs.deserialize(name, data);
  }

  @Override
  public ObjectInputStream deserialize(String className, byte[] data)
      throws OperationsException, ReflectionException {
    return mbs.deserialize(className, data);
  }

  @SuppressWarnings("deprecation")
  @Override
  public ObjectInputStream deserialize(String className, ObjectName loaderName, byte[] data)
      throws OperationsException, ReflectionException {
    return mbs.deserialize(className, loaderName, data);
  }

  @Override
  public ClassLoader getClassLoaderFor(ObjectName mbeanName) throws InstanceNotFoundException {
    return mbs.getClassLoaderFor(mbeanName);
  }

  @Override
  public ClassLoader getClassLoader(ObjectName loaderName) throws InstanceNotFoundException {
    return mbs.getClassLoader(loaderName);
  }

  @Override
  public ClassLoaderRepository getClassLoaderRepository() {
    return mbs.getClassLoaderRepository();
  }

  @Override
  public MBeanServer getMBeanServer() {
    return mbs;
  }

  @Override
  public void setMBeanServer(MBeanServer mbs) {
    this.mbs = mbs;
  }

}
