/******************************************************************************* * Copyright (c) 2013, 2014 Lectorius, Inc. * Authors: * Vijay Pandurangan ([email protected]) * Evan Jones ([email protected]) * Adam Hilss ([email protected]) * * * 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 [email protected]. *******************************************************************************/ package co.mitro.core.servlets; import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.servlet.annotation.WebServlet; import co.mitro.analysis.AuditLogProcessor.ActionType; import co.mitro.core.accesscontrol.AuthenticatedDB; import co.mitro.core.exceptions.MitroServletException; import co.mitro.core.server.Manager; import co.mitro.core.server.data.DBIdentity; import co.mitro.core.server.data.DBProcessedAudit; import co.mitro.core.server.data.RPC; import co.mitro.core.server.data.RPC.ListMySecretsAndGroupKeysResponse; import co.mitro.core.server.data.RPC.MitroRPC; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.stmt.Where; @WebServlet("/api/GetAuditLog") public class GetAuditLog extends MitroServlet { private static final long serialVersionUID = 1L; private static final Set<ActionType> USER_ACTION_TYPES = Sets.immutableEnumSet( ActionType.SIGNUP, ActionType.MITRO_LOGIN, ActionType.MITRO_AUTO_LOGIN, ActionType.EDIT_PASSWORD, ActionType.NEW_DEVICE, ActionType.INVITED_BY_USER); private static final Set<ActionType> SECRET_ACTION_TYPES = Sets.immutableEnumSet( ActionType.CREATE_SECRET, ActionType.DELETE_SECRET, ActionType.EDIT_SECRET, ActionType.EDIT_SECRET_ACL, ActionType.GRANTED_ACCESS_TO, ActionType.REVOKED_ACCESS_TO, ActionType.GET_SECRET_CRITICAL_DATA_FOR_LOGIN); private static final Set<ActionType> GROUP_ACTION_TYPES = Sets.immutableEnumSet( ActionType.CREATE_GROUP, ActionType.DELETE_GROUP, ActionType.EDIT_GROUP); // TODO: These events are currently unsupported // INVITE_USER // ORG_APPLY_SYNC // ORG_VIEW_SYNC // ORG_MUTATE /* OR a list of ormlite where clauses. * * rawtypes needed for ant compilation. Do not remove. */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected void orWhereClauses(Where<DBProcessedAudit, Integer> where, List<Where<DBProcessedAudit, Integer>> clauses) { // If list contains one clause, nothing needs to be done. if (clauses.size() == 2) { where.or(clauses.get(0), clauses.get(1)); } else if (clauses.size() > 2) { where.or(clauses.get(0), clauses.get(1), clauses.subList(2, clauses.size()).toArray((Where<DBProcessedAudit, Integer>[]) new Where[clauses.size() - 2])); } } // Needed to suppress warning about using a generic array for varargs parameter in WHERE clause below. // http://stackoverflow.com/questions/4257883/warning-for-generic-varargs?rq=1 @SuppressWarnings({ "unchecked" }) protected List<RPC.AuditEvent> queryAuditEvents( Manager manager, Set<Integer> userIds, Set<Integer> secretIds, Set<Integer> groupIds, Long limit, Long offset, Long startTimeMs, Long endTimeMs) throws SQLException, MitroServletException { List<RPC.AuditEvent> events = Lists.newArrayList(); QueryBuilder<DBProcessedAudit, Integer> query = manager.processedAuditDao.queryBuilder(); Where<DBProcessedAudit, Integer> where = query.where(); List<Where<DBProcessedAudit, Integer>> clauses = new ArrayList<>(); if (!userIds.isEmpty()) { clauses.add(where.and(where.in(DBProcessedAudit.ACTOR_FIELD_NAME, userIds), where.in(DBProcessedAudit.ACTION_FIELD_NAME, Manager.makeSelectArgsFromList(USER_ACTION_TYPES)))); } if (!secretIds.isEmpty()) { clauses.add(where.and(where.in(DBProcessedAudit.AFFECTED_SECRET_FIELD_NAME, secretIds), where.in(DBProcessedAudit.ACTION_FIELD_NAME, Manager.makeSelectArgsFromList(SECRET_ACTION_TYPES)))); } if (!groupIds.isEmpty()) { clauses.add(where.and(where.in(DBProcessedAudit.AFFECTED_GROUP_FIELD_NAME, groupIds), where.in(DBProcessedAudit.ACTION_FIELD_NAME, Manager.makeSelectArgsFromList(GROUP_ACTION_TYPES)))); } if (clauses.size() > 0) { orWhereClauses(where, clauses); if (startTimeMs != null) { where.and(where, where.ge(DBProcessedAudit.TIMESTAMP_FIELD_NAME, startTimeMs)); } if (endTimeMs != null) { if (startTimeMs != null && startTimeMs > endTimeMs) { throw new MitroServletException("start time must be before end time"); } where.and(where, where.le(DBProcessedAudit.TIMESTAMP_FIELD_NAME, endTimeMs)); } // offset and limit of null interpreted as no offset and no limit. // Boolean param of orderBy specifies DESC order. query.offset(offset).limit(limit).orderBy(DBProcessedAudit.TIMESTAMP_FIELD_NAME, false); for (DBProcessedAudit dbAudit : query.query()) { RPC.AuditEvent auditEvent = new RPC.AuditEvent(); fillRPCAuditEvent(manager, dbAudit, auditEvent); events.add(auditEvent); } } return events; } // userIds, secretIds, and groupIds are output parameters. private void getQueryIdsForUser(DBIdentity user, Manager mgr, Set<Integer> userIds, Set<Integer> secretIds, Set<Integer> groupIds) throws IOException, SQLException, MitroServletException { userIds.clear(); userIds.add(user.getId()); // TODO: This is a horrible hack for listing secrets. We need an API. RPC.ListMySecretsAndGroupKeysRequest request = new RPC.ListMySecretsAndGroupKeysRequest(); ListMySecretsAndGroupKeys servlet = new ListMySecretsAndGroupKeys(); RPC.ListMySecretsAndGroupKeysResponse out = (ListMySecretsAndGroupKeysResponse) servlet.processCommand(new MitroRequestContext(user, gson.toJson(request), mgr, null)); secretIds.clear(); secretIds.addAll(out.secretToPath.keySet()); groupIds.clear(); groupIds.addAll(out.groups.keySet()); } protected void getQueryIdsForOrg(MitroRequestContext context, int orgId, Set<Integer> userIds, Set<Integer> secretIds, Set<Integer> groupIds) throws MitroServletException, SQLException { // TODO: We need to create an API for orgs and clean this up. @SuppressWarnings("deprecation") AuthenticatedDB userDb = AuthenticatedDB.deprecatedNew(context.manager, context.requestor); if (!userDb.isOrganizationAdmin(orgId)) { throw new MitroServletException("Not org or no access"); } RPC.GetOrganizationStateResponse out = GetOrganizationState.doOperation(context, orgId); userIds.clear(); // TODO: change GetOrganizationState query to return the user id column. if (!out.members.isEmpty()) { QueryBuilder<DBIdentity, Integer> query = context.manager.identityDao.queryBuilder(); query.selectColumns(DBIdentity.ID_NAME); Where<DBIdentity, Integer> where = query.where(); where.in(DBIdentity.NAME_FIELD_NAME, Manager.makeSelectArgsFromList(out.members)); for (DBIdentity identity : query.query()) { userIds.add(identity.getId()); } } secretIds.clear(); secretIds.addAll(out.orgSecretsToPath.keySet()); secretIds.addAll(out.orphanedSecretsToPath.keySet()); groupIds.clear(); groupIds.addAll(out.groups.keySet()); } @Override protected MitroRPC processCommand(MitroRequestContext context) throws IOException, SQLException, MitroServletException { // TODO: Support all audit event types. Currently just supports events for secret. RPC.GetAuditLogRequest in = gson.fromJson(context.jsonRequest, RPC.GetAuditLogRequest.class); Set<Integer> userIds = Sets.newHashSet(); Set<Integer> secretIds = Sets.newHashSet(); Set<Integer> groupIds = Sets.newHashSet(); if (in.orgId == null) { getQueryIdsForUser(context.requestor, context.manager, userIds, secretIds, groupIds); } else { getQueryIdsForOrg(context, in.orgId.intValue(), userIds, secretIds, groupIds); } RPC.GetAuditLogResponse out = new RPC.GetAuditLogResponse(); out.events = queryAuditEvents(context.manager, userIds, secretIds, groupIds, in.limit, in.offset, in.startTimeMs, in.endTimeMs); return out; } protected static void fillRPCAuditEvent(Manager mgr, final DBProcessedAudit dbAudit, RPC.AuditEvent auditEvent) throws SQLException { auditEvent.id = dbAudit.getId(); auditEvent.secretId = dbAudit.getAffectedSecret(); if (dbAudit.getAffectedGroup() != null) { auditEvent.groupId = dbAudit.getAffectedGroup().getId(); } else { auditEvent.groupId = null; } if (dbAudit.getSourceIp() == null) { auditEvent.sourceIp = ""; } else { auditEvent.sourceIp = dbAudit.getSourceIp(); } auditEvent.action = dbAudit.getAction(); assert(dbAudit.getActor() != null); DBIdentity user = mgr.identityDao.queryForId(dbAudit.getActor().getId()); auditEvent.userId = user.getId(); auditEvent.username = user.getName(); auditEvent.timestampMs = dbAudit.getTimestampMs(); if (dbAudit.getAffectedUser() != null) { auditEvent.affectedUserId = dbAudit.getAffectedUser().getId(); auditEvent.affectedUsername = dbAudit.getAffectedUserName(); } } }