/*
 * 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.jackrabbit.core.security.authentication;

import java.io.IOException;
import java.security.Principal;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import javax.jcr.Credentials;
import javax.jcr.GuestCredentials;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import org.apache.jackrabbit.api.security.principal.PrincipalIterator;
import org.apache.jackrabbit.core.config.LoginModuleConfig;
import org.apache.jackrabbit.core.security.SecurityConstants;
import org.apache.jackrabbit.core.security.principal.PrincipalProvider;
import org.apache.jackrabbit.core.security.principal.PrincipalProviderRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <code>AbstractLoginModule</code> provides the means for the common
 * authentication tasks within the Repository.
 * <p/>
 * On successful authentication it associates the credentials to principals
 * using the {@link PrincipalProvider} configured for this LoginModule<p />
 * Jackrabbit distinguishes between Login and Impersonation dispatching the
 * the corresponding Repository/Session methods to
 * {@link #authenticate(java.security.Principal, javax.jcr.Credentials)} and
 * {@link #impersonate(java.security.Principal, javax.jcr.Credentials)}, respectively.
 * <br>
 * This LoginModule implements default behavior for either method.
 *
 * @see LoginModule
 */
public abstract class AbstractLoginModule implements LoginModule {

    private static final Logger log = LoggerFactory.getLogger(AbstractLoginModule.class);

    private static final String KEY_CREDENTIALS = "org.apache.jackrabbit.credentials";
    private static final String KEY_LOGIN_NAME = "javax.security.auth.login.name";

    /**
     * The name of the login module configuration option providing the name
     * of the SimpleCredentials attribute used to identify a pre-authenticated
     * login.
     *
     * @see #isPreAuthenticated(Credentials)
     */
    private static final String PRE_AUTHENTICATED_ATTRIBUTE_OPTION = "trust_credentials_attribute";

    private String principalProviderClassName;
    private boolean initialized;

    protected String adminId;
    protected String anonymousId;

    /**
     * The name of the credentials attribute providing a hint that the
     * credentials should be taken as is and the user requesting access
     * has already been authenticated outside of this LoginModule.
     *
     * @see #getPreAuthAttributeName()
     */
    private String preAuthAttributeName;


    protected CallbackHandler callbackHandler;

    protected Principal principal;
    protected SimpleCredentials credentials;
    protected Subject subject;
    protected PrincipalProvider principalProvider;

    protected Map sharedState;

