/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *  
 */
 
// https://issues.jboss.org/browse/TEIID-2476
// https://issues.jboss.org/browse/TEIID-5516
// https://issues.redhat.com/browse/TEIID-2844
package org.teiid.dqp.internal.process;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import org.teiid.PolicyDecider;
import org.teiid.adminapi.DataPolicy.Context;
import org.teiid.adminapi.DataPolicy.PermissionType;
import org.teiid.api.exception.query.QueryValidatorException;
import org.teiid.core.CoreConstants;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.TeiidException;
import org.teiid.core.types.DataTypeManager;
import org.teiid.core.types.TransformationException;
import org.teiid.core.util.PropertiesUtils;
import org.teiid.dqp.internal.process.multisource.MultiSourceElement;
import org.teiid.metadata.*;
import org.teiid.query.QueryPlugin;
import org.teiid.query.metadata.QueryMetadataInterface;
import org.teiid.query.metadata.TempMetadataID;
import org.teiid.query.sql.LanguageObject;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.lang.Query;
import org.teiid.query.sql.navigator.DeepPreOrderNavigator;
import org.teiid.query.sql.symbol.ElementSymbol;
import org.teiid.query.sql.symbol.Expression;
import org.teiid.query.sql.symbol.MultipleElementSymbol;
import org.teiid.query.sql.symbol.ScalarSubquery;
import org.teiid.query.util.CommandContext;

import com.datavirtuality.dv.core.interfaces.DVCaller;
import org.teiid.query.validator.AbstractValidationVisitor;

/**
 * The default Teiid authorization validator
 */
public class DefaultAuthorizationValidator implements AuthorizationValidator {

	public static final String IGNORE_UNAUTHORIZED_ASTERISK = "ignore_unauthorized_asterisk"; //$NON-NLS-1$
	private PolicyDecider policyDecider;

	/** ignoreUnauthorizedAsteriskDefault parameter (true by default) */
	private boolean ignoreUnauthorizedAsteriskDefault = PropertiesUtils.getBooleanProperty(System.getProperties(), "org.teiid.ignoreUnauthorizedAsterisk", true); //$NON-NLS-1$
	
	public DefaultAuthorizationValidator() {
	}

	@Override
	public boolean validate(String[] originalSql, Command command,
			QueryMetadataInterface metadata, CommandContext commandContext,
			CommandType commandType) throws QueryValidatorException,
			TeiidComponentException {
		AtomicReference<Boolean> modified = new AtomicReference<>(false);
		if (policyDecider != null && policyDecider.validateCommand(commandContext)) {
			AbstractValidationVisitor validationVisitor = new AbstractValidationVisitor() {
				@Override
				public void visit(Query obj) {
					try {
						modified.set(authorizeInAsterisk(obj, metadata, commandContext));
					} catch (TeiidException e) {
						handleException(e);
					}
				}
			};
			DeepPreOrderNavigator nav = new DeepPreOrderNavigator(validationVisitor) {
				@Override
				protected void postVisitVisitor(LanguageObject obj) {
					if (obj instanceof ScalarSubquery) {
						ScalarSubquery subQuery = (ScalarSubquery) obj;
						Expression symbol = subQuery.getCommand().getProjectedSymbols().iterator().next();
						subQuery.setType(symbol.getType());
					}
					super.postVisitVisitor(obj);
				}
			};
			command.acceptVisitor(nav);
			if (validationVisitor.getException() != null) {
				if (validationVisitor.getException().getCause() instanceof QueryValidatorException)
					throw (QueryValidatorException) validationVisitor.getException().getCause();
				throw validationVisitor.getException();
			}
			validateWithVisitor(command, metadata, commandContext);
		}
		return modified.get();
	}

	private boolean authorizeInAsterisk(Command command, QueryMetadataInterface metadata, CommandContext commandContext) throws QueryValidatorException,
			TeiidComponentException {
		boolean modified = false;
		if (ignoreUnathorizedInAsterisk(command, commandContext)) {
			Query query = (Query)command;
			HashMap<String, LanguageObject> map = null;
			for (Expression ex : query.getSelect().getSymbols()) {
				if (ex instanceof MultipleElementSymbol) {
					MultipleElementSymbol mes = (MultipleElementSymbol)ex;
					if (map == null) {
						map = new HashMap<>();
					}
					for (Iterator<ElementSymbol> iter = mes.getElementSymbols().iterator(); iter.hasNext();) {
						ElementSymbol es = iter.next();
						Object metadataObject = es.getMetadataID();
						if (metadataObject instanceof MultiSourceElement || metadataObject instanceof TempMetadataID) {
							continue;
						}
						map.clear();
						AuthorizationValidationVisitor.addToNameMap(metadataObject, es, map, commandContext.getMetadata());
						Set<String> results = this.policyDecider.getInaccessibleResources(PermissionType.READ, map.keySet(), Context.QUERY, commandContext);
						if (!results.isEmpty()) {
							iter.remove(); //remove from the select
							modified = true;
						}
					}
				}
			}
			if (query.getProjectedSymbols().isEmpty()) {
				validateWithVisitor(command, metadata, commandContext);
				throw new QueryValidatorException(QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31151));
			}
		}

