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

import java.security.Principal;
import java.security.acl.Group;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.jcr.AccessDeniedException;
import javax.jcr.Credentials;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.security.auth.Subject;

import org.apache.jackrabbit.api.security.principal.PrincipalIterator;
import org.apache.jackrabbit.api.security.principal.PrincipalManager;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.core.RepositoryImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.config.AccessManagerConfig;
import org.apache.jackrabbit.core.config.LoginModuleConfig;
import org.apache.jackrabbit.core.config.SecurityConfig;
import org.apache.jackrabbit.core.config.SecurityManagerConfig;
import org.apache.jackrabbit.core.security.AMContext;
import org.apache.jackrabbit.core.security.AccessManager;
import org.apache.jackrabbit.core.security.AnonymousPrincipal;
import org.apache.jackrabbit.core.security.JackrabbitSecurityManager;
import org.apache.jackrabbit.core.security.SecurityConstants;
import org.apache.jackrabbit.core.security.UserPrincipal;
import org.apache.jackrabbit.core.security.authentication.AuthContext;
import org.apache.jackrabbit.core.security.authentication.AuthContextProvider;
import org.apache.jackrabbit.core.security.authorization.AccessControlProvider;
import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager;
import org.apache.jackrabbit.core.security.principal.AdminPrincipal;
import org.apache.jackrabbit.core.security.principal.EveryonePrincipal;
import org.apache.jackrabbit.core.security.principal.PrincipalIteratorAdapter;
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.principal.ProviderRegistryImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <code>SimpleSecurityManager</code>: simple implementation ignoring both
 * configuration entries for 'principalProvider' and for 'workspaceAccessManager'.
 * The AccessManager is initialized using
 * {@link AccessManager#init(org.apache.jackrabbit.core.security.AMContext)}.
 */
public class SimpleSecurityManager implements JackrabbitSecurityManager {

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

    private boolean initialized;

    private SecurityConfig config;

    /**
     * session on the system workspace.
     */
    private Session systemSession;

    /**
     * the principal provider registry
     */
    private PrincipalProviderRegistry principalProviderRegistry;

    /**
     * The workspace access manager
     */
    private WorkspaceAccessManager workspaceAccessManager;

    /**
     * factory for login-context {@see Repository#login())
     */
    private AuthContextProvider authCtxProvider;

    private String adminID;
    private String anonymID;

    /**
     * Always returns <code>null</code>. AccessControlProvider configuration
     * is ignored with this security manager. Subclasses may overwrite this
     * lazy behavior that originates from the <code>SimpleAccessManager</code>.
     *
     * @param systemSession The system session used to init the security manager.
     * @param workspaceName The name of the workspace for which the provider
     * should be retrieved.
     * @return Always returns <code>null</code>.
     */
    protected AccessControlProvider getAccessControlProvider(Session systemSession, String workspaceName) {
        return null;
    }

    //------------------------------------------< JackrabbitSecurityManager >---
    /**
     * @see JackrabbitSecurityManager#init(Repository, Session)
     */
    public void init(Repository repository, Session systemSession) throws RepositoryException {
        if (initialized) {
            throw new IllegalStateException("already initialized");
        }
        if (!(repository instanceof RepositoryImpl)) {
            throw new RepositoryException("RepositoryImpl expected");
        }

        this.systemSession = systemSession;
        config = ((RepositoryImpl) repository).getConfig().getSecurityConfig();

        // read the LoginModule configuration
        LoginModuleConfig loginModConf = config.getLoginModuleConfig();
        authCtxProvider = new AuthContextProvider(config.getAppName(), loginModConf);
        if (authCtxProvider.isLocal()) {
            log.info("init: using Repository LoginModule configuration for " + config.getAppName());
        } else if (authCtxProvider.isJAAS()) {
            log.info("init: using JAAS LoginModule configuration for " + config.getAppName());
        } else {
            String msg = "No valid LoginModule configuriation for " + config.getAppName();
            log.error(msg);
            throw new RepositoryException(msg);
        }

        Properties[] moduleConfig = authCtxProvider.getModuleConfig();

        // retrieve default-ids (admin and anonymous) from login-module-configuration.
        for (Properties aModuleConfig1 : moduleConfig) {
            if (aModuleConfig1.containsKey(LoginModuleConfig.PARAM_ADMIN_ID)) {
                adminID = aModuleConfig1.getProperty(LoginModuleConfig.PARAM_ADMIN_ID);
            }
            if (aModuleConfig1.containsKey(LoginModuleConfig.PARAM_ANONYMOUS_ID)) {
                anonymID = aModuleConfig1.getProperty(LoginModuleConfig.PARAM_ANONYMOUS_ID);
            }
        }
        // fallback:
        if (adminID == null) {
            log.debug("No adminID defined in LoginModule/JAAS config -> using default.");
            adminID = SecurityConstants.ADMIN_ID;
        }
        if (anonymID == null) {
            log.debug("No anonymousID defined in LoginModule/JAAS config -> using default.");
            anonymID = SecurityConstants.ANONYMOUS_ID;
        }

        // most simple principal provider registry, that does not read anything
        // from configuration
        PrincipalProvider principalProvider = new SimplePrincipalProvider();
        // skip init of provider (nop)
        principalProviderRegistry = new ProviderRegistryImpl(principalProvider);
        // register all configured principal providers.
        for (Properties aModuleConfig : moduleConfig) {
            principalProviderRegistry.registerProvider(aModuleConfig);
        }

        SecurityManagerConfig smc = config.getSecurityManagerConfig();
        if (smc != null && smc.getWorkspaceAccessConfig() != null) {
            workspaceAccessManager =
                smc.getWorkspaceAccessConfig().newInstance(WorkspaceAccessManager.class);
        } else {
            // fallback -> the default simple implementation
            log.debug("No WorkspaceAccessManager configured; using default.");
            workspaceAccessManager = new SimpleWorkspaceAccessManager();
        }
        workspaceAccessManager.init(systemSession);

        initialized = true;
    }

    /**
     * @see JackrabbitSecurityManager#dispose(String)
     */
    public void dispose(String workspaceName) {
        checkInitialized();
        // nop
    }

    /**
     * @see JackrabbitSecurityManager#close()
     */
    public void close() {
        checkInitialized();
    }

    /**
     * @see JackrabbitSecurityManager#getAccessManager(Session,AMContext)
     */
    public AccessManager getAccessManager(Session session, AMContext amContext) throws RepositoryException {
        checkInitialized();
        try {
            String wspName = session.getWorkspace().getName();
            AccessControlProvider acP = getAccessControlProvider(systemSession, wspName);

            AccessManagerConfig amc = config.getAccessManagerConfig();
            AccessManager accessMgr;
            if (amc == null) {
                accessMgr = new SimpleAccessManager();
            } else {
                accessMgr = amc.newInstance(AccessManager.class);
            }
            accessMgr.init(amContext, acP, workspaceAccessManager);
            return accessMgr;
        } catch (AccessDeniedException ade) {
            // re-throw
            throw ade;
        } catch (Exception e) {
            // wrap in RepositoryException
            String msg = "failed to instantiate AccessManager implementation: " + SimpleAccessManager.class.getName();
            log.error(msg, e);
            throw new RepositoryException(msg, e);
        }
    }

    /**
     * @see JackrabbitSecurityManager#getPrincipalManager(Session)
     */
    public synchronized PrincipalManager getPrincipalManager(Session session)
            throws RepositoryException {
        checkInitialized();
        if (session instanceof SessionImpl) {
            SessionImpl sImpl = ((SessionImpl)session);
            return new PrincipalManagerImpl(sImpl, principalProviderRegistry.getProviders());
        } else {
            throw new RepositoryException("Internal error: SessionImpl expected.");
        }
    }

    /**
     * @see JackrabbitSecurityManager#getUserManager(Session)
     */
    public UserManager getUserManager(Session session) throws RepositoryException {
        checkInitialized();
        throw new UnsupportedRepositoryOperationException("UserManager not supported.");
    }

    /**
     * @see JackrabbitSecurityManager#getUserID(javax.security.auth.Subject, String)
     */
    public String getUserID(Subject subject, String workspaceName) throws RepositoryException {
        String uid = null;

        // if SimpleCredentials are present, the UserID can easily be retrieved.
        Iterator<SimpleCredentials> creds = subject.getPublicCredentials(SimpleCredentials.class).iterator();
        if (creds.hasNext()) {
            SimpleCredentials sc = creds.next();
            uid = sc.getUserID();
        } else if (anonymID != null && !subject.getPrincipals(AnonymousPrincipal.class).isEmpty()) {
            uid = anonymID;
        } else {
            // assume that UserID and principal name
            // are the same (not totally correct) and thus return the name
            // of the first non-group principal.
            for (Principal p : subject.getPrincipals()) {
                if (!(p instanceof Group)) {
                    uid = p.getName();
                    break;
                }
            }
        }
        return uid;
    }

    /**
     * Creates an AuthContext for the given {@link Credentials} and
     * {@link Subject}.<br>
     * This includes selection of applicatoin specific LoginModules and
     * initalization with credentials and Session to System-Workspace
     *
     * @return an {@link AuthContext} for the given Credentials, Subject
     * @throws RepositoryException in other exceptional repository states
     */
    public AuthContext getAuthContext(Credentials creds, Subject subject, String workspaceName)
            throws RepositoryException {
        checkInitialized();
        return authCtxProvider.getAuthContext(creds, subject, systemSession, principalProviderRegistry, adminID, anonymID);
    }

    //--------------------------------------------------------------------------
    private void checkInitialized() {
        if (!initialized) {
            throw new IllegalStateException("Not initialized");
        }
    }

    /**
     * Simple Principal provider
     */
    private class SimplePrincipalProvider implements PrincipalProvider {

        private final Map<String, Principal> principals = new HashMap<String, Principal>();

        private SimplePrincipalProvider() {
            if (adminID != null) {
                principals.put(adminID, new AdminPrincipal(adminID));
            }
            if (anonymID != null) {
                principals.put(anonymID, new AnonymousPrincipal());
            }

            EveryonePrincipal everyone = EveryonePrincipal.getInstance();
            principals.put(everyone.getName(), everyone);
        }

        public Principal getPrincipal(String principalName) {
            if (principals.containsKey(principalName)) {
                return principals.get(principalName);
            } else {
                return new UserPrincipal(principalName);
            }
        }

        public PrincipalIterator findPrincipals(String simpleFilter) {
            return findPrincipals(simpleFilter, PrincipalManager.SEARCH_TYPE_ALL);
        }

        public PrincipalIterator findPrincipals(String simpleFilter, int searchType) {
            Principal p = getPrincipal(simpleFilter);
            if (p == null) {
                return PrincipalIteratorAdapter.EMPTY;
            } else if (p instanceof Group && searchType == PrincipalManager.SEARCH_TYPE_NOT_GROUP ||
                       !(p instanceof Group) && searchType == PrincipalManager.SEARCH_TYPE_GROUP) {
                return PrincipalIteratorAdapter.EMPTY;
            } else {
                return new PrincipalIteratorAdapter(Collections.singletonList(p));
            }
        }

        public PrincipalIterator getPrincipals(int searchType) {
            PrincipalIterator it;
            switch (searchType) {
                case PrincipalManager.SEARCH_TYPE_GROUP:
                    it = new PrincipalIteratorAdapter(Collections.singletonList(EveryonePrincipal.getInstance()));
                    break;
                case PrincipalManager.SEARCH_TYPE_NOT_GROUP:
                    Set<Principal> set = new HashSet<Principal>(principals.values());
                    set.remove(EveryonePrincipal.getInstance());
                    it = new PrincipalIteratorAdapter(set);
                    break;
                case PrincipalManager.SEARCH_TYPE_ALL:
                    it = new PrincipalIteratorAdapter(principals.values());
                    break;
                // no default
                default:
                    throw new IllegalArgumentException("Unknown search type " + searchType);
            }
            return it;
        }

        public PrincipalIterator getGroupMembership(Principal principal) {
            if (principal instanceof EveryonePrincipal) {
                return PrincipalIteratorAdapter.EMPTY;
            } else {
                return new PrincipalIteratorAdapter(Collections.singletonList(EveryonePrincipal.getInstance()));
            }
        }

        public void init(Properties options) {
            // nothing to do
        }

        public void close() {
            // nothing to do
        }

        public boolean canReadPrincipal(Session session, Principal principal) {
            return true;
        }
    }
}
