/*
 * 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;

import org.apache.jackrabbit.api.security.principal.PrincipalManager;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.core.config.LoginModuleConfig;
import org.apache.jackrabbit.core.config.UserManagerConfig;
import org.apache.jackrabbit.core.security.authentication.AuthContext;
import org.apache.jackrabbit.core.security.authentication.AuthContextProvider;
import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager;
import org.apache.jackrabbit.core.security.principal.AbstractPrincipalProvider;
import org.apache.jackrabbit.core.security.principal.DefaultPrincipalProvider;
import org.apache.jackrabbit.core.security.principal.PrincipalManagerImpl;
import org.apache.jackrabbit.core.security.principal.PrincipalProvider;
import org.apache.jackrabbit.core.security.principal.PrincipalProviderRegistry;
import org.apache.jackrabbit.core.security.simple.SimpleWorkspaceAccessManager;
import org.apache.jackrabbit.core.security.user.MembershipCache;
import org.apache.jackrabbit.core.security.user.UserPerWorkspaceUserManager;
import org.apache.jackrabbit.core.security.user.UserManagerImpl;
import org.apache.jackrabbit.core.security.user.action.AuthorizableAction;

import javax.jcr.Credentials;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.security.auth.Subject;
import java.security.Principal;
import java.security.acl.Group;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * Derived security manager implementation that expects that users information
 * is present in each workspace instead of having a single, dedicated
 * "security-workspace" that provides user information. Consequently, the
 * UserManager used to retrieve and manipulate user content is always
 * bound to the <code>Session</code> passed to {@link #getUserManager(Session)}.
 * <p/> In addition the default (user-based) principal provider created by
 * {@link org.apache.jackrabbit.core.DefaultSecurityManager}
 * cannot be used to retrieve principals. Instead this implementation keeps
 * a distinct pp-registry for each workspace.
 * </p>
 * NOTE: While this security manager asserts that a minimal set of system
 * users (admin and anonymous) is present in each workspace
 * it doesn't make any attempt to set or define the access permissions on the
 * tree containing user related information. 
 */
public class UserPerWorkspaceSecurityManager extends DefaultSecurityManager {

    private final Map<String, PrincipalProviderRegistry> ppRegistries = new HashMap<String, PrincipalProviderRegistry>();
    private final Object monitor = new Object();

    /**
     * List of workspace names for which {@link #createSystemUsers} has already
     * been called.
     */
    private final List<String> systemUsersInitialized = new ArrayList<String>();

    private PrincipalProviderRegistry getPrincipalProviderRegistry(SessionImpl s) throws RepositoryException {
        String wspName = s.getWorkspace().getName();
        synchronized (monitor) {
            PrincipalProviderRegistry p = ppRegistries.get(wspName);
            if (p == null) {
                SystemSession systemSession;
                if (s instanceof SystemSession) {
                    systemSession = (SystemSession) s;
                } else {
                    RepositoryImpl repo = (RepositoryImpl) getRepository();
                    systemSession = repo.getSystemSession(wspName);
                    // TODO: review again... this workaround is used in several places.
                    repo.markWorkspaceActive(wspName);
                }

                Properties[] moduleConfig = new AuthContextProvider("", ((RepositoryImpl) getRepository()).getConfig().getSecurityConfig().getLoginModuleConfig()).getModuleConfig();

                PrincipalProvider defaultPP = new DefaultPrincipalProvider(systemSession, (UserManagerImpl) getUserManager(systemSession));

                boolean initialized = false;
                for (Properties props : moduleConfig) {
                    //GRANITE-4470: apply config to DefaultPrincipalProvider if there is no explicit PrincipalProvider configured
                    if (!props.containsKey(LoginModuleConfig.PARAM_PRINCIPAL_PROVIDER_CLASS) && props.containsKey(AbstractPrincipalProvider.MAXSIZE_KEY)) {
                        defaultPP.init(props);
                        initialized = true;
                        break;
                    }
                }
                if (!initialized) {
                    defaultPP.init(new Properties());
                }

                p = new WorkspaceBasedPrincipalProviderRegistry(defaultPP);
                ppRegistries.put(wspName, p);
            }
            return p;
        }
    }

    //------------------------------------------< JackrabbitSecurityManager >---
    /**
     * @see org.apache.jackrabbit.core.security.JackrabbitSecurityManager#init(Repository, Session)
     */
    @Override
    public void init(Repository repository, Session systemSession) throws RepositoryException {
        super.init(repository, systemSession);

        systemUsersInitialized.add(systemSession.getWorkspace().getName());
    }

