/*
 * Copyright 2004-present the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.security.taglibs.authz;

import java.io.IOException;
import java.util.Locale;
import java.util.Map;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

import org.springframework.context.ApplicationContext;
import org.springframework.core.GenericTypeResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParseException;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
import org.springframework.security.web.context.support.SecurityWebApplicationContextUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * A base class for an &lt;authorize&gt; tag that is independent of the tag rendering
 * technology (JSP, Facelets). It treats tag attributes as simple strings rather than
 * strings that may contain expressions with the exception of the "access" attribute,
 * which is always expected to contain a Spring EL expression.
 * <p>
 * Subclasses are expected to extract tag attribute values from the specific rendering
 * technology, evaluate them as expressions if necessary, and set the String-based
 * attributes of this class.
 *
 * @author Francois Beausoleil
 * @author Luke Taylor
 * @author Rossen Stoyanchev
 * @author Rob Winch
 * @since 3.1.0
 */
public abstract class AbstractAuthorizeTag {

	@SuppressWarnings("NullAway.Init")
	private @Nullable String access;

	@SuppressWarnings("NullAway.Init")
	private @Nullable String url;

	private @Nullable String method = "GET";

	/**
	 * This method allows subclasses to provide a way to access the ServletRequest
	 * according to the rendering technology.
	 */
	protected abstract ServletRequest getRequest();

	/**
	 * This method allows subclasses to provide a way to access the ServletResponse
	 * according to the rendering technology.
	 */
	protected abstract ServletResponse getResponse();

	/**
	 * This method allows subclasses to provide a way to access the ServletContext
	 * according to the rendering technology.
	 */
	protected abstract ServletContext getServletContext();

	/**
	 * Make an authorization decision by considering all &lt;authorize&gt; tag attributes.
	 * The following are valid combinations of attributes:
	 * <ul>
	 * <li>access</li>
	 * <li>url, method</li>
	 * </ul>
	 * The above combinations are mutually exclusive and evaluated in the given order.
	 * @return the result of the authorization decision
	 * @throws IOException
	 */
	public boolean authorize() throws IOException {
		if (StringUtils.hasText(getAccess())) {
			return authorizeUsingAccessExpression();
		}
		if (StringUtils.hasText(getUrl())) {
			return authorizeUsingUrlCheck();
		}
		return false;
	}

	/**
	 * Make an authorization decision based on a Spring EL expression. See the
	 * "Expression-Based Access Control" chapter in Spring Security for details on what
	 * expressions can be used.
	 * @return the result of the authorization decision
	 * @throws IOException
	 */
	@SuppressWarnings("NullAway") // Dataflow analysis limitation
	public boolean authorizeUsingAccessExpression() throws IOException {
		if (getContext().getAuthentication() == null) {
			return false;
		}
		String access = getAccess();
		Assert.notNull(access, "access cannot be null");
		SecurityExpressionHandler<FilterInvocation> handler = getExpressionHandler();
		Expression accessExpression;
		try {
			accessExpression = handler.getExpressionParser().parseExpression(access);
		}
		catch (ParseException ex) {
			throw new IOException(ex);
		}
		return ExpressionUtils.evaluateAsBoolean(accessExpression, createExpressionEvaluationContext(handler));
	}

	/**
	 * Allows the {@code EvaluationContext} to be customized for variable lookup etc.
	 */
	protected EvaluationContext createExpressionEvaluationContext(SecurityExpressionHandler<FilterInvocation> handler) {
		FilterInvocation f = new FilterInvocation(getRequest(), getResponse(), (request, response) -> {
			throw new UnsupportedOperationException();
		});
		return handler.createEvaluationContext(getContext().getAuthentication(), f);
	}

	/**
	 * Make an authorization decision based on the URL and HTTP method attributes. True is
	 * returned if the user is allowed to access the given URL as defined.
	 * @return the result of the authorization decision
	 * @throws IOException
	 */
	@SuppressWarnings("NullAway") // Dataflow analysis limitation
	public boolean authorizeUsingUrlCheck() throws IOException {
		String url = getUrl();
		Assert.notNull(url, "url cannot be null");
		String contextPath = ((HttpServletRequest) getRequest()).getContextPath();
		Authentication currentUser = getContext().getAuthentication();
		return getPrivilegeEvaluator().isAllowed(contextPath, url, getMethod(), currentUser);
	}

	public @Nullable String getAccess() {
		return this.access;
	}

	public void setAccess(String access) {
		this.access = access;
	}

	public @Nullable String getUrl() {
		return this.url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public @Nullable String getMethod() {
		return this.method;
	}

	@NullUnmarked
	public void setMethod(String method) {
		this.method = (method != null) ? method.toUpperCase(Locale.ENGLISH) : null;
	}

	private SecurityContext getContext() {
		ApplicationContext appContext = SecurityWebApplicationContextUtils
			.findRequiredWebApplicationContext(getServletContext());
		String[] names = appContext.getBeanNamesForType(SecurityContextHolderStrategy.class);
		if (names.length == 1) {
			SecurityContextHolderStrategy strategy = appContext.getBean(SecurityContextHolderStrategy.class);
			return strategy.getContext();
		}
		return SecurityContextHolder.getContext();
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private SecurityExpressionHandler<FilterInvocation> getExpressionHandler() throws IOException {
		ApplicationContext appContext = SecurityWebApplicationContextUtils
			.findRequiredWebApplicationContext(getServletContext());
		Map<String, SecurityExpressionHandler> handlers = appContext.getBeansOfType(SecurityExpressionHandler.class);
		for (SecurityExpressionHandler handler : handlers.values()) {
			if (FilterInvocation.class
				.equals(GenericTypeResolver.resolveTypeArgument(handler.getClass(), SecurityExpressionHandler.class))) {
				return handler;
			}
		}
		throw new IOException("No visible WebSecurityExpressionHandler instance could be found in the application "
				+ "context. There must be at least one in order to support expressions in JSP 'authorize' tags.");
	}

	private WebInvocationPrivilegeEvaluator getPrivilegeEvaluator() throws IOException {
		WebInvocationPrivilegeEvaluator privEvaluatorFromRequest = (WebInvocationPrivilegeEvaluator) getRequest()
			.getAttribute(WebAttributes.WEB_INVOCATION_PRIVILEGE_EVALUATOR_ATTRIBUTE);
		if (privEvaluatorFromRequest != null) {
			return privEvaluatorFromRequest;
		}
		ApplicationContext ctx = SecurityWebApplicationContextUtils
			.findRequiredWebApplicationContext(getServletContext());
		Map<String, WebInvocationPrivilegeEvaluator> wipes = ctx.getBeansOfType(WebInvocationPrivilegeEvaluator.class);
		if (wipes.isEmpty()) {
			throw new IOException(
					"No visible WebInvocationPrivilegeEvaluator instance could be found in the application "
							+ "context. There must be at least one in order to support the use of URL access checks in 'authorize' tags.");
		}
		return (WebInvocationPrivilegeEvaluator) wipes.values().toArray()[0];
	}

}
