/*
 * ModeShape (http://www.modeshape.org)
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * See the AUTHORS.txt file in the distribution for a full listing of 
 * individual contributors.
 *
 * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
 * is licensed to you under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 * 
 * ModeShape is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.modeshape.jboss.security;

import java.security.AccessController;
import java.util.Map;
import javax.jcr.Credentials;
import javax.jcr.SimpleCredentials;
import javax.security.auth.Subject;
import org.jboss.security.AuthenticationManager;
import org.modeshape.jboss.service.RepositoryService;
import org.modeshape.jcr.ExecutionContext;
import org.modeshape.jcr.api.JaasCredentials;
import org.modeshape.jcr.security.JaasSecurityContext;
import org.modeshape.jcr.security.JaccSubjectResolver;
import org.modeshape.jcr.security.SecurityContext;
import org.modeshape.jcr.security.EnvironmentAuthenticationProvider;
import org.modeshape.jcr.security.SimplePrincipal;

/**
 * {@link org.modeshape.jcr.security.EnvironmentAuthenticationProvider} used to interact with the security subsystem from a
 * JBoss Application Server.
 *
 * @author Horia Chiorean (hchiorea@redhat.com)
 * @see <a href="http://issues.jboss.org/browse/MODE-2411">MODE-2411</a>
 */
public class JBossDomainAuthenticationProvider extends EnvironmentAuthenticationProvider {

    private static final org.jboss.logging.Logger LOGGER = org.jboss.logging.Logger.getLogger(
            JBossDomainAuthenticationProvider.class.getPackage().getName());
    
    private AuthenticationManager authenticationManager;
    private JaccSubjectResolver jaccSubjectResolver;
    
    @Override
    public void initialize() {
        String domainName = securityDomain();
        this.authenticationManager = environment().getSecurityManagementServiceInjector().getValue().getAuthenticationManager(domainName);
        assert this.authenticationManager != null;
        // any JBoss container should be JACC compliant, so the necessary jars should be present in the classpath
        this.jaccSubjectResolver = new JaccSubjectResolver();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Initialized JBoss authentication provider using the container's authentication manager....");
        }
    }

    @Override
    public ExecutionContext authenticate( Credentials credentials, String repositoryName, String workspaceName,
                                          ExecutionContext repositoryContext, Map<String, Object> sessionAttributes ) {
        if (credentials == null) {
            return getPreauthenticatedSubject(repositoryContext);
        }
        if (credentials instanceof SimpleCredentials) {
            return validateSimpleCredentials((SimpleCredentials)credentials, repositoryContext);
        }
        if (credentials instanceof JaasCredentials) {
            return getSubjectFromJaas((JaasCredentials)credentials, repositoryContext);             
        }
        LOGGER.debugv("Unknown {0} implementation: {1}. Please user either {2} or {3}", Credentials.class.getName(),
                     credentials.getClass().getName(), SimpleCredentials.class.getName(), JaasCredentials.class.getName());
        return null;
    }

    private ExecutionContext getSubjectFromJaas( JaasCredentials credentials, ExecutionContext repositoryContext ) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Looking for an active subject in the JaasCredentials instance...");
        }
        Subject subject = credentials.getLoginContext().getSubject();
        if (subject == null) {
            LOGGER.warn("Cannot authenticate because the JassCredentials instance has a login context with a null subject...");
            return null; 
        }
        return repositoryContext.with(new JBossSecurityContext(new JaasSecurityContext(subject)));
    }

    private ExecutionContext validateSimpleCredentials( SimpleCredentials credentials, ExecutionContext repositoryContext) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debugv("Authenticating {0} in the {1} security domain using the JBoss Server security manager", credentials.getUserID(),
                          securityDomain());
        }
        Subject subject = new Subject();
        if (authenticationManager.isValid(SimplePrincipal.newInstance(credentials.getUserID()), credentials.getPassword(),
                                          subject)) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Authentication successful....");
            }
            return repositoryContext.with(new JBossSecurityContext(new JaasSecurityContext(subject)));
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debugv("Credentials for {0} are not valid for the {1} security domain", credentials.getUserID(), securityDomain());
            }
            return null;
        }
    }

    private ExecutionContext getPreauthenticatedSubject( ExecutionContext repositoryContext ) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Received null credentials, attempting to search for an active subject on the calling thread via JACC");
        }
        // There are no credentials, so see if there is an authenticated Subject ...
        Subject subject = Subject.getSubject(AccessController.getContext());
        if (subject != null) {
            // There is, so use this subject ...
            return repositoryContext.with(new JBossSecurityContext(new JaasSecurityContext(subject)));
        }
        subject = jaccSubjectResolver.resolveSubject();
        // there are no credentials and we failed to find a pre-authenticated subject, so return null.
        return subject != null ? repositoryContext.with(new JBossSecurityContext(new JaasSecurityContext(subject))) : null;
    }
     
    @Override
    protected RepositoryService environment() {
        return (RepositoryService)super.environment();
    }

    private final class JBossSecurityContext implements SecurityContext {
        private final JaasSecurityContext jaasSecurityContext;

        private JBossSecurityContext( JaasSecurityContext jaasSecurityContext ) {
            assert jaasSecurityContext != null;
            this.jaasSecurityContext = jaasSecurityContext;
        }

        @Override
        public boolean isAnonymous() {
            return jaasSecurityContext.isAnonymous();
        }

        @Override
        public String getUserName() {
            return jaasSecurityContext.getUserName();
        }

        @Override
        public boolean hasRole( String roleName ) {
            return jaasSecurityContext.hasRole(roleName);
        }

        @Override
        public void logout() {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Logging out security context....");
            }
            authenticationManager.logout(SimplePrincipal.newInstance(jaasSecurityContext.getUserName()), null);
            jaasSecurityContext.logout();
        }
    }
}