    /**
     * @see org.apache.jackrabbit.core.security.JackrabbitSecurityManager#dispose(String)
     */
    @Override
    public void dispose(String workspaceName) {
        super.dispose(workspaceName);
        synchronized (monitor) {
            PrincipalProviderRegistry reg = ppRegistries.remove(workspaceName);
            if (reg != null) {
                reg.getDefault().close();
            }
        }
    }

    /**
     * @see org.apache.jackrabbit.core.security.JackrabbitSecurityManager#close()
     */
    @Override
    public void close() {
        super.close();
        synchronized (monitor) {
            for (PrincipalProviderRegistry registry : ppRegistries.values()) {
                registry.getDefault().close();
            }
            ppRegistries.clear();
        }
    }

    /**
     * As this implementation expects that users information in present in
     * every workspace, the UserManager is always created with the given
     * session.
     * 
     * @see org.apache.jackrabbit.core.security.JackrabbitSecurityManager#getUserManager(javax.jcr.Session)
     */
    @Override
    public UserManager getUserManager(Session session) throws RepositoryException {
        checkInitialized();                    
        if (session == getSystemSession()) {
            return super.getUserManager(session);
        } else if (session instanceof SessionImpl) {
            UserManager uMgr = createUserManager((SessionImpl) session);
            // Since users are not stored in a dedicated security workspace:
            // make sure the system users are present. this is always the case
            // for the configured security-workspace (or if missing the default
            // workspace) but not for other workspaces.
            // However, the check is only executed if the given session is a
            // SystemSession (see also #getPrincipalProviderRegistry(Session)
            // that initializes a SystemSession based UserManager for each workspace).
            String wspName = session.getWorkspace().getName();
            if (session instanceof SystemSession && !systemUsersInitialized.contains(wspName)) {
                createSystemUsers(uMgr, (SystemSession) session, adminId, anonymousId);
                systemUsersInitialized.add(wspName);
            }
            return uMgr;
        } else {
            throw new RepositoryException("Internal error: SessionImpl expected.");
        }
    }

    /**
     * Creates an AuthContext for the given {@link javax.jcr.Credentials} and
     * {@link javax.security.auth.Subject}.<br>
     * This includes selection of application specific LoginModules and
     * initialization with credentials and Session to System-Workspace
     *
     * @return an {@link org.apache.jackrabbit.core.security.authentication.AuthContext} for the given Credentials, Subject
     * @throws javax.jcr.RepositoryException in other exceptional repository states
     */
    @Override
    public AuthContext getAuthContext(Credentials creds, Subject subject, String workspaceName)
            throws RepositoryException {
        checkInitialized();
        SystemSession systemSession = ((RepositoryImpl) getRepository()).getSystemSession(workspaceName);
        return getAuthContextProvider().getAuthContext(creds, subject, systemSession,
                getPrincipalProviderRegistry(systemSession), adminId, anonymousId);
    }

    //--------------------------------------------------------------------------
    /**
     * Always returns <code>null</code>. The default principal provider is
     * workspace depending as users are expected to exist in every workspace.
     * 
     * @return <code>null</code>
     * @throws RepositoryException
     */
    @Override
    protected PrincipalProvider createDefaultPrincipalProvider(Properties[] moduleConfig) throws RepositoryException {
        return null;
    }

    @Override
    protected UserManager getSystemUserManager(String workspaceName) throws RepositoryException {
        if (workspaceName.equals(getSystemSession().getWorkspace().getName())) {
            return super.getSystemUserManager(workspaceName);
        } else {
            return ((RepositoryImpl) getRepository()).getWorkspaceInfo(workspaceName).getSystemSession().getUserManager();
        }
    }

    /**
     * Creates a new instanceof <code>TransientChangeUserManagerImpl</code>.
     * 
     * @param session session
     * @return an instanceof <code>TransientChangeUserManagerImpl</code>
     * @throws RepositoryException
     */
    @Override
    protected UserManagerImpl createUserManager(SessionImpl session) throws RepositoryException {
        UserManagerConfig umc = getConfig().getUserManagerConfig();
        UserManagerImpl umgr;
        // in contrast to the DefaultSecurityManager users are not retrieved
        // from a dedicated workspace: the system session of each workspace must
        // get a system user manager that asserts the existence of the admin user.
        if (umc != null) {
            Class<?>[] paramTypes = new Class[] {
                    SessionImpl.class,
                    String.class,
                    Properties.class,
                    MembershipCache.class};
            umgr = (UserPerWorkspaceUserManager) umc.getUserManager(UserPerWorkspaceUserManager.class,
                    paramTypes, session, adminId, umc.getParameters(), getMembershipCache(session));
        } else {
            umgr = new UserPerWorkspaceUserManager(session, adminId, null, getMembershipCache(session));
        }

        if (umc != null && !(session instanceof SystemSession)) {
            AuthorizableAction[] actions = umc.getAuthorizableActions();
            umgr.setAuthorizableActions(actions);
        }
        return umgr;
    }

