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

import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.action.Action;
import org.apache.nifi.action.Component;
import org.apache.nifi.action.FlowChangeAction;
import org.apache.nifi.action.Operation;
import org.apache.nifi.action.details.ActionDetails;
import org.apache.nifi.action.details.FlowChangeConfigureDetails;
import org.apache.nifi.authorization.Group;
import org.apache.nifi.web.api.dto.UserGroupDTO;
import org.apache.nifi.web.dao.UserGroupDAO;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.text.Collator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * Audits user creation/removal and configuration changes.
 */
@Service
@Aspect
public class UserGroupAuditor extends NiFiAuditor {

    private static final Logger logger = LoggerFactory.getLogger(UserGroupAuditor.class);

    private static final String NAME = "Name";
    private static final String USERS = "Users";

    /**
     * Audits the creation of policies via createUser().
     *
     * This method only needs to be run 'after returning'. However, in Java 7 the order in which these methods are returned from Class.getDeclaredMethods (even though there is no order guaranteed)
     * seems to differ from Java 6. SpringAOP depends on this ordering to determine advice precedence. By normalizing all advice into Around advice we can alleviate this issue.
     *
     * @param proceedingJoinPoint join point
     * @return node
     * @throws Throwable ex
     */
    @Around("within(org.apache.nifi.web.dao.UserGroupDAO+) && "
            + "execution(org.apache.nifi.authorization.Group createUserGroup(org.apache.nifi.web.api.dto.UserGroupDTO))")
    public Group createUserGroupAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // create the access user group
        Group userGroup = (Group) proceedingJoinPoint.proceed();

        // if no exceptions were thrown, add the user action...
        final Action action = generateAuditRecord(userGroup, Operation.Add);

        // save the actions
        if (action != null) {
            saveAction(action, logger);
        }

        return userGroup;
    }

    /**
     * Audits the configuration of a single user.
     *
     * @param proceedingJoinPoint join point
     * @param userGroupDTO dto
     * @param userGroupDAO dao
     * @return node
     * @throws Throwable ex
     */
    @Around("within(org.apache.nifi.web.dao.UserGroupDAO+) && "
            + "execution(org.apache.nifi.authorization.Group updateUserGroup(org.apache.nifi.web.api.dto.UserGroupDTO)) && "
            + "args(userGroupDTO) && "
            + "target(userGroupDAO)")
    public Group updateUserAdvice(ProceedingJoinPoint proceedingJoinPoint, UserGroupDTO userGroupDTO, UserGroupDAO userGroupDAO) throws Throwable {
        // determine the initial values for each property/setting that's changing
        Group user = userGroupDAO.getUserGroup(userGroupDTO.getId());
        final Map<String, String> values = extractConfiguredPropertyValues(user, userGroupDTO);

        // update the user state
        final Group updatedUserGroup = (Group) proceedingJoinPoint.proceed();

        // if no exceptions were thrown, add the user group action...
        user = userGroupDAO.getUserGroup(updatedUserGroup.getIdentifier());

        if (isAuditable()) {
            // determine the updated values
            Map<String, String> updatedValues = extractConfiguredPropertyValues(user, userGroupDTO);

            // create a user action
            Date actionTimestamp = new Date();
            Collection<Action> actions = new ArrayList<>();

            // go through each updated value
            for (String property : updatedValues.keySet()) {
                String newValue = updatedValues.get(property);
                String oldValue = values.get(property);
                Operation operation = null;

                // determine the type of operation
                if (oldValue == null || newValue == null || !newValue.equals(oldValue)) {
                    operation = Operation.Configure;
                }

                // create a configuration action accordingly
                if (operation != null) {
                    final FlowChangeConfigureDetails actionDetails = new FlowChangeConfigureDetails();
                    actionDetails.setName(property);
                    actionDetails.setValue(newValue);
                    actionDetails.setPreviousValue(oldValue);

                    // create a configuration action
                    final FlowChangeAction configurationAction = createFlowChangeAction();
                    configurationAction.setOperation(operation);
                    configurationAction.setTimestamp(actionTimestamp);
                    configurationAction.setSourceId(user.getIdentifier());
                    configurationAction.setSourceName(user.getName());
                    configurationAction.setSourceType(Component.UserGroup);
                    configurationAction.setActionDetails(actionDetails);
                    actions.add(configurationAction);
                }
            }

            // ensure there are actions to record
            if (!actions.isEmpty()) {
                // save the actions
                saveActions(actions, logger);
            }
        }

        return updatedUserGroup;
    }

    /**
     * Audits the removal of a user via deleteUser().
     *
     * @param proceedingJoinPoint join point
     * @param userGroupId user id
     * @param userGroupDAO dao
     * @throws Throwable ex
     */
    @Around("within(org.apache.nifi.web.dao.UserGroupDAO+) && "
            + "execution(org.apache.nifi.authorization.Group deleteUserGroup(java.lang.String)) && "
            + "args(userGroupId) && "
            + "target(userGroupDAO)")
    public Group removeUserAdvice(ProceedingJoinPoint proceedingJoinPoint, String userGroupId, UserGroupDAO userGroupDAO) throws Throwable {
        // get the user group before removing it
        Group userGroup = userGroupDAO.getUserGroup(userGroupId);

        // remove the user group
        final Group removedUserGroup = (Group) proceedingJoinPoint.proceed();

        // if no exceptions were thrown, add removal actions...
        // audit the user removal
        final Action action = generateAuditRecord(userGroup, Operation.Remove);

        // save the actions
        if (action != null) {
            saveAction(action, logger);
        }

        return removedUserGroup;
    }

    /**
     * Generates an audit record for the creation of a user group.
     *
     * @param userGroup userGroup
     * @param operation operation
     * @return action
     */
    public Action generateAuditRecord(Group userGroup, Operation operation) {
        return generateAuditRecord(userGroup, operation, null);
    }

    /**
     * Generates an audit record for the creation of a user group.
     *
     * @param userGroup userGroup
     * @param operation operation
     * @param actionDetails details
     * @return action
     */
    public Action generateAuditRecord(Group userGroup, Operation operation, ActionDetails actionDetails) {
        FlowChangeAction action = null;

        if (isAuditable()) {
            // create the user action for adding this user
            action = createFlowChangeAction();
            action.setOperation(operation);
            action.setSourceId(userGroup.getIdentifier());
            action.setSourceName(userGroup.getName());
            action.setSourceType(Component.UserGroup);

            if (actionDetails != null) {
                action.setActionDetails(actionDetails);
            }
        }

        return action;
    }

    /**
     * Extracts the values for the configured properties from the specified user group.
     */
    private Map<String, String> extractConfiguredPropertyValues(Group group, UserGroupDTO userGroupDTO) {
        Map<String, String> values = new HashMap<>();

        if (userGroupDTO.getIdentity() != null) {
            values.put(NAME, group.getName());
        }
        if (userGroupDTO.getUsers() != null) {
            // get each of the auto terminated relationship names
            final List<String> currentUsers = new ArrayList<>(group.getUsers());

            // sort them and include in the configuration
            Collections.sort(currentUsers, Collator.getInstance(Locale.US));
            values.put(USERS, StringUtils.join(currentUsers, ", "));
        }

        return values;
    }

}