    /**
     * Initialize this LoginModule and sets the following fields for later usage:
     * <ul>
     * <li>{@link PrincipalProvider} for user-{@link Principal} resolution.</li>
     * <li>{@link LoginModuleConfig#PARAM_ADMIN_ID} option is evaluated</li>
     * <li>{@link LoginModuleConfig#PARAM_ANONYMOUS_ID} option is evaluated</li>
     * </ul>
     * Implementations are called via
     * {@link #doInit(CallbackHandler, Session, Map)} to implement
     * additional initialization
     *
     * @param subject         the <code>Subject</code> to be authenticated. <p>
     * @param callbackHandler a <code>CallbackHandler</code> for communicating
     *                        with the end user (prompting for usernames and
     *                        passwords, for example). <p>
     * @param sharedState     state shared with other configured
     *                        LoginModules.<p>
     * @param options         options specified in the login <code>Configuration</code>
     *                        for this particular <code>LoginModule</code>.
     * @see LoginModule#initialize(Subject, CallbackHandler, Map, Map)
     * @see #doInit(CallbackHandler, Session, Map)
     * @see #isInitialized()
     */
    public void initialize(Subject subject, CallbackHandler callbackHandler,
                           Map<String,?> sharedState, Map<String,?> options) {
        // common jaas state variables
        this.callbackHandler = callbackHandler;
        this.subject = subject;
        this.sharedState = sharedState;

        // initialize the login module
        try {
            log.debug("Initialize LoginModule: ");
            RepositoryCallback repositoryCb = new RepositoryCallback();
            callbackHandler.handle(new Callback[]{repositoryCb});

            PrincipalProviderRegistry registry = repositoryCb.getPrincipalProviderRegistry();
            // check if the class name of a PrincipalProvider implementation
            // is present with the module configuration.
            if (options.containsKey(LoginModuleConfig.PARAM_PRINCIPAL_PROVIDER_CLASS)) {
                Object pcOption = options.get(LoginModuleConfig.PARAM_PRINCIPAL_PROVIDER_CLASS);
                if (pcOption != null) {
                    principalProviderClassName = pcOption.toString();
                }
            }
            if (principalProviderClassName == null) {
                // try compatibility parameters
                if (options.containsKey(LoginModuleConfig.COMPAT_PRINCIPAL_PROVIDER_NAME)) {
                    principalProviderClassName = options.get(LoginModuleConfig.COMPAT_PRINCIPAL_PROVIDER_NAME).toString();
                } else if (options.containsKey(LoginModuleConfig.COMPAT_PRINCIPAL_PROVIDER_CLASS)) {
                    principalProviderClassName = options.get(LoginModuleConfig.COMPAT_PRINCIPAL_PROVIDER_CLASS).toString();
                }
            }
            if (principalProviderClassName != null) {
                principalProvider = registry.getProvider(principalProviderClassName);
            }
            if (principalProvider == null) {
                principalProvider = registry.getDefault();
                if (principalProvider == null) {
                    return; // abort. not even a default principal provider
                }
            }
            log.debug("- PrincipalProvider -> '" + principalProvider.getClass().getName() + "'");

            // call implementation for additional setup
            doInit(callbackHandler, repositoryCb.getSession(), options);

            // adminId: if not present in options -> retrieve from callback
            if (options.containsKey(LoginModuleConfig.PARAM_ADMIN_ID)) {
                adminId = (String) options.get(LoginModuleConfig.PARAM_ADMIN_ID);
            }
            if (adminId == null) {
                adminId = repositoryCb.getAdminId();
            }
            // anonymousId: if not present in options -> retrieve from callback
            if (options.containsKey(LoginModuleConfig.PARAM_ANONYMOUS_ID)) {
                anonymousId = (String) options.get(LoginModuleConfig.PARAM_ANONYMOUS_ID);
            }
            if (anonymousId == null) {
                anonymousId = repositoryCb.getAnonymousId();
            }
            // trusted credentials attribute name (may be missing to not
            // support) (normalized to null aka missing aka unset if an empty
            // string)
            preAuthAttributeName = (String) options.get(PRE_AUTHENTICATED_ATTRIBUTE_OPTION);
            if (preAuthAttributeName != null
                && preAuthAttributeName.length() == 0) {
                preAuthAttributeName = null;
            }

            //log config values for debug
            if (log.isDebugEnabled()) {
                for (String option : options.keySet()) {
                    log.debug("- Option: " + option + " -> '" + options.get(option) + "'");
                }
            }
            initialized = (this.subject != null);

        } catch (Exception e) {
            log.error("LoginModule failed to initialize.", e);
        }
    }

    /**
     * Implementations may set-up their own state.
     *
     * @param callbackHandler as passed by {@link javax.security.auth.login.LoginContext}
     * @param session         to security-workspace of Jackrabbit
     * @param options         options from LoginModule config
     * @throws LoginException in case initialization fails.
     */
    protected abstract void doInit(CallbackHandler callbackHandler,
                                   Session session,
                                   Map options) throws LoginException;


    /**
     * Returns <code>true</code> if this module has been successfully initialized.
     *
     * @return <code>true</code> if this module has been successfully initialized.
     * @see LoginModule#initialize(Subject, CallbackHandler, Map, Map)
     */
    protected boolean isInitialized() {
        return initialized;
    }

