package alien4cloud.audit.rest; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.PostConstruct; import javax.annotation.Resource; import com.google.common.collect.Sets; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import alien4cloud.audit.AuditService; import alien4cloud.audit.annotation.Audit; import alien4cloud.audit.model.AuditConfiguration; import alien4cloud.audit.model.AuditedMethod; import alien4cloud.audit.model.Method; import alien4cloud.dao.model.FacetedSearchResult; import alien4cloud.exception.InvalidArgumentException; import alien4cloud.exception.NotFoundException; import alien4cloud.rest.model.FilteredSearchRequest; import alien4cloud.rest.model.RestResponse; import alien4cloud.rest.model.RestResponseBuilder; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @RestController @RequestMapping({ "/rest/audit", "/rest/v1/audit", "/rest/latest/audit" }) @Slf4j public class AuditController { @Resource private AuditService auditService; @Resource private RequestMappingHandlerMapping requestMappingHandlerMapping; // Some mapping are registered from child context in a dynamic fashion (Alien main context, plugins). private Set<RequestMappingHandlerMapping> registeredRequestMappingHandlerMapping = Sets.newHashSet(); @PostConstruct private void postConstruct() { AuditConfiguration auditConfiguration = auditService.getAuditConfiguration(); Map<Method, Boolean> allAvailableMethodsForAudit = getAllAvailableMethodsForAudit(requestMappingHandlerMapping); if (auditConfiguration == null) { log.info("Generate default configuration for audit"); auditConfiguration = new AuditConfiguration(); } else { log.info("Try to merge with existing audit configuration"); Map<Method, Boolean> existingMethodsMap = auditConfiguration.getAuditedMethodsMap(); allAvailableMethodsForAudit.putAll(existingMethodsMap); } auditConfiguration.setAuditedMethodsMap(allAvailableMethodsForAudit); auditService.saveAuditConfiguration(auditConfiguration); } private interface IAuditedMethodFactory<T extends Method> { T buildAuditedMethod(Method auditedMethod, HandlerMethod method); } private Map<Method, Boolean> getAllAvailableMethodsForAudit(RequestMappingHandlerMapping requestMappingHandlerMapping) { return getAllAvailableMethodsForAudit(requestMappingHandlerMapping, (auditedMethod, method) -> auditedMethod); } private <T extends Method> Map<T, Boolean> getAllAvailableMethodsForAudit(RequestMappingHandlerMapping requestMappingHandlerMapping, IAuditedMethodFactory<T> methodFactory) { Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods(); Map<T, Boolean> allMethods = Maps.newHashMap(); for (Map.Entry<RequestMappingInfo, HandlerMethod> handlerMethodEntry : handlerMethods.entrySet()) { HandlerMethod method = handlerMethodEntry.getValue(); Method auditedMethod = auditService.getAuditedMethod(method); if (auditedMethod != null) { Audit audit = method.getMethodAnnotation(Audit.class); boolean enabledByDefault = (audit != null && audit.enabledByDefault()); allMethods.put(methodFactory.buildAuditedMethod(auditedMethod, method), enabledByDefault); } } return allMethods; } /** * Register a dynamic RequestMappingHandlerMapping in the audit management system. * * @param requestMappingHandlerMapping The dynamic RequestMappingHandlerMapping to handle for audit. */ public void register(RequestMappingHandlerMapping requestMappingHandlerMapping) { registeredRequestMappingHandlerMapping.add(requestMappingHandlerMapping); // update configuration to inclure methods from plugin or context Map<Method, Boolean> allAvailableMethodsForAudit = getAllAvailableMethodsForAudit(requestMappingHandlerMapping); AuditConfiguration configuration = auditService.getAuditConfiguration(); // Put all in new map to not override existing user settings if some methods are already defined. allAvailableMethodsForAudit.putAll(configuration.getAuditedMethodsMap()); configuration.setAuditedMethodsMap(allAvailableMethodsForAudit); auditService.saveAuditConfiguration(configuration); } /** * Unregister a dynamic RequestMappingHandlerMapping * * @param requestMappingHandlerMapping the dynamic RequestMappingHandlerMapping to unregister. */ public void unRegister(RequestMappingHandlerMapping requestMappingHandlerMapping) { registeredRequestMappingHandlerMapping.remove(requestMappingHandlerMapping); // TODO we should cleanup configuration when a plugin is removed. } /** * Search for audit trace * * @param searchRequest The element that contains criterias for search operation. * @return A rest response that contains a {@link FacetedSearchResult} containing audit trace. */ @ApiOperation(value = "Search for audit trace", notes = "Returns a search result with that contains auti traces matching the request. Audit search is only accessible to user with role [ ADMIN ]") @RequestMapping(value = "/search", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<FacetedSearchResult> search(@RequestBody FilteredSearchRequest searchRequest) { FacetedSearchResult searchResult = auditService.searchAuditTrace(searchRequest.getQuery(), searchRequest.getFilters(), searchRequest.getFrom(), searchRequest.getSize()); if (searchRequest.getFilters() == null || !searchRequest.getFilters().containsKey("category")) { searchResult.getFacets().remove("action"); } return RestResponseBuilder.<FacetedSearchResult> builder().data(searchResult).build(); } @ApiOperation(value = "Reset the audit configuration", notes = "Reset the audit configuration to its default state. Audit search is only accessible to user with role [ ADMIN ]") @RequestMapping(value = "/configuration/reset", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<AuditConfigurationDTO> generateDefaultConfiguration() { AuditConfiguration auditConfiguration = new AuditConfiguration(); Map<Method, Boolean> allAvailableMethodsForAudit = getAllAvailableMethodsForAudit(requestMappingHandlerMapping); for (RequestMappingHandlerMapping registeredHandlerMapping : this.registeredRequestMappingHandlerMapping) { allAvailableMethodsForAudit.putAll(getAllAvailableMethodsForAudit(registeredHandlerMapping)); } auditConfiguration.setAuditedMethodsMap(allAvailableMethodsForAudit); auditService.saveAuditConfiguration(auditConfiguration); return getAuditConfiguration(); } @ApiOperation(value = "Get audit configuration", notes = "Get the audit configuration object. Audit configuration is only accessible to user with role [ ADMIN ]") @RequestMapping(value = "/configuration", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<AuditConfigurationDTO> getAuditConfiguration() { AuditConfiguration currentConfiguration = auditService.getMandatoryAuditConfiguration(); boolean auditEnabled = currentConfiguration.isEnabled(); Map<String, List<AuditedMethod>> methodsConfigurationDTO = Maps.newHashMap(); for (AuditedMethod methodDTO : currentConfiguration.getAuditedMethods()) { List<AuditedMethod> currentMethodsForCategory = methodsConfigurationDTO.get(methodDTO.getCategory()); if (currentMethodsForCategory == null) { currentMethodsForCategory = Lists.newArrayList(); methodsConfigurationDTO.put(methodDTO.getCategory(), currentMethodsForCategory); } currentMethodsForCategory.add(methodDTO); } AuditConfigurationDTO auditConfigurationDTO = new AuditConfigurationDTO(auditEnabled, methodsConfigurationDTO); return RestResponseBuilder.<AuditConfigurationDTO> builder().data(auditConfigurationDTO).build(); } @ApiOperation(value = "Enable/Disable audit", notes = "Audit configuration update is only accessible to user with role [ ADMIN ]") @RequestMapping(value = "/configuration/enabled", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<Void> enableAudit(@RequestParam boolean enabled) { AuditConfiguration auditConfiguration = auditService.getMandatoryAuditConfiguration(); auditConfiguration.setEnabled(enabled); auditService.saveAuditConfiguration(auditConfiguration); return RestResponseBuilder.<Void> builder().build(); } private void enableMethodAudit(Map<Method, Boolean> auditedMethodsMap, AuditedMethod method) { if (method.getMethod() == null) { throw new InvalidArgumentException("Method's path or http method is null"); } Method auditedMethodKey = new Method(method.getSignature(), method.getMethod(), method.getCategory(), method.getAction()); if (!auditedMethodsMap.containsKey(auditedMethodKey)) { throw new NotFoundException("Method " + method + " does not exist "); } auditedMethodsMap.put(auditedMethodKey, method.isEnabled()); } @ApiOperation(value = "Enable/Disable audit on a list of methods", notes = "Audit configuration update is only accessible to user with role [ ADMIN ]") @RequestMapping(value = "/configuration/audited-methods", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public RestResponse<Void> enableMethodAudit(@RequestBody AuditedMethod[] methods) { AuditConfiguration auditConfiguration = auditService.getMandatoryAuditConfiguration(); Map<Method, Boolean> auditedMethodsMap = auditConfiguration.getAuditedMethodsMap(); for (AuditedMethod method : methods) { enableMethodAudit(auditedMethodsMap, method); } auditConfiguration.setAuditedMethodsMap(auditedMethodsMap); auditService.saveAuditConfiguration(auditConfiguration); return RestResponseBuilder.<Void> builder().build(); } }