/*
 * 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.oak.spi.security.user.action;

import java.util.Collections;
import java.util.Set;
import javax.jcr.RepositoryException;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.AccessControlPolicyIterator;
import javax.jcr.security.Privilege;

import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
import org.apache.jackrabbit.oak.spi.security.authorization.AuthorizationConfiguration;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionConstants;
import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.jackrabbit.guava.common.base.Preconditions.checkState;

/**
 * The {@code AccessControlAction} allows to setup permissions upon creation
 * of a new authorizable; namely the privileges the new authorizable should be
 * granted on it's own 'home directory' being represented by the new node
 * associated with that new authorizable.
 *
 * <p>The following to configuration parameters are available with this implementation:</p>
 * <ul>
 *    <li><strong>groupPrivilegeNames</strong>: the value is expected to be a
 *    comma separated list of privileges that will be granted to the new group on
 *    the group node</li>
 *    <li><strong>userPrivilegeNames</strong>: the value is expected to be a
 *    comma separated list of privileges that will be granted to the new user on
 *    the user node.</li>
 * </ul>
 * <p>Example configuration:
 * <pre>
 *    groupPrivilegeNames : ["jcr:read"]
 *    userPrivilegeNames  : ["jcr:read,rep:write"]
 * </pre>
 * <p>This configuration could for example lead to the following content
 * structure upon user or group creation. Note however that the resulting
 * structure depends on the actual access control management being in place:</p>
 *
 * <pre>
 *     UserManager umgr = ((JackrabbitSession) session).getUserManager();
 *     User user = umgr.createUser("testUser", "t");
 *
 *     + t                           rep:AuthorizableFolder
 *       + te                        rep:AuthorizableFolder
 *         + testUser                rep:User, mix:AccessControllable
 *           + rep:policy            rep:ACL
 *             + allow               rep:GrantACE
 *               - rep:principalName = "testUser"
 *               - rep:privileges    = ["jcr:read","rep:write"]
 *           - rep:password
 *           - rep:principalName     = "testUser"
 * </pre>
 *
 * <pre>
 *     UserManager umgr = ((JackrabbitSession) session).getUserManager();
 *     Group group = umgr.createGroup("testGroup");
 *
 *     + t                           rep:AuthorizableFolder
 *       + te                        rep:AuthorizableFolder
 *         + testGroup               rep:Group, mix:AccessControllable
 *           + rep:policy            rep:ACL
 *             + allow               rep:GrantACE
 *               - rep:principalName = "testGroup"
 *               - rep:privileges    = ["jcr:read"]
 *           - rep:principalName     = "testGroup"
 * </pre>
 */
public class AccessControlAction extends AbstractAuthorizableAction {

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

    public static final String USER_PRIVILEGE_NAMES = "userPrivilegeNames";
    public static final String GROUP_PRIVILEGE_NAMES = "groupPrivilegeNames";

    private SecurityProvider securityProvider;
    private String[] groupPrivilegeNames = new String[0];
    private String[] userPrivilegeNames = new String[0];
    private Set<String> administrativePrincipals = Collections.emptySet();

    //-------------------------------------------------< AuthorizableAction >---
    @Override
    public void init(@NotNull SecurityProvider securityProvider, @NotNull ConfigurationParameters config) {
        this.securityProvider = securityProvider;
        userPrivilegeNames = privilegeNames(config, USER_PRIVILEGE_NAMES);
        groupPrivilegeNames = privilegeNames(config, GROUP_PRIVILEGE_NAMES);

        administrativePrincipals = securityProvider.getConfiguration(AuthorizationConfiguration.class).getParameters().getConfigValue(PermissionConstants.PARAM_ADMINISTRATIVE_PRINCIPALS, Collections.emptySet());
    }

    @Override
    public void onCreate(@NotNull Group group, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException {
        setAC(group, root, namePathMapper);
    }

    @Override
    public void onCreate(@NotNull User user, @Nullable String password, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException {
        setAC(user, root, namePathMapper);
    }

    //------------------------------------------------------------< private >---
    /**
     * Gets the privilege names that be granted on the group's home directory.
     *
     * @param config the configuration parameters.
     * @param paramName the name of the configuration option
     * @return a valid string array containing the privilege names.
     */
    @NotNull
    private static String[] privilegeNames(@NotNull ConfigurationParameters config, @NotNull String paramName) {
        String[] privilegeNames = config.getConfigValue(paramName, null, String[].class);
        if (privilegeNames != null && privilegeNames.length > 0) {
            return privilegeNames;
        } else {
            return new String[0];
        }
    }

    private void setAC(@NotNull Authorizable authorizable, @NotNull Root root,
                       @NotNull NamePathMapper namePathMapper) throws RepositoryException {
        checkState(securityProvider != null, "Not initialized");
        if (omitSetup(authorizable)) {
            return;
        }

        String path = authorizable.getPath();
        AuthorizationConfiguration acConfig = securityProvider.getConfiguration(AuthorizationConfiguration.class);
        AccessControlManager acMgr = acConfig.getAccessControlManager(root, namePathMapper);
        JackrabbitAccessControlList acl = null;
        for (AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); it.hasNext();) {
            AccessControlPolicy plc = it.nextAccessControlPolicy();
            if (plc instanceof JackrabbitAccessControlList) {
                acl = (JackrabbitAccessControlList) plc;
                break;
            }
        }

        if (acl == null) {
            log.warn("Cannot process AccessControlAction: no applicable ACL at {}", path);
        } else {
            // setup acl according to configuration.
            String[] privNames = (authorizable.isGroup()) ? groupPrivilegeNames : userPrivilegeNames;
            if (acl.addAccessControlEntry(authorizable.getPrincipal(), getPrivileges(privNames, acMgr))) {
                acMgr.setPolicy(path, acl);
            }
        }
    }

    private boolean omitSetup(@NotNull Authorizable authorizable) throws RepositoryException {
        if (authorizable.isGroup()) {
            if (groupPrivilegeNames.length == 0) {
                log.debug("No privileges configured for groups; omit ac setup.");
                return true;
            }
        } else {
            if (userPrivilegeNames.length == 0) {
                log.debug("No privileges configured for users; omit ac setup.");
                return true;
            }
            if (isBuiltInUser(authorizable)) {
                log.debug("System user: {}; omit ac setup.", authorizable.getID());
                return true;
            }
        }

        String principalName = authorizable.getPrincipal().getName();
        if (administrativePrincipals.contains(principalName)) {
            log.debug("Administrative principal: {}; omit ac setup.", principalName);
            return true;
        }
        return false;
    }

    private boolean isBuiltInUser(@NotNull Authorizable authorizable) throws RepositoryException {
        ConfigurationParameters userConfig = securityProvider.getConfiguration(UserConfiguration.class).getParameters();
        String userId = authorizable.getID();
        return UserUtil.getAdminId(userConfig).equals(userId) || UserUtil.getAnonymousId(userConfig).equals(userId);
    }

    /**
     * Retrieve privileges for the specified privilege names.
     *
     * @param privNames The privilege names.
     * @param acMgr The access control manager.
     * @return Array of {@code Privilege}
     * @throws javax.jcr.RepositoryException If a privilege name cannot be
     * resolved to a valid privilege.
     */
    @NotNull
    private static Privilege[] getPrivileges(@NotNull String[] privNames,
                                             @NotNull AccessControlManager acMgr) throws RepositoryException {
        Privilege[] privileges = new Privilege[privNames.length];
        for (int i = 0; i < privNames.length; i++) {
            privileges[i] = acMgr.privilegeFromName(privNames[i]);
        }
        return privileges;
    }
}