    /**
     * Method to authenticate a <code>Subject</code> (phase 1).<p/>
     * The login is divided into 3 Phases:<p/>
     *
     * <b>1) User-ID resolution</b><br>
     * In a first step it is tried to resolve a User-ID for further validation.
     * As for JCR the identification is marked with the {@link Credentials}
     * interface, credentials are accessed in this phase.<br>
     * If no User-ID can be found, anonymous access is granted with the ID of
     * the anonymous user (as defined in the security configuration).
     * Anonymous access can be switched off removing the configuration entry.
     * <br> This implementation uses two helper-methods, which allow for
     * customization:
     * <ul>
     * <li>{@link #getCredentials()}</li> and
     * <li>{@link #getUserID(Credentials)}</li>
     * </ul>
     * <p/>
     *
     * <b>2) User-Principal resolution </b><br>
     * In a second step it is tested, if the resolved User-ID belongs to a User
     * known to the system, i.e. if the {@link PrincipalProvider} has a principal
     * for the given ID and the principal can be found via
     * {@link PrincipalProvider#findPrincipals(String)}.<br>
     * The provider implementation can be set by the LoginModule configuration.
     * If the option is missing, the system default principal provider will
     * be used.<p/>
     *
     * <b>3) Verification</b><br>
     * There are four cases, how the User-ID can be verified:
     * The login is anonymous, pre-authenticated or the login is the result of
     * an impersonation request (see {@link javax.jcr.Session#impersonate(Credentials)}
     * or of a login to the Repository ({@link javax.jcr.Repository#login(Credentials)}).
     * The concrete implementation of the LoginModule is responsible for all
     * four cases:
     * <ul>
     * <li>{@link #isAnonymous(Credentials)}</li>
     * <li>{@link #isPreAuthenticated(Credentials)}</li>
     * <li>{@link #authenticate(Principal, Credentials)}</li>
     * <li>{@link #impersonate(Principal, Credentials)}</li>
     * </ul>
     *
     * Under the following conditions, the login process is aborted and the
     * module is marked to be ignored:
     * <ul>
     * <li>No User-ID could be resolve, and anonymous access is switched off</li>
     * <li>No Principal is found for the User-ID resolved</li>
     * </ul>
     *
     * Under the following conditions, the login process is marked to be invalid
     * by throwing an LoginException:
     * <ul>
     * <li>It is an impersonation request, but the impersonator is not allowed
     * to impersonate to the requested User-ID</li>
     * <li>The user tries to login, but the Credentials can not be verified.</li>
     * </ul>
     * <p/>
     * The LoginModule keeps the Credentials and the Principal as instance fields,
     * to mark that login has been successful.
     *
     * @return true if the authentication succeeded, or false if this
     *         <code>LoginModule</code> should be ignored.
     * @throws LoginException if the authentication fails
     * @see javax.security.auth.spi.LoginModule#login()
     * @see #getCredentials()
     * @see #getUserID(Credentials)
     * @see #getImpersonatorSubject(Credentials)
     */
    public boolean login() throws LoginException {
        if (!isInitialized()) {
            log.warn("Unable to perform login: initialization not completed.");
            return false;
        }

        // check the availability and validity of Credentials
        Credentials creds = getCredentials();
        if (creds == null) {
            log.debug("No credentials available -> try default (anonymous) authentication.");
        } else if (!supportsCredentials(creds)) {
            log.debug("Unsupported credentials implementation : " + creds.getClass().getName());
            return false;
        }
        
        try {
            Principal userPrincipal = getPrincipal(creds);
            if (userPrincipal == null) {
                // unknown or disabled user or a group
                log.debug("No valid user -> ignore.");
                return false;
            }
            boolean authenticated;
            // test for anonymous, pre-authentication, impersonation or common authentication.
            if (isAnonymous(creds) || isPreAuthenticated(creds)) {
                authenticated = true;
            } else if (isImpersonation(creds)) {
                authenticated = impersonate(userPrincipal, creds);
            } else {
                authenticated = authenticate(userPrincipal, creds);
            }

            // process authenticated user
            if (authenticated) {
                if (creds instanceof SimpleCredentials) {
                    credentials = (SimpleCredentials) creds;
                } else {
                    credentials = new SimpleCredentials(getUserID(creds), new char[0]);
                }
                principal = userPrincipal;
                return true;
            }
        } catch (RepositoryException e) {
            log.error("Login failed:", e);
        }
        return false;
    }

