diff --git a/CONFIGURATION.md b/CONFIGURATION.md index d4f26e43e80320c4eafeaa1a5d9628031c89d58f..a82a147edef4a2e29f8f1a282c9aeeffe09cf44d 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -6,6 +6,8 @@ Configuration may be set using SpringBoot's configuration file (`application.pro | Variable | Default | Description | |-------------------------------------|---------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| **Application** | | | +| | | | | NAMING_DATABASE_URL | jdbc:postgresql://postgres:5432/discs_names | JDBC URL for the database connection | | NAMING_DATABASE_USERNAME | discs_names | user name for the database connection | | NAMING_DATABASE_PASSWORD | discs_names | password for the database connection | @@ -22,4 +24,17 @@ Configuration may be set using SpringBoot's configuration file (`application.pro | NAMING_SMTP_PORT | 587 | port to use for SMTP, 587 for SMTP Secure | | NAMING_SMTP_USERNAME | | username for SMTP server | | NAMING_SMTP_PASSWORD | | password for SMTP server | +| | | | +| **Springdoc, Swagger** | | | +| | | | | NAMING_SWAGGER_URL | http://localhost:8080/swagger-ui.html | default url for Swagger UI | +| API_DOCS_PATH | /api-docs | custom path of the OpenAPI documentation in JSON | +| SWAGGER_UI_PATH | /swagger-ui.html | custom path of the swagger-ui documentation in HTML | +| | | | +| **Security** | | | +| | | | +| NAMING_SECURITY_ENABLED | false | enable security for application | +| AES_KEY | aes_secret_key | secret that is used to hash the tokens in the JWT token | +| JWT_SECRET | secret_password_to_hash_token | secret that is used sign the JWT token | +| JWT_EXP_MIN | 240 | time interval until the JWT token is valid (in minutes) | +| RBAC_SERVER_ADDRESS | | RBAC server address used for log in users if NAMING_SECURITY_ENABLED is enabled | diff --git a/pom.xml b/pom.xml index 7b58ad2f093e2a5b6e272764707f6d72aed9142e..c8e4c652732f8d325234a858b2a1907e73ed1f08 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,10 @@ <groupId>javax.persistence</groupId> <artifactId>javax.persistence-api</artifactId> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> @@ -59,6 +63,28 @@ <artifactId>springdoc-openapi-ui</artifactId> <version>1.8.0</version> </dependency> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-security</artifactId> + <version>1.8.0</version> + </dependency> + <dependency> + <groupId>io.fusionauth</groupId> + <artifactId>fusionauth-jwt</artifactId> + <version>5.3.2</version> + </dependency> + <dependency> + <groupId>com.squareup.okhttp3</groupId> + <artifactId>okhttp</artifactId> + </dependency> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>jackson-dataformat-xml</artifactId> + </dependency> <dependency> <groupId>com.github.spotbugs</groupId> diff --git a/src/main/java/org/openepics/names/NamingApplication.java b/src/main/java/org/openepics/names/NamingApplication.java index 379b5211c3b06bafea38e27f0058a8fff16ae1e1..3277fb7aad41972148c70951f035f861c391dd50 100644 --- a/src/main/java/org/openepics/names/NamingApplication.java +++ b/src/main/java/org/openepics/names/NamingApplication.java @@ -22,6 +22,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import io.swagger.v3.oas.models.ExternalDocumentation; import io.swagger.v3.oas.models.OpenAPI; @@ -67,4 +69,18 @@ public class NamingApplication { .version(appVersion)); } + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry + .addMapping("/**") + .allowedOriginPatterns("*") + .allowCredentials(true) + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE"); + } + }; + } + } diff --git a/src/main/java/org/openepics/names/configuration/SecurityConfiguration.java b/src/main/java/org/openepics/names/configuration/SecurityConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..b2c8b8431550d824eb61d4a02322fce280872cf6 --- /dev/null +++ b/src/main/java/org/openepics/names/configuration/SecurityConfiguration.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.configuration; + +import java.util.Arrays; +import java.util.List; + +import org.openepics.names.rest.filter.JwtRequestFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.http.HttpMethod; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +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.WebSecurityCustomizer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.StrictHttpFirewall; + +import com.google.common.collect.ImmutableList; + +/** + * Set up security configuration for Naming backend. + * + * @author Lars Johansson + */ +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = false) +public class SecurityConfiguration { + + // note + // ability to run application with and without security + // spring and swagger work together + // spring + // @EnableMethodSecurity + // @PreAuthorize + // + + // AuthorizationManagerBeforeMethodInterceptor + // SecurityFilterChain + // swagger + // @SecurityRequirement + + private static final String API_V1_AUTHENTICATION = "/api/v1/authentication"; + private static final String API_V1_NAMES = "/api/v1/names"; + private static final String API_V1_NAMES_PATHS = "/api/v1/names/*"; + private static final String API_V1_STRUCTURES = "/api/v1/structures"; + private static final String API_V1_STRUCTURES_PATHS = "/api/v1/structures/*"; + private static final String API_V1_PV_NAMES = "/api/v1/pvNames"; + private static final String API_V1_CONVERT = "/api/v1/convert"; + + private static final String ACTUATOR = "/actuator"; + private static final String ACTUATOR_PATHS = "/actuator/**"; + private static final String HEALTHCHECK = "/healthcheck"; + private static final String REPORT_PATHS = "/report/**"; + + public static final String ROLE_ADMINISTRATOR = "NamingAdministrator"; + public static final String ROLE_USER = "NamingUser"; + public static final String IS_ADMINISTRATOR = "hasAuthority('" + ROLE_ADMINISTRATOR + "')"; + public static final String IS_USER = "hasAuthority('" + ROLE_USER + "')"; + public static final String IS_ADMINISTRATOR_OR_USER = IS_ADMINISTRATOR + " or " + IS_USER; + + private static final List<String> ALLOWED_ROLES_TO_LOGIN = + Arrays.asList( + SecurityConfiguration.ROLE_ADMINISTRATOR, + SecurityConfiguration.ROLE_USER); + + @Value("${naming.security.enabled:false}") + private boolean namingSecurityEnabled; + @Value("${springdoc.api-docs.path}") + private String apiDocsPath; + @Value("${springdoc.swagger-ui.path}") + private String swaggerUiPath; + + private final UserDetailsService jwtUserDetailsService; + private final JwtRequestFilter jwtRequestFilter; + + @Autowired + public SecurityConfiguration( + UserDetailsService jwtUserDetailsService, + JwtRequestFilter jwtRequestFilter) { + this.jwtUserDetailsService = jwtUserDetailsService; + this.jwtRequestFilter = jwtRequestFilter; + } + + public static List<String> getAllowedRolesToLogin() { + return ImmutableList.copyOf(ALLOWED_ROLES_TO_LOGIN); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnProperty(name = "naming.security.enabled", havingValue = "true") + static AuthorizationManagerBeforeMethodInterceptor preAuthorizeAuthorizationMethodInterceptor() { + return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + // configure AuthenticationManager so that it knows from where to load + // user for matching credentials + // Use BCryptPasswordEncoder + auth.userDetailsService(jwtUserDetailsService); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // disable csrf since internal + // authentication and authorization + // create, read, update, delete + // not needed for read but needed for others, user for names, admin for structures + // accept requests to + // openapi, swagger + // healthcheck + // login, logout + // patterns that are not mentioned are to be authenticated + // stateless session as session not used to store user's state + + // TODO naming-convention-tool + // about + // paths to be removed when such functionality is removed + // /verification to verify data migration + // /rest rest api for comparison + // paths + // /verification/** + // /rest/deviceNames/** + // /rest/healthcheck + // /rest/history/** + // /rest/parts/** + + http + .csrf().disable() + .cors(); + + if (namingSecurityEnabled) { + http + .authorizeRequests() + // api docs, swagger + // authentication - login, logout + // names - read + // structures - read + // other + .mvcMatchers("/v3/api-docs/**", apiDocsPath + "/**", "/api/swagger-ui/**", "/swagger-ui/**", swaggerUiPath).permitAll() + .mvcMatchers(HttpMethod.POST, API_V1_AUTHENTICATION + "/login").permitAll() + .mvcMatchers(HttpMethod.GET, API_V1_NAMES, API_V1_NAMES_PATHS).permitAll() + .mvcMatchers(HttpMethod.GET, API_V1_STRUCTURES, API_V1_STRUCTURES_PATHS).permitAll() + .mvcMatchers(HttpMethod.GET, API_V1_PV_NAMES).permitAll() + .mvcMatchers(HttpMethod.GET, API_V1_CONVERT).permitAll() + .mvcMatchers(HttpMethod.GET, REPORT_PATHS).permitAll() + .mvcMatchers(HttpMethod.GET, HEALTHCHECK).permitAll() + .mvcMatchers(HttpMethod.GET, ACTUATOR, ACTUATOR_PATHS).permitAll() + // naming-convention-tool + .mvcMatchers(HttpMethod.GET, "/verification/**").permitAll() + .mvcMatchers(HttpMethod.GET, "/rest/deviceNames/**").permitAll() + .mvcMatchers(HttpMethod.GET, "/rest/healthcheck").permitAll() + .mvcMatchers(HttpMethod.GET, "/rest/history/**").permitAll() + .mvcMatchers(HttpMethod.GET, "/rest/parts/**").permitAll() + // any other requests to be authenticated + .anyRequest().authenticated() + // add a filter to validate the tokens with every request + .and() + .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) + // make sure we use stateless session; session won't be used to store user's state. + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + + return http.build(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + // allow % that is URL encoded %25 may be in path + // see org.springframework.security.web.firewall.StrictHttpFirewall + return web -> web.httpFirewall(allowUrlEncodedPercenthHttpFirewall()); + } + + private HttpFirewall allowUrlEncodedPercenthHttpFirewall() { + StrictHttpFirewall firewall = new StrictHttpFirewall(); + firewall.setAllowUrlEncodedPercent(true); + return firewall; + } + +} diff --git a/src/main/java/org/openepics/names/configuration/SwaggerConfiguration.java b/src/main/java/org/openepics/names/configuration/SwaggerConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..f1cdf2b291f97f00ce180d1827787d08d09c6c9d --- /dev/null +++ b/src/main/java/org/openepics/names/configuration/SwaggerConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.configuration; + +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.security.SecuritySchemes; + +/** + * Set up Swagger configuration for Naming backend. + * + * @author Lars Johansson + */ +@Configuration +@SecuritySchemes(value = { + @SecurityScheme( + name = "bearerAuth", + type = SecuritySchemeType.HTTP, + in = SecuritySchemeIn.HEADER, + bearerFormat = "JWT", + scheme = "bearer") +}) +public class SwaggerConfiguration { + + @Bean + public GroupedOpenApi v1Api() { + return GroupedOpenApi.builder().group("api-v1").pathsToMatch("/api/v1/**").build(); + } + + @Bean + public GroupedOpenApi base() { + return GroupedOpenApi.builder().group("base").pathsToMatch("/report/**", "/healthcheck").build(); + } + + +} diff --git a/src/main/java/org/openepics/names/exception/ServiceException.java b/src/main/java/org/openepics/names/exception/ServiceException.java index a0bf606cf81acbb1ce5b6985ec983ec8877049cf..9bab827afbc406877ec8d1b9854ee09bb937aa70 100644 --- a/src/main/java/org/openepics/names/exception/ServiceException.java +++ b/src/main/java/org/openepics/names/exception/ServiceException.java @@ -33,6 +33,17 @@ public class ServiceException extends RuntimeException { private final String details; private final String field; + /** + * Public constructor. + * + * @param message message + */ + public ServiceException(String message) { + super(message); + this.details = null; + this.field = null; + } + /** * Public constructor. * diff --git a/src/main/java/org/openepics/names/exception/handler/GlobalControllerExceptionHandler.java b/src/main/java/org/openepics/names/exception/handler/GlobalControllerExceptionHandler.java index a7c60a99b2d6047d7fae98db2ec43c4f277e796a..f18b7b52f4b628c32f44ea904d1778f1f9ce9a8e 100644 --- a/src/main/java/org/openepics/names/exception/handler/GlobalControllerExceptionHandler.java +++ b/src/main/java/org/openepics/names/exception/handler/GlobalControllerExceptionHandler.java @@ -31,6 +31,10 @@ import org.openepics.names.exception.InputNotCorrectException; import org.openepics.names.exception.InputNotEmptyException; import org.openepics.names.exception.InputNotValidException; import org.openepics.names.exception.ServiceException; +import org.openepics.names.exception.security.AuthenticationException; +import org.openepics.names.exception.security.EntityNotFoundException; +import org.openepics.names.exception.security.RemoteException; +import org.openepics.names.exception.security.UnauthorizedException; import org.openepics.names.rest.beans.response.Response; import org.openepics.names.util.TextUtil; import org.springframework.http.HttpStatus; @@ -46,10 +50,13 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep * Note * <ul> * <li>400 {@link HttpStatus#BAD_REQUEST}</li> + * <li>401 {@link HttpStatus#UNAUTHORIZED}</li> + * <li>403 {@link HttpStatus#FORBIDDEN}</li> * <li>404 {@link HttpStatus#NOT_FOUND}</li> * <li>409 {@link HttpStatus#CONFLICT}</li> * <li>422 {@link HttpStatus#UNPROCESSABLE_ENTITY}</li> * <li>500 {@link HttpStatus#INTERNAL_SERVER_ERROR}</li> + * <li>503 {@link HttpStatus#SERVICE_UNAVAILABLE}</li> * </ul> * * @author Lars Johansson @@ -58,7 +65,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler - protected ResponseEntity<Response> handleConflict(RuntimeException ex, WebRequest request) { + public ResponseEntity<Response> handleConflict(RuntimeException ex, WebRequest request) { Response response = new Response("", "", ""); response.setMessage(TextUtil.OPERATION_COULD_NOT_BE_PERFORMED); @@ -71,15 +78,17 @@ public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHan // HttpStatus.BAD_REQUEST handled by Spring - if (ex instanceof InputNotAvailableException - || ex instanceof InputNotCorrectException - || ex instanceof InputNotEmptyException - || ex instanceof InputNotValidException) { - resultStatus = HttpStatus.UNPROCESSABLE_ENTITY; + if (ex instanceof AuthenticationException) { + resultStatus = HttpStatus.UNAUTHORIZED; + } + + if (ex instanceof UnauthorizedException) { + resultStatus = HttpStatus.FORBIDDEN; } if (ex instanceof DataNotAvailableException - || ex instanceof DataNotFoundException) { + || ex instanceof DataNotFoundException + || ex instanceof EntityNotFoundException) { resultStatus = HttpStatus.NOT_FOUND; } @@ -90,9 +99,17 @@ public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHan resultStatus = HttpStatus.CONFLICT; } - if (ex instanceof DataNotCorrectException) { + if (ex instanceof InputNotAvailableException + || ex instanceof InputNotCorrectException + || ex instanceof InputNotEmptyException + || ex instanceof InputNotValidException + || ex instanceof DataNotCorrectException) { resultStatus = HttpStatus.UNPROCESSABLE_ENTITY; } + + if (ex instanceof RemoteException) { + resultStatus = HttpStatus.SERVICE_UNAVAILABLE; + } } return new ResponseEntity<>(response, Response.getHeaderJson(), resultStatus); diff --git a/src/main/java/org/openepics/names/exception/security/AuthenticationException.java b/src/main/java/org/openepics/names/exception/security/AuthenticationException.java new file mode 100644 index 0000000000000000000000000000000000000000..ec1ad8e0521707b25455147693d519be3bd878c7 --- /dev/null +++ b/src/main/java/org/openepics/names/exception/security/AuthenticationException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.exception.security; + +import org.openepics.names.exception.ServiceException; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +public class AuthenticationException extends ServiceException { + + /** + * + */ + private static final long serialVersionUID = -5434486411943794281L; + + private static final String ERROR = "Authentication error"; + + public AuthenticationException(String description) { + super(ERROR, description, null, null); + } + +} diff --git a/src/main/java/org/openepics/names/exception/security/EntityNotFoundException.java b/src/main/java/org/openepics/names/exception/security/EntityNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..f8780d711e79ca45d9b2c8a5b8e16997a50acc51 --- /dev/null +++ b/src/main/java/org/openepics/names/exception/security/EntityNotFoundException.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.exception.security; + +import org.openepics.names.exception.ServiceException; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +public class EntityNotFoundException extends ServiceException { + + /** + * + */ + private static final long serialVersionUID = 564746316436100483L; + + private static final String ENTITY_NOT_FOUND = "Entity not found"; + + public EntityNotFoundException(String entityType, long entityId) { + super(ENTITY_NOT_FOUND, entityType + " (ID: " + entityId + ") not found", null, null); + } + + public EntityNotFoundException(String entityType, String name) { + super(ENTITY_NOT_FOUND, entityType + " (name: " + name + ") not found", null, null); + } + + public EntityNotFoundException(String message) { + super(ENTITY_NOT_FOUND, message, null, null); + } + +} diff --git a/src/main/java/org/openepics/names/exception/security/ParseException.java b/src/main/java/org/openepics/names/exception/security/ParseException.java new file mode 100644 index 0000000000000000000000000000000000000000..0df3e3ff07db0544652388929e89d3a0089961c9 --- /dev/null +++ b/src/main/java/org/openepics/names/exception/security/ParseException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.exception.security; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +public class ParseException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 7711905654503012121L; + + public ParseException() {} + + public ParseException(String message) { + super(message); + } + +} diff --git a/src/main/java/org/openepics/names/exception/security/RemoteException.java b/src/main/java/org/openepics/names/exception/security/RemoteException.java new file mode 100644 index 0000000000000000000000000000000000000000..c54cf6ac2d96a20c135d4d547507dd5b5869383a --- /dev/null +++ b/src/main/java/org/openepics/names/exception/security/RemoteException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.exception.security; + +import org.openepics.names.exception.ServiceException; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +public class RemoteException extends ServiceException { + + /** + * + */ + private static final long serialVersionUID = -1202935975436997437L; + + public RemoteException(String error, String description) { + super(error, description, null, null); + } + +} diff --git a/src/main/java/org/openepics/names/exception/security/RemoteServiceException.java b/src/main/java/org/openepics/names/exception/security/RemoteServiceException.java new file mode 100644 index 0000000000000000000000000000000000000000..efdb01a788267642bc8b7551237e3b7a1b3ec8c1 --- /dev/null +++ b/src/main/java/org/openepics/names/exception/security/RemoteServiceException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.exception.security; + +/** + * @author <a href="mailto:zoltan.runyo@ess.eu">Zoltan Runyo</a> + */ +public class RemoteServiceException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 3744513029294293528L; + + public RemoteServiceException(String message, Throwable cause) { + super(message, cause); + } + + public RemoteServiceException(String message) { + super(message); + } + +} diff --git a/src/main/java/org/openepics/names/exception/security/UnauthorizedException.java b/src/main/java/org/openepics/names/exception/security/UnauthorizedException.java new file mode 100644 index 0000000000000000000000000000000000000000..3128be98001febc704c34900d059315651dc270d --- /dev/null +++ b/src/main/java/org/openepics/names/exception/security/UnauthorizedException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.exception.security; + +import org.openepics.names.exception.ServiceException; + +/** + * @author <a href="mailto:zoltan.runyo@ess.eu">Zoltan Runyo</a> + */ +public class UnauthorizedException extends ServiceException { + + /** + * + */ + private static final long serialVersionUID = -3051588358017181877L; + + public UnauthorizedException(String description) { + super("Operation forbidden", description, null, null); + } + +} diff --git a/src/main/java/org/openepics/names/rest/api/v1/IAuthentication.java b/src/main/java/org/openepics/names/rest/api/v1/IAuthentication.java new file mode 100644 index 0000000000000000000000000000000000000000..612fb1fe37aef887ed0d3f7915aef906cee6effb --- /dev/null +++ b/src/main/java/org/openepics/names/rest/api/v1/IAuthentication.java @@ -0,0 +1,113 @@ +package org.openepics.names.rest.api.v1; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; + +import org.openepics.names.exception.ServiceException; +import org.openepics.names.rest.beans.security.Login; +import org.openepics.names.rest.beans.security.LoginResponse; +import org.openepics.names.service.security.dto.UserDetails; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @author <a href="mailto:zoltan.runyo@ess.eu">Zoltan Runyo</a> + */ +@RequestMapping("/api/v1/authentication") +@Tag(name = "5. Authentication") +public interface IAuthentication { + + @Operation( + summary = "Login user and acquire JWT token", + description = "Login user with username, and password in order to acquire JWT token") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Ok", + content = @Content(schema = @Schema(implementation = LoginResponse.class))), + @ApiResponse( + responseCode = "401", + description = "Bad username, and/or password", + content = @Content(schema = @Schema(implementation = ServiceException.class))), + @ApiResponse( + responseCode = "403", + description = "User doesn't have permission to log in", + content = @Content(schema = @Schema(implementation = ServiceException.class))), + @ApiResponse( + responseCode = "503", + description = "Login server unavailable", + content = @Content(schema = @Schema(implementation = ServiceException.class))), + @ApiResponse( + responseCode = "500", + description = "Service exception", + content = @Content(schema = @Schema(implementation = ServiceException.class))) + }) + @PostMapping( + value = "/login", + produces = {"application/json"}, + consumes = {"application/json"}) + ResponseEntity<LoginResponse> login(@RequestBody Login loginInfo); + + @Operation( + summary = "Renewing JWT token", + description = "Renewing valid, non-expired JWT token", + security = {@SecurityRequirement(name = "bearerAuth")}) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Ok", + content = @Content(schema = @Schema(implementation = LoginResponse.class))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized", + content = @Content(schema = @Schema(implementation = ServiceException.class))), + @ApiResponse( + responseCode = "403", + description = "Permission denied", + content = @Content(schema = @Schema(implementation = ServiceException.class))), + @ApiResponse( + responseCode = "503", + description = "Login server unavailable", + content = @Content(schema = @Schema(implementation = ServiceException.class))), + @ApiResponse( + responseCode = "500", + description = "Service exception", + content = @Content(schema = @Schema(implementation = ServiceException.class))) + }) + @PostMapping( + value = "/renew", + produces = {"application/json"}) + ResponseEntity<LoginResponse> tokenRenew(@AuthenticationPrincipal UserDetails user); + + @Operation( + summary = "Logout", + description = "Logging out the user", + security = {@SecurityRequirement(name = "bearerAuth")}) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Ok"), + @ApiResponse( + responseCode = "500", + description = "Service exception", + content = @Content(schema = @Schema(implementation = ServiceException.class))), + @ApiResponse( + responseCode = "503", + description = "Login server unavailable or remote error", + content = @Content(schema = @Schema(implementation = ServiceException.class))) + }) + @DeleteMapping( + value = "/logout", + produces = {"application/json"}) + ResponseEntity<Object> logout(@AuthenticationPrincipal UserDetails user); + +} diff --git a/src/main/java/org/openepics/names/rest/api/v1/INames.java b/src/main/java/org/openepics/names/rest/api/v1/INames.java index 9dab9f0eaddb356447295de9a585abaf4e65207c..7626cb87baf74056d3dfdb6975bb07bc16dc092f 100644 --- a/src/main/java/org/openepics/names/rest/api/v1/INames.java +++ b/src/main/java/org/openepics/names/rest/api/v1/INames.java @@ -20,6 +20,7 @@ package org.openepics.names.rest.api.v1; import java.util.List; +import org.openepics.names.configuration.SecurityConfiguration; import org.openepics.names.rest.beans.FieldName; import org.openepics.names.rest.beans.element.NameElement; import org.openepics.names.rest.beans.element.NameElementCommandConfirm; @@ -29,7 +30,10 @@ import org.openepics.names.rest.beans.response.Response; import org.openepics.names.rest.beans.response.ResponseBoolean; import org.openepics.names.rest.beans.response.ResponseBooleanList; import org.openepics.names.rest.beans.response.ResponsePageNameElements; +import org.openepics.names.service.security.dto.UserDetails; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -48,6 +52,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; /** @@ -142,7 +147,8 @@ public interface INames { Note - Uuid is created by Naming - """) + """, + security = {@SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse( responseCode = "201", @@ -154,6 +160,14 @@ public interface INames { responseCode = "400", description = "Bad request. Reason and information such as message, details, field are available.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "403", + description = "Forbidden. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), @ApiResponse( responseCode = "404", description = "Not Found. Reason and information such as message, details, field are available.", @@ -174,7 +188,9 @@ public interface INames { @PostMapping( produces = {"application/json"}, consumes = {"application/json"}) + @PreAuthorize(SecurityConfiguration.IS_ADMINISTRATOR_OR_USER) public ResponseEntity<List<NameElement>> createNames( + @AuthenticationPrincipal UserDetails user, @Parameter( in = ParameterIn.DEFAULT, description = "array of name elements", @@ -664,7 +680,8 @@ public interface INames { Optional attributes: - parentDeviceStructure - index - """) + """, + security = {@SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse( responseCode = "200", @@ -676,6 +693,14 @@ public interface INames { responseCode = "400", description = "Bad request. Reason and information such as message, details, field are available.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "403", + description = "Forbidden. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), @ApiResponse( responseCode = "404", description = "Not Found. Reason and information such as message, details, field are available.", @@ -696,7 +721,9 @@ public interface INames { @PutMapping( produces = {"application/json"}, consumes = {"application/json"}) + @PreAuthorize(SecurityConfiguration.IS_ADMINISTRATOR_OR_USER) public List<NameElement> updateNames( + @AuthenticationPrincipal UserDetails user, @Parameter( in = ParameterIn.DEFAULT, description = "array of name elements", @@ -725,7 +752,8 @@ public interface INames { Required attributes: - uuid - """) + """, + security = {@SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse( responseCode = "204", @@ -734,6 +762,14 @@ public interface INames { responseCode = "400", description = "Bad request. Reason and information such as message, details, field are available.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "403", + description = "Forbidden. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), @ApiResponse( responseCode = "404", description = "Not Found. Reason and information such as message, details, field are available.", @@ -750,7 +786,9 @@ public interface INames { @DeleteMapping( produces = {"application/json"}, consumes = {"application/json"}) + @PreAuthorize(SecurityConfiguration.IS_ADMINISTRATOR_OR_USER) public ResponseEntity<Response> deleteNames( + @AuthenticationPrincipal UserDetails user, @Parameter( in = ParameterIn.DEFAULT, description = "array of name elements", diff --git a/src/main/java/org/openepics/names/rest/api/v1/IStructures.java b/src/main/java/org/openepics/names/rest/api/v1/IStructures.java index fed8d373a414f4c9e9b8a6be5908c8dae9e0ba8e..fa9b542e42123288228d99b379cb5bcfd87adf4c 100644 --- a/src/main/java/org/openepics/names/rest/api/v1/IStructures.java +++ b/src/main/java/org/openepics/names/rest/api/v1/IStructures.java @@ -20,6 +20,7 @@ package org.openepics.names.rest.api.v1; import java.util.List; +import org.openepics.names.configuration.SecurityConfiguration; import org.openepics.names.rest.beans.FieldStructure; import org.openepics.names.rest.beans.Type; import org.openepics.names.rest.beans.element.StructureElement; @@ -30,7 +31,10 @@ import org.openepics.names.rest.beans.response.Response; import org.openepics.names.rest.beans.response.ResponseBoolean; import org.openepics.names.rest.beans.response.ResponseBooleanList; import org.openepics.names.rest.beans.response.ResponsePageStructureElements; +import org.openepics.names.service.security.dto.UserDetails; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -49,6 +53,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; /** @@ -146,7 +151,8 @@ public interface IStructures { - Uuid is created by Naming - Parent is required for System, Subsystem, DeviceGroup, DeviceType - Mnemonic is required for System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup - """) + """, + security = {@SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse( responseCode = "201", @@ -158,6 +164,14 @@ public interface IStructures { responseCode = "400", description = "Bad request. Reason and information such as message, details, field are available.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "403", + description = "Forbidden. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), @ApiResponse( responseCode = "404", description = "Not Found. Reason and information such as message, details, field are available.", @@ -178,7 +192,9 @@ public interface IStructures { @PostMapping( produces = {"application/json"}, consumes = {"application/json"}) + @PreAuthorize(SecurityConfiguration.IS_ADMINISTRATOR) public ResponseEntity<List<StructureElement>> createStructures( + @AuthenticationPrincipal UserDetails user, @Parameter( in = ParameterIn.DEFAULT, description = "array of structure elements", @@ -726,7 +742,8 @@ public interface IStructures { Note - Parent is required for System, Subsystem, DeviceGroup, DeviceType - Mnemonic is required for System, Subsystem, Discipline, DeviceType, may be set for SystemGroup, not allowed for DeviceGroup - """) + """, + security = {@SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse( responseCode = "200", @@ -738,6 +755,14 @@ public interface IStructures { responseCode = "400", description = "Bad request. Reason and information such as message, details, field are available.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "403", + description = "Forbidden. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), @ApiResponse( responseCode = "404", description = "Not Found. Reason and information such as message, details, field are available.", @@ -758,7 +783,9 @@ public interface IStructures { @PutMapping( produces = {"application/json"}, consumes = {"application/json"}) + @PreAuthorize(SecurityConfiguration.IS_ADMINISTRATOR) public List<StructureElement> updateStructures( + @AuthenticationPrincipal UserDetails user, @Parameter( in = ParameterIn.DEFAULT, description = "array of structure elements", @@ -788,7 +815,8 @@ public interface IStructures { Required attributes: - uuid - type - """) + """, + security = {@SecurityRequirement(name = "bearerAuth")}) @ApiResponses(value = { @ApiResponse( responseCode = "204", @@ -797,6 +825,14 @@ public interface IStructures { responseCode = "400", description = "Bad request. Reason and information such as message, details, field are available.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "401", + description = "Unauthorized. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), + @ApiResponse( + responseCode = "403", + description = "Forbidden. Reason and information such as message, details, field are available.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Response.class))), @ApiResponse( responseCode = "404", description = "Not Found. Reason and information such as message, details, field are available.", @@ -813,7 +849,9 @@ public interface IStructures { @DeleteMapping( produces = {"application/json"}, consumes = {"application/json"}) + @PreAuthorize(SecurityConfiguration.IS_ADMINISTRATOR) public ResponseEntity<Response> deleteStructures( + @AuthenticationPrincipal UserDetails user, @Parameter( in = ParameterIn.DEFAULT, description = "array of structure elements", diff --git a/src/main/java/org/openepics/names/rest/beans/security/Login.java b/src/main/java/org/openepics/names/rest/beans/security/Login.java new file mode 100644 index 0000000000000000000000000000000000000000..6beba544bd486d8979ef75b32a1de46fa61abc32 --- /dev/null +++ b/src/main/java/org/openepics/names/rest/beans/security/Login.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.rest.beans.security; + +/** + * Class is used for sending Login request trough REST API. + * + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +public class Login { + + private String username; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/src/main/java/org/openepics/names/rest/beans/security/LoginResponse.java b/src/main/java/org/openepics/names/rest/beans/security/LoginResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..498b02564df3b0c5bcd723f622dc1912a74b2c83 --- /dev/null +++ b/src/main/java/org/openepics/names/rest/beans/security/LoginResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.rest.beans.security; + +/** + * Class used for sending back JWT token in response to the client if login was successful. + * + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +public class LoginResponse { + + private String token; + + /** + * Setting the JWT token for the Login response + * + * @param token the JWT token that will be used for the communication + */ + public LoginResponse(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + +} diff --git a/src/main/java/org/openepics/names/rest/controller/AuthenticationController.java b/src/main/java/org/openepics/names/rest/controller/AuthenticationController.java new file mode 100644 index 0000000000000000000000000000000000000000..21a31a0d99fab9f79ecc016eaccdc5bf2c7129da --- /dev/null +++ b/src/main/java/org/openepics/names/rest/controller/AuthenticationController.java @@ -0,0 +1,129 @@ +package org.openepics.names.rest.controller; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.openepics.names.exception.ServiceException; + +import org.openepics.names.exception.security.AuthenticationException; +import org.openepics.names.exception.security.RemoteException; +import org.openepics.names.exception.security.UnauthorizedException; +import org.openepics.names.rest.api.v1.IAuthentication; +import org.openepics.names.rest.beans.security.Login; +import org.openepics.names.rest.beans.security.LoginResponse; +import org.openepics.names.service.LogService; +import org.openepics.names.service.security.AuthenticationService; +import org.openepics.names.service.security.dto.UserDetails; +import org.openepics.names.service.security.util.SecurityTextUtil; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author <a href="mailto:zoltan.runyo@ess.eu">Zoltan Runyo</a> + */ +@RestController +public class AuthenticationController implements IAuthentication { + + private static final Logger LOGGER = Logger.getLogger(AuthenticationController.class.getName()); + + private final AuthenticationService authenticationService; + private final LogService logService; + + @Autowired + public AuthenticationController( + AuthenticationService authenticationService, + LogService logService) { + this.authenticationService = authenticationService; + this.logService = logService; + } + + @Override + public ResponseEntity<LoginResponse> login(Login loginInfo) { + try { + LoginResponse login = + authenticationService.login(loginInfo.getUsername(), loginInfo.getPassword()); + + ResponseCookie loginCookie = + ResponseCookie.from(SecurityTextUtil.COOKIE_AUTH_HEADER, login.getToken()) + .httpOnly(true) + .secure(true) + .path("/") + .build(); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, loginCookie.toString()) + .body(login); + } catch (UnauthorizedException e) { + logService.logServiceException(LOGGER, Level.WARNING, e, "User doesn't have permission to log in"); + throw e; + } catch (AuthenticationException e) { + logService.logServiceException(LOGGER, Level.WARNING, e, "Error while user tried to log in"); + throw e; + } catch (RemoteException e) { + logService.logServiceException(LOGGER, Level.WARNING, e, "Remote exception while user tried to log in"); + throw e; + } catch (Exception e) { + logService.logException(LOGGER, Level.WARNING, e, "Error while trying to log in user"); + throw e; + } + } + + @Override + public ResponseEntity<LoginResponse> tokenRenew(UserDetails user) { + try { + LoginResponse renew = authenticationService.renewToken(user); + + ResponseCookie renewCookie = + ResponseCookie.from(SecurityTextUtil.COOKIE_AUTH_HEADER, renew.getToken()) + .httpOnly(true) + .secure(true) + .path("/") + .build(); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, renewCookie.toString()) + .body(renew); + } catch (UnauthorizedException e) { + logService.logServiceException(LOGGER, Level.WARNING, e, "Authorization error while trying to renew token"); + throw e; + } catch (RemoteException e) { + logService.logServiceException(LOGGER, Level.WARNING, e, "Remote exception while trying to renew token"); + throw e; + } catch (Exception e) { + logService.logException(LOGGER, Level.WARNING, e, "Error while trying to renew token"); + throw new ServiceException(e.getMessage()); + } + } + + @Override + public ResponseEntity<Object> logout(UserDetails user) { + try { + authenticationService.logout(user); + + ResponseCookie deleteSpringCookie = + ResponseCookie.from(SecurityTextUtil.COOKIE_AUTH_HEADER, null) + .path("/") + .httpOnly(true) + .secure(true) + .build(); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, deleteSpringCookie.toString()) + .build(); + } catch (RemoteException e) { + logService.logServiceException(LOGGER, Level.WARNING, e); + throw e; + } catch (ServiceException e) { + logService.logServiceException(LOGGER, Level.WARNING, e); + throw e; + } catch (Exception e) { + logService.logException(LOGGER, Level.WARNING, e, "Error while trying to logout user"); + throw new ServiceException(e.getMessage()); + } + } + +} diff --git a/src/main/java/org/openepics/names/rest/controller/HealthcheckController.java b/src/main/java/org/openepics/names/rest/controller/HealthcheckController.java index 524d885640975b749170e506823318105aaab951..93cd1f64dd3f800b48576258319635ef8810cb80 100644 --- a/src/main/java/org/openepics/names/rest/controller/HealthcheckController.java +++ b/src/main/java/org/openepics/names/rest/controller/HealthcheckController.java @@ -38,7 +38,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; * * @author Lars Johansson */ -@Tag(name = "6. Healthcheck", +@Tag(name = "Healthcheck", description = "perform healthcheck for Naming application") @RestController @RequestMapping("/healthcheck") diff --git a/src/main/java/org/openepics/names/rest/controller/NamesController.java b/src/main/java/org/openepics/names/rest/controller/NamesController.java index 33569e4aaff56c9168b5c045a0c5c15a5ae68ce8..2e0eb347b8293ba1a343ac95aab49662e30f3822 100644 --- a/src/main/java/org/openepics/names/rest/controller/NamesController.java +++ b/src/main/java/org/openepics/names/rest/controller/NamesController.java @@ -36,6 +36,8 @@ import org.openepics.names.rest.beans.response.ResponseBooleanList; import org.openepics.names.rest.beans.response.ResponsePageNameElements; import org.openepics.names.service.LogService; import org.openepics.names.service.NamesService; +import org.openepics.names.service.security.dto.UserDetails; +import org.openepics.names.service.security.util.SecurityUtil; import org.openepics.names.util.NameElementUtil; import org.openepics.names.util.TextUtil; import org.openepics.names.util.ValidateNameElementUtil; @@ -76,7 +78,8 @@ public class NamesController implements INames { } @Override - public ResponseEntity<List<NameElement>> createNames(List<NameElementCommandCreate> nameElementCommands) { + public ResponseEntity<List<NameElement>> createNames(UserDetails user, + List<NameElementCommandCreate> nameElementCommands) { // validate authority - user & admin // convert // validate @@ -85,7 +88,7 @@ public class NamesController implements INames { try { List<NameElementCommand> commands = NameElementUtil.convertCommandCreate2Command(nameElementCommands); namesService.validateNamesCreate(commands); - return new ResponseEntity<>(namesService.createNames(commands, TextUtil.TEST_WHO), Response.getHeaderJson(), HttpStatus.CREATED); + return new ResponseEntity<>(namesService.createNames(commands, SecurityUtil.getUsername(user)), Response.getHeaderJson(), HttpStatus.CREATED); } catch (ServiceException e) { logService.logServiceException(LOGGER, Level.WARNING, e); throw e; @@ -346,7 +349,8 @@ public class NamesController implements INames { // ---------------------------------------------------------------------------------------------------- @Override - public List<NameElement> updateNames(List<NameElementCommandUpdate> nameElementCommands) { + public List<NameElement> updateNames(UserDetails user, + List<NameElementCommandUpdate> nameElementCommands) { // validate authority - user & admin // convert // validate @@ -355,7 +359,7 @@ public class NamesController implements INames { try { List<NameElementCommand> commands = NameElementUtil.convertCommandUpdate2Command(nameElementCommands); namesService.validateNamesUpdate(commands); - return namesService.updateNames(commands, TextUtil.TEST_WHO); + return namesService.updateNames(commands, SecurityUtil.getUsername(user)); } catch (ServiceException e) { logService.logServiceException(LOGGER, Level.WARNING, e); throw e; @@ -368,7 +372,8 @@ public class NamesController implements INames { // ---------------------------------------------------------------------------------------------------- @Override - public ResponseEntity<Response> deleteNames(List<NameElementCommandConfirm> nameElementCommands) { + public ResponseEntity<Response> deleteNames(UserDetails user, + List<NameElementCommandConfirm> nameElementCommands) { // validate authority - user & admin // convert // validate @@ -377,7 +382,7 @@ public class NamesController implements INames { try { List<NameElementCommand> commands = NameElementUtil.convertCommandConfirm2Command(nameElementCommands); namesService.validateNamesDelete(commands); - namesService.deleteNames(commands, TextUtil.TEST_WHO); + namesService.deleteNames(commands, SecurityUtil.getUsername(user)); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (ServiceException e) { logService.logServiceException(LOGGER, Level.WARNING, e); diff --git a/src/main/java/org/openepics/names/rest/controller/ReportController.java b/src/main/java/org/openepics/names/rest/controller/ReportController.java index 292f4da94b7b5819344180490c81ec5f93db20e3..f3cb13134ce51a152418eeedaef73a966030ba47 100644 --- a/src/main/java/org/openepics/names/rest/controller/ReportController.java +++ b/src/main/java/org/openepics/names/rest/controller/ReportController.java @@ -61,7 +61,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; * * @author Lars Johansson */ -@Tag(name = "5. Report", +@Tag(name = "Report", description = "provide reports for Naming application") @RestController @RequestMapping("/report") diff --git a/src/main/java/org/openepics/names/rest/controller/StructuresController.java b/src/main/java/org/openepics/names/rest/controller/StructuresController.java index 5b7fe1697829c76dcecf9318b905b2c630d0aac7..32c51cfc47ac9a1d677217d56a8ab9a3443da82b 100644 --- a/src/main/java/org/openepics/names/rest/controller/StructuresController.java +++ b/src/main/java/org/openepics/names/rest/controller/StructuresController.java @@ -37,6 +37,8 @@ import org.openepics.names.rest.beans.response.ResponseBooleanList; import org.openepics.names.rest.beans.response.ResponsePageStructureElements; import org.openepics.names.service.LogService; import org.openepics.names.service.StructuresService; +import org.openepics.names.service.security.dto.UserDetails; +import org.openepics.names.service.security.util.SecurityUtil; import org.openepics.names.util.StructureElementUtil; import org.openepics.names.util.TextUtil; import org.openepics.names.util.ValidateStructureElementUtil; @@ -77,7 +79,8 @@ public class StructuresController implements IStructures { } @Override - public ResponseEntity<List<StructureElement>> createStructures(List<StructureElementCommandCreate> structureElementCommands) { + public ResponseEntity<List<StructureElement>> createStructures(UserDetails user, + List<StructureElementCommandCreate> structureElementCommands) { // validate authority - user & admin // convert // validate @@ -86,7 +89,7 @@ public class StructuresController implements IStructures { try { List<StructureElementCommand> commands = StructureElementUtil.convertCommandCreate2Command(structureElementCommands); structuresService.validateStructuresCreate(commands); - return new ResponseEntity<>(structuresService.createStructures(commands, TextUtil.TEST_WHO), Response.getHeaderJson(), HttpStatus.CREATED); + return new ResponseEntity<>(structuresService.createStructures(commands, SecurityUtil.getUsername(user)), Response.getHeaderJson(), HttpStatus.CREATED); } catch (ServiceException e) { logService.logServiceException(LOGGER, Level.WARNING, e); throw e; @@ -347,7 +350,8 @@ public class StructuresController implements IStructures { // ---------------------------------------------------------------------------------------------------- @Override - public List<StructureElement> updateStructures(List<StructureElementCommandUpdate> structureElementCommands) { + public List<StructureElement> updateStructures(UserDetails user, + List<StructureElementCommandUpdate> structureElementCommands) { // validate authority - user & admin // convert // validate @@ -356,7 +360,7 @@ public class StructuresController implements IStructures { try { List<StructureElementCommand> commands = StructureElementUtil.convertCommandUpdate2Command(structureElementCommands); structuresService.validateStructuresUpdate(commands); - return structuresService.updateStructures(commands, TextUtil.TEST_WHO); + return structuresService.updateStructures(commands, SecurityUtil.getUsername(user)); } catch (ServiceException e) { logService.logServiceException(LOGGER, Level.WARNING, e); throw e; @@ -369,7 +373,8 @@ public class StructuresController implements IStructures { // ---------------------------------------------------------------------------------------------------- @Override - public ResponseEntity<Response> deleteStructures(List<StructureElementCommandConfirm> structureElementCommands) { + public ResponseEntity<Response> deleteStructures(UserDetails user, + List<StructureElementCommandConfirm> structureElementCommands) { // validate authority - user & admin // convert // validate @@ -378,7 +383,7 @@ public class StructuresController implements IStructures { try { List<StructureElementCommand> commands = StructureElementUtil.convertCommandConfirm2Command(structureElementCommands); structuresService.validateStructuresDelete(commands); - structuresService.deleteStructures(commands, TextUtil.TEST_WHO); + structuresService.deleteStructures(commands, SecurityUtil.getUsername(user)); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (ServiceException e) { logService.logServiceException(LOGGER, Level.WARNING, e); diff --git a/src/main/java/org/openepics/names/rest/filter/JwtRequestFilter.java b/src/main/java/org/openepics/names/rest/filter/JwtRequestFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..9ccc4cd6c4927a7041b7ad0d53bca8ce57e80061 --- /dev/null +++ b/src/main/java/org/openepics/names/rest/filter/JwtRequestFilter.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2021 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.rest.filter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.openepics.names.exception.handler.GlobalControllerExceptionHandler; +import org.openepics.names.rest.beans.response.Response; +import org.openepics.names.service.LogService; +import org.openepics.names.service.security.JwtTokenService; +import org.openepics.names.service.security.JwtUserDetailsService; +import org.openepics.names.service.security.dto.UserDetails; +import org.openepics.names.service.security.util.EncryptUtil; +import org.openepics.names.service.security.util.SecurityTextUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.WebUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.fusionauth.jwt.JWTExpiredException; + +/** + * Purpose of class to intercept rest calls and handle cookies and tokens in uniformed manner. + * + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +@Component +public class JwtRequestFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = Logger.getLogger(JwtRequestFilter.class.getName()); + + private final JwtUserDetailsService jwtUserDetailsService; + private final JwtTokenService jwtTokenService; + private final EncryptUtil encryptUtil; + private final GlobalControllerExceptionHandler globalControllerExceptionHandler; + private final LogService logService; + + @Autowired + public JwtRequestFilter( + JwtUserDetailsService jwtUserDetailsService, + JwtTokenService jwtTokenService, + EncryptUtil encryptUtil, + GlobalControllerExceptionHandler globalControllerExceptionHandler, + LogService logService) { + this.jwtUserDetailsService = jwtUserDetailsService; + this.jwtTokenService = jwtTokenService; + this.encryptUtil = encryptUtil; + this.globalControllerExceptionHandler = globalControllerExceptionHandler; + this.logService = logService; + } + + private static class TokenInfo { + private String userName; + private String rbacToken; + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getRbacToken() { + return rbacToken; + } + + public void setRbacToken(String rbacToken) { + this.rbacToken = rbacToken; + } + } + + private TokenInfo extractTokenInfo(String jwtToken) { + TokenInfo result = new TokenInfo(); + try { + result.setUserName(jwtTokenService.getUsernameFromToken(jwtToken)); + result.setRbacToken(encryptUtil.decrypt(jwtTokenService.getRBACTokenFromToken(jwtToken))); + } catch (JWTExpiredException e) { + // not log as it may be considerable amount of exceptions + // otherwise message: JWT Token has expired + logService.logException(LOGGER, Level.WARNING, e, "JWT Token has expired"); + return null; + } catch (Exception e) { + logService.logException(LOGGER, Level.WARNING, e, "Unable to get info from JWT Token"); + return null; + } + + return result; + } + + @Override + protected void doFilterInternal( + HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, + FilterChain filterChain) + throws ServletException, IOException { + + final String requestTokenHeader = httpServletRequest.getHeader("Authorization"); + + String jwtToken = null; + TokenInfo tokenInfo = null; + try { + // JWT Token is in the form "Bearer token". Remove Bearer word and get + // only the Token + if (requestTokenHeader != null) { + if (requestTokenHeader.startsWith(SecurityTextUtil.AUTHENTICATION_HEADER_PREFIX)) { + jwtToken = requestTokenHeader.substring(SecurityTextUtil.AUTHENTICATION_HEADER_PREFIX.length()); + } else { + jwtToken = requestTokenHeader; + } + + tokenInfo = extractTokenInfo(jwtToken); + } else { + Cookie cookie = WebUtils.getCookie(httpServletRequest, SecurityTextUtil.COOKIE_AUTH_HEADER); + + if (cookie != null) { + String cookieValue = cookie.getValue(); + if (StringUtils.isNotEmpty(cookieValue)) { + jwtToken = cookieValue; + tokenInfo = extractTokenInfo(cookieValue); + } + } + } + + // Once we get the token validate it. + if (tokenInfo != null + && tokenInfo.getRbacToken() != null + && SecurityContextHolder.getContext().getAuthentication() == null) { + + UserDetails userDetails = this.jwtUserDetailsService.loadUserByToken(tokenInfo.getRbacToken()); + + // if token is valid configure Spring Security to manually set + // authentication + if (jwtTokenService.validateToken(jwtToken, userDetails)) { + + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + usernamePasswordAuthenticationToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)); + // After setting the Authentication in the context, we specify + // that the current user is authenticated. So it passes the + // Spring Security Configurations successfully. + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + } + } + } catch (RuntimeException e) { + // MyObject is whatever the output of the below method + ResponseEntity<Response> objectResponseEntity = null; + try { + objectResponseEntity = globalControllerExceptionHandler.handleConflict(e, new ServletWebRequest(httpServletRequest)); + + // set the response object + httpServletResponse.setStatus(objectResponseEntity.getStatusCode().value()); + httpServletResponse.setContentType("application/json"); + + // pass down the actual obj that exception handler normally send + ObjectMapper mapper = new ObjectMapper(); + PrintWriter out = httpServletResponse.getWriter(); + out.print(mapper.writeValueAsString(objectResponseEntity.getBody())); + out.flush(); + + return; + } catch (Exception ex) { + logService.logException(LOGGER, Level.WARNING, ex, "JWT Request filter processing error"); + } + } + filterChain.doFilter(httpServletRequest, httpServletResponse); + } + +} diff --git a/src/main/java/org/openepics/names/service/security/AuthenticationService.java b/src/main/java/org/openepics/names/service/security/AuthenticationService.java new file mode 100644 index 0000000000000000000000000000000000000000..b3e8619e7e6ed29ded626c92a41a22f64da2451f --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/AuthenticationService.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.openepics.names.configuration.SecurityConfiguration; +import org.openepics.names.exception.ServiceException; +import org.openepics.names.exception.security.AuthenticationException; +import org.openepics.names.exception.security.UnauthorizedException; +import org.openepics.names.rest.beans.security.LoginResponse; +import org.openepics.names.service.security.dto.LoginTokenDto; +import org.openepics.names.service.security.dto.UserDetails; +import org.openepics.names.service.security.util.EncryptUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +@Service +public class AuthenticationService { + + private static final Logger LOGGER = Logger.getLogger(AuthenticationService.class.getName()); + + @Value("${jwt.expire.in.minutes}") + private Integer tokenExpiration; + + private final UserService userService; + private final JwtTokenService jwtTokenService; + private final EncryptUtil encryptUtil; + + @Autowired + public AuthenticationService( + UserService userService, JwtTokenService jwtTokenService, EncryptUtil encryptUtil) { + this.userService = userService; + this.jwtTokenService = jwtTokenService; + this.encryptUtil = encryptUtil; + } + + /** + * Checks if user has permission in RBAC, and if has, logs in. Successful login will result in + * creating a JWT for the REST API communication + * + * @param userName The login name for the user + * @param password The password for the user + * @return After successful login the backend will generate a JWT that can be user for the REST + * API communication + * @throws AuthenticationException If user has bad username/password, or doesn't have permission + * to log in + */ + public LoginResponse login(String userName, String password) throws AuthenticationException { + LoginTokenDto tokenDto = userService.loginUser(userName, password); + // checking user roles in RBAC + checkUserRights(tokenDto.getRoles(), userName); + + UserDetails userDetails = new UserDetails(); + userDetails.setUserName(userName); + userDetails.setToken(encryptUtil.encrypt(tokenDto.getToken())); + userDetails.setRoles(tokenDto.getRoles()); + + long tokenExpiresIn = Math.min(tokenExpiration, tokenDto.getExpirationDuration()); + + return new LoginResponse(jwtTokenService.generateToken(userDetails, tokenExpiresIn)); + } + + /** + * Checks if user has roles in RBAC at the beginning of the login process + * + * @param userRoles The roles of the user according to RBAC + * @param userName The name of the user + * @throws UnauthorizedException If user doesn't have permissions in RBAC + */ + private void checkUserRights(List<String> userRoles, String userName) throws UnauthorizedException { + List<String> rolesUserHas = + userRoles.stream() + .filter(SecurityConfiguration.getAllowedRolesToLogin()::contains) + .toList(); + + if (rolesUserHas.isEmpty()) { + LOGGER.log(Level.WARNING, "User ({}) doesn't have permission to log in", userName); + throw new UnauthorizedException("You don't have permission to log in"); + } + } + + public LoginResponse renewToken(UserDetails user) { + LoginTokenDto token = userService.renewToken(user.getToken()); + user.setToken(encryptUtil.encrypt(user.getToken())); + + long tokenExpiresIn = Math.min(tokenExpiration, token.getExpirationDuration()); + + return new LoginResponse(jwtTokenService.generateToken(user, tokenExpiresIn)); + } + + public void logout(UserDetails user) throws ServiceException { + userService.logoutUser(user.getToken()); + } + + public List<String> getUserRoles(UserDetails user) { + UserDetails userInfo = userService.getUserInfoFromToken(user.getToken()); + return userInfo.getRoles(); + } + +} diff --git a/src/main/java/org/openepics/names/service/security/JwtTokenService.java b/src/main/java/org/openepics/names/service/security/JwtTokenService.java new file mode 100644 index 0000000000000000000000000000000000000000..6a04701358c3719634314b14a855a195cbfa1047 --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/JwtTokenService.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security; + +import io.fusionauth.jwt.Signer; +import io.fusionauth.jwt.Verifier; +import io.fusionauth.jwt.domain.JWT; +import io.fusionauth.jwt.hmac.HMACSigner; +import io.fusionauth.jwt.hmac.HMACVerifier; + +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Date; + +import org.openepics.names.service.security.dto.UserDetails; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +@Service +public class JwtTokenService { + + private static final String TOKEN = "token"; + private static final String ROLES = "roles"; + + @Value("${jwt.secret}") + private String secret; + + /** + * Retrieves userName from JWT token. + * + * @param token the JWT token. + * @return the userName from the JWT token. + */ + public String getUsernameFromToken(String token) { + return decodeJWT(token).subject; + } + + public String getRBACTokenFromToken(String token) { + return decodeJWT(token).getAllClaims().get(TOKEN).toString(); + } + + // check if the token has expired + private Boolean isTokenExpired(String token) { + return decodeJWT(token).isExpired(); + } + + /** + * Retrieving a field value from the JWT token. + * + * @param token The JWT token. + * @return The decoded value of the JWT token. + */ + public JWT decodeJWT(String token) { + Verifier verifier = HMACVerifier.newVerifier(secret); + return JWT.getDecoder().decode(token, verifier); + } + + /** + * Generates JWT token from userInformation, and expiration interval. + * + * @param userDetails Information about the user. + * @param tokenExpiration JWT token expiration interval. + * @return The generated JWT token. + */ + // while creating the token - + // 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID + // 2. Sign the JWT using the HS512 algorithm and secret key. + // 3. According to JWS Compact + // Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1) + // compaction of the JWT to a URL-safe string + public String generateToken(UserDetails userDetails, long tokenExpiration) { + Date jwtTokenValidity = new Date(System.currentTimeMillis() + tokenExpiration * 60 * 1000); + Signer signer = HMACSigner.newSHA512Signer(secret); + JWT jwt = + new JWT() + .setIssuer("Naming-tool") + .setIssuedAt(ZonedDateTime.now(ZoneOffset.UTC)) + .setSubject(userDetails.getUserName()) + .addClaim(TOKEN, userDetails.getToken()) + .addClaim(ROLES, userDetails.getRoles()) + .setExpiration(ZonedDateTime.ofInstant(jwtTokenValidity.toInstant(), ZoneId.systemDefault())); + + return JWT.getEncoder().encode(jwt, signer); + } + + // validate token + + /** + * Checks if the JWT token is valid. + * + * @param token The JWT token that has to be checked. + * @param userDetails The users details. + * @return <code>true</code> if the JWT token is valid <code>false</code> if the JWT token is not + * valid + */ + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = getUsernameFromToken(token); + return (username.equals(userDetails.getUserName()) && !isTokenExpired(token)); + } + +} diff --git a/src/main/java/org/openepics/names/service/security/JwtUserDetailsService.java b/src/main/java/org/openepics/names/service/security/JwtUserDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..822e7d885cd168b1fa202ea1b3ab2d057b13863d --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/JwtUserDetailsService.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security; + +import org.openepics.names.service.security.dto.UserDetails; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +@Service +public class JwtUserDetailsService implements UserDetailsService { + + private final UserService userService; + + @Autowired + public JwtUserDetailsService(UserService userService) { + this.userService = userService; + } + + public UserDetails loadUserByToken(String token) { + return userService.getUserInfoFromToken(token); + } + + @Override + public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String s) + throws UsernameNotFoundException { + return null; + } + +} diff --git a/src/main/java/org/openepics/names/service/security/RBACService.java b/src/main/java/org/openepics/names/service/security/RBACService.java new file mode 100644 index 0000000000000000000000000000000000000000..a44877b32cd1336ffdc85533f26d2611576ba32e --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/RBACService.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security; + +import java.util.Base64; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.openepics.names.exception.security.AuthenticationException; +import org.openepics.names.exception.security.EntityNotFoundException; +import org.openepics.names.exception.security.ParseException; +import org.openepics.names.exception.security.RemoteException; +import org.openepics.names.exception.security.RemoteServiceException; +import org.openepics.names.exception.security.UnauthorizedException; +import org.openepics.names.service.LogService; +import org.openepics.names.service.security.rbac.RBACToken; +import org.openepics.names.service.security.rbac.RBACUserInfo; +import org.openepics.names.service.security.rbac.RBACUserRoles; +import org.openepics.names.service.security.util.HttpClientService; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import okhttp3.Headers; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +@Service +public class RBACService { + + private static final Logger LOGGER = Logger.getLogger(RBACService.class.getName()); + + private static final String RBAC_SERVER_BASEURL = "rbac.server.address"; + private static final String AUTH_PATH = "auth"; + private static final String TOKEN_PATH = AUTH_PATH + "/token"; + + private static final String AUTHENTICATION_EXCEPTION = "Authentication Exception"; + + private final Environment env; + private final HttpClientService httpClientService; + private final LogService logService; + + @Autowired + public RBACService(Environment env, HttpClientService httpClientService, LogService logService) { + this.env = env; + this.httpClientService = httpClientService; + this.logService = logService; + } + + /** + * Tries to log in user with specific parameters into RBAC, and if successful then give back + * userInformation, and a token. + * + * @param userName the loginName for the user + * @param password the password for the user + * @return userInformation, and token for the successfully logged in user + * @throws AuthenticationException if authentication fails + */ + public RBACToken loginUser(String userName, String password) throws RemoteException, AuthenticationException { + String secretHeader = Base64.getEncoder().encodeToString((userName + ":" + password).getBytes()); + Headers headers = new Headers.Builder().add("Authorization", "BASIC " + secretHeader).build(); + + // trying to log in + try { + HttpClientService.ServiceResponse<RBACToken> rbacTokenServiceResponse = + httpClientService.executePostRequest( + headers, + env.getProperty(RBAC_SERVER_BASEURL) + TOKEN_PATH, + null, + HttpClientService.XML_MEDIA_TYPE, + RBACToken.class); + + // successful login + if (HttpClientService.isSuccessHttpStatusCode(rbacTokenServiceResponse.getStatusCode())) { + return rbacTokenServiceResponse.getEntity(); + } + } catch (ParseException | RemoteServiceException e) { + logService.logException(LOGGER, Level.WARNING, e, "Login error"); + throw new RemoteException(AUTHENTICATION_EXCEPTION, "Error while trying to log in to RBAC"); + } + + throw new AuthenticationException("Bad username/password"); + } + + /** + * Gathers information about the logged in user. + * + * @param token the token for the logged in user. + * @return information about the logged in user. + * @throws UnauthorizedException if the token is not valid + */ + public RBACToken userInfoFromToken(String token) throws UnauthorizedException, RemoteException { + Headers headers = new Headers.Builder().build(); + + try { + HttpClientService.ServiceResponse<RBACToken> rbacTokenServiceResponse = + httpClientService.executeGetRequest( + headers, + env.getProperty(RBAC_SERVER_BASEURL) + TOKEN_PATH + "/" + token, + HttpClientService.XML_MEDIA_TYPE, + RBACToken.class); + + // token check was successful + if (HttpClientService.isSuccessHttpStatusCode(rbacTokenServiceResponse.getStatusCode())) { + return rbacTokenServiceResponse.getEntity(); + } + } catch (RemoteServiceException | ParseException e) { + throw new RemoteException(AUTHENTICATION_EXCEPTION, "Error while trying to getting token info from RBAC"); + } + + throw new UnauthorizedException("Token error"); + } + + /** + * Can be used to renew a valid client token in RBAC. + * + * @param token the token for the logged in user. + * @return the renewed token, and user information. + * @throws UnauthorizedException if token was not valid for renewing. + */ + public RBACToken renewToken(String token) throws UnauthorizedException, RemoteException { + Headers headers = new Headers.Builder().build(); + + try { + HttpClientService.ServiceResponse<RBACToken> rbacTokenServiceResponse = + httpClientService.executePostRequest( + headers, + env.getProperty(RBAC_SERVER_BASEURL) + TOKEN_PATH + "/" + token + "/renew", + null, + HttpClientService.XML_MEDIA_TYPE, + RBACToken.class); + + // token renewal was successful + if (HttpClientService.isSuccessHttpStatusCode(rbacTokenServiceResponse.getStatusCode())) { + return rbacTokenServiceResponse.getEntity(); + } + } catch (RemoteServiceException | ParseException e) { + throw new RemoteException(AUTHENTICATION_EXCEPTION, "Error while trying to renew token in RBAC"); + } + + throw new UnauthorizedException("Token renewal error"); + } + + /** + * Can be used to log out a user from RBAC (deletes the token). + * + * @param token the token for the logged in user. + * @throws RemoteException if token was not valid, or already deleted + */ + public void deleteToken(String token) throws RemoteException { + Headers headers = new Headers.Builder().build(); + + try { + Integer respCode = + httpClientService.executeDeleteRequest( + headers, env.getProperty(RBAC_SERVER_BASEURL) + TOKEN_PATH + "/" + token); + + // deleting token was successful + if (HttpClientService.isSuccessHttpStatusCode(respCode)) { + return; + } + LOGGER.log(Level.WARNING, "Failed to delete RBAC token, response code: {}", respCode); + throw new RemoteException( + "RBAC error", "Failed to delete RBAC token, response code: " + respCode); + } catch (RemoteServiceException | ParseException e) { + logService.logException(LOGGER, Level.WARNING, e, "Failed to delete RBAC token"); + throw new RemoteException("RBAC error", "Failed to delete RBAC token: " + e.getMessage()); + } + } + + public RBACUserInfo fetchUsersByRole(String role) throws AuthenticationException, RemoteException { + Headers headers = new Headers.Builder().build(); + + try { + HttpClientService.ServiceResponse<RBACUserInfo> rbacTokenServiceResponse = + httpClientService.executeGetRequest( + headers, + env.getProperty(RBAC_SERVER_BASEURL) + AUTH_PATH + "/" + role + "/users", + HttpClientService.XML_MEDIA_TYPE, + RBACUserInfo.class); + + // fetching users was successful + if (HttpClientService.isSuccessHttpStatusCode(rbacTokenServiceResponse.getStatusCode())) { + return rbacTokenServiceResponse.getEntity(); + } + + if (HttpStatus.NOT_FOUND.value() == rbacTokenServiceResponse.getStatusCode()) { + throw new EntityNotFoundException("RBAC role", role); + } + } catch (RemoteServiceException | ParseException e) { + throw new RemoteException(AUTHENTICATION_EXCEPTION, "Error while trying get users from RBAC"); + } + + throw new UnauthorizedException("Getting user info error"); + } + + /** + * Fetching roles from RBAC based on username. + * + * @param username The login name to fetch the user roles. + * @throws RemoteException When error occurs when trying to fetch roles from RBAC + * @return List of roles that is associated to the user in RBAC + */ + public RBACUserRoles fetchRolesByUsername(String username) throws AuthenticationException, RemoteException { + Headers headers = new Headers.Builder().build(); + + try { + HttpClientService.ServiceResponse<RBACUserRoles> rbacTokenServiceResponse = + httpClientService.executeGetRequest( + headers, + env.getProperty(RBAC_SERVER_BASEURL) + AUTH_PATH + "/" + username + "/role", + HttpClientService.XML_MEDIA_TYPE, + RBACUserRoles.class); + + // fetching users was successful + if (HttpClientService.isSuccessHttpStatusCode(rbacTokenServiceResponse.getStatusCode())) { + return rbacTokenServiceResponse.getEntity(); + } + } catch (RemoteServiceException | ParseException e) { + throw new RemoteException(AUTHENTICATION_EXCEPTION, "Error while trying get roles by username from RBAC"); + } + + throw new RemoteException(AUTHENTICATION_EXCEPTION, "Error while trying to fetch user-roles"); + } + +} diff --git a/src/main/java/org/openepics/names/service/security/UserService.java b/src/main/java/org/openepics/names/service/security/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..140f2f113fc2c5d242d0570da1b135279c105906 --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/UserService.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.openepics.names.exception.ServiceException; +import org.openepics.names.exception.security.AuthenticationException; +import org.openepics.names.exception.security.UnauthorizedException; +import org.openepics.names.service.LogService; +import org.openepics.names.service.security.dto.LoginTokenDto; +import org.openepics.names.service.security.dto.UserDetails; +import org.openepics.names.service.security.rbac.RBACToken; +import org.openepics.names.service.security.util.SecurityUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +@Service +public class UserService { + + private static final Logger LOGGER = Logger.getLogger(UserService.class.getName()); + + private final RBACService rbacService; + private final LogService logService; + + @Autowired + public UserService(RBACService rbacService, LogService logService) { + this.rbacService = rbacService; + this.logService = logService; + } + + /** + * Logs in user with username/password in RBAC, and if successful, generates JWT token + * + * @param userName The user's login name + * @param password The user's password + * @return LoginTokenDto with roles that can be user for generating JWT token + * @throws AuthenticationException When user tries to log in with bad username/password + * @throws ServiceException If _other_ error occurs during the authentication process + */ + public LoginTokenDto loginUser(String userName, String password) + throws ServiceException, AuthenticationException { + + try { + RBACToken rbacToken = rbacService.loginUser(userName, password); + + LocalDateTime createTime = LocalDateTime.ofInstant(rbacToken.getCreationTime().toInstant(), ZoneId.systemDefault()); + LocalDateTime expirationTime = LocalDateTime.ofInstant(rbacToken.getExpirationTime().toInstant(), ZoneId.systemDefault()); + + Duration duration = Duration.between(createTime, expirationTime); + long expireInMinutes = Math.abs(duration.toMinutes()); + + return new LoginTokenDto(rbacToken.getId(), expireInMinutes, rbacToken.getRoles()); + } catch (AuthenticationException e) { + logService.logServiceException(LOGGER, Level.WARNING, e, "Error while trying to log in user to RBAC"); + throw e; + } catch (ServiceException e) { + logService.logServiceException(LOGGER, Level.WARNING, e, "Error while trying to log in user to RBAC"); + throw e; + } + } + + /** + * Extracting user-info from RBAC using the token they have + * + * @param token The user's login token + * @return The user-information stored in RBAC + */ + public UserDetails getUserInfoFromToken(String token) { + return SecurityUtil.convertToUserDetails(rbacService.userInfoFromToken(token)); + } + + /** + * Trying to renew user's token in RBAC. + * + * @param token The user's login token. + * @return Information about the renewed token (including the new expiration date) + * @throws ServiceException When unexpected error occurs during the renewal process + * @throws UnauthorizedException When RBAC gives error during the token renewal process + */ + public LoginTokenDto renewToken(String token) throws ServiceException, UnauthorizedException { + RBACToken rbacToken = rbacService.renewToken(token); + + LocalDateTime createTime = LocalDateTime.ofInstant(rbacToken.getCreationTime().toInstant(), ZoneId.systemDefault()); + LocalDateTime expirationTime = LocalDateTime.ofInstant(rbacToken.getExpirationTime().toInstant(), ZoneId.systemDefault()); + + Duration duration = Duration.between(createTime, expirationTime); + long expireInMinutes = Math.abs(duration.toMinutes()); + + return new LoginTokenDto(rbacToken.getId(), expireInMinutes, rbacToken.getRoles()); + } + + /** + * Trying to log out user from RBAC. This will result in deleting token from the system. + * + * @param token The user's token that has to be deleted from RBAC + * @throws ServiceException When error occurs during the token logout process + */ + public void logoutUser(String token) throws ServiceException { + rbacService.deleteToken(token); + } + +} diff --git a/src/main/java/org/openepics/names/service/security/dto/LoginTokenDto.java b/src/main/java/org/openepics/names/service/security/dto/LoginTokenDto.java new file mode 100644 index 0000000000000000000000000000000000000000..5a64ece4472d4e1bd382fc8f5474b57ce18a361c --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/dto/LoginTokenDto.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security.dto; + +import java.util.List; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +public class LoginTokenDto { + + private String token; + private long expirationDuration; + private List<String> roles; + + public LoginTokenDto(String token, long expirationDuration, List<String> roles) { + this.token = token; + this.expirationDuration = expirationDuration; + this.roles = roles; + } + + public String getToken() { + return token; + } + + public long getExpirationDuration() { + return expirationDuration; + } + + public List<String> getRoles() { + return roles; + } + +} diff --git a/src/main/java/org/openepics/names/service/security/dto/RoleAuthority.java b/src/main/java/org/openepics/names/service/security/dto/RoleAuthority.java new file mode 100644 index 0000000000000000000000000000000000000000..cbc4a2f0b4ca74090e690c2044050ff856cd9317 --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/dto/RoleAuthority.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security.dto; + +import org.springframework.security.core.GrantedAuthority; + +/** + * @author <a href="mailto:zoltan.runyo@ess.eu">Zoltan Runyo</a> + */ +public class RoleAuthority implements GrantedAuthority { + + private static final long serialVersionUID = -47836193761080412L; + + private String role; + + public RoleAuthority(String role) { + this.role = role; + } + + @Override + public String getAuthority() { + return role; + } + +} diff --git a/src/main/java/org/openepics/names/service/security/dto/UserDetails.java b/src/main/java/org/openepics/names/service/security/dto/UserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..6c26717e459cf5bbf420f94bed3b855ad278e391 --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/dto/UserDetails.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security.dto; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + + +import org.springframework.security.core.GrantedAuthority; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +public class UserDetails { + + private String userName; + private String fullName; + private String token; + private Collection<RoleAuthority> authorities; + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public Collection<? extends GrantedAuthority> getAuthorities() { + return authorities; + } + + public List<String> getRoles() { + return authorities != null + ? authorities.stream().map(RoleAuthority::getAuthority).collect(Collectors.toList()) + : Collections.emptyList(); + } + + public void setRoles(List<String> roles) { + this.authorities = + roles != null + ? roles.stream().map(RoleAuthority::new).collect(Collectors.toList()) + : Collections.emptyList(); + } + +} diff --git a/src/main/java/org/openepics/names/service/security/rbac/RBACToken.java b/src/main/java/org/openepics/names/service/security/rbac/RBACToken.java new file mode 100644 index 0000000000000000000000000000000000000000..2c246a3e8928f4f4161a23899f78209f4f3e46e2 --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/rbac/RBACToken.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security.rbac; + +import java.util.Date; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class RBACToken { + + private String id; + + @JsonProperty("username") + private String userName; + private String firstName; + private String lastName; + private Date creationTime; + private Date expirationTime; + private String ip; + private List<String> roles; + private String signature; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public Date getCreationTime() { + return RBACToken.cloneDate(creationTime); + } + + public void setCreationTime(Date creationTime) { + this.creationTime = RBACToken.cloneDate(creationTime); + } + + public Date getExpirationTime() { + return RBACToken.cloneDate(expirationTime); + } + + public void setExpirationTime(Date expirationTime) { + this.expirationTime = RBACToken.cloneDate(expirationTime); + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public List<String> getRoles() { + return roles; + } + + public void setRoles(List<String> roles) { + this.roles = roles; + } + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + /** + * Clones a Date to make object immutable. + * + * @param dateToClone the date that has to be cloned. + * @return <code>null</code>, if date was null <code>cloned date</code>, if date was not null + */ + public static Date cloneDate(Date dateToClone) { + return dateToClone == null ? null : new Date(dateToClone.getTime()); + } + +} diff --git a/src/main/java/org/openepics/names/service/security/rbac/RBACUserInfo.java b/src/main/java/org/openepics/names/service/security/rbac/RBACUserInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..553642552e417b4e0525896b62d466886648df07 --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/rbac/RBACUserInfo.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security.rbac; + +import java.util.List; + +/** + * @author <a href="mailto:zoltan.runyo@ess.eu">Zoltan Runyo</a> + */ +public class RBACUserInfo { + + private String role; + private List<String> users; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public List<String> getUsers() { + return users; + } + + public void setUsers(List<String> users) { + this.users = users; + } + +} diff --git a/src/main/java/org/openepics/names/service/security/rbac/RBACUserRoles.java b/src/main/java/org/openepics/names/service/security/rbac/RBACUserRoles.java new file mode 100644 index 0000000000000000000000000000000000000000..f4fe1b8c7432ae7b5a0eab2c89b66716327bcfc9 --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/rbac/RBACUserRoles.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security.rbac; + +import java.util.List; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +public class RBACUserRoles { + + private String username; + private List<String> roles; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public List<String> getRoles() { + return roles; + } + + public void setRoles(List<String> roles) { + this.roles = roles; + } + +} diff --git a/src/main/java/org/openepics/names/service/security/util/EncryptUtil.java b/src/main/java/org/openepics/names/service/security/util/EncryptUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..9520e91558163ef81c2eb6d975b4a2dcf15e9361 --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/util/EncryptUtil.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Base64; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Encryption utility. + * + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +@Component +public class EncryptUtil { + + private static final Logger LOGGER = Logger.getLogger(EncryptUtil.class.getName()); + + @Value("${aes.key}") + private String secret; + + private static SecretKeySpec secretKey; + private static final String ALGORITHM = "AES"; + + private void prepareSecretKey(String myKey) { + MessageDigest sha = null; + try { + byte[] key = myKey.getBytes(StandardCharsets.UTF_8); + sha = MessageDigest.getInstance("SHA-1"); + key = sha.digest(key); + key = Arrays.copyOf(key, 16); + secretKey = new SecretKeySpec(key, ALGORITHM); + } catch (NoSuchAlgorithmException e) { + LOGGER.log(Level.WARNING, SecurityTextUtil.ALGORITHM_NOT_FOUND, e); + } + } + + /** + * String encryption wrapper. + * + * @param strToEncrypt string to encrypt + * @return encrypted string + */ + public String encrypt(String strToEncrypt) { + try { + prepareSecretKey(secret); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + return Base64.getEncoder() + .encodeToString(cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8))); + } catch (Exception e) { + LOGGER.log(Level.WARNING, SecurityTextUtil.ERROR_WHILE_ENCRYPTING, e); + } + return null; + } + + /** + * String decryption wrapper. + * + * @param strToDecrypt string to decrypt + * @return decrypted string + */ + public String decrypt(String strToDecrypt) { + try { + prepareSecretKey(secret); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt))); + } catch (Exception e) { + LOGGER.log(Level.WARNING, SecurityTextUtil.ERROR_WHILE_DECRYPTING, e); + } + return null; + } + +} diff --git a/src/main/java/org/openepics/names/service/security/util/HttpClientService.java b/src/main/java/org/openepics/names/service/security/util/HttpClientService.java new file mode 100644 index 0000000000000000000000000000000000000000..b93f95cf29c00295a5f6ce0509b6fd4fdbba09f7 --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/util/HttpClientService.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security.util; + +import java.io.IOException; +import java.lang.reflect.Type; + +import org.openepics.names.exception.security.ParseException; +import org.openepics.names.exception.security.RemoteServiceException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * @author <a href="mailto:zoltan.runyo@ess.eu">Zoltan Runyo</a> + */ +@Service +public class HttpClientService { + + public static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8"); + public static final MediaType XML_MEDIA_TYPE = MediaType.get("application/xml; charset=utf-8"); + + private static final String UNABLE_TO_CALL_SERVICE = "Unable to call service"; + + private final OkHttpClient okHttpClient; + + @Autowired + public HttpClientService(OkHttpClient okHttpClient) { + this.okHttpClient = okHttpClient; + } + + public static class ServiceResponse<T> { + private final T entity; + private final int statusCode; + private final String errorMessage; + private final Headers headers; + + public ServiceResponse(T entity, int statusCode, Headers headers) { + this.entity = entity; + this.statusCode = statusCode; + this.errorMessage = null; + this.headers = headers; + } + + public ServiceResponse(int statusCode, String errorMessage, Headers headers) { + this.entity = null; + this.statusCode = statusCode; + this.errorMessage = errorMessage; + this.headers = headers; + } + + public T getEntity() { + return entity; + } + + public int getStatusCode() { + return statusCode; + } + + public String getErrorMessage() { + return errorMessage; + } + + public Headers getHeaders() { + return headers; + } + } + + public <T> ServiceResponse<T> executeGetRequest( + Headers headers, String url, Class<T> responseClass) throws RemoteServiceException { + return executeGetRequest(headers, url, responseClass, null); + } + + public <T> ServiceResponse<T> executeGetRequest( + Headers headers, String url, Class<T> responseClass, String formatDate) + throws RemoteServiceException { + Request request; + request = new Request.Builder().headers(headers).url(url).build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + if (isSuccessHttpStatusCode(response.code())) { + Gson gson = new Gson(); + if (formatDate != null) { + gson = new GsonBuilder().setDateFormat(formatDate).create(); + } + return new ServiceResponse<>( + gson.fromJson(response.body().string(), responseClass), + response.code(), + response.headers()); + } + return new ServiceResponse<>(null, response.code(), response.headers()); + } catch (IOException e) { + throw new RemoteServiceException(UNABLE_TO_CALL_SERVICE, e); + } + } + + public <T> ServiceResponse<T> executeGetRequest( + Headers headers, String url, Type type, String formatDate) throws RemoteServiceException { + Request request; + request = new Request.Builder().headers(headers).url(url).build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + if (isSuccessHttpStatusCode(response.code())) { + return new ServiceResponse<>( + new GsonBuilder() + .setDateFormat(formatDate) + .create() + .fromJson(response.body().string(), type), + response.code(), + response.headers()); + } + return new ServiceResponse<>(null, response.code(), response.headers()); + } catch (IOException e) { + throw new RemoteServiceException(UNABLE_TO_CALL_SERVICE, e); + } + } + + public <T> ServiceResponse<T> executeGetRequest(Headers headers, String url, Type type) + throws RemoteServiceException { + Request request; + request = new Request.Builder().headers(headers).url(url).build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + if (isSuccessHttpStatusCode(response.code())) { + return new ServiceResponse<>( + new GsonBuilder().create().fromJson(response.body().string(), type), + response.code(), + response.headers()); + } + return new ServiceResponse<>(null, response.code(), response.headers()); + } catch (IOException e) { + throw new RemoteServiceException(UNABLE_TO_CALL_SERVICE, e); + } + } + + public <T> ServiceResponse<T> executeGetRequest( + Headers headers, String url, MediaType mediaType, Class<T> responseClass) + throws RemoteServiceException, ParseException { + + if (JSON_MEDIA_TYPE.equals(mediaType)) { + return executeGetRequest(headers, url, responseClass); + } + + Request request; + + request = new Request.Builder().headers(headers).url(url).build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + if (isSuccessHttpStatusCode(response.code())) { + return new ServiceResponse<>( + new XmlMapper().readValue(response.body().string(), responseClass), + response.code(), + response.headers()); + } + return new ServiceResponse<>(null, response.code(), response.headers()); + } catch (IOException e) { + throw new RemoteServiceException("Unable to call service with GET method", e); + } + } + + public ServiceResponse<String> executePlainGetRequest(Headers headers, String url) + throws RemoteServiceException { + Request request; + request = new Request.Builder().headers(headers).url(url).build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + if (isSuccessHttpStatusCode(response.code())) { + return new ServiceResponse<>(response.body().string(), response.code(), response.headers()); + } + return new ServiceResponse<>(null, response.code(), response.headers()); + } catch (IOException e) { + throw new RemoteServiceException(UNABLE_TO_CALL_SERVICE, e); + } + } + + public <T> ServiceResponse<T> executePostRequest( + Headers headers, String url, Object requestBody, Class<T> responseClass) + throws RemoteServiceException { + RequestBody body = RequestBody.create(new Gson().toJson(requestBody), JSON_MEDIA_TYPE); + Request request; + request = new Request.Builder().headers(headers).url(url).post(body).build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + if (isSuccessHttpStatusCode(response.code())) { + return new ServiceResponse<>( + new Gson().fromJson(response.body().string(), responseClass), + response.code(), + response.headers()); + } + return new ServiceResponse<>( + response.code(), + response.body() != null ? response.body().string() : null, + response.headers()); + } catch (IOException e) { + throw new RemoteServiceException(UNABLE_TO_CALL_SERVICE, e); + } + } + + public ServiceResponse<String> executePlainPostRequest( + Headers headers, String url, Object requestBody) throws RemoteServiceException { + RequestBody body = RequestBody.create(new Gson().toJson(requestBody), JSON_MEDIA_TYPE); + Request request; + request = new Request.Builder().headers(headers).url(url).post(body).build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + if (isSuccessHttpStatusCode(response.code())) { + return new ServiceResponse<>(response.body().string(), response.code(), response.headers()); + } + return new ServiceResponse<>(null, response.code(), response.headers()); + } catch (IOException e) { + throw new RemoteServiceException(UNABLE_TO_CALL_SERVICE, e); + } + } + + public <T> ServiceResponse<T> executePostRequest( + Headers headers, String url, Object requestBody, MediaType mediaType, Class<T> responseClass) + throws RemoteServiceException, ParseException { + if (JSON_MEDIA_TYPE.equals(mediaType)) { + return executePostRequest(headers, url, requestBody, responseClass); + } + + Request request; + + try { + RequestBody body = + RequestBody.create(new XmlMapper().writeValueAsString(requestBody), XML_MEDIA_TYPE); + + request = new Request.Builder().headers(headers).url(url).post(body).build(); + } catch (JsonProcessingException e) { + throw new ParseException("Unable to parse object"); + } + + try (Response response = okHttpClient.newCall(request).execute()) { + if (isSuccessHttpStatusCode(response.code())) { + return new ServiceResponse<>( + new XmlMapper().readValue(response.body().string(), responseClass), + response.code(), + response.headers()); + } + return new ServiceResponse<>(null, response.code(), response.headers()); + } catch (IOException e) { + throw new RemoteServiceException("Unable to call service with POST method", e); + } + } + + public Integer executeDeleteRequest(Headers headers, String url) + throws RemoteServiceException, ParseException { + + Request request; + + request = new Request.Builder().headers(headers).url(url).delete().build(); + + try (Response response = okHttpClient.newCall(request).execute()) { + return response.code(); + } catch (IOException e) { + throw new RemoteServiceException("Unable to call service with POST method", e); + } + } + + public static boolean isSuccessHttpStatusCode(int code) { + return code >= 200 && code < 300; + } + +} diff --git a/src/main/java/org/openepics/names/service/security/util/OkHttpConfiguration.java b/src/main/java/org/openepics/names/service/security/util/OkHttpConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..d9fbdf5b2763350a95ebe95012175c3d619fb6f5 --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/util/OkHttpConfiguration.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security.util; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.commons.lang3.BooleanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; + +/** + * @author <a href="mailto:imre.toth@ess.eu">Imre Toth</a> + */ +@Configuration +public class OkHttpConfiguration { + + public static final String ALLOW_UNTRUSTED_CERTS = "allow.untrusted.certs"; + + private final Environment env; + + @Autowired + public OkHttpConfiguration(Environment env) { + this.env = env; + } + + @Bean + public OkHttpClient okHttpClient() { + + OkHttpClient.Builder clientBuilder = + new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .connectionPool(pool()); + + boolean allowUntrusted = BooleanUtils.toBoolean(env.getProperty(ALLOW_UNTRUSTED_CERTS)); + + if (allowUntrusted) { + final X509TrustManager trustManager = + new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType) {} + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType) {} + }; + + SSLContext sslContext = null; + try { + sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, new TrustManager[] {trustManager}, new java.security.SecureRandom()); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + return null; + } + + clientBuilder.sslSocketFactory(sslContext.getSocketFactory(), trustManager); + + HostnameVerifier hostnameVerifier = (hostname, session) -> true; + clientBuilder.hostnameVerifier(hostnameVerifier); + } + + return clientBuilder.build(); + } + + @Bean + public ConnectionPool pool() { + return new ConnectionPool(40, 10, TimeUnit.SECONDS); + } + +} diff --git a/src/main/java/org/openepics/names/service/security/util/SecurityTextUtil.java b/src/main/java/org/openepics/names/service/security/util/SecurityTextUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..8a389269674f97b08549744da9811ded35e2ab8f --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/util/SecurityTextUtil.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.openepics.names.service.security.util; + +/** + * Utility class to assist in handling of text, in particular for security related matters. + * + * @author Lars Johansson + */ +public class SecurityTextUtil { + + // authentication & authorization + + public static final String AUTHENTICATION_HEADER_PREFIX = "Bearer "; + public static final String COOKIE_AUTH_HEADER = "naming-auth"; + + // encryption + + public static final String ALGORITHM_NOT_FOUND = "Algorithm not found"; + public static final String ERROR_WHILE_DECRYPTING = "Error while decrypting"; + public static final String ERROR_WHILE_ENCRYPTING = "Error while encrypting"; + + /** + * This class is not to be instantiated. + */ + private SecurityTextUtil() { + throw new IllegalStateException("Utility class"); + } + +} diff --git a/src/main/java/org/openepics/names/service/security/util/SecurityUtil.java b/src/main/java/org/openepics/names/service/security/util/SecurityUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..cfca9311278b4a50ded3afcd496a4b4b4f4a955b --- /dev/null +++ b/src/main/java/org/openepics/names/service/security/util/SecurityUtil.java @@ -0,0 +1,50 @@ +package org.openepics.names.service.security.util; + +import org.openepics.names.service.security.dto.UserDetails; +import org.openepics.names.service.security.rbac.RBACToken; + +/** + * @author <a href="mailto:zoltan.runyo@ess.eu">Zoltan Runyo</a> + * @author Lars Johansson + */ +public class SecurityUtil { + + /** + * This class is not to be instantiated. + */ + private SecurityUtil() { + throw new IllegalStateException("Utility class"); + } + + /** + * Converts RBAC token to user details DTO + * + * @param rbacInfo RBAC token descriptor + * @return User details DTO + */ + public static UserDetails convertToUserDetails(RBACToken rbacInfo) { + UserDetails result = null; + + if (rbacInfo != null) { + result = new UserDetails(); + + result.setUserName(rbacInfo.getUserName()); + result.setFullName(rbacInfo.getFirstName() + " " + rbacInfo.getLastName()); + result.setToken(rbacInfo.getId()); + result.setRoles(rbacInfo.getRoles()); + } + + return result; + } + + /** + * Return username for a user. + * + * @param userDetails user details + * @return username + */ + public static String getUsername(UserDetails userDetails) { + return userDetails != null ? userDetails.getUserName() : null; + } + +} diff --git a/src/main/java/org/openepics/names/util/TextUtil.java b/src/main/java/org/openepics/names/util/TextUtil.java index b47222d11f7dd6417713a40006f8e10a71761601..b5a972c7b0efa04d98bff8b9c010f279e5102e4c 100644 --- a/src/main/java/org/openepics/names/util/TextUtil.java +++ b/src/main/java/org/openepics/names/util/TextUtil.java @@ -132,9 +132,6 @@ public class TextUtil { public static final String ATTACHMENT_FILENAME_NAME_ELEMENT_XLSX = "attachment; filename=NameElement.xlsx"; public static final String ATTACHMENT_FILENAME_STRUCTURE_ELEMENT_XLSX = "attachment; filename=StructureElement.xlsx"; - // test - public static final String TEST_WHO = "test who"; - // log public static final String DESCRIPTION_NUMBER_ELEMENTS = "{0}, # elements: {1}"; public static final String DESCRIPTION_NUMBER_ELEMENTS_IN_OUT = "{0}, # elements (in): {1}, # elements (out): {2}"; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7655d823cc097f1c3522300c22f53f23f814f69f..17f3c225302b1d861aee3a62fab86a6a8bd155f1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,6 +17,18 @@ logging.level.org.springframework.web=INFO logging.level.org.hibernate.SQL=INFO spring.http.log-request-details=true +# security +# enable +# disabled by default - allow run of application without association to particular way of authentication & authorization +# encryption +# token +# rbac +naming.security.enabled=${NAMING_SECURITY_ENABLED:false} +aes.key=${AES_KEY:aes_secret_key} +jwt.secret=${JWT_SECRET:secret_password_to_hash_token} +jwt.expire.in.minutes=${JWT_EXP_MIN:240} +rbac.server.address=${RBAC_SERVER_ADDRESS} + # mail, notification # administrator mail - comma-separated list of email addresses # notification mail - comma-separated list of email addresses @@ -75,6 +87,8 @@ openapi.info.title=Naming REST API # api # swagger # url used for notification +springdoc.api-docs.path=${API_DOCS_PATH:/api-docs} +springdoc.swagger-ui.path=${SWAGGER_UI_PATH:/swagger-ui.html} springdoc.swagger-ui.tagsSorter=alpha springdoc.swagger-ui.operationsSorter=method naming.swagger.url=${NAMING_SWAGGER_URL:http://localhost:8080/swagger-ui.html} diff --git a/src/test/java/org/openepics/names/docker/NamesIT.java b/src/test/java/org/openepics/names/docker/NamesIT.java index 049ad1f2ab9c30260bf2145305601e1fdcb0905a..b2b142d1d97f5bf96624bd4bfb0e057201bf3a76 100644 --- a/src/test/java/org/openepics/names/docker/NamesIT.java +++ b/src/test/java/org/openepics/names/docker/NamesIT.java @@ -678,16 +678,8 @@ class NamesIT { ITUtilNames.assertRead("?description=updated description%", 1, -1); ITUtilNames.assertRead("?description=updated description again", 1); - ITUtilNames.assertRead("?who=test who", 13, -1); + ITUtilNames.assertRead("?who=", 13, -1); ITUtilNames.assertRead("?who=test", 0); - ITUtilNames.assertRead("?who=who", 0); - ITUtilNames.assertRead("?who=test%", 13, -1); - ITUtilNames.assertRead("?who=%who", 13, -1); - ITUtilNames.assertRead("?who=%est%", 13, -1); - ITUtilNames.assertRead("?who=%wh%", 13, -1); - ITUtilNames.assertRead("?who=wh%", 0); - ITUtilNames.assertRead("?who=asdf", 0); - ITUtilNames.assertRead("?who=%asdf%", 0); ITUtilNames.assertRead("?deviceStructure=EMR-FS&index=003", 1); diff --git a/src/test/java/org/openepics/names/docker/StructuresDeviceGroupIT.java b/src/test/java/org/openepics/names/docker/StructuresDeviceGroupIT.java index 6286303a3e49ba77f5e52e34dbc9162caa0d50f4..64f88b73e26deb95fe0bf93a8055f40bb7d10250 100644 --- a/src/test/java/org/openepics/names/docker/StructuresDeviceGroupIT.java +++ b/src/test/java/org/openepics/names/docker/StructuresDeviceGroupIT.java @@ -864,16 +864,8 @@ class StructuresDeviceGroupIT { ITUtilStructures.assertRead("?type=DEVICEGROUP&description=%sc%", 30, -1); ITUtilStructures.assertRead("?type=DEVICEGROUP&description=description", 10, -1); - ITUtilStructures.assertRead("?type=DEVICEGROUP&who=test who", 30, -1); + ITUtilStructures.assertRead("?type=DEVICEGROUP&who=", 30, -1); ITUtilStructures.assertRead("?type=DEVICEGROUP&who=test", 0); - ITUtilStructures.assertRead("?type=DEVICEGROUP&who=who", 0); - ITUtilStructures.assertRead("?type=DEVICEGROUP&who=test%", 30, -1); - ITUtilStructures.assertRead("?type=DEVICEGROUP&who=%who", 30, -1); - ITUtilStructures.assertRead("?type=DEVICEGROUP&who=%est%", 30, -1); - ITUtilStructures.assertRead("?type=DEVICEGROUP&who=%wh%", 30, -1); - ITUtilStructures.assertRead("?type=DEVICEGROUP&who=wh%", 0); - ITUtilStructures.assertRead("?type=DEVICEGROUP&who=asdf", 0); - ITUtilStructures.assertRead("?type=DEVICEGROUP&who=%asdf%", 0); // order by // avoid diff --git a/src/test/java/org/openepics/names/docker/StructuresDeviceTypeIT.java b/src/test/java/org/openepics/names/docker/StructuresDeviceTypeIT.java index a512138db27f4d3de7ce105ddb6c466158ff7baf..279460882ed465361d3dcedcbe94a48a6cc36e06 100644 --- a/src/test/java/org/openepics/names/docker/StructuresDeviceTypeIT.java +++ b/src/test/java/org/openepics/names/docker/StructuresDeviceTypeIT.java @@ -872,16 +872,8 @@ class StructuresDeviceTypeIT { ITUtilStructures.assertRead("?type=DEVICETYPE&description=%sc%", 30, -1); ITUtilStructures.assertRead("?type=DEVICETYPE&description=description", 10, -1); - ITUtilStructures.assertRead("?type=DEVICETYPE&who=test who", 30, -1); + ITUtilStructures.assertRead("?type=DEVICETYPE&who=", 30, -1); ITUtilStructures.assertRead("?type=DEVICETYPE&who=test", 0); - ITUtilStructures.assertRead("?type=DEVICETYPE&who=who", 0); - ITUtilStructures.assertRead("?type=DEVICETYPE&who=test%", 30, -1); - ITUtilStructures.assertRead("?type=DEVICETYPE&who=%who", 30, -1); - ITUtilStructures.assertRead("?type=DEVICETYPE&who=%est%", 30, -1); - ITUtilStructures.assertRead("?type=DEVICETYPE&who=%wh%", 30, -1); - ITUtilStructures.assertRead("?type=DEVICETYPE&who=wh%", 0); - ITUtilStructures.assertRead("?type=DEVICETYPE&who=asdf", 0); - ITUtilStructures.assertRead("?type=DEVICETYPE&who=%asdf%", 0); // order by // avoid diff --git a/src/test/java/org/openepics/names/docker/StructuresDisciplineIT.java b/src/test/java/org/openepics/names/docker/StructuresDisciplineIT.java index 510df00a0860f20b058cbddff05a6f5dd7eed62c..1d36c322a47563464710779dc930fd0789cffb77 100644 --- a/src/test/java/org/openepics/names/docker/StructuresDisciplineIT.java +++ b/src/test/java/org/openepics/names/docker/StructuresDisciplineIT.java @@ -766,16 +766,8 @@ class StructuresDisciplineIT { ITUtilStructures.assertRead("?type=DISCIPLINE&description=%sc%", 30, -1); ITUtilStructures.assertRead("?type=DISCIPLINE&description=description", 10, -1); - ITUtilStructures.assertRead("?type=DISCIPLINE&who=test who", 30, -1); + ITUtilStructures.assertRead("?type=DISCIPLINE&who=", 30, -1); ITUtilStructures.assertRead("?type=DISCIPLINE&who=test", 0); - ITUtilStructures.assertRead("?type=DISCIPLINE&who=who", 0); - ITUtilStructures.assertRead("?type=DISCIPLINE&who=test%", 30, -1); - ITUtilStructures.assertRead("?type=DISCIPLINE&who=%who", 30, -1); - ITUtilStructures.assertRead("?type=DISCIPLINE&who=%est%", 30, -1); - ITUtilStructures.assertRead("?type=DISCIPLINE&who=%wh%", 30, -1); - ITUtilStructures.assertRead("?type=DISCIPLINE&who=wh%", 0); - ITUtilStructures.assertRead("?type=DISCIPLINE&who=asdf", 0); - ITUtilStructures.assertRead("?type=DISCIPLINE&who=%asdf%", 0); // order by // avoid diff --git a/src/test/java/org/openepics/names/docker/StructuresSubsystemIT.java b/src/test/java/org/openepics/names/docker/StructuresSubsystemIT.java index eb3c04b4a20b8767d8b471b4d11c960be29681bb..cef336b790f3b87c11f445bcbe0ab89e53ffd5a5 100644 --- a/src/test/java/org/openepics/names/docker/StructuresSubsystemIT.java +++ b/src/test/java/org/openepics/names/docker/StructuresSubsystemIT.java @@ -908,16 +908,8 @@ class StructuresSubsystemIT { ITUtilStructures.assertRead("?type=SUBSYSTEM&description=%sc%", 30, -1); ITUtilStructures.assertRead("?type=SUBSYSTEM&description=description", 10, -1); - ITUtilStructures.assertRead("?type=SUBSYSTEM&who=test who", 30, -1); + ITUtilStructures.assertRead("?type=SUBSYSTEM&who=", 30, -1); ITUtilStructures.assertRead("?type=SUBSYSTEM&who=test", 0); - ITUtilStructures.assertRead("?type=SUBSYSTEM&who=who", 0); - ITUtilStructures.assertRead("?type=SUBSYSTEM&who=test%", 30, -1); - ITUtilStructures.assertRead("?type=SUBSYSTEM&who=%who", 30, -1); - ITUtilStructures.assertRead("?type=SUBSYSTEM&who=%est%", 30, -1); - ITUtilStructures.assertRead("?type=SUBSYSTEM&who=%wh%", 30, -1); - ITUtilStructures.assertRead("?type=SUBSYSTEM&who=wh%", 0); - ITUtilStructures.assertRead("?type=SUBSYSTEM&who=asdf", 0); - ITUtilStructures.assertRead("?type=SUBSYSTEM&who=%asdf%", 0); // order by // avoid diff --git a/src/test/java/org/openepics/names/docker/StructuresSystemGroupIT.java b/src/test/java/org/openepics/names/docker/StructuresSystemGroupIT.java index e284a557275a2c9989053baefdbfdb63e87feda6..b4b05e973ebe4a7f3f8b676cb33cf364ef816add 100644 --- a/src/test/java/org/openepics/names/docker/StructuresSystemGroupIT.java +++ b/src/test/java/org/openepics/names/docker/StructuresSystemGroupIT.java @@ -813,16 +813,8 @@ class StructuresSystemGroupIT { ITUtilStructures.assertRead("?type=SYSTEMGROUP&description=%sc%", 30, -1); ITUtilStructures.assertRead("?type=SYSTEMGROUP&description=description", 10, -1); - ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=test who", 30, -1); + ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=", 30, -1); ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=test", 0); - ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=who", 0); - ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=test%", 30, -1); - ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=%who", 30, -1); - ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=%est%", 30, -1); - ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=%wh%", 30, -1); - ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=wh%", 0); - ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=asdf", 0); - ITUtilStructures.assertRead("?type=SYSTEMGROUP&who=%asdf%", 0); // order by // avoid diff --git a/src/test/java/org/openepics/names/docker/StructuresSystemIT.java b/src/test/java/org/openepics/names/docker/StructuresSystemIT.java index e307784abc6da007861b60526d3024704b0b1cb4..9afadef27c0cfd0bb16ed6d2bd1427d8d8514ace 100644 --- a/src/test/java/org/openepics/names/docker/StructuresSystemIT.java +++ b/src/test/java/org/openepics/names/docker/StructuresSystemIT.java @@ -852,16 +852,8 @@ class StructuresSystemIT { ITUtilStructures.assertRead("?type=SYSTEM&description=%sc%", 30, -1); ITUtilStructures.assertRead("?type=SYSTEM&description=description", 10, -1); - ITUtilStructures.assertRead("?type=SYSTEM&who=test who", 30, -1); + ITUtilStructures.assertRead("?type=SYSTEM&who=", 30, -1); ITUtilStructures.assertRead("?type=SYSTEM&who=test", 0); - ITUtilStructures.assertRead("?type=SYSTEM&who=who", 0); - ITUtilStructures.assertRead("?type=SYSTEM&who=test%", 30, -1); - ITUtilStructures.assertRead("?type=SYSTEM&who=%who", 30, -1); - ITUtilStructures.assertRead("?type=SYSTEM&who=%est%", 30, -1); - ITUtilStructures.assertRead("?type=SYSTEM&who=%wh%", 30, -1); - ITUtilStructures.assertRead("?type=SYSTEM&who=wh%", 0); - ITUtilStructures.assertRead("?type=SYSTEM&who=asdf", 0); - ITUtilStructures.assertRead("?type=SYSTEM&who=%asdf%", 0); // order by // avoid diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index fc49f4c13c7a21160da0ed3b37bbbb9a41703334..4f1fdda9658f60e7c1c1d273b32a438ed590ff70 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -17,6 +17,18 @@ logging.level.org.springframework.web=INFO logging.level.org.hibernate.SQL=INFO spring.http.log-request-details=true +# security +# enable +# disabled by default - allow run of application without association to particular way of authentication & authorization +# encryption +# token +# rbac +naming.security.enabled=${NAMING_SECURITY_ENABLED:false} +aes.key=${AES_KEY:aes_secret_key} +jwt.secret=${JWT_SECRET:secret_password_to_hash_token} +jwt.expire.in.minutes=${JWT_EXP_MIN:240} +rbac.server.address=${RBAC_SERVER_ADDRESS} + # mail, notification # administrator mail - comma-separated list of email addresses # notification mail - comma-separated list of email addresses @@ -76,6 +88,8 @@ openapi.info.title=Naming REST API # api # swagger # url used for notification +springdoc.api-docs.path=${API_DOCS_PATH:/api-docs} +springdoc.swagger-ui.path=${SWAGGER_UI_PATH:/swagger-ui.html} springdoc.swagger-ui.tagsSorter=alpha springdoc.swagger-ui.operationsSorter=method naming.swagger.url=${NAMING_SWAGGER_URL:http://localhost:8080/swagger-ui.html}