/* Copyright (c) restSQL Project Contributors. Licensed under MIT. */
package org.restsql.security.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TreeMap;

import org.restsql.core.Config;
import org.restsql.core.Request;
import org.restsql.security.Authorizer;
import org.restsql.security.SecurityContext;
import org.restsql.security.SqlResourceRolePrivileges;
import org.restsql.security.SqlResourceRolePrivileges.Privilege;

/**
 * Authorizes restSQL requests using privileges properties file, which maps roles to request types on SQL Resources. If
 * privileges properties are not defined or not able to load, authorization is disabled. In that case,
 * <code>isAuthorized()</code> always returns <code>true</code>.
 * 
 * @author Mark Sawers
 */
public class AuthorizerImpl implements Authorizer {
	private boolean authEnabled = false;
	private String authStatusMessage;
	private String configFileName;
	// Map<sqlResource, List<SqlResourceRolePrivileges>>
	private Map<String, List<SqlResourceRolePrivileges>> privilegeMap;

	/** For use by the Factory. Uses properties file specified in standard restSQL properties. */
	public AuthorizerImpl() {
		configFileName = Config.properties.getProperty(Config.KEY_SECURITY_PRIVILEGES, null);
		loadPrivileges();
	}

	/** Creates object in test environment with specified properties file. */
	AuthorizerImpl(String configFileName) {
		this.configFileName = configFileName;
		loadPrivileges();
	}

	/** Returns true if authorization is enabled. */
	public boolean isAuthorizationEnabled() {
		return authEnabled;
	}
	
	/** Returns status message and string representation of all roles and associated privileges. */
	@Override
	public String dumpConfig() {
		final StringBuilder string = new StringBuilder(1000);
		string.append(authStatusMessage);
		if (authEnabled && privilegeMap.size() > 0) {
			string.append("\n\n[SqlResource,*].[requestType,*]=role\n");
			string.append("---------------------------------------------------------------------------\n");
			for (final List<SqlResourceRolePrivileges> privObjList : privilegeMap.values()) {
				for (final SqlResourceRolePrivileges privileges : privObjList) {
					string.append(privileges.toString());
					string.append("\n");
				}
			}
		}
		return string.toString();
	}

	// ** Returns true if user is authorized (or authorization is disabled), false otherwise. */
	@Override
	public boolean isAuthorized(final SecurityContext context, final Request request) {
		return isAuthorized(context, request.getType(), request.getSqlResource());
	}

	/** Returns true if user is authorized (or authorization is disabled), false otherwise. */
	@Override
	public boolean isAuthorized(SecurityContext context, Request.Type requestType, String sqlResource) {
		boolean authorized = false;
		if (authEnabled) {
			authorized = isAuthorized(context, requestType, privilegeMap.get(sqlResource));
			if (!authorized) {
				authorized = isAuthorized(context, requestType, privilegeMap.get(SqlResourceRolePrivileges.TOKEN_WILDCARD));
			}
		} else {
			authorized = true;
		}
		return authorized;
	}

	// Private utils

	private boolean isAuthorized(final SecurityContext context, final Request.Type requestType,
			final List<SqlResourceRolePrivileges> privilegeList) {
		boolean authorized = false;
		if (privilegeList != null) {
			for (final SqlResourceRolePrivileges privileges : privilegeList) {
				if (context.isUserInRole(privileges.getRoleName())
						&& privileges.hasPrivilege(requestType)) {
					authorized = true;
					break;
				}
			}
		}
		return authorized;
	}