    /**
     * Method to commit the authentication process (phase 2).
     * <p/>
     * This method is called if the LoginContext's overall authentication
     * succeeded (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL
     * LoginModules succeeded).
     * <p/>
     * If this LoginModule's own authentication attempt succeeded (checked
     * by retrieving the private state saved by the <code>login</code> method),
     * then this method associates relevant Principals and Credentials with the
     * <code>Subject</code> located in the <code>LoginModule</code>.  If this
     * LoginModule's own authentication attempted failed, then this method
     * removes/destroys any state that was originally saved.
     * <p/>
     * The login is considered as succeeded if there is a principal set.
     * <p/>
     * The implementation stores the principal associated to the UserID and all
     * the Groups it is member of with the Subject and in addition adds an
     * instance of (#link SimpleCredentials} to the Subject's public credentials.
     *
     * @return true if this method succeeded, or false if this
     *         <code>LoginModule</code> should be ignored.
     * @throws LoginException if the commit fails
     * @see javax.security.auth.spi.LoginModule#commit()
     */
    public boolean commit() throws LoginException {
        if (!isInitialized() || principal == null) {
            return false;
        }

        Set<Principal> principals = getPrincipals();
        subject.getPrincipals().addAll(principals);
        subject.getPublicCredentials().add(credentials);
        return true;
    }

    /**
     * Method to abort the authentication process (phase 2).
     * <p/>
     * <p> This method is called if the LoginContext's overall authentication
     * failed. (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL
     * LoginModules did not succeed).
     * <p/>
     * <p> If this LoginModule's own authentication attempt succeeded (checked
     * by retrieving the private state saved by the <code>login</code> method),
     * then this method cleans up any state that was originally saved.
     * <p/>
     * <p/>
     *
     * @return true if this method succeeded, or false if this
     *         <code>LoginModule</code> should be ignored.
     * @throws LoginException if the abort fails
     * @see javax.security.auth.spi.LoginModule#abort()
     */
    public boolean abort() throws LoginException {
        if (!isInitialized()) {
            return false;
        } else {
            sharedState.remove(KEY_CREDENTIALS);
            callbackHandler = null;
            principal = null;
            credentials = null;
            return logout();
        }
    }

    /**
     * @return <code>true</code> if this method succeeded,
     * or <code>false</code> if this <code>LoginModule</code> should be ignored.
     * @throws LoginException if the logout fails
     * @see javax.security.auth.spi.LoginModule#logout()
     */
    public boolean logout() throws LoginException {
        if (subject.getPrincipals().isEmpty() || subject.getPublicCredentials(Credentials.class).isEmpty()) {
            return false;
        } else {
            // clear subject if not readonly
            if (!subject.isReadOnly()) {
                subject.getPrincipals().clear();
                subject.getPublicCredentials().clear();
            }
            return true;
        }
    }

    /**
     * @param principal Principal used to retrieve the <code>Authentication</code>
     * object.
     * @param credentials Credentials used for the authentication.
     * @return <code>true</code> if Credentials authenticate,
     *         <code>false</code> if no <code>Authentication</code> can handle
     *         the given <code>Credentials</code>
     * @throws javax.security.auth.login.FailedLoginException
     *          if the authentication failed.
     * @throws RepositoryException If another error occurs.
     * @see AbstractLoginModule#getAuthentication(java.security.Principal, javax.jcr.Credentials)
     * @see AbstractLoginModule#authenticate(java.security.Principal, javax.jcr.Credentials)
     */
    protected boolean authenticate(Principal principal, Credentials credentials)
            throws FailedLoginException, RepositoryException {

        Authentication auth = getAuthentication(principal, credentials);
        if (auth == null) {
            return false;
        } else if (auth.authenticate(credentials)) {
            return true;
        }
        throw new FailedLoginException();
    }

