/**
 * 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.storm.messaging.netty;

import java.io.IOException;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KerberosTicket;
import javax.security.auth.login.LoginException;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import org.apache.storm.security.auth.ClientAuthUtils;
import org.apache.storm.security.auth.KerberosPrincipalToLocal;
import org.apache.storm.security.auth.SubjectCompat;
import org.apache.storm.shade.org.apache.zookeeper.server.auth.KerberosName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


class KerberosSaslNettyServer {

    private static final Logger LOG = LoggerFactory
        .getLogger(KerberosSaslNettyServer.class);

    private SaslServer saslServer;
    private Subject subject;
    private List<String> authorizedUsers;

    KerberosSaslNettyServer(Map<String, Object> topoConf, String jaasSection, List<String> authorizedUsers) {
        this.authorizedUsers = authorizedUsers;

        LOG.debug("KerberosSaslNettyServer: authmethod {}", SaslUtils.KERBEROS);

        KerberosSaslCallbackHandler ch = new KerberosSaslNettyServer.KerberosSaslCallbackHandler(authorizedUsers);
        String jaasConfFile = ClientAuthUtils.getJaasConf(topoConf);

        //login our principal
        subject = null;
        try {
            LOG.debug("Trying to login using {}.", jaasConfFile);
            Login login = new Login(jaasSection, ch, jaasConfFile);
            subject = login.getSubject();
            LOG.debug("Got Subject: {}", subject.toString());
        } catch (LoginException ex) {
            LOG.error("Server failed to login in principal:", ex);
            throw new RuntimeException(ex);
        }

        //check the credential of our principal
        if (subject.getPrivateCredentials(KerberosTicket.class).isEmpty()) {
            LOG.error("Failed to verifyuser principal.");
            throw new RuntimeException("Fail to verify user principal with section \""
                                       + jaasSection
                                       + "\" in login configuration file "
                                       + jaasConfFile);
        }

        try {
            LOG.info("Creating Kerberos Server.");
            final CallbackHandler fch = ch;
            Principal p = (Principal) subject.getPrincipals().toArray()[0];
            KerberosName kerberosName = new KerberosName(p.getName());
            final String hostName = kerberosName.getHostName();
            final String serviceName = kerberosName.getServiceName();
            LOG.debug("Server with host: {}", hostName);
            saslServer = SubjectCompat.doAs(subject, () -> {
                try {
                    Map<String, String> props = new TreeMap<String, String>();
                    props.put(Sasl.QOP, "auth");
                    props.put(Sasl.SERVER_AUTH, "false");
                    return Sasl.createSaslServer(SaslUtils.KERBEROS,
                                                 serviceName,
                                                 hostName, props, fch);
                } catch (Exception e) {
                    LOG.error("Subject failed to create sasl server.", e);
                    return null;
                }
            });
            LOG.info("Got Server: {}", saslServer);

        } catch (Exception e) {
            LOG.error("KerberosSaslNettyServer: Could not create SaslServer: ", e);
            throw new RuntimeException(e);
        }
    }

    public boolean isComplete() {
        return saslServer.isComplete();
    }

    public String getUserName() {
        return saslServer.getAuthorizationID();
    }

    /**
     * Used by SaslTokenMessage::processToken() to respond to server SASL tokens.
     *
     * @param token Server's SASL token
     * @return token to send back to the server.
     */
    public byte[] response(final byte[] token) {
        try {
            return SubjectCompat.doAs(subject, () -> {
                try {
                    LOG.debug("response: Responding to input token of length: {}",
                              token.length);
                    return saslServer.evaluateResponse(token);
                } catch (SaslException e) {
                    LOG.error("response: Failed to evaluate client token of length: {} : {}",
                              token.length, e);
                    throw new RuntimeException(e);
                }
            });
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            LOG.error("Failed to generate response for token: ", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * CallbackHandler for SASL DIGEST-MD5 mechanism.
     */
    public static class KerberosSaslCallbackHandler implements CallbackHandler {

        /**
         * Used to authenticate the clients.
         */
        private List<String> authorizedUsers;

        KerberosSaslCallbackHandler(List<String> authorizedUsers) {
            LOG.debug("KerberosSaslCallback: Creating KerberosSaslCallback handler.");
            this.authorizedUsers = authorizedUsers;
        }

        @Override
        public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
            for (Callback callback : callbacks) {
                LOG.info("Kerberos Callback Handler got callback: {}", callback.getClass());
                if (callback instanceof AuthorizeCallback) {
                    AuthorizeCallback ac = (AuthorizeCallback) callback;
                    if (!ac.getAuthenticationID().equals(ac.getAuthorizationID())) {
                        LOG.debug("{} != {}", ac.getAuthenticationID(), ac.getAuthorizationID());
                        continue;
                    }

                    LOG.debug("Authorized Users: {}", authorizedUsers);
                    LOG.debug("Checking authorization for: {}", ac.getAuthorizationID());
                    for (String user : authorizedUsers) {
                        String requester = ac.getAuthorizationID();

                        KerberosPrincipal principal = new KerberosPrincipal(requester);
                        requester = new KerberosPrincipalToLocal().toLocal(principal);

                        if (requester.equals(user)) {
                            ac.setAuthorized(true);
                            break;
                        }
                    }
                }
            }
        }
    }
}