	/** Loads authorization config from configured properties file. */
	private void loadPrivileges() {
		if (configFileName == null) {
			// File not configured, log warning
			authStatusMessage = "Authorization disabled -- No privileges defined. Use "
					+ Config.KEY_SECURITY_PRIVILEGES + " in restsql properties.";
			Config.logger.warn(authStatusMessage);

		} else {
			authEnabled = true;
			privilegeMap = new TreeMap<String, List<SqlResourceRolePrivileges>>();
			InputStream inputStream = null;
			try {
				final File file = new File(configFileName);
				if (!file.exists()) {
					// File not found, log error
					authStatusMessage = "Authorization enabled -- But privileges properties file "
							+ configFileName + " not found";
					Config.logger.error(authStatusMessage);
				} else {
					if (Config.logger.isDebugEnabled()) {
						Config.logger.debug("Loading privileges from " + configFileName);
					}

					// Load file into properties object
					final Properties properties = new Properties();
					inputStream = new FileInputStream(file);
					properties.load(inputStream);

					// Parse file contents
					// Format is [SqlResource,*].[requestType,*]=[*,role]
					for (final String name : properties.stringPropertyNames()) {
						// Parse name/value pair contents
						final String definition = name + "=" + properties.getProperty(name);
						final StringTokenizer nameTokenizer = new StringTokenizer(name, ".");
						if (nameTokenizer.hasMoreTokens()) {
							final String sqlResource = nameTokenizer.nextToken();
							if (nameTokenizer.hasMoreTokens()) {
								// Parse privileges string
								final String privilegesString = nameTokenizer.nextToken();
								final StringTokenizer privilegeTokenizer = new StringTokenizer(
										privilegesString, ",");
								final List<Privilege> privilegesList = new ArrayList<SqlResourceRolePrivileges.Privilege>(
										4);
								while (privilegeTokenizer.hasMoreTokens()) {
									final String token = privilegeTokenizer.nextToken();
									try {
										final Privilege privilege = Privilege.fromString(token);
										privilegesList.add(privilege);
									} catch (final IllegalArgumentException exception) {
										// privilege not found
										Config.logger
												.warn("Privilege definition '"
														+ definition
														+ "' contains invalid request type '"
														+ token
														+ "' -- privilege must be a request type (select, insert, update, delete) or wildcard (*)");
									}
								}

								if (privilegesList.size() > 0) {
									// Create collection of privileges
									final List<SqlResourceRolePrivileges> privObjectsList = new ArrayList<SqlResourceRolePrivileges>(
											5);

									// Parse value string
									final StringTokenizer roleTokenizer = new StringTokenizer(
											properties.getProperty(name), ",");
									while (roleTokenizer.hasMoreTokens()) {
										final String roleName = roleTokenizer.nextToken();
										if (roleName != null) {
											// Create priv object and add to object list
											final SqlResourceRolePrivileges privileges = new SqlResourceRolePrivileges(
													sqlResource, privilegesList, roleName);
											privObjectsList.add(privileges);
										}
									}

									if (privObjectsList.size() > 0) {
										// Initialize priv map if necessary, add object list to map
										if (privilegeMap.containsKey(sqlResource)) {
											privilegeMap.get(sqlResource).addAll(privObjectsList);
										} else {
											privilegeMap.put(sqlResource, privObjectsList);
										}
										authEnabled = true;
									}
								}

							} else {
								// definition is missing dot
								Config.logger.warn("Privilege definition '" + definition + "' ignored "
										+ " -- Use format [SqlResource,*].[requestType,*]=[*,role]");
							}
						}
						// else line is blank, move on
					}

					// Log results
					if (privilegeMap.size() == 0) {
						// No valid privs found
						authStatusMessage = "Authorization enabled -- But no privileges valid in "
								+ configFileName;
						Config.logger.error(authStatusMessage);
					} else {
						// We're good!
						authStatusMessage = "Authorization enabled -- Loaded privileges from "
								+ configFileName;
						if (Config.logger.isInfoEnabled()) {
							Config.logger.info(authStatusMessage);
						}
					}
				}
			} catch (final IOException exception) {
				// Error reading file or parsing contents, log error
				authStatusMessage = "Authorization enabled -- Failed to load privileges properties from "
						+ configFileName;
				Config.logger.error(authStatusMessage, exception);
			} finally {
				// Close stream
				if (inputStream != null) {
					try {
						inputStream.close();
					} catch (final IOException exception) {
						// ignore
					}
				}
			}
		}
	}
}