Java tutorial
/******************************************************************************* * 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); } } } }