/*
 * 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.guacamole.auth.tacacs;

import com.google.inject.Inject;
import com.google.inject.Provider;

import java.util.Arrays;

import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.guacamole.auth.tacacs.user.AuthenticatedUser;
import org.apache.guacamole.auth.tacacs.utils.AuthenticationTacacs;
import org.apache.guacamole.auth.tacacs.utils.Utils;
import org.apache.guacamole.auth.jdbc.security.PasswordEncryptionService;
import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
import org.apache.guacamole.auth.jdbc.user.ModeledUser;
import org.apache.guacamole.auth.jdbc.user.UserMapper;
import org.apache.guacamole.auth.jdbc.user.UserModel;

/**
 * Service providing convenience functions for the Tacacs
 * AuthenticationProvider implementation.
 */
public class AuthenticationProviderService {
    /**
     * Logger for this class.
     */
    private static final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);

    /**
     * Guacamole's administrator user.
     */
    private static final String GUACAMOLE_ADMINISTRATOR = "guacadmin";
    
    /**
     * Mapper for accessing users.
     */
    @Inject
    private UserMapper userMapper;

    /**
     * Provider for creating users.
     */
    @Inject
    private Provider<ModeledUser> userProvider;
    
    /**
     * Service for hashing passwords.
     */
    @Inject
    private PasswordEncryptionService encryptionService;
    
    /**
     * Provider for AuthenticatedUser objects.
     */
    @Inject
    private Provider<AuthenticatedUser> authenticatedUserProvider;
    
    /**
     * Returns the permission set associated with the given user and related
     * to the type of objects handled by this directory object service.
     *
     * @param user
     *     The user whose permissions are being retrieved.
     *
     * @return
     *     A permission set which contains the permissions associated with the
     *     given user and related to the type of objects handled by this
     *     directory object service.
     * 
     * @throws GuacamoleException
     *     If permission to read the user's permissions is denied.
     */
    private ObjectPermissionSet getPermissionSet(ModeledAuthenticatedUser user)
            throws GuacamoleException {

        // Return permissions related to users
        return user.getUser().getUserPermissions();
    }
    
    /**
     * Returns whether the given user has permission to perform a certain
     * action on a specific object managed by this directory object service.
     *
     * @param user
     *     The user being checked.
     *
     * @param identifier
     *     The identifier of the object to check.
     *
     * @param type
     *     The type of action that will be performed.
     *
     * @return
     *     true if the user has object permission relevant described, false
     *     otherwise.
     * 
     * @throws GuacamoleException
     *     If permission to read the user's permissions is denied.
     */
    private boolean hasObjectPermission(ModeledAuthenticatedUser user,
            String identifier, ObjectPermission.Type type)
            throws GuacamoleException {

        // Get object permissions
        ObjectPermissionSet permissionSet = getPermissionSet(user);
        
        // Return whether permission is granted
        return user.getUser().isAdministrator()
            || permissionSet.hasPermission(type, identifier);

    }
    
    /**
     * Returns an instance of an object which is backed by the given model
     * object.
     *
     * @param currentUser
     *     The user for whom this object is being created.
     *
     * @param model
     *     The model object to use to back the returned object.
     *
     * @return
     *     An object which is backed by the given model object.
     *
     * @throws GuacamoleException
     *     If the object instance cannot be created.
     */
    private ModeledUser getObjectInstance(ModeledAuthenticatedUser currentUser,
            UserModel model) throws GuacamoleException {

        boolean exposeRestrictedAttributes;

        // Expose restricted attributes if the user does not yet exist
        if (model.getObjectID() == null)
            exposeRestrictedAttributes = true;

        // Otherwise, if the user permissions are available, expose restricted
        // attributes only if the user has ADMINISTER permission
        else if (currentUser != null)
            exposeRestrictedAttributes = hasObjectPermission(currentUser,
                    model.getIdentifier(), ObjectPermission.Type.ADMINISTER);

        // If user permissions are not available, do not expose anything
        else
            exposeRestrictedAttributes = false;

        // Produce ModeledUser exposing only those attributes for which the
        // current user has permission
        ModeledUser user = userProvider.get();
        user.init(currentUser, model, exposeRestrictedAttributes);
        return user;
    }

    private String getOriginalPassword(Credentials credentials) throws GuacamoleException {
    	// Get username and password
    	String username = credentials.getUsername();
    	String password = credentials.getPassword();

    	// Retrieve corresponding user model, if such a user exists
    	UserModel userModel = userMapper.selectOne(username);
    	if (userModel != null) {
    		// Create corresponding user object, set up cyclic reference
    		ModeledUser user = getObjectInstance(null, userModel);
    		byte[] hash = encryptionService.createPasswordHash(password, userModel.getPasswordSalt());
    
    		// Verify provided password is correct (return MySQL saved password)
    		if (username.indexOf(GUACAMOLE_ADMINISTRATOR) == -1
    			&& !Arrays.equals(hash, userModel.getPasswordHash())) {
    			return user.getPassword();
    		}
    	}
    	return null;
	}

    /**
     * Returns an AuthenticatedUser representing the user authenticated by the
     * given credentials.
     *
     * @param credentials
     *     The credentials to use for authentication.
     *
     * @return
     *     An AuthenticatedUser representing the user authenticated by the
     *     given credentials.
     *
     * @throws GuacamoleException
     *     If an error occurs while authenticating the user, or if access is
     *     denied.
     */
    public AuthenticatedUser authenticateUser(Credentials credentials)
            throws GuacamoleException {
    	validateTacacsAuthentication(credentials);
        AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
        if (authenticatedUser != null && credentials.getUsername() != null
        	&& credentials.getUsername().indexOf(GUACAMOLE_ADMINISTRATOR) == -1) {
        	logger.debug("User:" + credentials.getUsername() + " [" + credentials.getPassword() + "]");
        	String originalPassword = getOriginalPassword(credentials);
        	if (originalPassword != null) {
        		credentials.setPassword(originalPassword);
        		authenticatedUser.init(credentials.getUsername(), credentials);
        		return authenticatedUser;
        	}
        }
        // Authentication not provided via Tacacs, yet, so we request it.
        throw new GuacamoleInvalidCredentialsException("Invalid login.", CredentialsInfo.USERNAME_PASSWORD);
    }
    
    private void validateTacacsAuthentication(Credentials credentials) throws GuacamoleException {
    	if (credentials.getUsername() != null
    		&& credentials.getUsername().indexOf(GUACAMOLE_ADMINISTRATOR) == -1) {
    		if (!AuthenticationTacacs.authenticate(credentials.getUsername(),
                    credentials.getPassword(),
                    credentials.getRemoteAddress())) {
    			logger.warn("Tacacs authentication attempt from {} for user \"{}\" failed.",
    					credentials.getRemoteAddress(), credentials.getUsername());
    			throw new GuacamoleInvalidCredentialsException(
    					"Tacacs authentication attempt from " + credentials.getRemoteAddress()
    					+ " for user \"" + credentials.getUsername() + "\" failed.",
    					CredentialsInfo.USERNAME_PASSWORD);
            } else {
                if (logger.isInfoEnabled())
                    logger.info("User \"{}\" successfully authenticated with Tacacs from {}.",
                            credentials.getUsername(),
                            Utils.getLoggableAddress(credentials.getRequest()));

            }
        }
    }
}