    /**
     * @param session Session for the principal manager must be created.
     * @return A new instance of PrincipalManagerImpl. Note that this implementation
     * uses a workspace specific principal provider registry, that retrieves
     * the configured providers from the registry obtained through
     * {@link #getPrincipalProviderRegistry()} but has a workspace specific
     * default provider.
     * @throws RepositoryException
     */
    @Override
    protected PrincipalManager createPrincipalManager(SessionImpl session) throws RepositoryException {
        return new PrincipalManagerImpl(session, getPrincipalProviderRegistry(session).getProviders());
    }

    /**
     * Returns a new instance of <code>SimpleWorkspaceAccessManager</code>, since
     * with the <code>DefaultLoginModule</code> the existence of the user
     * is checked in order to successfully complete the login. Since with this
     * SecurityManager users are stored separately in each workspace, a user
     * may only login to a workspace if the corresponding user node exists.
     * Consequently a lazy workspace access manager is sufficient.<p/>
     *
     * If this SecurityManager is used with a distinct <code>LoginModule</code>
     * implementation, the {@link org.apache.jackrabbit.core.config.SecurityManagerConfig#getWorkspaceAccessConfig() configuration}
     * for <code>WorkspaceAccessManager</code> should be adjusted as well.
     *
     * @return An new instance of {@link SimpleWorkspaceAccessManager}.
     */
    @Override
    protected WorkspaceAccessManager createDefaultWorkspaceAccessManager() {
        return new WorkspaceAccessManagerImpl();
    }

    //--------------------------------------------------------------------------
    /**
     * Workaround to get a default (user-based) principal provider depending
     * on the workspace being accessed. This is required for this security
     * manager as users aren't stored in a single, dedicated workspace.
     */
    private final class WorkspaceBasedPrincipalProviderRegistry implements PrincipalProviderRegistry {

        private final PrincipalProvider defaultPrincipalProvider;

        public WorkspaceBasedPrincipalProviderRegistry(PrincipalProvider defaultPrincipalProvider) {
            this.defaultPrincipalProvider = defaultPrincipalProvider;
        }

        public PrincipalProvider registerProvider(Properties configuration) throws RepositoryException {
            return getPrincipalProviderRegistry().registerProvider(configuration);
        }

        public PrincipalProvider getDefault() {
            return defaultPrincipalProvider;
        }

        public PrincipalProvider getProvider(String className) {
            PrincipalProvider p = getPrincipalProviderRegistry().getProvider(className);
            if (p == null && defaultPrincipalProvider.getClass().getName().equals(className)) {
                p = defaultPrincipalProvider;
            }
            return p;
        }

        public PrincipalProvider[] getProviders() {
            List<PrincipalProvider> l = new ArrayList<PrincipalProvider>();
            l.addAll(Arrays.asList(getPrincipalProviderRegistry().getProviders()));
            l.add(defaultPrincipalProvider);
            return l.toArray(new PrincipalProvider[l.size()]);
        }
    }

    private final class WorkspaceAccessManagerImpl implements WorkspaceAccessManager {
        /**
         * Does nothing.
         * @see WorkspaceAccessManager#init(javax.jcr.Session)
         */
        public void init(Session systemSession) throws RepositoryException {
            // nothing to do.
        }

        /**
         * Does nothing.
         * @see org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager#close()
         */
        public void close() throws RepositoryException {
            // nothing to do.
        }

        /**
         * Returns <code>true</code> if a workspace with the given
         * <code>workspaceName</code> exists and if that workspace defines a
         * user that matches any of the given <code>principals</code>;
         * <code>false</code> otherwise.
         *
         * @see WorkspaceAccessManager#grants(java.util.Set, String)
         */
        public boolean grants(Set<Principal> principals, String workspaceName) throws RepositoryException {
            if (!(Arrays.asList(((RepositoryImpl) getRepository()).getWorkspaceNames())).contains(workspaceName)) {
                return false;
            } else {
                UserManager umgr = UserPerWorkspaceSecurityManager.this.getSystemUserManager(workspaceName);
                for (Principal principal : principals) {
                    if (!(principal instanceof Group)) {
                        // check if the workspace identified by the given workspace
                        // name contains a user with this principal
                        if (umgr.getAuthorizable(principal) != null) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }
    }
}