Repository: metron Updated Branches: refs/heads/master e2de1caa4 -> e5d876371
METRON-1085 Add REST endpoint to save a user profile for the Alerts UI (merrimanr) closes apache/metron#694 Project: http://git-wip-us.apache.org/repos/asf/metron/repo Commit: http://git-wip-us.apache.org/repos/asf/metron/commit/e5d87637 Tree: http://git-wip-us.apache.org/repos/asf/metron/tree/e5d87637 Diff: http://git-wip-us.apache.org/repos/asf/metron/diff/e5d87637 Branch: refs/heads/master Commit: e5d876371e523294598bfdb80a9332fa8917a08f Parents: e2de1ca Author: merrimanr <[email protected]> Authored: Thu Sep 28 20:01:36 2017 -0500 Committer: merrimanr <[email protected]> Committed: Thu Sep 28 20:01:36 2017 -0500 ---------------------------------------------------------------------- dependencies_with_url.csv | 8 + metron-interface/metron-rest-client/pom.xml | 11 + .../metron/rest/converter/JsonConverter.java | 53 ++++ .../apache/metron/rest/model/AlertProfile.java | 88 ++++++ .../apache/metron/rest/model/SavedSearch.java | 66 +++++ metron-interface/metron-rest/README.md | 39 ++- metron-interface/metron-rest/pom.xml | 121 +++++++++ .../apache/metron/rest/MetronRestConstants.java | 3 + .../metron/rest/config/JpaConfiguration.java | 51 ++++ .../metron/rest/config/WebSecurityConfig.java | 13 +- .../metron/rest/controller/AlertController.java | 119 +++++++++ .../rest/controller/AlertsController.java | 55 ---- .../rest/repository/AlertProfileRepository.java | 25 ++ .../metron/rest/security/SecurityUtils.java | 40 +++ .../metron/rest/service/AlertService.java | 9 + .../rest/service/AlertsProfileService.java | 32 +++ .../rest/service/impl/AlertServiceImpl.java | 37 ++- .../service/impl/AlertsProfileServiceImpl.java | 66 +++++ .../src/main/resources/application.yml | 2 + .../src/main/resources/log4j.properties | 2 +- .../AlertControllerIntegrationTest.java | 265 ++++++++++++++++++- .../rest/service/impl/AlertServiceImplTest.java | 85 +++++- .../indexing/dao/search/SearchRequest.java | 30 +++ 23 files changed, 1153 insertions(+), 67 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/dependencies_with_url.csv ---------------------------------------------------------------------- diff --git a/dependencies_with_url.csv b/dependencies_with_url.csv index b95f25b..f022647 100644 --- a/dependencies_with_url.csv +++ b/dependencies_with_url.csv @@ -113,6 +113,7 @@ com.fasterxml.jackson.core:jackson-core:jar:2.6.6:compile,ASLv2,https://github.c com.fasterxml.jackson.core:jackson-core:jar:2.7.4:compile,ASLv2,https://github.com/FasterXML/jackson-core com.fasterxml.jackson.core:jackson-core:jar:2.8.3:compile,ASLv2,https://github.com/FasterXML/jackson-core com.fasterxml.jackson.core:jackson-databind:jar:2.2.3:compile,ASLv2,http://wiki.fasterxml.com/JacksonHome +com.fasterxml.jackson.core:jackson-databind:jar:2.4.3:compile,ASLv2,http://github.com/FasterXML/jackson com.fasterxml.jackson.core:jackson-databind:jar:2.7.4:compile,ASLv2,http://github.com/FasterXML/jackson com.fasterxml.jackson.core:jackson-databind:jar:2.8.3:compile,ASLv2,http://github.com/FasterXML/jackson com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:jar:2.6.6:compile,ASLv2,http://wiki.fasterxml.com/JacksonForCbor @@ -309,3 +310,10 @@ org.springframework.security.kerberos:spring-security-kerberos-core:jar:1.0.1.RE org.springframework.kafka:spring-kafka:jar:1.1.1.RELEASE:compile,ASLv2,https://github.com/spring-projects/spring-kafka ch.hsr:geohash:jar:1.3.0:compile,ASLv2,https://github.com/kungfoo/geohash-java org.locationtech.spatial4j:spatial4j:jar:0.6:compile,ASLv2,https://github.com/locationtech/spatial4j +org.glassfish:javax.json:jar:1.0.4:compile,Common Development and Distribution License (CDDL) v1.0,https://github.com/javaee/jsonp +org.eclipse.persistence:javax.persistence:jar:2.1.1:compile,EPL 1.0,http://www.eclipse.org/eclipselink +org.eclipse.persistence:org.eclipse.persistence.antlr:jar:2.6.4:compile,EPL 1.0,http://www.eclipse.org/eclipselink +org.eclipse.persistence:org.eclipse.persistence.asm:jar:2.6.4:compile,EPL 1.0,http://www.eclipse.org/eclipselink +org.eclipse.persistence:org.eclipse.persistence.core:jar:2.6.4:compile,EPL 1.0,http://www.eclipse.org/eclipselink +org.eclipse.persistence:org.eclipse.persistence.jpa.jpql:jar:2.6.4:compile,EPL 1.0,http://www.eclipse.org/eclipselink +org.eclipse.persistence:org.eclipse.persistence.jpa:jar:2.6.4:compile,EPL 1.0,http://www.eclipse.org/eclipselink http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest-client/pom.xml ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest-client/pom.xml b/metron-interface/metron-rest-client/pom.xml index 21e4ffc..95f1bc2 100644 --- a/metron-interface/metron-rest-client/pom.xml +++ b/metron-interface/metron-rest-client/pom.xml @@ -23,6 +23,7 @@ <artifactId>metron-rest-client</artifactId> <url>https://metron.apache.org/</url> <properties> + <eclipse.javax.persistence.version>2.1.1</eclipse.javax.persistence.version> </properties> <dependencies> <dependency> @@ -36,6 +37,16 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>org.apache.metron</groupId> + <artifactId>metron-indexing</artifactId> + <version>${project.parent.version}</version> + </dependency> + <dependency> + <groupId>org.eclipse.persistence</groupId> + <artifactId>javax.persistence</artifactId> + <version>${eclipse.javax.persistence.version}</version> + </dependency> </dependencies> </project> http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/converter/JsonConverter.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/converter/JsonConverter.java b/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/converter/JsonConverter.java new file mode 100644 index 0000000..1059f5c --- /dev/null +++ b/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/converter/JsonConverter.java @@ -0,0 +1,53 @@ +/** + * 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.metron.rest.converter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; +import org.apache.metron.common.utils.JSONUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Converter +public class JsonConverter implements AttributeConverter<Object, String> { + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @Override + public String convertToDatabaseColumn(Object savedSearches) { + try { + return JSONUtils.INSTANCE.toJSON(savedSearches, false); + } catch (JsonProcessingException e) { + LOG.error("Error converting value to JSON", e); + } + return null; + } + + @Override + public Object convertToEntityAttribute(String savedSearches) { + try { + return JSONUtils.INSTANCE.load(savedSearches, Object.class); + } catch (IOException e) { + LOG.error("Error converting JSON to value", e); + } + return null; + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/AlertProfile.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/AlertProfile.java b/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/AlertProfile.java new file mode 100644 index 0000000..c126ce7 --- /dev/null +++ b/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/AlertProfile.java @@ -0,0 +1,88 @@ +/** + * 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.metron.rest.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.Id; +import org.apache.metron.rest.converter.JsonConverter; + +@Entity +public class AlertProfile { + + @Id + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private String id; + + @Convert(converter = JsonConverter.class) + private List<String> tableColumns; + + @Convert(converter = JsonConverter.class) + private List<SavedSearch> savedSearches; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public List<String> getTableColumns() { + return tableColumns; + } + + public void setTableColumns(List<String> tableColumns) { + this.tableColumns = tableColumns; + } + + public List<SavedSearch> getSavedSearches() { + return savedSearches; + } + + public void setSavedSearches(List<SavedSearch> savedSearches) { + this.savedSearches = savedSearches; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AlertProfile that = (AlertProfile) o; + + return id != null ? id.equals(that.id) : that.id == null && + (tableColumns != null ? tableColumns.equals(that.tableColumns) : that.tableColumns == null && + (savedSearches != null ? savedSearches.equals(that.savedSearches) : that.savedSearches == null)); + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (tableColumns != null ? tableColumns.hashCode() : 0); + result = 31 * result + (savedSearches != null ? savedSearches.hashCode() : 0); + return result; + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/SavedSearch.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/SavedSearch.java b/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/SavedSearch.java new file mode 100644 index 0000000..9b71d06 --- /dev/null +++ b/metron-interface/metron-rest-client/src/main/java/org/apache/metron/rest/model/SavedSearch.java @@ -0,0 +1,66 @@ +/** + * 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.metron.rest.model; + +import org.apache.metron.indexing.dao.search.SearchRequest; + +public class SavedSearch { + + private String name; + private SearchRequest searchRequest; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public SearchRequest getSearchRequest() { + return searchRequest; + } + + public void setSearchRequest(SearchRequest searchRequest) { + this.searchRequest = searchRequest; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SavedSearch that = (SavedSearch) o; + + return name != null ? name.equals(that.name) : that.name == null + && (searchRequest != null ? searchRequest.equals(that.searchRequest) + : that.searchRequest == null); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (searchRequest != null ? searchRequest.hashCode() : 0); + return result; + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/README.md ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/README.md b/metron-interface/metron-rest/README.md index 3d8d83b..394f4a8 100644 --- a/metron-interface/metron-rest/README.md +++ b/metron-interface/metron-rest/README.md @@ -80,11 +80,12 @@ These are set in the `/etc/sysconfig/metron` file. ## Database setup -The REST application persists data in a relational database and requires a dedicated database user and database (see https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html for more detail). +The REST application persists data in a relational database and requires a dedicated database user and database (see https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html for more detail). +Spring uses Hibernate as the default ORM framework but another framework is needed becaused Hibernate is not compatible with the Apache 2 license. For this reason Metron uses [EclipseLink](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html#boot-features-embedded-database-support). See the [Spring Data JPA - EclipseLink](https://github.com/spring-projects/spring-data-examples/tree/master/jpa/eclipselink) project for an example on how to configure EclipseLink in Spring. ### Development -The REST application comes with embedded database support for development purposes (https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html#boot-features-embedded-database-support). +The REST application comes with [embedded database support](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html#boot-features-embedded-database-support) for development purposes. For example, edit these variables in `/etc/sysconfig/metron` before starting the application to configure H2: ``` @@ -196,6 +197,10 @@ Request and Response objects are JSON formatted. The JSON schemas are available | | | ---------- | | [ `POST /api/v1/alert/escalate`](#get-apiv1alertescalate)| +| [ `GET /api/v1/alert/profile`](#get-apiv1alertprofile)| +| [ `GET /api/v1/alert/profile/all`](#get-apiv1alertprofileall)| +| [ `DELETE /api/v1/alert/profile`](#delete-apiv1alertprofile)| +| [ `POST /api/v1/alert/profile`](#post-apiv1alertprofile)| | [ `GET /api/v1/global/config`](#get-apiv1globalconfig)| | [ `DELETE /api/v1/global/config`](#delete-apiv1globalconfig)| | [ `POST /api/v1/global/config`](#post-apiv1globalconfig)| @@ -269,6 +274,36 @@ Request and Response objects are JSON formatted. The JSON schemas are available * alerts - The alerts to be escalated * Returns: * 200 - Alerts were escalated + +### `GET /api/v1/alert/profile` + * Description: Retrieves the current user's alerts profile + * Returns: + * 200 - Alerts profile + * 404 - The current user does not have an alerts profile + +### `GET /api/v1/alert/profile/all` + * Description: Retrieves all users' alerts profiles. Only users that are part of the "ROLE_ADMIN" role are allowed to get all alerts profiles. + * Returns: + * 200 - List of all alerts profiles + * 403 - The current user does not have permission to get all alerts profiles + +### `DELETE /api/v1/alert/profile` + * Description: Deletes a user's alerts profile. Only users that are part of the "ROLE_ADMIN" role are allowed to delete user alerts profiles. + * Input: + * user - The user whose prolife will be deleted + * Returns: + * 200 - Alerts profile was deleted + * 403 - The current user does not have permission to delete alerts profiles + * 404 - Alerts profile could not be found + +### `POST /api/v1/alert/profile` + * Description: Creates or updates the current user's alerts profile + * Input: + * alertsProfile - The alerts profile to be saved + * Returns: + * 200 - Alerts profile updated. Returns saved alerts profile. + * 201 - Alerts profile created. Returns saved alerts profile. + ### `GET /api/v1/global/config` * Description: Retrieves the current Global Config from Zookeeper http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/pom.xml ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/pom.xml b/metron-interface/metron-rest/pom.xml index 71caecc..dcc3b85 100644 --- a/metron-interface/metron-rest/pom.xml +++ b/metron-interface/metron-rest/pom.xml @@ -34,6 +34,8 @@ <swagger.version>2.5.0</swagger.version> <mysql.client.version>5.1.40</mysql.client.version> <spring-kafka.version>1.1.1.RELEASE</spring-kafka.version> + <spring.version>4.2.2.RELEASE</spring.version> + <eclipse.link.version>2.6.4</eclipse.link.version> </properties> <dependencies> <dependency> @@ -290,6 +292,34 @@ <artifactId>reflections</artifactId> <version>${global_reflections_version}</version> </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-jpa</artifactId> + <exclusions> + <exclusion> + <groupId>org.hibernate</groupId> + <artifactId>hibernate-entitymanager</artifactId> + </exclusion> + <exclusion> + <groupId>org.hibernate</groupId> + <artifactId>hibernate-core</artifactId> + </exclusion> + <exclusion> + <groupId>org.hibernate</groupId> + <artifactId>hibernate-commons-annotations</artifactId> + </exclusion> + <exclusion> + <groupId>org.hibernate.javax.persistence</groupId> + <artifactId>hibernate-jpa-2.1-api</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.eclipse.persistence</groupId> + <artifactId>org.eclipse.persistence.jpa</artifactId> + <version>${eclipse.link.version}</version> + </dependency> </dependencies> <dependencyManagement> @@ -385,4 +415,95 @@ </plugin> </plugins> </build> + + <profiles> + <profile> + + <!-- + Sets up the build to run the EclipseLink Maven plugin at compile time and instrument + domain types. This will prevent the need for load-time weaving when running the app. + --> + + <id>static-weaving</id> + + <build> + <plugins> + + <!-- Static weaver for EclipseLink --> + <plugin> + <groupId>com.ethlo.persistence.tools</groupId> + <artifactId>eclipselink-maven-plugin</artifactId> + <version>2.6.4.2</version> + <executions> + <execution> + <phase>process-classes</phase> + <goals> + <goal>weave</goal> + </goals> + </execution> + </executions> + + <dependencies> + <dependency> + <groupId>org.eclipse.persistence</groupId> + <artifactId>org.eclipse.persistence.jpa</artifactId> + <version>${eclipse.link.version}</version> + </dependency> + </dependencies> + + </plugin> + </plugins> + </build> + + <repositories> + <repository> + <id>com.ethlo.eclipselink.tools</id> + <url>http://ethlo.com/maven</url> + </repository> + </repositories> + + <pluginRepositories> + <pluginRepository> + <id>com.ethlo.eclipselink.tools</id> + <url>http://ethlo.com/maven</url> + </pluginRepository> + </pluginRepositories> + + </profile> + + <profile> + + <!-- + Sets up the build to use load-time weaving by configuring the Surefire and Boot + plugins to run with Spring's instrumentation agent. They will automatically pick + up the EclipseLink weaver and instrument domain types at load time. + --> + + <id>load-time-weaving</id> + + <activation> + <activeByDefault>true</activeByDefault> + </activation> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <dependencies> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-instrument</artifactId> + <version>${spring.version}</version> + </dependency> + </dependencies> + <configuration> + <argLine>-javaagent:${settings.localRepository}/org/springframework/spring-instrument/${spring.version}/spring-instrument-${spring.version}.jar</argLine> + </configuration> + </plugin> + </plugins> + </build> + </profile> + + </profiles> </project> http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java index b0f553f..0bd0ea3 100644 --- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java @@ -62,4 +62,7 @@ public class MetronRestConstants { public static final String META_DAO_IMPL = "meta.dao.impl"; public static final String META_DAO_SORT = "meta.dao.sort"; + + public static final String SECURITY_ROLE_USER = "USER"; + public static final String SECURITY_ROLE_ADMIN = "ADMIN"; } http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/JpaConfiguration.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/JpaConfiguration.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/JpaConfiguration.java new file mode 100644 index 0000000..80c9d1a --- /dev/null +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/JpaConfiguration.java @@ -0,0 +1,51 @@ +/** + * 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.metron.rest.config; + +import java.util.Collections; +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; +import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter; +import org.springframework.transaction.jta.JtaTransactionManager; + +@Configuration +@EntityScan("org.apache.metron") +public class JpaConfiguration extends JpaBaseConfiguration { + + protected JpaConfiguration(DataSource dataSource, JpaProperties properties, + ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider) { + super(dataSource, properties, jtaTransactionManagerProvider); + } + + @Override + protected AbstractJpaVendorAdapter createJpaVendorAdapter() { + return new EclipseLinkJpaVendorAdapter(); + } + + @Override + protected Map<String, Object> getVendorProperties() { + return Collections.singletonMap("eclipselink.weaving", "false"); + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java index 789098a..8310df1 100644 --- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java @@ -17,11 +17,15 @@ */ package org.apache.metron.rest.config; +import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_ADMIN; +import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_USER; + import org.apache.metron.rest.MetronRestConstants; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -36,6 +40,7 @@ import java.util.List; @Configuration @EnableWebSecurity +@EnableGlobalMethodSecurity(securedEnabled = true) @Controller public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @@ -82,10 +87,10 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { if (activeProfiles.contains(MetronRestConstants.DEV_PROFILE) || activeProfiles.contains(MetronRestConstants.TEST_PROFILE)) { auth.jdbcAuthentication().dataSource(dataSource) - .withUser("user").password("password").roles("USER").and() - .withUser("user1").password("password").roles("USER").and() - .withUser("user2").password("password").roles("USER").and() - .withUser("admin").password("password").roles("USER", "ADMIN"); + .withUser("user").password("password").roles(SECURITY_ROLE_USER).and() + .withUser("user1").password("password").roles(SECURITY_ROLE_USER).and() + .withUser("user2").password("password").roles(SECURITY_ROLE_USER).and() + .withUser("admin").password("password").roles(SECURITY_ROLE_USER, SECURITY_ROLE_ADMIN); } else { auth.jdbcAuthentication().dataSource(dataSource); } http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertController.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertController.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertController.java new file mode 100644 index 0000000..d8d5411 --- /dev/null +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertController.java @@ -0,0 +1,119 @@ +/** + * 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.metron.rest.controller; + +import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_ADMIN; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import java.util.List; +import java.util.Map; +import org.apache.metron.rest.RestException; +import org.apache.metron.rest.model.AlertProfile; +import org.apache.metron.rest.service.AlertService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * The API resource that is used for alert-related operations. + */ +@RestController +@RequestMapping("/api/v1/alert") +public class AlertController { + + /** + * Service used to interact with alerts. + */ + @Autowired + private AlertService alertService; + + @ApiOperation(value = "Escalates a list of alerts by producing it to the Kafka escalate topic") + @ApiResponse(message = "Alerts were escalated", code = 200) + @RequestMapping(value = "/escalate", method = RequestMethod.POST) + ResponseEntity<Void> escalate(final @ApiParam(name = "alerts", value = "The alerts to be escalated", required = true) @RequestBody List<Map<String, Object>> alerts) throws RestException { + alertService.escalateAlerts(alerts); + return new ResponseEntity<>(HttpStatus.OK); + } + + @ApiOperation(value = "Retrieves the current user's alerts profile") + @ApiResponses(value = {@ApiResponse(message = "Alerts profile", code = 200), + @ApiResponse(message = "The current user does not have an alerts profile", code = 404)}) + @RequestMapping(value = "/profile", method = RequestMethod.GET) + ResponseEntity<AlertProfile> get() throws RestException { + AlertProfile alertsProfile = alertService.getProfile(); + if (alertsProfile != null) { + return new ResponseEntity<>(alertsProfile, HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @Secured({"ROLE_" + SECURITY_ROLE_ADMIN}) + @ApiOperation(value = "Retrieves all users' alerts profiles. Only users that are part of " + + "the \"ROLE_ADMIN\" role are allowed to get all alerts profiles.") + @ApiResponses(value = {@ApiResponse(message = "List of all alerts profiles", code = 200), + @ApiResponse(message = + "The current user does not have permission to get all alerts profiles", code = 403)}) + @RequestMapping(value = "/profile/all", method = RequestMethod.GET) + ResponseEntity<Iterable<AlertProfile>> findAll() throws RestException { + return new ResponseEntity<>(alertService.findAllProfiles(), HttpStatus.OK); + } + + @ApiOperation(value = "Creates or updates the current user's alerts profile") + @ApiResponses(value = { + @ApiResponse(message = "Alerts profile updated. Returns saved alerts profile.", code = 200), + @ApiResponse(message = "Alerts profile created. Returns saved alerts profile.", code = 201)}) + @RequestMapping(value = "/profile", method = RequestMethod.POST) + ResponseEntity<AlertProfile> save(@ApiParam(name = "alertsProfile", value = + "The alerts profile to be saved", required = true) @RequestBody AlertProfile alertsProfile) + throws RestException { + if (alertService.getProfile() == null) { + return new ResponseEntity<>(alertService.saveProfile(alertsProfile), HttpStatus.CREATED); + } else { + return new ResponseEntity<>(alertService.saveProfile(alertsProfile), HttpStatus.OK); + } + } + + @Secured({"ROLE_" + SECURITY_ROLE_ADMIN}) + @ApiOperation(value = "Deletes a user's alerts profile. Only users that are part of " + + "the \"ROLE_ADMIN\" role are allowed to delete user alerts profiles.") + @ApiResponses(value = {@ApiResponse(message = "Alerts profile was deleted", code = 200), + @ApiResponse(message = "The current user does not have permission to delete alerts profiles", + code = 403), + @ApiResponse(message = "Alerts profile could not be found", code = 404)}) + @RequestMapping(value = "/profile/{user}", method = RequestMethod.DELETE) + ResponseEntity<Void> delete( + @ApiParam(name = "user", value = "The user whose prolife will be deleted", required = true) + @PathVariable String user) + throws RestException { + if (alertService.deleteProfile(user)) { + return new ResponseEntity<>(HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsController.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsController.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsController.java deleted file mode 100644 index 6f028a1..0000000 --- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsController.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 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.metron.rest.controller; - -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import java.util.List; -import java.util.Map; -import org.apache.metron.rest.RestException; -import org.apache.metron.rest.service.AlertService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -/** - * The API resource that is used for alert-related operations. - */ -@RestController -@RequestMapping("/api/v1/alert") -public class AlertsController { - - /** - * Service used to interact with alerts. - */ - @Autowired - private AlertService alertService; - - @ApiOperation(value = "Escalates a list of alerts by producing it to the Kafka escalate topic") - @ApiResponse(message = "Alerts were escalated", code = 200) - @RequestMapping(value = "/escalate", method = RequestMethod.POST) - ResponseEntity<Void> escalate(final @ApiParam(name = "alerts", value = "The alerts to be escalated", required = true) @RequestBody List<Map<String, Object>> alerts) throws RestException { - alertService.escalateAlerts(alerts); - return new ResponseEntity<>(HttpStatus.OK); - } -} http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/repository/AlertProfileRepository.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/repository/AlertProfileRepository.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/repository/AlertProfileRepository.java new file mode 100644 index 0000000..1466d90 --- /dev/null +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/repository/AlertProfileRepository.java @@ -0,0 +1,25 @@ +/** + * 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.metron.rest.repository; + +import org.apache.metron.rest.model.AlertProfile; +import org.springframework.data.repository.CrudRepository; + +public interface AlertProfileRepository extends CrudRepository<AlertProfile, String> { +} http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java new file mode 100644 index 0000000..ed10f14 --- /dev/null +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java @@ -0,0 +1,40 @@ +/** + * 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.metron.rest.security; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +public class SecurityUtils { + + /** Returns the username of the currently logged in user. + * + * @return username + */ + public static String getCurrentUser() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + String user; + if (principal instanceof UserDetails) { + user = ((UserDetails)principal).getUsername(); + } else { + user = principal.toString(); + } + return user; + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/AlertService.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/AlertService.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/AlertService.java index 9668b7c..a5f244b 100644 --- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/AlertService.java +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/AlertService.java @@ -20,6 +20,7 @@ package org.apache.metron.rest.service; import java.util.List; import java.util.Map; import org.apache.metron.rest.RestException; +import org.apache.metron.rest.model.AlertProfile; /** * This is a set of operations created to interact with alerts. @@ -27,4 +28,12 @@ import org.apache.metron.rest.RestException; public interface AlertService { void escalateAlerts(List<Map<String, Object>> alerts) throws RestException; + + AlertProfile getProfile(); + + Iterable<AlertProfile> findAllProfiles(); + + AlertProfile saveProfile(AlertProfile alertsProfile); + + boolean deleteProfile(String user); } http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/AlertsProfileService.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/AlertsProfileService.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/AlertsProfileService.java new file mode 100644 index 0000000..84672b6 --- /dev/null +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/AlertsProfileService.java @@ -0,0 +1,32 @@ +/** + * 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.metron.rest.service; + +import org.apache.metron.rest.model.AlertProfile; + +public interface AlertsProfileService { + + AlertProfile get(); + + Iterable<AlertProfile> findAll(); + + AlertProfile save(AlertProfile alertsProfile); + + boolean delete(String user); +} http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/AlertServiceImpl.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/AlertServiceImpl.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/AlertServiceImpl.java index 46370eb..73babbe 100644 --- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/AlertServiceImpl.java +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/AlertServiceImpl.java @@ -23,10 +23,14 @@ import java.util.Map; import org.apache.metron.common.utils.JSONUtils; import org.apache.metron.rest.MetronRestConstants; import org.apache.metron.rest.RestException; +import org.apache.metron.rest.model.AlertProfile; +import org.apache.metron.rest.repository.AlertProfileRepository; +import org.apache.metron.rest.security.SecurityUtils; import org.apache.metron.rest.service.AlertService; import org.apache.metron.rest.service.KafkaService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.stereotype.Service; /** @@ -39,12 +43,15 @@ public class AlertServiceImpl implements AlertService { private Environment environment; private final KafkaService kafkaService; + private AlertProfileRepository alertProfileRepository; @Autowired public AlertServiceImpl(final KafkaService kafkaService, - final Environment environment) { + final Environment environment, + final AlertProfileRepository alertsProfileRepository) { this.kafkaService = kafkaService; this.environment = environment; + this.alertProfileRepository = alertsProfileRepository; } @Override @@ -59,4 +66,32 @@ public class AlertServiceImpl implements AlertService { throw new RestException(e); } } + + @Override + public AlertProfile getProfile() { + return alertProfileRepository.findOne(SecurityUtils.getCurrentUser()); + } + + @Override + public Iterable<AlertProfile> findAllProfiles() { + return alertProfileRepository.findAll(); + } + + @Override + public AlertProfile saveProfile(AlertProfile alertsProfile) { + String user = SecurityUtils.getCurrentUser(); + alertsProfile.setId(user); + return alertProfileRepository.save(alertsProfile); + } + + @Override + public boolean deleteProfile(String user) { + boolean success = true; + try { + alertProfileRepository.delete(user); + } catch (EmptyResultDataAccessException e) { + success = false; + } + return success; + } } http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/AlertsProfileServiceImpl.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/AlertsProfileServiceImpl.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/AlertsProfileServiceImpl.java new file mode 100644 index 0000000..239dbdc --- /dev/null +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/service/impl/AlertsProfileServiceImpl.java @@ -0,0 +1,66 @@ +/** + * 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.metron.rest.service.impl; + +import org.apache.metron.rest.model.AlertProfile; +import org.apache.metron.rest.repository.AlertProfileRepository; +import org.apache.metron.rest.security.SecurityUtils; +import org.apache.metron.rest.service.AlertsProfileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +@Service +public class AlertsProfileServiceImpl implements AlertsProfileService { + + private AlertProfileRepository alertsProfileRepository; + + @Autowired + public AlertsProfileServiceImpl(AlertProfileRepository alertsProfileRepository) { + this.alertsProfileRepository = alertsProfileRepository; + } + + @Override + public AlertProfile get() { + return alertsProfileRepository.findOne(SecurityUtils.getCurrentUser()); + } + + @Override + public Iterable<AlertProfile> findAll() { + return alertsProfileRepository.findAll(); + } + + @Override + public AlertProfile save(AlertProfile alertsProfile) { + String user = SecurityUtils.getCurrentUser(); + alertsProfile.setId(user); + return alertsProfileRepository.save(alertsProfile); + } + + @Override + public boolean delete(String user) { + boolean success = true; + try { + alertsProfileRepository.delete(user); + } catch (EmptyResultDataAccessException e) { + success = false; + } + return success; + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/resources/application.yml ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/resources/application.yml b/metron-interface/metron-rest/src/main/resources/application.yml index 764bd40..c3012fa 100644 --- a/metron-interface/metron-rest/src/main/resources/application.yml +++ b/metron-interface/metron-rest/src/main/resources/application.yml @@ -58,3 +58,5 @@ meta: dao: # By default, we use the ElasticsearchMetaAlertDao impl: org.apache.metron.elasticsearch.dao.ElasticsearchMetaAlertDao + +spring.jpa.generate-ddl: true \ No newline at end of file http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/main/resources/log4j.properties ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/main/resources/log4j.properties b/metron-interface/metron-rest/src/main/resources/log4j.properties index 492cecf..e5f4095 100644 --- a/metron-interface/metron-rest/src/main/resources/log4j.properties +++ b/metron-interface/metron-rest/src/main/resources/log4j.properties @@ -13,4 +13,4 @@ log4j.rootLogger=ERROR, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd'T'HH:mm:ss.SSS} %-5p [%c] - %m%n \ No newline at end of file +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd'T'HH:mm:ss.SSS} %-5p [%c] - %m%n http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/AlertControllerIntegrationTest.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/AlertControllerIntegrationTest.java b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/AlertControllerIntegrationTest.java index be320fc..c3a4ac4 100644 --- a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/AlertControllerIntegrationTest.java +++ b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/controller/AlertControllerIntegrationTest.java @@ -18,13 +18,24 @@ package org.apache.metron.rest.controller; import static org.apache.metron.rest.MetronRestConstants.TEST_PROFILE; +import static org.hamcrest.Matchers.hasSize; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.adrianwalker.multilinestring.Multiline; +import org.apache.metron.integration.ComponentRunner; +import org.apache.metron.integration.UnableToStartException; +import org.apache.metron.integration.components.KafkaComponent; +import org.apache.metron.rest.model.AlertProfile; +import org.apache.metron.rest.service.AlertService; +import org.apache.metron.rest.service.AlertsProfileService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -57,17 +68,80 @@ public class AlertControllerIntegrationTest { @Multiline public static String alerts; + /** + * { + * "tableColumns": ["user1_field"], + * "savedSearches": [ + * { + * "name": "user1 search 1", + * "searchRequest": { + * "from": 0, + * "indices": ["bro"], + * "query": "*", + * "size": 5 + * } + * }, + * { + * "name": "user1 search 2", + * "searchRequest": { + * "from": 10, + * "indices": ["snort"], + * "query": "*", + * "size": 10 + * } + * } + * ] + * } + */ + @Multiline + public static String user1ProfileJson; + + + /** + * { + * "tableColumns": ["user2_field"], + * "savedSearches": [ + * { + * "name": "user2 search 1", + * "searchRequest": { + * "from": 0, + * "indices": ["bro", "snort"], + * "query": "ip_src_addr:192.168.1.1", + * "size": 100 + * } + * } + * ] + * } + */ + @Multiline + public static String user2ProfileJson; + + // A bug in Spring and/or Kafka forced us to move into a component that is spun up and down per test-case + // Given the large spinup time of components, please avoid this pattern until we upgrade Spring. + // See: https://issues.apache.org/jira/browse/METRON-1009 + @Autowired + private KafkaComponent kafkaWithZKComponent; + private ComponentRunner runner; + @Autowired private WebApplicationContext wac; + @Autowired + private AlertService alertService; + private MockMvc mockMvc; private String alertUrl = "/api/v1/alert"; - private String user = "user"; + private String user1 = "user1"; + private String user2 = "user2"; + private String admin = "admin"; private String password = "password"; @Before public void setup() throws Exception { + for (AlertProfile alertsProfile : alertService.findAllProfiles()) { + alertService.deleteProfile(alertsProfile.getId()); + } this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).apply(springSecurity()).build(); } @@ -75,12 +149,197 @@ public class AlertControllerIntegrationTest { public void testSecurity() throws Exception { this.mockMvc.perform(post(alertUrl + "/escalate").with(csrf()).contentType(MediaType.parseMediaType("application/json;charset=UTF-8")).content(alerts)) .andExpect(status().isUnauthorized()); + this.mockMvc.perform(get(alertUrl + "/profile")) + .andExpect(status().isUnauthorized()); + this.mockMvc.perform(get(alertUrl + "/profile/all")) + .andExpect(status().isUnauthorized()); + this.mockMvc.perform(post(alertUrl + "/profile").with(csrf()) + .contentType(MediaType.parseMediaType("application/json;charset=UTF-8")) + .content(user1ProfileJson)) + .andExpect(status().isUnauthorized()); + this.mockMvc.perform(get(alertUrl + "/profile/all").with(httpBasic(user1, password)).with(csrf())) + .andExpect(status().isForbidden()); + this.mockMvc.perform(delete(alertUrl + "/profile/user1").with(httpBasic(user1, password)).with(csrf())) + .andExpect(status().isForbidden()); } @Test - public void test() throws Exception { - this.mockMvc.perform(post(alertUrl + "/escalate").with(httpBasic(user,password)).with(csrf()).contentType(MediaType.parseMediaType("application/json;charset=UTF-8")).content(alerts)) + public void escalateShouldEscalateAlerts() throws Exception { + startKafka(); + this.mockMvc.perform(post(alertUrl + "/escalate").with(httpBasic(user1,password)).with(csrf()).contentType(MediaType.parseMediaType("application/json;charset=UTF-8")).content(alerts)) .andExpect(status().isOk()); + stopKafka(); + } + + @Test + public void testAlertProfiles() throws Exception { + emptyProfileShouldReturnNotFound(); + alertsProfilesShouldBeCreatedOrUpdated(); + alertsProfilesShouldBeProperlyDeleted(); + } + + /** Ensures a 404 is returned when an alerts profile cannot be found. In the case of an admin getting + * all profiles, an empty list should be returned. This tests depends on the alertsProfileRepository + * being empty. + * + * @throws Exception + */ + private void emptyProfileShouldReturnNotFound() throws Exception { + + // user1 should get a 404 because an alerts profile has not been created + this.mockMvc.perform(get(alertUrl + "/profile").with(httpBasic(user1, password))) + .andExpect(status().isNotFound()); + + // user2 should get a 404 because an alerts profile has not been created + this.mockMvc.perform(get(alertUrl + "/profile").with(httpBasic(user2, password))) + .andExpect(status().isNotFound()); + + // getting all alerts profiles should return an empty list + this.mockMvc.perform(get(alertUrl + "/profile/all").with(httpBasic(admin, password))) + .andExpect(status().isOk()) + .andExpect( + content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8"))) + .andExpect(jsonPath("$.*", hasSize(0))); + } + + /** Ensures users can update their profiles independently of other users. When user1 updates an + * alerts profile, alerts profile for user2 should not be affected. Tests that an initial update + * returns a 201 status and subsequent updates return 200 statuses. A call to get all alerts profiles + * by an admin user should also work properly. This tests depends on the alertsProfileRepository + * being empty initially. + * + * @throws Exception + */ + private void alertsProfilesShouldBeCreatedOrUpdated() throws Exception { + + // user1 creates their alerts profile + this.mockMvc.perform(post(alertUrl + "/profile").with(httpBasic(user1, password)).with(csrf()) + .contentType(MediaType.parseMediaType("application/json;charset=UTF-8")) + .content(user1ProfileJson)) + .andExpect(status().isCreated()) + .andExpect(content().json(user1ProfileJson)); + + // user1 updates their alerts profile + this.mockMvc.perform(post(alertUrl + "/profile").with(httpBasic(user1, password)).with(csrf()) + .contentType(MediaType.parseMediaType("application/json;charset=UTF-8")) + .content(user1ProfileJson)) + .andExpect(status().isOk()) + .andExpect(content().json(user1ProfileJson)); + + // user1 gets their alerts profile + this.mockMvc.perform(get(alertUrl + "/profile").with(httpBasic(user1, password))) + .andExpect(status().isOk()) + .andExpect( + content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8"))) + .andExpect(content().json(user1ProfileJson)); + + // user2 alerts profile should still be empty + this.mockMvc.perform(get(alertUrl + "/profile").with(httpBasic(user2, password))) + .andExpect(status().isNotFound()); + + // getting all alerts profiles should only return user1's + this.mockMvc.perform(get(alertUrl + "/profile/all").with(httpBasic(admin, password))) + .andExpect(status().isOk()) + .andExpect( + content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8"))) + .andExpect(content().json("[" + user1ProfileJson + "]")); + + // user2 creates their alerts profile + this.mockMvc.perform(post(alertUrl + "/profile").with(httpBasic(user2, password)).with(csrf()) + .contentType(MediaType.parseMediaType("application/json;charset=UTF-8")) + .content(user2ProfileJson)) + .andExpect(status().isCreated()) + .andExpect(content().json(user2ProfileJson)); + + // user2 updates their alerts profile + this.mockMvc.perform(get(alertUrl + "/profile").with(httpBasic(user1, password))) + .andExpect(status().isOk()) + .andExpect( + content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8"))) + .andExpect(content().json(user1ProfileJson)); + + // user2 gets their alerts profile + this.mockMvc.perform(get(alertUrl + "/profile").with(httpBasic(user2, password))) + .andExpect(status().isOk()) + .andExpect( + content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8"))) + .andExpect(content().json(user2ProfileJson)); + + // getting all alerts profiles should return both + this.mockMvc.perform(get(alertUrl + "/profile/all").with(httpBasic(admin, password))) + .andExpect(status().isOk()) + .andExpect( + content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8"))) + .andExpect(content().json("[" + user1ProfileJson + "," + user2ProfileJson + "]")); + } + + /** Ensures users can delete their profiles independently of other users. When user1 deletes an + * alerts profile, alerts profile for user2 should not be deleted. This tests depends on alerts + * profiles existing for user1 and user2. + * + * @throws Exception + */ + private void alertsProfilesShouldBeProperlyDeleted() throws Exception { + + // user1 deletes their profile + this.mockMvc.perform(delete(alertUrl + "/profile/user1").with(httpBasic(admin, password))) + .andExpect(status().isOk()); + + // user1 should get a 404 when trying to delete an alerts profile that doesn't exist + this.mockMvc.perform(delete(alertUrl + "/profile/user1").with(httpBasic(admin, password))) + .andExpect(status().isNotFound()); + + // user1 should get a 404 when trying to retrieve their alerts profile + this.mockMvc.perform(get(alertUrl + "/profile").with(httpBasic(user1, password))) + .andExpect(status().isNotFound()); + + // user2's alerts profile should still exist + this.mockMvc.perform(get(alertUrl + "/profile").with(httpBasic(user2, password))) + .andExpect(status().isOk()) + .andExpect( + content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8"))) + .andExpect(content().json(user2ProfileJson)); + + // getting all alerts profiles should only return user2's + this.mockMvc.perform(get(alertUrl + "/profile/all").with(httpBasic(admin, password))) + .andExpect(status().isOk()) + .andExpect( + content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8"))) + .andExpect(content().json("[" + user2ProfileJson + "]")); + + // user2 deletes their profile + this.mockMvc.perform(delete(alertUrl + "/profile/user2").with(httpBasic(admin, password))) + .andExpect(status().isOk()); + + // user2 should get a 404 when trying to delete an alerts profile that doesn't exist + this.mockMvc.perform(get(alertUrl + "/profile").with(httpBasic(user1, password))) + .andExpect(status().isNotFound()); + + // user2 should get a 404 when trying to retrieve their alerts profile + this.mockMvc.perform(get(alertUrl + "/profile").with(httpBasic(user2, password))) + .andExpect(status().isNotFound()); + + // getting all alerts profiles should return an empty list + this.mockMvc.perform(get(alertUrl + "/profile/all").with(httpBasic(admin, password))) + .andExpect(status().isOk()) + .andExpect( + content().contentType(MediaType.parseMediaType("application/json;charset=UTF-8"))) + .andExpect(jsonPath("$.*", hasSize(0))); + } + + private void startKafka() { + runner = new ComponentRunner.Builder() + .withComponent("kafka", kafkaWithZKComponent) + .withCustomShutdownOrder(new String[]{"kafka"}) + .build(); + try { + runner.start(); + } catch (UnableToStartException e) { + e.printStackTrace(); + } + } + private void stopKafka() { + runner.stop(); } } http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/AlertServiceImplTest.java ---------------------------------------------------------------------- diff --git a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/AlertServiceImplTest.java b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/AlertServiceImplTest.java index c55e0a5..8bed6b3 100644 --- a/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/AlertServiceImplTest.java +++ b/metron-interface/metron-rest/src/test/java/org/apache/metron/rest/service/impl/AlertServiceImplTest.java @@ -17,35 +17,58 @@ */ package org.apache.metron.rest.service.impl; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.metron.rest.MetronRestConstants; +import org.apache.metron.rest.model.AlertProfile; +import org.apache.metron.rest.repository.AlertProfileRepository; import org.apache.metron.rest.service.AlertService; import org.apache.metron.rest.service.KafkaService; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; import org.springframework.core.env.Environment; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; @SuppressWarnings("unchecked") public class AlertServiceImplTest { private KafkaService kafkaService; private Environment environment; + private AlertProfileRepository alertProfileRepository; private AlertService alertService; + private String testUser = "user1"; @SuppressWarnings("unchecked") @Before public void setUp() throws Exception { kafkaService = mock(KafkaService.class); environment = mock(Environment.class); - alertService = new AlertServiceImpl(kafkaService, environment); + alertProfileRepository = Mockito.mock(AlertProfileRepository.class); + alertService = new AlertServiceImpl(kafkaService, environment, alertProfileRepository); + + Authentication authentication = Mockito.mock(Authentication.class); + UserDetails userDetails = Mockito.mock(UserDetails.class); + when(authentication.getPrincipal()).thenReturn(userDetails); + when(userDetails.getUsername()).thenReturn(testUser); + SecurityContextHolder.getContext().setAuthentication(authentication); } @Test @@ -66,4 +89,64 @@ public class AlertServiceImplTest { verify(kafkaService).produceMessage("escalation", expectedMessage2); verifyZeroInteractions(kafkaService); } + + @Test + public void getShouldProperlyReturnActiveProfile() throws Exception { + AlertProfile alertsProfile = new AlertProfile(); + alertsProfile.setId(testUser); + when(alertProfileRepository.findOne(testUser)).thenReturn(alertsProfile); + + AlertProfile expectedAlertsProfile = new AlertProfile(); + expectedAlertsProfile.setId(testUser); + assertEquals(expectedAlertsProfile, alertService.getProfile()); + verify(alertProfileRepository, times(1)).findOne(testUser); + verifyNoMoreInteractions(alertProfileRepository); + } + + @Test + public void findAllShouldProperlyReturnActiveProfiles() throws Exception { + AlertProfile alertsProfile1 = new AlertProfile(); + alertsProfile1.setId(testUser); + AlertProfile alertsProfile2 = new AlertProfile(); + alertsProfile2.setId(testUser); + when(alertProfileRepository.findAll()) + .thenReturn(Arrays.asList(alertsProfile1, alertsProfile2)); + + AlertProfile expectedAlertsProfile1 = new AlertProfile(); + expectedAlertsProfile1.setId(testUser); + AlertProfile expectedAlertsProfile2 = new AlertProfile(); + expectedAlertsProfile2.setId(testUser); + Iterator<AlertProfile> actualAlertsProfiles = alertService.findAllProfiles().iterator(); + assertEquals(expectedAlertsProfile1, actualAlertsProfiles.next()); + assertEquals(expectedAlertsProfile2, actualAlertsProfiles.next()); + assertFalse(actualAlertsProfiles.hasNext()); + verify(alertProfileRepository, times(1)).findAll(); + verifyNoMoreInteractions(alertProfileRepository); + } + + @Test + public void saveShouldProperlySaveActiveProfile() throws Exception { + AlertProfile savedAlertsProfile = new AlertProfile(); + savedAlertsProfile.setId(testUser); + when(alertProfileRepository.save(savedAlertsProfile)).thenReturn(savedAlertsProfile); + + AlertProfile expectedAlertsProfile = new AlertProfile(); + expectedAlertsProfile.setId(testUser); + AlertProfile alertsProfile = new AlertProfile(); + assertEquals(expectedAlertsProfile, alertService.saveProfile(alertsProfile)); + + verify(alertProfileRepository, times(1)).save(savedAlertsProfile); + verifyNoMoreInteractions(alertProfileRepository); + } + + @Test + public void deleteShouldProperlyDeleteActiveProfile() throws Exception { + assertTrue(alertService.deleteProfile(testUser)); + + doThrow(new EmptyResultDataAccessException(1)).when(alertProfileRepository).delete(testUser); + assertFalse(alertService.deleteProfile(testUser)); + + verify(alertProfileRepository, times(2)).delete(testUser); + verifyNoMoreInteractions(alertProfileRepository); + } } http://git-wip-us.apache.org/repos/asf/metron/blob/e5d87637/metron-platform/metron-indexing/src/main/java/org/apache/metron/indexing/dao/search/SearchRequest.java ---------------------------------------------------------------------- diff --git a/metron-platform/metron-indexing/src/main/java/org/apache/metron/indexing/dao/search/SearchRequest.java b/metron-platform/metron-indexing/src/main/java/org/apache/metron/indexing/dao/search/SearchRequest.java index 3a72002..f4b6196 100644 --- a/metron-platform/metron-indexing/src/main/java/org/apache/metron/indexing/dao/search/SearchRequest.java +++ b/metron-platform/metron-indexing/src/main/java/org/apache/metron/indexing/dao/search/SearchRequest.java @@ -15,6 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.metron.indexing.dao.search; import java.util.ArrayList; @@ -115,4 +116,33 @@ public class SearchRequest { public void setFacetFields(List<String> facetFields) { this.facetFields = facetFields; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SearchRequest that = (SearchRequest) o; + + return indices != null ? indices.equals(that.indices) : that.indices == null && + (query != null ? query.equals(that.query) : that.query == null) && size == that.size && + from == that.from && + (sort != null ? sort.equals(that.sort) : that.sort == null) && + (facetFields != null ? facetFields.equals(that.facetFields) : that.facetFields == null); + } + + @Override + public int hashCode() { + int result = indices != null ? indices.hashCode() : 0; + result = 31 * result + (query != null ? query.hashCode() : 0); + result = 31 * result + getSize(); + result = 31 * result + getFrom(); + result = 31 * result + (sort != null ? sort.hashCode() : 0); + result = 31 * result + (facetFields != null ? facetFields.hashCode() : 0); + return result; + } }
