co.mitro.analysis.AuditLogProcessor.java Source code

Java tutorial

Introduction

Here is the source code for co.mitro.analysis.AuditLogProcessor.java

Source

/*******************************************************************************
 * Copyright (c) 2013, 2014 Lectorius, Inc.
 * Authors:
 * Vijay Pandurangan (vijayp@mitro.co)
 * Evan Jones (ej@mitro.co)
 * Adam Hilss (ahilss@mitro.co)
 *
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *     
 *     You can contact the authors at inbound@mitro.co.
 *******************************************************************************/
package co.mitro.analysis;

import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import co.mitro.core.alerts.EmailAlertManager;
import co.mitro.core.server.Main;
import co.mitro.core.server.Manager;
import co.mitro.core.server.ManagerFactory;
import co.mitro.core.server.data.DBAudit;
import co.mitro.core.server.data.DBAudit.ACTION;
import co.mitro.core.server.data.DBGroup;
import co.mitro.core.server.data.DBProcessedAudit;
import co.mitro.core.server.data.DBServerVisibleSecret;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.j256.ormlite.stmt.DeleteBuilder;
import com.j256.ormlite.stmt.SelectArg;

public class AuditLogProcessor {
    private static final Logger logger = LoggerFactory.getLogger(AuditLogProcessor.class);

    public static enum ActionType {
        MITRO_AUTO_LOGIN, GET_SECRET_NON_CRITICAL_DATA, GET_SECRET_CRITICAL_DATA_FOR_LOGIN, GET_SECRET_CRITICAL_DATA_FOR_EDIT, EDIT_PASSWORD, EDIT_SECRET, CREATE_GROUP, EDIT_GROUP, DELETE_GROUP, CREATE_SECRET, DELETE_SECRET, EDIT_SECRET_ACL, INVITE_USER, INVITED_BY_USER, SIGNUP, NEW_DEVICE, ORG_APPLY_SYNC, ORG_VIEW_SYNC, ORG_MUTATE,
        // for operations that don't affect the audit log, like pings or refreshes
        NOOP, UNKNOWN, MITRO_LOGIN, GRANTED_ACCESS_TO, REVOKED_ACCESS_TO, DELETE_IDENTITY,
    };

    /**
     * creates and inserts into the DB processed audit logs for a specific transaction ids.
     * 
     * Additionally, this enqueues alerts to be sent out in the future.
     * 
     * NB: This function commits the transaction in the manager that is provided.
     * 
     * @return the number of rows we added to the processed audit log table.
     */
    public static final int putActionsForTransactionId(Manager manager, String transactionId) throws SQLException {
        Collection<DBProcessedAudit> actions = getActionsForTransactionId(manager, transactionId);
        for (DBProcessedAudit pa : actions) {
            manager.processedAuditDao.create(pa);
        }
        // some processed audit logs are added directly in the transaction.
        actions = manager.processedAuditDao.queryForEq(DBProcessedAudit.TRANSACTION_ID_FIELD_NAME,
                new SelectArg(transactionId));
        long minTimestampMs = Long.MAX_VALUE;
        for (DBProcessedAudit action : actions) {
            minTimestampMs = Math.min(action.getTimestampMs(), minTimestampMs);
        }
        EmailAlertManager.getInstance().createFutureAlertsFromAudits(manager, actions, minTimestampMs);
        manager.commitTransaction();

        return actions.size();
    }

    private static final Map<String, ActionType> OP_NAME_TO_ACTION_TYPE = ImmutableMap.<String, AuditLogProcessor.ActionType>builder()
            .put("VERIFY_DEVICE", ActionType.NEW_DEVICE).put("addGroup", ActionType.CREATE_GROUP)
            .put("addSecret", ActionType.CREATE_SECRET).put("addSite", ActionType.CREATE_SECRET)
            .put("applyPendingGroups", ActionType.ORG_APPLY_SYNC).put("checkTwoFactor", ActionType.UNKNOWN)
            .put("deleteSecret", ActionType.DELETE_SECRET).put("getAuditLog", ActionType.NOOP)
            .put("getGroup", ActionType.NOOP).put("getPendingGroups", ActionType.ORG_VIEW_SYNC)
            .put("mutateGroup", ActionType.EDIT_GROUP).put("mutateOrganization", ActionType.ORG_MUTATE)
            .put("mutatePrivateKeyPassword", ActionType.EDIT_PASSWORD).put("mutateSecret", ActionType.EDIT_SECRET)
            .put("mutateSite", ActionType.EDIT_SECRET).put("editSitePassword", ActionType.EDIT_SECRET)
            .put("removeGroup", ActionType.DELETE_GROUP).put("shareSite", ActionType.EDIT_SECRET_ACL)
            .put("shareSiteAndOptionallySetOrg", ActionType.EDIT_SECRET_ACL).build();