    /**
     * Test if the current request is an Impersonation attempt. The default
     * implementation returns <code>true</code> if an
     * {@link #getImpersonatorSubject(Credentials) subject} for the
     * impersonation can be retrieved.<p/>
     *
     * @param credentials potentially containing impersonation data
     * @return true if this is an impersonation attempt
     * @see #getImpersonatorSubject(Credentials)
     */
    protected boolean isImpersonation(Credentials credentials) {
        return getImpersonatorSubject(credentials) != null;
    }

    /**
     * Handles the impersonation of given Credentials.
     *
     * @param principal Principal to impersonate.
     * @param credentials Credentials used to create the impersonation subject.
     * @return false, if there is no User to impersonate,
     *         true if impersonation is allowed
     * @throws LoginException If credentials don't allow to impersonate to principal.
     * @throws RepositoryException If another error occurs.
     */
    protected abstract boolean impersonate(Principal principal, Credentials credentials)
            throws RepositoryException, LoginException;

    /**
     * Retrieve the <code>Authentication</code>.
     *
     * @param principal A principal.
     * @param creds The Credentials used for the login.
     * @return Authentication object for the given principal / credentials.
     * @throws RepositoryException If an error occurs.
     */
    protected abstract Authentication getAuthentication(Principal principal, Credentials creds)
            throws RepositoryException;

    /**
     * Method tries to acquire an Impersonator in the following order:
     * <ul>
     * <li> Try to access it from the {@link Credentials} via {@link SimpleCredentials#getAttribute(String)}</li>
     * <li> Ask CallbackHandler for Impersonator with use of {@link ImpersonationCallback}.</li>
     * </ul>
     *
     * @param credentials which, may contain an impersonation Subject
     * @return impersonation subject or null if non contained
     * @see #login()
     * @see #impersonate(java.security.Principal, javax.jcr.Credentials)
     */
    protected Subject getImpersonatorSubject(Credentials credentials) {
        Subject impersonator = null;
        if (credentials == null) {
            try {
                ImpersonationCallback impers = new ImpersonationCallback();
                callbackHandler.handle(new Callback[]{impers});
                impersonator = impers.getImpersonator();
            } catch (UnsupportedCallbackException e) {
                log.warn(e.getCallback().getClass().getName() + " not supported: Unable to perform Impersonation.");
            } catch (IOException e) {
                log.error("Impersonation-Callback failed: " + e.getMessage() + ": Unable to perform Impersonation.");
            }
        } else if (credentials instanceof SimpleCredentials) {
            SimpleCredentials sc = (SimpleCredentials) credentials;
            impersonator = (Subject) sc.getAttribute(SecurityConstants.IMPERSONATOR_ATTRIBUTE);
        }
        return impersonator;
    }