		return modified;
	}

	private void validateWithVisitor(Command command, QueryMetadataInterface metadata, CommandContext commandContext)
			throws QueryValidatorException, TeiidComponentException {
		// EXTENSION
		// ISSUE: DVCORE-716
		// FIXED BY: Roman Pedchenko
		// DATE: 2013.10.24
//			AuthorizationValidationVisitor visitor = new AuthorizationValidationVisitor(this.policyDecider, commandContext);
		AuthorizationValidationVisitor visitor = (AuthorizationValidationVisitor) DVCaller.call(28, this.policyDecider, commandContext);
		if (visitor == null)
			visitor = new AuthorizationValidationVisitor(this.policyDecider, commandContext);
		// END EXTENSION
		Request.validateWithVisitor(visitor, metadata, command);
	}
	private boolean ignoreUnathorizedInAsterisk(Command command, CommandContext commandContext) {
		if (!(command instanceof Query)) {
			return false;
		}
		Query query = (Query)command;
		if (query.getIsXML() || query.getInto() != null) {
			return false;
		}
		if (ignoreUnauthorizedAsteriskDefault) {
			return true;
		}
		Object value = commandContext.getSessionVariable(IGNORE_UNAUTHORIZED_ASTERISK);
		if (value != null) {
			try {
				return Boolean.TRUE.equals(DataTypeManager.transformValue(value, DataTypeManager.DefaultDataClasses.BOOLEAN));
			} catch (TransformationException e) {
			}
		}
		return false;
	}
	
	@Override
	public boolean hasRole(String roleName, CommandContext commandContext) {
		if (policyDecider == null) {
			return true;
		}
		return this.policyDecider.hasRole(roleName, commandContext);
	}
	
	public void setPolicyDecider(PolicyDecider policyDecider) {
		this.policyDecider = policyDecider;
	}
	
	public PolicyDecider getPolicyDecider() {
		return policyDecider;
	}
	
	@Override
 	public boolean isAccessible(AbstractMetadataRecord record,
 		CommandContext commandContext) {
		if (policyDecider == null || !policyDecider.validateCommand(commandContext)) {
			return true;
		}
		AbstractMetadataRecord parent = record;
		while (parent.getParent() != null) {
			parent = parent.getParent();
			if (parent instanceof Procedure) {
				return true; //don't check procedure params/rs columns
			}
		}
		if (!(parent instanceof Schema) || (CoreConstants.SYSTEM_MODEL.equalsIgnoreCase(parent.getName()) || CoreConstants.ODBC_MODEL.equalsIgnoreCase(parent.getName()))) {
			//access is always allowed to system tables / procedures or unrooted objects
			return true;
		}
		PermissionType action = PermissionType.READ;
//		DVCORE-8142
//		if (record instanceof FunctionMethod || record instanceof Procedure) {
//			action = PermissionType.EXECUTE;
//		}

		//cache permission check
		Context context = getContext(record); 
		Boolean result = isAccessible(record, commandContext, action, context);
		if (!result && record instanceof Schema) {
			Schema s = (Schema)record;
			//check if anything can be seen below the schema level
	 	
			if (anyAccessible(s.getTables().values().stream().filter(t -> !t.isVirtual()).collect(Collectors.toList()), commandContext, PermissionType.READ, Context.QUERY)) {
				result = true;
			} else if (anyAccessible(s.getTables().values().stream().filter(t -> t.isVirtual()).collect(Collectors.toList()), commandContext, PermissionType.READ, Context.VIEW)) {
				result = true;	
//			DVCORE-8142
//			} else if (anyAccessible(s.getProcedures().values(), commandContext, PermissionType.EXECUTE)) {
			} else if (anyAccessible(s.getProcedures().values(), commandContext, PermissionType.READ, Context.PROCEDURE)) {
				result = true;
//			DVCORE-8142
//			} else if (anyAccessible(s.getFunctions().values(), commandContext, PermissionType.EXECUTE)) {
			} else if (anyAccessible(s.getFunctions().values(), commandContext, PermissionType.READ, Context.FUNCTION)) {
				result = true;
			}
	 	}
	 	commandContext.setAccessible(record, result);
	 	return result;
	 }

	private Context getContext(AbstractMetadataRecord record) {
		if (record instanceof Procedure)
			return Context.PROCEDURE;
		if (record instanceof FunctionMethod)
			return Context.FUNCTION;
		if (record instanceof FunctionParameter)
			return Context.FUNCTION;

		AbstractMetadataRecord target = record;
		if (record instanceof Column) {
			target = target.getParent();
			if (target != null && !(target instanceof Table))
				target = target.getParent();
			if (!(target instanceof Table))
				return Context.QUERY;
		}
		if (target instanceof Table) {
			if (((Table) target).isVirtual())
				return Context.VIEW;
			else
				return Context.QUERY;
		}

		return Context.METADATA;
	} 

	private boolean anyAccessible(Collection<? extends AbstractMetadataRecord> records, CommandContext commandContext, PermissionType permission, Context context) {
		for (AbstractMetadataRecord record : records) {
	 		if (isAccessible(record, commandContext, permission, context)) {
	 			return true;
	 		}
	 	}
	 	return false;
	}

	private Boolean isAccessible(AbstractMetadataRecord record,
			CommandContext commandContext, PermissionType action, Context context) {
		Boolean result = commandContext.isAccessible(record);
		if (result != null) {
			return result;
		}

		HashSet<String> resources = new HashSet<String>(2);
		resources.add(record.getFullName());
		return this.policyDecider.getInaccessibleResources(action, resources, context, commandContext).isEmpty();
	}


	@Override
	public boolean isAccessible(String fullName,
								CommandContext commandContext, PermissionType action) {
		HashSet<String> resources = new HashSet<>(2);
		resources.add(fullName);
		return this.policyDecider.getInaccessibleResources(action, resources, Context.METADATA, commandContext).isEmpty();
	}
}