    private static final Set<ActionType> TRACK_SECRETS = Sets.immutableEnumSet(ActionType.CREATE_SECRET,
            ActionType.DELETE_SECRET, ActionType.EDIT_SECRET, ActionType.EDIT_SECRET_ACL,
            ActionType.GET_SECRET_CRITICAL_DATA_FOR_LOGIN);
    private static final Set<ActionType> TRACK_GROUPS = Sets.immutableEnumSet(ActionType.CREATE_GROUP,
            ActionType.DELETE_GROUP, ActionType.EDIT_GROUP);

    public static Collection<DBProcessedAudit> getActionsForTransactionId(Manager manager, String transactionId)
            throws SQLException {
        Set<DBAudit.ACTION> actions = Sets.newHashSet();
        List<DBAudit> matchingAuditLogs = manager.auditDao.queryForEq(DBAudit.TRANSACTION_ID_FIELD_NAME,
                new SelectArg(transactionId));
        String operationName = null;

        List<DBProcessedAudit> rval = Lists.newArrayList();
        List<DBProcessedAudit> invites = Lists.newArrayList();

        // these need to be maps and not sets because equals() and hashCode() aren't
        // properly implemented in these db objects.
        Map<Integer, DBGroup> affectedGroups = Maps.newHashMap();
        Map<Integer, DBServerVisibleSecret> affectedSecrets = Maps.newHashMap();
        Map<Integer, DBAudit> actionTargets = Maps.newHashMap();

        // First look through the audit logs that match the txn id, and figure
        // out if we've invited any users. If so, we make special events for them.
        for (DBAudit audit : matchingAuditLogs) {
            if (audit.getUser() == null && !DBAudit.TRANSACTION_ACTIONS.contains(audit.getAction())) {
                continue;
            }
            if (audit.getTargetGroup() != null) {
                affectedGroups.put(audit.getTargetGroup().getId(), audit.getTargetGroup());
            }
            if (audit.getTargetSVS() != null) {
                affectedSecrets.put(audit.getTargetSVS().getId(), audit.getTargetSVS());
            }

            actions.add(audit.getAction());
            operationName = (operationName == null) ? audit.getOperationName() : operationName;
            if (ACTION.INVITE_NEW_USER == audit.getAction()) {
                invites.add(new DBProcessedAudit(ActionType.INVITE_USER, audit));
                invites.add(new DBProcessedAudit(ActionType.INVITED_BY_USER, audit));
                // we don't care about the new user's private group
                audit.setTargetGroup(null);
                if (null != audit.getTargetUser()) {
                    actionTargets.put(audit.getId(), audit);
                }
            }
        }

        // has this transaction been cancelled or rolled back? If so, don't add any events.
        if (!Sets.intersection(actions, DBAudit.UNCOMMITTED_TRANSACTIONS).isEmpty()
                || !actions.contains(DBAudit.ACTION.TRANSACTION_COMMIT)) {
            return Collections.emptyList();
        }

        ActionType actionType = null;
        if (!Strings.isNullOrEmpty(operationName)) {
            // operation name is present in the log. This is pretty easy.
            actionType = OP_NAME_TO_ACTION_TYPE.get(operationName);
            if (actionType != null && actionType != ActionType.UNKNOWN && actionType != ActionType.NOOP) {
                if (actionTargets.isEmpty()) {
                    // if we don't have any action targets, it doesn't matter which audit object we use to create
                    // the processed audit.
                    addFromMatchingAudits(actionType, matchingAuditLogs, rval);
                } else {
                    // here, we have information about action targets. We must add a processed audit record for each.
                    for (DBAudit audit : actionTargets.values())
                        rval.add(new DBProcessedAudit(actionType, audit));
                }
            }
        } else {
            // no operation name, thus we must infer what happened in this transaction.
            if (actions.contains(DBAudit.ACTION.CREATE_IDENTITY)) {
                actionType = ActionType.SIGNUP;
            } else if (actions.contains(DBAudit.ACTION.GET_SECRET_WITH_CRITICAL) && actions.size() == 2) {
                // there are a bunch of different txns that could have GET_SECRET_WITH_CRITICAL
                // the ones that have only that and commit txn are for logins. 
                actionType = ActionType.GET_SECRET_CRITICAL_DATA_FOR_LOGIN;
            } else if (actions.contains(DBAudit.ACTION.GET_PRIVATE_KEY)) {
                actionType = ActionType.MITRO_LOGIN;
            }
            addFromMatchingAudits(actionType, matchingAuditLogs, rval);
        }

        // some kinds of transactions affect at most one group.
        if (TRACK_GROUPS.contains(actionType)) {
            if (affectedGroups.size() > 1) {
                logger.warn("transaction {} has more than one affected group. Ignoring groups for now...",
                        transactionId);
            } else if (!affectedGroups.isEmpty()) {
                final DBGroup g = affectedGroups.values().iterator().next();
                for (DBProcessedAudit a : rval) {
                    a.setAffectedGroup(g);
                }
            }
        }

        // some kinds of transactions affect at most one secret.
        if (TRACK_SECRETS.contains(actionType)) {
            if (affectedSecrets.size() > 1) {
                logger.warn("transaction {} has more than one affected secret. Ignoring secrets for now...",
                        transactionId);
            } else if (!affectedSecrets.isEmpty()) {
                final DBServerVisibleSecret s = affectedSecrets.values().iterator().next();
                for (DBProcessedAudit a : rval) {
                    a.setAffectedSecret(s);
                }
            }
        }

        rval.addAll(invites);
        return rval;
    }

