/*
 * Copyright (c) 2013 Denis Mikhalkin.
 *
 * This software is provided 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 com.denismo.apacheds.auth;

import com.denismo.aws.iam.*;
import org.apache.directory.server.core.api.LdapPrincipal;
import org.apache.directory.server.core.api.entry.ClonedServerEntry;
import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
import org.apache.directory.server.core.authn.AbstractAuthenticator;
import org.apache.directory.server.core.authn.SimpleAuthenticator;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.mina.core.session.IoSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.SocketAddress;
import java.util.Properties;

/**
 * User: Denis Mikhalkin
 * Date: 30/03/13
 * Time: 10:31 PM
 */
public class AWSIAMAuthenticator extends AbstractAuthenticator {
    private static final Logger LOG = LoggerFactory.getLogger(AWSIAMAuthenticator.class);

    public static class Config {
        public static final String PASSWORD_VALIDATOR = "iam_password";
        public static final String SECRET_KEY_VALIDATOR = "iam_secret_key";
        public static final String DUAL_VALIDATOR = "iam_dual";
        public String rootDN = "dc=iam,dc=aws,dc=org";
        public int pollPeriod = 600;
        public String validator = "iam_secret_key";

        public boolean isPasswordLogin() { return PASSWORD_VALIDATOR.equals(validator); }
        public boolean isSecretKeyLogin() { return SECRET_KEY_VALIDATOR.equals(validator); }
        public boolean isDualLogin() { return DUAL_VALIDATOR.equals(validator); }
    }

    private static Config s_config;
    public static void setConfig(Config config) {
        s_config = config;
    }

    public static Config getConfig() {
        return s_config;
    }

    private _IAMPasswordValidator validator;
    private LDAPIAMPoller poller;
    private SimpleAuthenticator delegatedAuth;
    private boolean disabled;

    public AWSIAMAuthenticator() {
        super(AuthenticationLevel.SIMPLE);
        delegatedAuth = new SimpleAuthenticator();
        LOG.info("AWSIAMAuthenticator has been created");
    }

    @Override
    protected void doInit() {
        super.doInit();
        LOG.debug("Init called");
        if (getDirectoryService() != null) {
            try {
                delegatedAuth.init(getDirectoryService());

                readIAMProperties();

                poller = new LDAPIAMPoller(getDirectoryService());
                poller.start();
            } catch (Exception e) {
                LOG.error("Exception initializing AWSIAMAuthenticator", e);
                disabled=true;
            }
        } else {
            LOG.debug("doInit without directory service");
        }
    }


    private void readIAMProperties() throws LdapException {
        String propsPath = System.getProperty("iamLdapPropertiesPath", "/etc/iam_ldap.conf");
        File propsFile = new File(propsPath);
        // Read the config file if exists
        if (propsFile.exists()) {
            try {
                Properties props = new Properties();
                props.load(new FileInputStream(propsFile));
                AWSIAMAuthenticator.Config config = new AWSIAMAuthenticator.Config();
                if (props.containsKey("pollPeriod")) config.pollPeriod = Integer.parseInt(props.getProperty("pollPeriod"));
                if (props.containsKey("rootDN")) config.rootDN = props.getProperty("rootDN");
                if (props.containsKey("validator")) config.validator = props.getProperty("validator");
                AWSIAMAuthenticator.setConfig(config);
            } catch (IOException e) {
                LOG.error("Unable to read IAM LDAP config file");
                AWSIAMAuthenticator.setConfig(new AWSIAMAuthenticator.Config());
            }
        } else {
            // Populate from defaults
            AWSIAMAuthenticator.setConfig(new AWSIAMAuthenticator.Config());
        }
        createValidator();
    }

    private void createValidator() throws LdapException {
        Config config = getConfig();
        if (config.isPasswordLogin()) {
            validator = new IAMAccountPasswordValidator();
        } else if (config.isSecretKeyLogin()) {
            validator = new IAMSecretKeyValidator();
        } else if (config.isDualLogin()) {
            validator = new IAMDualValidator();
        } else {
            throw new LdapException("Unsupported validator mode: " + config.validator);
        }
    }

    @Override
    public LdapPrincipal authenticate(BindOperationContext bindContext) throws Exception {
        if (!isAWSAccount(bindContext) || disabled) {
            LOG.debug("Skipping " + bindContext.getDn() + " - not an AWS account");
            if (delegatedAuth == null) {
                LOG.error("Delegated auth is null");
                return null;
            }
            return delegatedAuth.authenticate(bindContext);
        }

        LOG.debug("Authenticating " + bindContext.getDn());

        byte[] password = bindContext.getCredentials();

        LookupOperationContext lookupContext = new LookupOperationContext( getDirectoryService().getAdminSession(),
                bindContext.getDn(), SchemaConstants.ALL_USER_ATTRIBUTES, SchemaConstants.ALL_OPERATIONAL_ATTRIBUTES);

        Entry userEntry = getDirectoryService().getPartitionNexus().lookup( lookupContext );

        if (validator.verifyIAMPassword(userEntry, new String(password))) {
            LdapPrincipal principal = new LdapPrincipal( getDirectoryService().getSchemaManager(), bindContext.getDn(),
                    AuthenticationLevel.SIMPLE, password);
            IoSession session = bindContext.getIoSession();

            if ( session != null )
            {
                SocketAddress clientAddress = session.getRemoteAddress();
                principal.setClientAddress( clientAddress );
                SocketAddress serverAddress = session.getServiceAddress();
                principal.setServerAddress( serverAddress );
            }

            bindContext.setEntry( new ClonedServerEntry( userEntry ) );
            return principal;
        } else {
            // Bad password ...
            String message = I18n.err( I18n.ERR_230, bindContext.getDn().getName() );
            LOG.info( message );
            throw new LdapAuthenticationException( message );
        }
    }

    private boolean isAWSAccount(BindOperationContext bindContext) throws LdapException {
        LookupOperationContext lookupContext = new LookupOperationContext( getDirectoryService().getAdminSession(),
                bindContext.getDn(), SchemaConstants.ALL_USER_ATTRIBUTES, SchemaConstants.ALL_OPERATIONAL_ATTRIBUTES);

        Entry userEntry = getDirectoryService().getPartitionNexus().lookup( lookupContext );
        return userEntry.hasObjectClass("iamaccount");
    }
}