/*
 * Copyright 2017 Red Hat, Inc.
 *
 * 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
 *
 *   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.jboss.as.connector.security;

import static org.jboss.as.connector.logging.ConnectorLogger.SUBSYSTEM_RA_LOGGER;

import java.io.IOException;
import java.io.Serializable;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import jakarta.resource.spi.security.PasswordCredential;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import jakarta.security.auth.message.callback.CallerPrincipalCallback;
import jakarta.security.auth.message.callback.GroupPrincipalCallback;
import jakarta.security.auth.message.callback.PasswordValidationCallback;

import org.jboss.jca.core.spi.security.Callback;
import org.wildfly.security.auth.principal.NamePrincipal;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.auth.server.SecurityDomain;
import org.wildfly.security.auth.server.SecurityIdentity;
import org.wildfly.security.auth.server.ServerAuthenticationContext;
import org.wildfly.security.authz.RoleMapper;
import org.wildfly.security.authz.Roles;
import org.wildfly.security.evidence.PasswordGuessEvidence;
import org.wildfly.security.manager.WildFlySecurityManager;

/**
 * An Elytron based {@link CallbackHandler} implementation designed for the Jakarta Connectors security inflow. It uses the information
 * obtained from the {@link javax.security.auth.callback.Callback}s to authenticate and authorize the identity supplied
 * by the resource adapter and inserts the {@link SecurityIdentity} representing the authorized identity in the subject's
 * private credentials set.
 *
 * @author Flavia Rainone
 * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
 */
public class ElytronCallbackHandler implements CallbackHandler, Serializable {

    private final SecurityDomain securityDomain;

    private final Callback mappings;

    private Subject executionSubject;

    /**
     * Constructor
     * @param securityDomain the Elytron security domain used to establish the caller principal.
     * @param mappings The mappings.
     */
    public ElytronCallbackHandler(final SecurityDomain securityDomain, final Callback mappings) {
        this.securityDomain = securityDomain;
        this.mappings = mappings;
    }

    /**
     * {@inheritDoc}
     */
    public void handle(javax.security.auth.callback.Callback[] callbacks) throws UnsupportedCallbackException, IOException {
        if (SUBSYSTEM_RA_LOGGER.isTraceEnabled())
            SUBSYSTEM_RA_LOGGER.elytronHandlerHandle(Arrays.toString(callbacks));

        // work wrapper calls the callback handler a second time with default callback values after the handler was invoked
        // by the RA. We must check if the execution subject already contains an identity and allow for replacement of the
        // identity with values found in the default callbacks only if the subject has no identity yet or if the identity
        // is the anonymous one.
        if (this.executionSubject != null) {
            final SecurityIdentity subjectIdentity = this.getPrivateCredential(this.executionSubject, SecurityIdentity.class);
            if (subjectIdentity != null && !subjectIdentity.isAnonymous()) {
                return;
            }
        }

        if (callbacks != null && callbacks.length > 0)
        {
            if (this.mappings != null && this.mappings.isMappingRequired())
            {
                callbacks = this.mappings.mapCallbacks(callbacks);
            }

            GroupPrincipalCallback groupPrincipalCallback = null;
            CallerPrincipalCallback callerPrincipalCallback = null;
            PasswordValidationCallback passwordValidationCallback = null;

            for (javax.security.auth.callback.Callback callback : callbacks) {
                if (callback instanceof GroupPrincipalCallback) {
                    groupPrincipalCallback = (GroupPrincipalCallback) callback;
                    if (this.executionSubject == null) {
                        this.executionSubject = groupPrincipalCallback.getSubject();
                    } else if (!this.executionSubject.equals(groupPrincipalCallback.getSubject())) {
                        // TODO merge the contents of the subjects?
                    }
                } else if (callback instanceof CallerPrincipalCallback) {
                    callerPrincipalCallback = (CallerPrincipalCallback) callback;
                    if (this.executionSubject == null) {
                        this.executionSubject = callerPrincipalCallback.getSubject();
                    } else if (!this.executionSubject.equals(callerPrincipalCallback.getSubject())) {
                        // TODO merge the contents of the subjects?
                    }
                } else if (callback instanceof PasswordValidationCallback) {
                    passwordValidationCallback = (PasswordValidationCallback) callback;
                    if (this.executionSubject == null) {
                        this.executionSubject = passwordValidationCallback.getSubject();
                    } else if (!this.executionSubject.equals(passwordValidationCallback.getSubject())) {
                        // TODO merge the contents of the subjects?
                    }
                } else {
                    throw new UnsupportedCallbackException(callback);
                }
            }
            this.handleInternal(callerPrincipalCallback, groupPrincipalCallback, passwordValidationCallback);
        }
    }