    private static void addFromMatchingAudits(ActionType actionType, List<DBAudit> matchingAuditLogs,
            List<DBProcessedAudit> rval) {
        // if we've discovered what kind of action this is, we should add it.
        if (actionType != null) {
            for (DBAudit audit : matchingAuditLogs) {
                // some old crappy logs don't set the user properly on transaction close properties
                if (audit.getUser() != null) {
                    rval.add(new DBProcessedAudit(actionType, audit));
                    break;
                }
            }
        }
    }

    /**
     * Tries to create processed audit logs for any audit records that are missing 
     * processed logs. This could take a while...
     */
    public static void main(String[] args) throws SQLException {
        Main.exitIfAssertionsDisabled();
        Set<String> transactionsToProcess = Sets.newHashSet();
        try (Manager mgr = ManagerFactory.getInstance().newManager()) {
            mgr.disableAuditLogs();
            if (args.length == 0) { // find all transactions
                // this crazy string is necessary because postgres 9.1 does not properly optimize NOT IN queries
                String QUERY = "SELECT DISTINCT transaction_id FROM audit WHERE audit.action = 'INVITE_NEW_USER'";
                List<String[]> inviteResults = Lists.newArrayList(mgr.processedAuditDao.queryRaw(QUERY));
                for (String[] row : inviteResults) {
                    String tid = row[0];
                    if (Strings.isNullOrEmpty(tid)) {
                        continue;
                    }
                    transactionsToProcess.add(tid);
                }
            } else { // use specified transaction ids.
                for (int i = 0; i < args.length; ++i) {
                    transactionsToProcess.add(args[i]);
                }
            }

            if (true) {
                // ONLY FOR RE-CREATING ALL LOGS. THIS IS DANGEROUS
                for (String tid : transactionsToProcess) {
                    System.out.println("deleting " + tid);
                    DeleteBuilder<DBProcessedAudit, Integer> deleter = mgr.processedAuditDao.deleteBuilder();
                    deleter.where().eq("transaction_id", tid);
                    deleter.delete();
                }
            }
            /////

            logger.info("we must process logs for {} transactions.", transactionsToProcess.size());
            for (String tid : transactionsToProcess) {

                int count = putActionsForTransactionId(mgr, tid);
                mgr.commitTransaction();
                logger.info("transaction {} -> {} events.", tid, count);
            }
        }
    }
}