    /**
     * Method tries to resolve the {@link Credentials} used for login. It takes
     * authentication-extension of an already authenticated {@link Subject} into
     * account.
     * <p/>
     * Therefore the credentials are retrieved as follows:
     * <ol>
     * <li>Test if the shared state contains credentials.</li>
     * <li>Ask CallbackHandler for Credentials with using a {@link
     * CredentialsCallback}. Expects {@link CredentialsCallback#getCredentials}
     * to return an instance of {@link Credentials}.</li>
     * <li>Ask the Subject for its public <code>SimpleCredentials</code> see
     * {@link Subject#getPublicCredentials(Class)}, thus enabling to
     * pre-authenticate the Subject.</li>
     * </ol>
     *
     * @return Credentials or null if not found
     * @see #login()
     */
    protected Credentials getCredentials() {
        Credentials credentials = null;
        if (sharedState.containsKey(KEY_CREDENTIALS)) {
            credentials = (Credentials) sharedState.get(KEY_CREDENTIALS);
        } else {
            try {
                CredentialsCallback callback = new CredentialsCallback();
                callbackHandler.handle(new Callback[]{callback});
                credentials = callback.getCredentials();
                if (credentials != null && supportsCredentials(credentials)) {
                    sharedState.put(KEY_CREDENTIALS, credentials);                    
                }
            } catch (UnsupportedCallbackException e) {
                log.warn("Credentials-Callback not supported try Name-Callback");
            } catch (IOException e) {
                log.error("Credentials-Callback failed: " + e.getMessage() + ": try Name-Callback");
            }
        }
        // if still no credentials -> try to retrieve them from the subject.
        if (null == credentials) {
            // try if subject contains SimpleCredentials
            Set<SimpleCredentials> preAuthCreds = subject.getPublicCredentials(SimpleCredentials.class);
            if (!preAuthCreds.isEmpty()) {
                credentials = preAuthCreds.iterator().next();
            }
        }
        if (null == credentials) {
            // try if subject contains GuestCredentials
            Set<GuestCredentials> preAuthCreds = subject.getPublicCredentials(GuestCredentials.class);
            if (!preAuthCreds.isEmpty()) {
                credentials = preAuthCreds.iterator().next();
            }
        }
        return credentials;
    }

    /**
     * Return a flag indicating whether the credentials are supported by
     * this login module. Default implementation supports
     * {@link SimpleCredentials} and {@link GuestCredentials}.
     *
     * @param creds credentials
     * @return <code>true</code> if the credentials are supported;
     *         <code>false</code> otherwise
     */
    protected boolean supportsCredentials(Credentials creds) {
        return creds instanceof SimpleCredentials ||
            creds instanceof GuestCredentials;
    }

    /**
     * Method supports tries to acquire a UserID in the following order:
     * <ol>
     * <li>If passed credentials are {@link GuestCredentials} the anonymous user id
     * is returned.</li>
     * <li>Try to access it from the {@link Credentials} via {@link
     * SimpleCredentials#getUserID()}</li>
     * <li>Ask CallbackHandler for User-ID with use of {@link NameCallback}.</li>
     * <li>Test if the 'sharedState' contains a login name.</li>
     * <li>Fallback: return the anonymous UserID.</li>
     * </ol>
     *
     * @param credentials which, may contain a User-ID
     * @return The userId retrieved from the credentials or by any other means
     * described above.
     * @see #login()
     */
    protected String getUserID(Credentials credentials) {
        String userId = null;
        if (credentials != null) {
            if (credentials instanceof GuestCredentials) {
                userId = anonymousId;
            } else if (credentials instanceof SimpleCredentials) {
                userId = ((SimpleCredentials) credentials).getUserID();
            } else {
                try {
                    NameCallback callback = new NameCallback("User-ID: ");
                    callbackHandler.handle(new Callback[]{callback});
                    userId = callback.getName();
                } catch (UnsupportedCallbackException e) {
                    log.warn("Credentials- or NameCallback must be supported");
                } catch (IOException e) {
                    log.error("Name-Callback failed: " + e.getMessage());
                }
            }
        }
        if (userId == null && sharedState.containsKey(KEY_LOGIN_NAME)) {
            userId = (String) sharedState.get(KEY_LOGIN_NAME);
        }

        // still no userId -> anonymousID if its has been defined.
        // TODO: check again if correct when used with 'extendedAuth'
        if (userId == null) {
            userId = anonymousId;
        }
        return userId;
    }