    protected void handleInternal(final CallerPrincipalCallback callerPrincipalCallback, final GroupPrincipalCallback groupPrincipalCallback,
                                  final PasswordValidationCallback passwordValidationCallback) throws IOException {

        if(this.executionSubject == null) {
            throw SUBSYSTEM_RA_LOGGER.executionSubjectNotSetInHandler();
        }
        SecurityIdentity identity = this.securityDomain.getAnonymousSecurityIdentity();

        // establish the caller principal using the info from the callback.
        Principal callerPrincipal = null;
        if (callerPrincipalCallback != null) {
            Principal callbackPrincipal = callerPrincipalCallback.getPrincipal();
            callerPrincipal = callbackPrincipal != null ? new NamePrincipal(callbackPrincipal.getName()) :
                    callerPrincipalCallback.getName() != null ? new NamePrincipal(callerPrincipalCallback.getName()) : null;
        }

        // a null principal is the ra contract for requiring the use of the unauthenticated identity - no point in attempting to authenticate.
        if (callerPrincipal != null) {
            // check if we have a username/password pair to authenticate - first try the password validation callback.
            if (passwordValidationCallback != null) {
                final String username = passwordValidationCallback.getUsername();
                final char[] password = passwordValidationCallback.getPassword();
                try {
                    identity = this.authenticate(username, password);
                    // add a password credential to the execution subject and set the successful result in the callback.
                    this.addPrivateCredential(this.executionSubject, new PasswordCredential(username, password));
                    passwordValidationCallback.setResult(true);
                } catch (SecurityException e) {
                    passwordValidationCallback.setResult(false);
                    return;
                }
            } else {
                // identity not established using the callback - check if the execution subject contains a password credential.
                PasswordCredential passwordCredential = this.getPrivateCredential(this.executionSubject, PasswordCredential.class);
                if (passwordCredential != null) {
                    try {
                        identity = this.authenticate(passwordCredential.getUserName(), passwordCredential.getPassword());
                    } catch (SecurityException e) {
                        return;
                    }
                } else {
                    identity = securityDomain.createAdHocIdentity(callerPrincipal);
                }
            }

            // at this point we either have an authenticated identity or an anonymous one. We must now check if the caller principal
            // is different from the identity principal and switch to the caller principal identity if needed.
            if (!callerPrincipal.equals(identity.getPrincipal())) {
                identity = identity.createRunAsIdentity(callerPrincipal.getName());
            }

            // if we have new roles coming from the group callback, set a new mapper in the identity.
            if (groupPrincipalCallback != null) {
                String[] groups = groupPrincipalCallback.getGroups();
                if (groups != null) {
                    Set<String> roles = new HashSet<>(Arrays.asList(groups));
                    // TODO what category should we use here?
                    identity = identity.withRoleMapper(ElytronSecurityIntegration.SECURITY_IDENTITY_ROLE, RoleMapper.constant(Roles.fromSet(roles)));
                }
            }
        }

        // set the authenticated identity as a private credential in the subject.
        this.executionSubject.getPrincipals().add(identity.getPrincipal());
        this.addPrivateCredential(executionSubject, identity);
    }

    /**
     * Authenticate the user with the given credential against the configured Elytron security domain.
     *
     * @param username the user being authenticated.
     * @param credential the credential used as evidence to verify the user's identity.
     * @return the authenticated and authorized {@link SecurityIdentity}.
     * @throws IOException if an error occurs while authenticating the user.
     */
    private SecurityIdentity authenticate(final String username, final char[] credential) throws IOException {
        final ServerAuthenticationContext context = this.securityDomain.createNewAuthenticationContext();
        final PasswordGuessEvidence evidence = new PasswordGuessEvidence(credential != null ? credential : null);
        try {
            context.setAuthenticationName(username);
            if (context.verifyEvidence(evidence)) {
                if (context.authorize()) {
                    context.succeed();
                    return context.getAuthorizedIdentity();
                } else {
                    context.fail();
                    throw new SecurityException("Authorization failed");
                }
            } else {
                context.fail();
                throw new SecurityException("Authentication failed");
            }
        } catch (IllegalArgumentException | IllegalStateException | RealmUnavailableException e) {
            context.fail();
            throw e;
        } finally {
            if (!context.isDone()) {
                context.fail();
            }
            evidence.destroy();
        }
    }

    protected<T> T getPrivateCredential(final Subject subject, final Class<T> credentialClass) {
        T credential = null;
        if (subject != null) {
            Set<T> credentialSet;
            if (!WildFlySecurityManager.isChecking()) {
                credentialSet = subject.getPrivateCredentials(credentialClass);
            } else {
                credentialSet = AccessController.doPrivileged((PrivilegedAction<Set<T>>) () ->
                        subject.getPrivateCredentials(credentialClass));
            }
            if (!credentialSet.isEmpty()) {
                credential = credentialSet.iterator().next();
            }
        }
        return credential;
    }

    /**
     * Add the specified credential to the subject's private credentials set.
     *
     * @param subject the {@link Subject} to add the credential to.
     * @param credential a reference to the credential.
     */
    protected void addPrivateCredential(final Subject subject, final Object credential) {
        if (!WildFlySecurityManager.isChecking()) {
            subject.getPrivateCredentials().add(credential);
        }
        else {
            AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
                subject.getPrivateCredentials().add(credential);
                return null;
            });
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();

        sb.append("ElytronCallbackHandler@").append(Integer.toHexString(System.identityHashCode(this)));
        sb.append("[mappings=").append(mappings);
        sb.append("]");

        return sb.toString();
    }
}