    /**
     * Indicate if the given Credentials are considered to be anonymous.
     *
     * @param credentials The Credentials to be tested.
     * @return <code>true</code> if is anonymous; <code>false</code> otherwise.
     */
    protected boolean isAnonymous(Credentials credentials) {
        if (credentials instanceof GuestCredentials) {
            return true;
        } else {
            // TODO: review again. former simple-login-module treated 'null' as anonymous (probably wrong).
            String userId = getUserID(credentials);
            return (anonymousId == null) ? userId == null : anonymousId.equals(userId);
        }
    }


    /**
     * Authentication process associates a Principal to Credentials<br>
     * This method resolves the Principal for the given Credentials. If no valid
     * Principal can be determined, the LoginModule should be ignored.
     *
     * @param credentials Credentials used for to login.
     * @return the principal associated with the given credentials or <code>null</code>.
     */
    protected abstract Principal getPrincipal(Credentials credentials);

    /**
     * @return a Collection of principals that contains the current user
     * principal and all groups it is member of.
     */
    protected Set<Principal> getPrincipals() {
        // use linked HashSet instead of HashSet in order to maintain the order
        // of principals (as in the Subject).
        Set<Principal> principals = new LinkedHashSet<Principal>();
        principals.add(principal);
        PrincipalIterator groups = principalProvider.getGroupMembership(principal);
        while (groups.hasNext()) {
            principals.add(groups.nextPrincipal());
        }
        return principals;
    }

    //--------------------------------------------------------------------------
    /**
     * Returns the admin user id.
     *
     * @return admin user id
     */
    public String getAdminId() {
        return adminId;
    }

    /**
     * Sets the administrator's user id.
     *
     * @param adminId the administrator's user id.
     */
    public void setAdminId(String adminId) {
        this.adminId = adminId;
    }

    /**
     * Returns the anonymous user id.
     *
     * @return anonymous user id
     */
    public String getAnonymousId() {
        return anonymousId;
    }

    /**
     * Sets the anonymous user id.
     *
     * @param anonymousId anonymous user id
     */
    public void setAnonymousId(String anonymousId) {
        this.anonymousId = anonymousId;
    }

    /**
     * Returns the configured name of the principal provider class.
     *
     * @return name of the principal provider class.
     */
    public String getPrincipalProvider() {
        return principalProviderClassName;
    }

    /**
     * Sets the configured name of the principal provider class
     *
     * @param principalProvider Name of the principal provider class.
     */
    public void setPrincipalProvider(String principalProvider) {
        this.principalProviderClassName = principalProvider;
    }

    /**
     * The name of the credentials attribute providing a hint that the
     * credentials should be taken as is and the user requesting access
     * has already been authenticated outside of this LoginModule.
     * <p>
     * This name is configured as the value of the LoginModule configuration
     * parameter <code>trust_credentials_attribute</code>. If the configuration
     * parameter is missing (or empty) the name is not set and this method
     * returns <code>null</code>.
     *
     * @see #isPreAuthenticated(Credentials)
     */
    protected final String getPreAuthAttributeName() {
        return preAuthAttributeName;
    }

    /**
     * Returns <code>true</code> if the credentials should be considered as
     * pre-authenticated and a password check is not required.
     * <p>
     * This base class implementation returns <code>true</code> if the
     * <code>creds</code> object is a SimpleCredentials instance and the
     * configured {@link #getPreAuthAttributeName() trusted
     * credentials property} is set to a non-<code>null</code> value in the
     * credentials attributes.
     * <p>
     * Extensions of this class may overwrite this method to apply more or
     * different checks to the credentials.
     *
     * @param creds The Credentials to check
     *
     * @see #getPreAuthAttributeName()
     */
    protected boolean isPreAuthenticated(final Credentials creds) {
        final String preAuthAttrName = getPreAuthAttributeName();
        return preAuthAttrName != null
            && (creds instanceof SimpleCredentials)
            && ((SimpleCredentials) creds).getAttribute(preAuthAttrName) != null;
    }
}
