diff --git a/.gitignore b/.gitignore index b78b82ccaff..cfa8596e02b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ gradle-app.setting .gradletasknamecache !**/src/main/**/build/ !**/src/test/**/build/ +*.jks ### STS ### .apt_generated diff --git a/CHANGELOG.md b/CHANGELOG.md index b8936f318c0..0e1ca562203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * [REST]: Updated synchronizing of sample data to remove sequencing objects and assemblies that no longer exist on the remote sample. See [PR 1345](https://github.com/phac-nml/irida/pull/1345) * [UI]: Fixed issue with filtering samples by files using a windows encoded text file causing sample name truncation. See [PR 1346](https://github.com/phac-nml/irida/pull/1346) * [Developer]: Fixed deleting a project with project subscriptions. See [PR 1348](https://github.com/phac-nml/irida/pull/1348) +* [Developer]: Updated OAuth2 implemention to use Spring Security 5 OAuth2 libraries. See [PR 1339](https://github.com/phac-nml/irida/pull/1339) ## [22.05.5] - 2022/06/28 * [UI]: Fixed bug preventing export of project samples table due to invalid url. [PR 1331](https://github.com/phac-nml/irida/pull/1331) diff --git a/build.gradle.kts b/build.gradle.kts index c09315f4e07..790b0dd3977 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -121,6 +121,8 @@ dependencies { } implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.security:spring-security-oauth2-authorization-server:0.3.1") + implementation("org.springframework.security:spring-security-oauth2-resource-server:5.7.2") implementation("org.apache.oltu.oauth2:org.apache.oltu.oauth2.client:1.0.0") { exclude(group = "org.slf4j") } @@ -146,9 +148,6 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web-services") implementation("org.springframework.boot:spring-boot-starter-hateoas") - implementation("org.springframework.security.oauth:spring-security-oauth2:2.3.6.RELEASE") { - exclude(group = "org.codehaus.jackson", module = "jackson-mapper-asl") - } implementation("commons-io:commons-io:2.11.0") implementation("commons-fileupload:commons-fileupload:1.4") implementation("org.apache.poi:poi-ooxml:5.2.2") { @@ -266,6 +265,7 @@ tasks.war { exclude("entries.js") exclude(".eslintrc.js") exclude("postcss.config.js") + rootSpec.exclude("**/jwk-key-store.jks") } node { @@ -423,6 +423,12 @@ tasks.named("bootRun") { } } +task("generateJWKKeyStore") { + workingDir(file("${projectDir}/src/main/resources")) + commandLine(listOf("keytool", "-genkeypair", "-alias", "JWK", "-keyalg", "RSA", "-noprompt", "-dname", "CN=irida.bioinformatics.corefacility.ca, OU=ID, O=IRIDA, L=IRIDA, S=IRIDA, C=CA", "-keystore", "jwk-key-store.jks", "-validity", "3650", "-storepass", "SECRET", "-keypass", "SECRET", "-storetype", "PKCS12")) + outputs.file(file("${projectDir}/src/main/resources/jwk-key-store.jks")) +} + openApi { outputDir.set(file("${projectDir}/doc/swagger-ui")) outputFileName.set("open-api.json") @@ -434,6 +440,7 @@ tasks.processResources { expand(project.properties) } dependsOn(":buildWebapp") + dependsOn(":generateJWKKeyStore") } tasks.javadoc { diff --git a/doc/administrator/web/config/irida.conf b/doc/administrator/web/config/irida.conf index c7a30ed745f..405afedd5cb 100644 --- a/doc/administrator/web/config/irida.conf +++ b/doc/administrator/web/config/irida.conf @@ -18,6 +18,10 @@ file.processing.max.size=8 file.processing.queue.capacity=512 file.processing.process=true +##### OAuth2 JWT security settings. +# Default keystore for holding public/private keys required for OAuth2 JWK Access Token encryption/decryption +oauth2.jwk.key-store=/etc/irida/jwk-key-store.jks +oauth2.jwk.key-store-password=SECRET ##### The database-specific settings. Several examples of how to specify a ##### Hibernate driver are listed below (but commented out). diff --git a/doc/administrator/web/index.md b/doc/administrator/web/index.md index d3a2cfc2e85..66666bf9657 100644 --- a/doc/administrator/web/index.md +++ b/doc/administrator/web/index.md @@ -98,6 +98,17 @@ The main configuration parameters you will need to change are: * `ncbi.upload.namespace` - Prefix for file upload identifiers to NCBI. The namespace is used to guarantee upload IDs are unique. This configuration option is used as a placeholder and may still be set by the user. 5. **Security configuration** * `security.password.expiry` - The number of days a password is valid for in IRIDA. After a password expires the user will be required to create a new one. Passwords cannot be reused. +6. **OAuth2 JWK security configuration** - IRIDA uses JWT (Json Web Tokens) for OAuth2 and as such requires a Java Key Store with an entry stored in PKCS12 format. The key pair entry can be in either RSA or EC (curves allowed are P-256, P-384, and P-121) format. The password for the keystore must be the same as the password for the key entry (This is default for PKCS12 format). (Note: these keys have nothing to do with SSL). + * `oauth2.jwk.key-store=/etc/irida/jwk-key-store.jks` - The location of the Java Key Store + * `oauth2.jwk.key-store-password=SECRET` - The Java Key Store password + +### OAuth2 JWK Java Key Store generation +The Java Key Store required to encrypt and decrypt the JWT's can be generated with the following commands: +```bash +keytool -genkeypair -alias JWK -keyalg RSA -noprompt -dname "CN=irida.bioinformatics.corefacility.ca, OU=ID, O=IRIDA, L=IRIDA, S=IRIDA, C=CA" -keystore /etc/irida/jwk-key-store.jks -validity 3650 -storepass SECRET -keypass SECRET -storetype PKCS12 +``` + +This will generate a Java Key Store with an entry aliased `JWK` with an expiry of 10 years and a password of `SECRET`. Web Configuration ----------------- diff --git a/doc/images/tutorials/clients/client-details.png b/doc/images/tutorials/clients/client-details.png index 88de942710c..10f22a7a098 100644 Binary files a/doc/images/tutorials/clients/client-details.png and b/doc/images/tutorials/clients/client-details.png differ diff --git a/doc/user/administrator/images/edit-client-details.png b/doc/user/administrator/images/edit-client-details.png index 6f235b48555..d7fabf33de6 100644 Binary files a/doc/user/administrator/images/edit-client-details.png and b/doc/user/administrator/images/edit-client-details.png differ diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/config/repository/IridaApiRepositoriesConfig.java b/src/main/java/ca/corefacility/bioinformatics/irida/config/repository/IridaApiRepositoriesConfig.java index edfa1ee6900..0be7ae42831 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/config/repository/IridaApiRepositoriesConfig.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/config/repository/IridaApiRepositoriesConfig.java @@ -1,7 +1,6 @@ package ca.corefacility.bioinformatics.irida.config.repository; import javax.persistence.EntityManagerFactory; -import javax.sql.DataSource; import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; @@ -13,8 +12,6 @@ import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.security.oauth2.provider.token.TokenStore; -import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; import org.springframework.transaction.annotation.EnableTransactionManagement; import ca.corefacility.bioinformatics.irida.config.data.IridaApiJdbcDataSourceConfig; @@ -23,14 +20,15 @@ /** * Configuration for repository/data storage classes. - * - * */ @Configuration @EnableTransactionManagement(order = IridaApiRepositoriesConfig.TRANSACTION_MANAGEMENT_ORDER) -@EnableJpaRepositories(basePackages = "ca.corefacility.bioinformatics.irida.repositories", repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) +@EnableJpaRepositories(basePackages = "ca.corefacility.bioinformatics.irida.repositories", + repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) @ComponentScan("ca.corefacility.bioinformatics.irida.repositories.remote") -@Import({ IridaApiPropertyPlaceholderConfig.class, IridaApiJdbcDataSourceConfig.class, +@Import({ + IridaApiPropertyPlaceholderConfig.class, + IridaApiJdbcDataSourceConfig.class, IridaApiFilesystemRepositoryConfig.class }) @EnableJpaAuditing public class IridaApiRepositoriesConfig { @@ -49,10 +47,4 @@ public RevisionListener revisionListener() { public AuditReader auditReader(EntityManagerFactory entityManagerFactory) { return AuditReaderFactory.get(entityManagerFactory.createEntityManager()); } - - @Bean(name="iridaTokenStore") - public TokenStore tokenStore(DataSource dataSource) { - TokenStore store = new JdbcTokenStore(dataSource); - return store; - } } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaOauthSecurityConfig.java b/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaOauthSecurityConfig.java index f9eca9f8ea0..a7fa235b7bf 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaOauthSecurityConfig.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaOauthSecurityConfig.java @@ -1,42 +1,75 @@ package ca.corefacility.bioinformatics.irida.config.security; -import ca.corefacility.bioinformatics.irida.web.controller.api.exception.CustomOAuth2ExceptionTranslator; -import ca.corefacility.bioinformatics.irida.web.filter.UnauthenticatedAnonymousAuthenticationFilter; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpMethod; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configuration.*; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; -import org.springframework.security.oauth2.provider.ClientDetailsService; -import org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService; -import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; -import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices; -import org.springframework.security.oauth2.provider.error.AbstractOAuth2SecurityExceptionHandler; -import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; -import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; -import org.springframework.security.oauth2.provider.token.DefaultTokenServices; -import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; -import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.oauth2.core.*; +import org.springframework.security.oauth2.jwt.*; +import org.springframework.security.oauth2.server.authorization.*; +import org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; +import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; +import org.springframework.security.oauth2.server.authorization.token.*; +import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter; +import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.util.ReflectionUtils; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.ResourceUtils; -import java.lang.reflect.Field; +import ca.corefacility.bioinformatics.irida.jackson2.mixin.RoleMixin; +import ca.corefacility.bioinformatics.irida.jackson2.mixin.TimestampMixin; +import ca.corefacility.bioinformatics.irida.jackson2.mixin.UserMixin; +import ca.corefacility.bioinformatics.irida.model.user.Role; +import ca.corefacility.bioinformatics.irida.model.user.User; +import ca.corefacility.bioinformatics.irida.oauth2.IridaOAuth2AuthorizationService; +import ca.corefacility.bioinformatics.irida.oauth2.OAuth2ResourceOwnerPasswordAuthenticationConverter; +import ca.corefacility.bioinformatics.irida.oauth2.OAuth2ResourceOwnerPasswordAuthenticationProvider; +import ca.corefacility.bioinformatics.irida.web.filter.UnauthenticatedAnonymousAuthenticationFilter; + +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.PasswordLookup; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; /** * Configuration for REST API security using OAuth2 @@ -45,121 +78,162 @@ public class IridaOauthSecurityConfig { private static final Logger logger = LoggerFactory.getLogger(IridaOauthSecurityConfig.class); - @Bean - @Primary - public ResourceServerTokenServices tokenServices(@Qualifier("clientDetails") ClientDetailsService clientDetails, - @Qualifier("iridaTokenStore") TokenStore tokenStore) { - DefaultTokenServices services = new DefaultTokenServices(); - services.setTokenStore(tokenStore); - services.setSupportRefreshToken(true); - services.setClientDetailsService(clientDetails); - return services; - } - - @Bean - public ClientDetailsUserDetailsService clientDetailsUserDetailsService( - @Qualifier("clientDetails") ClientDetailsService clientDetails) { - ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService( - clientDetails); - - return clientDetailsUserDetailsService; - } - - @Bean - public WebResponseExceptionTranslator exceptionTranslator() { - return new CustomOAuth2ExceptionTranslator(); - } + private static final String AUTHORITIES_CLAIM = "authorities"; /** * Class for configuring the OAuth resource server security */ @Configuration - @EnableResourceServer - @ComponentScan(basePackages = "ca.corefacility.bioinformatics.irida.repositories.remote") - @Order(Ordered.HIGHEST_PRECEDENCE + 2) - protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { + protected static class ResourceServerConfig { - @Autowired - private ResourceServerTokenServices tokenServices; + @Value("${server.base.url}") + private String serverBase; @Autowired - private WebResponseExceptionTranslator exceptionTranslator; + OAuth2AuthorizationService authorizationService; - @Override - public void configure(final ResourceServerSecurityConfigurer resources) { - resources.resourceId("NmlIrida").tokenServices(tokenServices); - forceExceptionTranslator(resources, exceptionTranslator); - } - - @Override - public void configure(final HttpSecurity httpSecurity) throws Exception { - httpSecurity.antMatcher("/api/**").authorizeRequests().regexMatchers(HttpMethod.GET, "/api.*") - .access("#oauth2.hasScope('read')").regexMatchers("/api.*") - .access("#oauth2.hasScope('read') and #oauth2.hasScope('write')"); - httpSecurity.antMatcher("/api/**").headers().frameOptions().disable(); - httpSecurity.antMatcher("/api/**").csrf() - .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/api/oauth/authorize")).disable(); - httpSecurity.antMatcher("/api/**").csrf().disable(); + @Autowired + JwtDecoder jwtDecoder; + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 2) // lower precedence by 2 (i.e. apply this SecurityFilterChain last) + public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception { + http.antMatcher("/api/**") + .authorizeRequests() + .regexMatchers(HttpMethod.GET, "/api.*") + .hasAuthority("SCOPE_read") + .regexMatchers("/api.*") + .hasAuthority("SCOPE_write"); + http.antMatcher("/api/**").headers().frameOptions().disable(); + http.antMatcher("/api/**") + .csrf() + .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/api/oauth/authorize")) + .disable(); + http.antMatcher("/api/**").csrf().disable(); + http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter()); // SecurityContextPersistenceFilter appears pretty high up (well // before any OAuth related filters), so we'll put our anonymous // user filter into the filter chain after that. - httpSecurity.antMatcher("/api/**").addFilterAfter( - new UnauthenticatedAnonymousAuthenticationFilter("anonymousTokenAuthProvider"), - SecurityContextPersistenceFilter.class); + http.antMatcher("/api/**") + .addFilterAfter(new UnauthenticatedAnonymousAuthenticationFilter("anonymousTokenAuthProvider"), + SecurityContextPersistenceFilter.class); + + return http.build(); + } + + @Bean + public Converter jwtAuthenticationConverter() { + final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AUTHORITIES_CLAIM); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix(""); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); + return jwtAuthenticationConverter; } } /** * Class for configuring the OAuth authorization server */ - @Configuration - @EnableAuthorizationServer - protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { + @Configuration(proxyBeanMethods = false) + protected static class AuthorizationServerConfig { + + private static final String CUSTOM_CONSENT_PAGE_URI = "/api/oauth/consent"; + + @Value("${server.base.url}") + private String serverBase; + @Autowired - @Qualifier("clientDetails") - private ClientDetailsService clientDetailsService; + private OAuth2AuthorizationService authorizationService; @Autowired - @Qualifier("iridaTokenStore") - private TokenStore tokenStore; + private ClientSecretAuthenticationProvider clientSecretAuthenticationProvider; @Autowired - @Qualifier("userAuthenticationManager") private AuthenticationManager authenticationManager; @Autowired - private WebResponseExceptionTranslator exceptionTranslator; + private OAuth2TokenGenerator tokenGenerator; + + // @formatter:off + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) // apply this SecurityFilterChain first + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>(); + + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + authorizationServerConfigurer.clientAuthentication(clientAuthentication -> clientAuthentication.authenticationProvider(clientSecretAuthenticationProvider)); - @Override - public void configure(ClientDetailsServiceConfigurer clients) throws Exception { - clients.withClientDetails(clientDetailsService); + authorizationServerConfigurer.tokenEndpoint((tokenEndpoint) -> tokenEndpoint.accessTokenRequestConverter( + new DelegatingAuthenticationConverter(Arrays.asList( + new OAuth2AuthorizationCodeAuthenticationConverter(), + new OAuth2RefreshTokenAuthenticationConverter(), + new OAuth2ClientCredentialsAuthenticationConverter(), + new OAuth2ResourceOwnerPasswordAuthenticationConverter())) + )); + + authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)); + + http + .requestMatcher(endpointsMatcher) + .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated()) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) + .apply(authorizationServerConfigurer) + .and() + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))); + + addCustomOAuth2ResourceOwnerPasswordAuthenticationProvider(http); + + return http.build(); } + // @formatter:on - @Autowired - private ResourceServerTokenServices tokenServices; - - @Override - public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception { - endpoints.tokenStore(tokenStore); - endpoints.authenticationManager(authenticationManager); - endpoints.authorizationCodeServices(authorizationCodeServices()); - endpoints.pathMapping("/oauth/token", "/api/oauth/token").allowedTokenEndpointRequestMethods(HttpMethod.GET, - HttpMethod.POST); - endpoints.pathMapping("/oauth/check_token", "/api/oauth/check_token"); - endpoints.pathMapping("/oauth/confirm_access", "/api/oauth/confirm_access"); - endpoints.pathMapping("/oauth/error", "/api/oauth/error"); - endpoints.pathMapping("/oauth/authorize", "/api/oauth/authorize"); - endpoints.tokenServices((DefaultTokenServices) tokenServices); - endpoints.exceptionTranslator(exceptionTranslator); + private void addCustomOAuth2ResourceOwnerPasswordAuthenticationProvider(HttpSecurity http) { + OAuth2ResourceOwnerPasswordAuthenticationProvider resourceOwnerPasswordAuthenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider( + authenticationManager, authorizationService, tokenGenerator); + + // This will add new authentication provider in the list of existing authentication providers. + http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider); } - @Override - public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception { - oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()") - .allowFormAuthenticationForClients(); - oauthServer.passwordEncoder(new PasswordEncoder() { + @Bean + public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, + RegisteredClientRepository registeredClientRepository) { + IridaOAuth2AuthorizationService service = new IridaOAuth2AuthorizationService(jdbcTemplate, + registeredClientRepository); + JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper( + registeredClientRepository); + ObjectMapper objectMapper = new ObjectMapper(); + ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader(); + List securityModules = SecurityJackson2Modules.getModules(classLoader); + + objectMapper.registerModules(securityModules); + objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); + objectMapper.addMixIn(User.class, UserMixin.class); + objectMapper.addMixIn(Role.class, RoleMixin.class); + objectMapper.addMixIn(Timestamp.class, TimestampMixin.class); + rowMapper.setObjectMapper(objectMapper); + service.setAuthorizationRowMapper(rowMapper); + return service; + } + + @Bean + public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, + RegisteredClientRepository registeredClientRepository) { + return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); + } + + @Bean + public ClientSecretAuthenticationProvider oauthClientAuthProvider( + RegisteredClientRepository registeredClientRepository, + OAuth2AuthorizationService oAuth2AuthorizationService) { + ClientSecretAuthenticationProvider clientAuthenticationProvider = new ClientSecretAuthenticationProvider( + registeredClientRepository, oAuth2AuthorizationService); + + clientAuthenticationProvider.setPasswordEncoder(new PasswordEncoder() { @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return rawPassword.equals(encodedPassword); @@ -170,65 +244,108 @@ public String encode(CharSequence rawPassword) { return rawPassword.toString(); } }); + + return clientAuthenticationProvider; + } + + @Bean + public OAuth2TokenCustomizer jwtCustomizer() { + return context -> { + Set grantTypes = Set.of(AuthorizationGrantType.AUTHORIZATION_CODE, + AuthorizationGrantType.PASSWORD); + if (grantTypes.contains(context.getAuthorizationGrantType()) + && OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { + Authentication principal = context.getPrincipal(); + Set authorities = new HashSet<>(); + for (GrantedAuthority authority : principal.getAuthorities()) { + authorities.add(authority.getAuthority()); + } + for (String authorizedScope : context.getAuthorizedScopes()) { + authorities.add("SCOPE_" + authorizedScope); + } + context.getClaims().claim(AUTHORITIES_CLAIM, authorities); + } + }; + } + + @Bean + @SuppressWarnings("unused") + public OAuth2TokenGenerator oAuth2TokenGenerator(JwtEncoder jwtEncoder, + OAuth2TokenCustomizer jwtCustomizer) { + JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder); + jwtGenerator.setJwtCustomizer(jwtCustomizer); + OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); + OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); + return new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator); } @Bean - public AuthorizationCodeServices authorizationCodeServices() { - return new InMemoryAuthorizationCodeServices(); + public ProviderSettings providerSettings() { + return ProviderSettings.builder() + .issuer(serverBase) + .authorizationEndpoint("/api/oauth/authorize") + .tokenEndpoint("/api/oauth/token") + .jwkSetEndpoint("/api/oauth/jwks") + .tokenRevocationEndpoint("/api/oauth/revoke") + .tokenIntrospectionEndpoint("/api/oauth/introspect") + .build(); } } /** - * This adds our own custom filter before the OAuth2 filters are run to put - * an anonymous authentication object into the security context *before* - * {@link ClientDetailsService#loadClientByClientId(String)} is called. + * Class for configuring the JSON Web Key for JSON Web Tokens used in OAuth2 */ @Configuration - @Order(Ordered.HIGHEST_PRECEDENCE) - protected static class AuthorizationServerConfigurer extends AuthorizationServerSecurityConfiguration { + protected static class JWKConfig { + @Autowired - @Qualifier("clientDetails") - private ClientDetailsService clientDetailsService; + private OAuth2AuthorizationService authorizationService; - @Override - protected void configure(HttpSecurity http) throws Exception { - super.configure(http); - // SecurityContextPersistenceFilter appears pretty high up (well - // before any OAuth related filters), so we'll put our anonymous - // user filter into the filter chain after that. - http.addFilterAfter(new UnauthenticatedAnonymousAuthenticationFilter("anonymousTokenAuthProvider"), - SecurityContextPersistenceFilter.class); + @Bean + public JWKSource jwkSource(@Value("${oauth2.jwk.key-store}") String keyStoreLocation, + @Value("${oauth2.jwk.key-store-password}") String keyStorePassword) throws KeyStoreException, + NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException { + KeyStore keyStore = KeyStore.getInstance("JKS"); + + keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStoreLocation)), keyStorePassword.toCharArray()); + + JWKSet jwkSet = JWKSet.load(keyStore, new PasswordLookup() { + @Override + public char[] lookupPassword(String name) { + return keyStorePassword.toCharArray(); + } + }); + return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } - } - /** - * Forcibly set the exception translator on the `authenticationEntryPoint` - * so that we can supply our own errors on authentication failure. The - * `authenticationEntryPoint` field on - * {@link AbstractOAuth2SecurityExceptionHandler} is marked `private`, and - * is not accessible for customizing. - * - * @param configurer - * the instance of the configurer that we're customizing - * @param exceptionTranslator - * the {@link WebResponseExceptionTranslator} that we want to - * set. - * @param - * The type of security configurer - */ - private static void forceExceptionTranslator(final T configurer, - final WebResponseExceptionTranslator exceptionTranslator) { - try { - final Field authenticationEntryPointField = ReflectionUtils.findField(configurer.getClass(), - "authenticationEntryPoint"); - ReflectionUtils.makeAccessible(authenticationEntryPointField); - final OAuth2AuthenticationEntryPoint authenticationEntryPoint = (OAuth2AuthenticationEntryPoint) authenticationEntryPointField - .get(configurer); - - logger.debug("Customizing the authentication entry point by brute force."); - authenticationEntryPoint.setExceptionTranslator(exceptionTranslator); - } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) { - logger.error("Failed to configure the authenticationEntryPoint on ResourceServerSecurityConfigurer.", e); + @Bean + @SuppressWarnings("unused") + public JwtEncoder jwtEncoder(JWKSource jwkSource) { + return new NimbusJwtEncoder(jwkSource); + } + + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + OAuth2TokenValidator defaultValidator = JwtValidators.createDefault(); + + OAuth2TokenValidator revocationValidator = jwt -> { + OAuth2Authorization authorization = this.authorizationService.findByToken(jwt.getTokenValue(), + OAuth2TokenType.ACCESS_TOKEN); + + if (authorization != null && authorization.getAccessToken().isActive()) { + return OAuth2TokenValidatorResult.success(); + } else { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "The token is revoked", + "https://tools.ietf.org/html/rfc6750#section-3.1"); + return OAuth2TokenValidatorResult.failure(error); + } + }; + + OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<>(defaultValidator, + revocationValidator); + NimbusJwtDecoder decoder = (NimbusJwtDecoder) OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + decoder.setJwtValidator(validator); + return decoder; } } } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaWebSecurityConfig.java b/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaWebSecurityConfig.java index bef48e1d9e7..471854c64e5 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaWebSecurityConfig.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/config/security/IridaWebSecurityConfig.java @@ -7,9 +7,8 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.firewall.DefaultHttpFirewall; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.web.filter.GenericFilterBean; @@ -24,16 +23,14 @@ * Configuration for web security using OAuth2 */ @Configuration -@EnableWebSecurity @Import({ IridaOauthSecurityConfig.class }) -public class IridaWebSecurityConfig extends WebSecurityConfigurerAdapter { +public class IridaWebSecurityConfig { /** * UI security config for IRIDA */ @Configuration - @Order(Ordered.HIGHEST_PRECEDENCE + 1) - protected static class UISecurityConfig extends WebSecurityConfigurerAdapter { + protected static class UISecurityConfig { @Autowired private UserRepository userRepository; @@ -49,21 +46,9 @@ public LoginSuccessHandler getLoginSuccessHandler() { @Autowired IridaPostAuthenticationFailureHandler authFailureHandler; - @Override - public void configure(WebSecurity web) throws Exception { - // @formatter:off - web.ignoring() - .antMatchers("/node_modules/**") - .antMatchers("/dist/**") - .antMatchers("/static/**") - .antMatchers("/resources/**"); - - // @formatter:on - web.httpFirewall(new DefaultHttpFirewall()); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 1) + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { authFailureHandler.setDefaultFailureUrl("/login?error=true"); // @formatter:off http.requestMatcher(request -> { @@ -94,6 +79,20 @@ protected void configure(HttpSecurity http) throws Exception { .antMatchers("/**").fullyAuthenticated() .and().addFilterAfter(getSessionModelFilter(), SecurityContextHolderAwareRequestFilter.class); // @formatter:on + + return http.build(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> { + web.ignoring() + .antMatchers("/node_modules/**") + .antMatchers("/dist/**") + .antMatchers("/static/**") + .antMatchers("/resources/**"); + web.httpFirewall(new DefaultHttpFirewall()); + }; } @Bean diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/jackson2/mixin/RoleMixin.java b/src/main/java/ca/corefacility/bioinformatics/irida/jackson2/mixin/RoleMixin.java new file mode 100644 index 00000000000..75e0afbec44 --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/jackson2/mixin/RoleMixin.java @@ -0,0 +1,21 @@ +package ca.corefacility.bioinformatics.irida.jackson2.mixin; + +import ca.corefacility.bioinformatics.irida.model.user.Role; +import ca.corefacility.bioinformatics.irida.oauth2.IridaOAuth2AuthorizationService; + +import com.fasterxml.jackson.annotation.*; + +/** + * This mixin class is used to serialize/deserialize {@link Role}. + *

+ * This is used to by the {@link IridaOAuth2AuthorizationService} + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, + getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY, isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class RoleMixin { + @JsonCreator + public RoleMixin(@JsonProperty("name") String name) { + } +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/jackson2/mixin/TimestampMixin.java b/src/main/java/ca/corefacility/bioinformatics/irida/jackson2/mixin/TimestampMixin.java new file mode 100644 index 00000000000..58f0a61c512 --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/jackson2/mixin/TimestampMixin.java @@ -0,0 +1,25 @@ +package ca.corefacility.bioinformatics.irida.jackson2.mixin; + +import java.sql.Timestamp; + +import ca.corefacility.bioinformatics.irida.oauth2.IridaOAuth2AuthorizationService; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.DateDeserializers.TimestampDeserializer; + +/** + * This mixin class is used to serialize/deserialize {@link Timestamp}. + *

+ * This is used to by the {@link IridaOAuth2AuthorizationService} + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = TimestampDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class TimestampMixin { + +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/jackson2/mixin/UserMixin.java b/src/main/java/ca/corefacility/bioinformatics/irida/jackson2/mixin/UserMixin.java new file mode 100644 index 00000000000..e6138a35e93 --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/jackson2/mixin/UserMixin.java @@ -0,0 +1,18 @@ +package ca.corefacility.bioinformatics.irida.jackson2.mixin; + +import ca.corefacility.bioinformatics.irida.model.user.User; +import ca.corefacility.bioinformatics.irida.oauth2.IridaOAuth2AuthorizationService; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * This mixin class is used to serialize/deserialize {@link User}. + *

+ * This is used to by the {@link IridaOAuth2AuthorizationService} + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonIgnoreProperties(ignoreUnknown = true, value = { "links", "systemRole" }) +public abstract class UserMixin { + +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/model/ClientRole.java b/src/main/java/ca/corefacility/bioinformatics/irida/model/ClientRole.java deleted file mode 100644 index 54b87f600a5..00000000000 --- a/src/main/java/ca/corefacility/bioinformatics/irida/model/ClientRole.java +++ /dev/null @@ -1,93 +0,0 @@ -package ca.corefacility.bioinformatics.irida.model; - -import java.util.Objects; - -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.validation.constraints.NotNull; - -import org.hibernate.envers.Audited; -import org.springframework.security.core.GrantedAuthority; - -import ca.corefacility.bioinformatics.irida.model.user.Role; - -/** - * Role of an OAuth2 client in the Irida system - * - */ -@Entity -@Table(name = "client_role") -@Audited -public class ClientRole implements GrantedAuthority, Comparable { - - private static final long serialVersionUID = -5872715742283126858L; - - /** - * Constant reference for the OAuth2 client role - */ - public static final ClientRole ROLE_CLIENT = new ClientRole("ROLE_CLIENT"); - - @Id - private String name; - - @NotNull - private String description; - - private ClientRole() { - } - - private ClientRole(String name) { - this.name = name; - } - - private ClientRole(String name, String description) { - this.name = name; - this.description = description; - } - - @Override - public int hashCode() { - return Objects.hash(name); - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - @Override - public String getAuthority() { - return name; - } - - /** - * Return a {@link Role} for the given string value - * - * @param value - * The string value to create a {@link Role} for - * @return A new {@link Role} instance for the given string value - */ - public static ClientRole valueOf(String value) { - return new ClientRole(value); - } - - @Override - public int compareTo(ClientRole r) { - return name.compareTo(r.name); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof ClientRole) { - ClientRole r = (ClientRole) obj; - return Objects.equals(name, r.name); - } - - return false; - } - -} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/model/IridaClientDetails.java b/src/main/java/ca/corefacility/bioinformatics/irida/model/IridaClientDetails.java index 4e1f62f9297..abf3d8a7b12 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/model/IridaClientDetails.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/model/IridaClientDetails.java @@ -1,38 +1,36 @@ package ca.corefacility.bioinformatics.irida.model; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import org.hibernate.envers.Audited; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.provider.ClientDetails; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; import javax.persistence.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; -import java.util.*; + +import org.hibernate.envers.Audited; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import com.google.common.collect.Sets; /** - * Object representing a client that has been registered to communicate with - * this API via OAuth2 - * - * + * Object representing a client that has been registered to communicate with this API via OAuth2 */ @Entity -@Table(name = "client_details", uniqueConstraints = { @UniqueConstraint(columnNames = "clientId", name = IridaClientDetails.CLIENT_ID_CONSTRAINT_NAME) }) +@Table(name = "client_details", + uniqueConstraints = { + @UniqueConstraint(columnNames = "clientId", name = IridaClientDetails.CLIENT_ID_CONSTRAINT_NAME) }) @Audited @EntityListeners(AuditingEntityListener.class) -public class IridaClientDetails implements ClientDetails, MutableIridaThing { - private static final long serialVersionUID = -1593194281520695701L; - +public class IridaClientDetails implements MutableIridaThing { public final static String CLIENT_ID_CONSTRAINT_NAME = "UK_CLIENT_DETAILS_CLIENT_ID"; - + // 12 hours public final static Integer DEFAULT_TOKEN_VALIDITY = 43200; - + // 1 month public final static Integer DEFAULT_REFRESH_TOKEN_VALIDITY = 2592000; @@ -41,19 +39,13 @@ public class IridaClientDetails implements ClientDetails, MutableIridaThing { private Long id; @NotNull - @Pattern(regexp = "[^\\s]+", message="{remoteapi.details.nospace}") + @Pattern(regexp = "[^\\s]+", message = "{remoteapi.details.nospace}") private String clientId; - @NotNull - @ElementCollection(fetch = FetchType.EAGER) - @Column(name = "resource_id", nullable = false) - @CollectionTable(name = "client_details_resource_ids", joinColumns = @JoinColumn(name = "client_details_id")) - private Set resourceIds; - @NotNull private String clientSecret; - @Column(name="redirect_uri") + @Column(name = "redirect_uri") private String registeredRedirectUri; @Size(min = 1, message = "{client.details.scope.notempty}") @@ -62,11 +54,6 @@ public class IridaClientDetails implements ClientDetails, MutableIridaThing { @Column(name = "scope", nullable = false) @CollectionTable(name = "client_details_scope", joinColumns = @JoinColumn(name = "client_details_id")) private Set scope; - - @ElementCollection(fetch = FetchType.EAGER) - @Column(name = "auto_approvable_scope") - @CollectionTable(name = "client_details_auto_approvable_scope", joinColumns = @JoinColumn(name = "client_details_id")) - private Set autoApprovableScopes; @Size(min = 1, message = "{client.details.grant.notempty}") @NotNull @@ -82,16 +69,6 @@ public class IridaClientDetails implements ClientDetails, MutableIridaThing { @Column(name = "refresh_validity") private Integer refreshTokenValiditySeconds; - @ElementCollection(fetch = FetchType.EAGER) - @MapKeyColumn(name = "info_key") - @Column(name = "info_value") - @CollectionTable(name = "client_details_additional_information", joinColumns = @JoinColumn(name = "client_details_id")) - private Map additionalInformation; - - @ManyToMany(fetch = FetchType.EAGER) - @JoinTable(name = "client_details_authorities", joinColumns = @JoinColumn(name = "client_details_id", nullable = false), inverseJoinColumns = @JoinColumn(name = "authority_name", nullable = false)) - Collection authorities; - @LastModifiedDate @Temporal(TemporalType.TIMESTAMP) private Date modifiedDate; @@ -103,106 +80,58 @@ public class IridaClientDetails implements ClientDetails, MutableIridaThing { private Date createdDate; /** - * Default constructor with empty scopes, grant types, resource ids, - * redirect uris, and additional information + * Default constructor with empty scopes, grant types, and redirect uris */ public IridaClientDetails() { createdDate = new Date(); accessTokenValiditySeconds = DEFAULT_TOKEN_VALIDITY; refreshTokenValiditySeconds = DEFAULT_REFRESH_TOKEN_VALIDITY; - resourceIds = new HashSet<>(); scope = new HashSet<>(); authorizedGrantTypes = new HashSet<>(); - additionalInformation = new HashMap<>(); - authorities = new HashSet<>(); } /** * Construct new IridaClientDetails with the following params * - * @param clientId - * The ID of the client for this object - * @param clientSecret - * The Client Secret for this client - * @param resourceIds - * The resource IDs this client will access - * @param scope - * The scopes this client can access - * @param authorizedGrantTypes - * The grant types allowed for this client - * @param authorities - * the collection of {@link ClientRole} that this client should - * have. + * @param clientId The ID of the client for this object + * @param clientSecret The Client Secret for this client + * @param scope The scopes this client can access + * @param authorizedGrantTypes The grant types allowed for this client */ - public IridaClientDetails(String clientId, String clientSecret, Set resourceIds, Set scope, - Set authorizedGrantTypes, Collection authorities) { + public IridaClientDetails(String clientId, String clientSecret, Set scope, + Set authorizedGrantTypes) { this(); this.clientId = clientId; - this.resourceIds = resourceIds; this.clientSecret = clientSecret; this.scope = scope; this.authorizedGrantTypes = authorizedGrantTypes; - this.authorities = authorities; } - /** - * {@inheritDoc} - */ - @Override public String getClientId() { return clientId; } - /** - * {@inheritDoc} - */ - @Override - public Set getResourceIds() { - return resourceIds; - } - - /** - * {@inheritDoc} - */ - @Override public boolean isSecretRequired() { return true; } - /** - * {@inheritDoc} - */ - @Override public String getClientSecret() { return clientSecret; } - /** - * {@inheritDoc} - */ - @Override public boolean isScoped() { return true; } - /** - * {@inheritDoc} - */ - @Override public Set getScope() { return scope; } - /** - * {@inheritDoc} - */ - @Override public Set getAuthorizedGrantTypes() { return authorizedGrantTypes; } - @Override public Set getRegisteredRedirectUri() { return Sets.newHashSet(registeredRedirectUri); } @@ -211,133 +140,65 @@ public void setRegisteredRedirectUri(String registeredRedirectUri) { this.registeredRedirectUri = registeredRedirectUri; } - public String getRedirectUri(){ + public String getRedirectUri() { return registeredRedirectUri; } - /** - * {@inheritDoc} - */ - @Override - public Collection getAuthorities() { - return new ArrayList<>(authorities); - } - - /** - * {@inheritDoc} - */ - @Override public Integer getAccessTokenValiditySeconds() { return accessTokenValiditySeconds; } - /** - * {@inheritDoc} - */ - @Override public Integer getRefreshTokenValiditySeconds() { return refreshTokenValiditySeconds; } - /** - * {@inheritDoc} - */ - @Override - public Map getAdditionalInformation() { - return Maps.newHashMap(additionalInformation); - } - - /** - * @return the id - */ public Long getId() { return id; } - /** - * @param id - * the id to set - */ public void setId(Long id) { this.id = id; } - /** - * @param clientId - * the clientId to set - */ public void setClientId(String clientId) { this.clientId = clientId; } /** - * @param resourceIds - * the resourceIds to set - */ - public void setResourceIds(Set resourceIds) { - this.resourceIds = resourceIds; - } - - /** - * @param clientSecret - * the clientSecret to set + * @param clientSecret the clientSecret to set */ public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; } /** - * @param scope - * the scope to set + * @param scope the scope to set */ public void setScope(Set scope) { this.scope = scope; } /** - * @param authorizedGrantTypes - * the authorizedGrantTypes to set + * @param authorizedGrantTypes the authorizedGrantTypes to set */ public void setAuthorizedGrantTypes(Set authorizedGrantTypes) { this.authorizedGrantTypes = authorizedGrantTypes; } /** - * @param accessTokenValiditySeconds - * the accessTokenValiditySeconds to set + * @param accessTokenValiditySeconds the accessTokenValiditySeconds to set */ public void setAccessTokenValiditySeconds(Integer accessTokenValiditySeconds) { this.accessTokenValiditySeconds = accessTokenValiditySeconds; } /** - * @param refreshTokenValiditySeconds - * the refreshTokenValiditySeconds to set + * @param refreshTokenValiditySeconds the refreshTokenValiditySeconds to set */ public void setRefreshTokenValiditySeconds(Integer refreshTokenValiditySeconds) { this.refreshTokenValiditySeconds = refreshTokenValiditySeconds; } - /** - * @param additionalInformation - * the additionalInformation to set - */ - public void setAdditionalInformation(Map additionalInformation) { - Map newMap = new HashMap<>(); - // create the new map by calling toString on all the values. I don't - // think we'll see non-string values here. - additionalInformation.entrySet().forEach(entry -> newMap.put(entry.getKey(), entry.getValue().toString())); - this.additionalInformation = newMap; - } - - /** - * @param authorities - * the authorities to set - */ - public void setAuthorities(Collection authorities) { - this.authorities = authorities; - } - @Override public String getLabel() { return clientId; @@ -357,21 +218,4 @@ public void setModifiedDate(Date modifiedDate) { public Date getCreatedDate() { return createdDate; } - - @Override - public boolean isAutoApprove(String scope) { - boolean approved = false; - if(autoApprovableScopes != null) { - approved = autoApprovableScopes.contains(scope); - } - return approved; - } - - public Set getAutoApprovableScopes() { - return autoApprovableScopes; - } - - public void setAutoApprovableScopes(Set autoApprovableScopes) { - this.autoApprovableScopes = autoApprovableScopes; - } } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/model/RemoteAPIToken.java b/src/main/java/ca/corefacility/bioinformatics/irida/model/RemoteAPIToken.java index c3d4184fe9b..6acc57c3424 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/model/RemoteAPIToken.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/model/RemoteAPIToken.java @@ -3,15 +3,7 @@ import java.util.Date; import java.util.Objects; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; +import javax.persistence.*; import javax.validation.constraints.NotNull; import org.hibernate.envers.Audited; @@ -19,13 +11,12 @@ import ca.corefacility.bioinformatics.irida.model.user.User; /** - * OAuth2 token for communicating with a {@link RemoteAPI} for a given - * {@link User} - * - * + * OAuth2 token for communicating with a {@link RemoteAPI} for a given {@link User} */ @Entity -@Table(name = "remote_api_token", uniqueConstraints = @UniqueConstraint(columnNames = { "user_id", "remote_api_id" }, name = "UK_remote_api_token_user")) +@Table(name = "remote_api_token", + uniqueConstraints = @UniqueConstraint(columnNames = { "user_id", "remote_api_id" }, + name = "UK_remote_api_token_user")) @Audited public class RemoteAPIToken { @Id @@ -33,9 +24,10 @@ public class RemoteAPIToken { private Long id; @NotNull + @Column(name = "tokenString", columnDefinition = "TEXT") private String tokenString; - - @Column(name = "refresh_token") + + @Column(name = "refresh_token", columnDefinition = "TEXT") private String refreshToken; @NotNull @@ -60,7 +52,7 @@ public RemoteAPIToken(String tokenString, RemoteAPI remoteApi, Date expiryDate) this.remoteApi = remoteApi; this.expiryDate = expiryDate; } - + public RemoteAPIToken(String tokenString, String refreshToken, RemoteAPI remoteApi, Date expiryDate) { super(); this.tokenString = tokenString; @@ -77,8 +69,7 @@ public Long getId() { } /** - * @param id - * the id to set + * @param id the id to set */ public void setId(Long id) { this.id = id; @@ -92,8 +83,7 @@ public String getTokenString() { } /** - * @param tokenString - * the tokenString to set + * @param tokenString the tokenString to set */ public void setTokenString(String tokenString) { this.tokenString = tokenString; @@ -107,8 +97,7 @@ public RemoteAPI getRemoteApi() { } /** - * @param remoteApi - * the remoteApi to set + * @param remoteApi the remoteApi to set */ public void setRemoteApi(RemoteAPI remoteApi) { this.remoteApi = remoteApi; @@ -122,8 +111,7 @@ public User getUser() { } /** - * @param user - * the user to set + * @param user the user to set */ public void setUser(User user) { this.user = user; @@ -160,11 +148,11 @@ public void setExpiryDate(Date expiryDate) { public boolean isExpired() { return (new Date()).after(expiryDate); } - + public String getRefreshToken() { return refreshToken; } - + public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/IridaOAuth2AuthorizationService.java b/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/IridaOAuth2AuthorizationService.java new file mode 100644 index 00000000000..d076f8344a8 --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/IridaOAuth2AuthorizationService.java @@ -0,0 +1,62 @@ +package ca.corefacility.bioinformatics.irida.oauth2; + +import java.util.List; + +import javax.annotation.Nullable; +import javax.transaction.Transactional; + +import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; + +/** + * A customized version of {@link JdbcOAuth2AuthorizationService} that adds a method to find + * {@link OAuth2Authorization}s with {@link OAuth2AccessToken}s for a specific {@link RegisteredClient}. + */ +public class IridaOAuth2AuthorizationService extends JdbcOAuth2AuthorizationService { + + private final String QUERY_AUTHORIZATION_BY_REGISTERED_CLIENT_ID = "SELECT * FROM oauth2_authorization WHERE access_token_value IS NOT NULL and registered_client_id = ?"; + + public IridaOAuth2AuthorizationService(JdbcOperations jdbcOperations, + RegisteredClientRepository registeredClientRepository) { + super(jdbcOperations, registeredClientRepository); + } + + @Transactional + public List findAccessTokensByRegisteredClientId(String registeredClientId) { + PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(new Object[] { registeredClientId }); + List authorizations = getJdbcOperations() + .query(QUERY_AUTHORIZATION_BY_REGISTERED_CLIENT_ID, pss, getAuthorizationRowMapper()); + return authorizations; + } + + @Override + @Transactional + public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) { + return super.findByToken(token, tokenType); + } + + @Override + @Transactional + public OAuth2Authorization findById(String id) { + return super.findById(id); + } + + @Override + @Transactional + public void remove(OAuth2Authorization authorization) { + super.remove(authorization); + } + + @Override + @Transactional + public void save(OAuth2Authorization authorization) { + super.save(authorization); + } +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/IridaRegisteredClientsRepository.java b/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/IridaRegisteredClientsRepository.java new file mode 100644 index 00000000000..bb501c99af0 --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/IridaRegisteredClientsRepository.java @@ -0,0 +1,110 @@ +package ca.corefacility.bioinformatics.irida.oauth2; + +import java.time.Duration; +import java.util.stream.Collectors; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.ClientSettings; +import org.springframework.security.oauth2.server.authorization.config.TokenSettings; +import org.springframework.stereotype.Component; + +import ca.corefacility.bioinformatics.irida.model.IridaClientDetails; +import ca.corefacility.bioinformatics.irida.repositories.IridaClientDetailsRepository; + +/** + * A converter implementation of {@link RegisteredClientRepository}, that transforms {@link IridaClientDetails} to/from + * {@link RegisteredClient}. + */ +@Component +public class IridaRegisteredClientsRepository implements RegisteredClientRepository { + + private final IridaClientDetailsRepository clientDetailsRepository; + + public IridaRegisteredClientsRepository(IridaClientDetailsRepository clientDetailsRepository) { + this.clientDetailsRepository = clientDetailsRepository; + } + + @Override + public void save(RegisteredClient registeredClient) { + IridaClientDetails entity = toEntity(registeredClient); + + clientDetailsRepository.save(entity); + } + + @Override + public RegisteredClient findById(String id) { + if (clientDetailsRepository.existsById(Long.parseLong(id))) { + IridaClientDetails client = clientDetailsRepository.findById(Long.parseLong(id)).get(); + + return toObject(client); + } else { + return null; + } + } + + @Override + public RegisteredClient findByClientId(String clientId) { + IridaClientDetails client = clientDetailsRepository.loadClientDetailsByClientId(clientId); + + if (client == null) { + return null; + } else { + return toObject(client); + } + } + + private IridaClientDetails toEntity(RegisteredClient registeredClient) { + IridaClientDetails client = new IridaClientDetails(); + + client.setId(Long.parseLong(registeredClient.getId())); + client.setClientId(registeredClient.getClientId()); + client.setClientSecret(registeredClient.getClientSecret()); + client.setRegisteredRedirectUri(registeredClient.getRedirectUris().stream().findFirst().get()); + client.setScope(registeredClient.getScopes()); + client.setAuthorizedGrantTypes(registeredClient.getAuthorizationGrantTypes() + .stream() + .map(AuthorizationGrantType::getValue) + .collect(Collectors.toSet())); + client.setAccessTokenValiditySeconds( + (int) registeredClient.getTokenSettings().getAccessTokenTimeToLive().toSeconds()); + client.setRefreshTokenValiditySeconds( + (int) registeredClient.getTokenSettings().getRefreshTokenTimeToLive().toSeconds()); + + return client; + } + + private RegisteredClient toObject(IridaClientDetails client) { + RegisteredClient.Builder builder = RegisteredClient.withId(client.getId().toString()) + .clientId(client.getClientId()) + .clientIdIssuedAt(client.getCreatedDate().toInstant()) + .clientSecret(client.getClientSecret()) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) + .authorizationGrantTypes((grantTypes) -> { + // if refresh token validaty is null disable refresh tokens + if (client.getRefreshTokenValiditySeconds() == null) { + grantTypes.removeIf(filter -> filter.equals(AuthorizationGrantType.REFRESH_TOKEN)); + } + client.getAuthorizedGrantTypes() + .forEach(grantType -> grantTypes.add(new AuthorizationGrantType(grantType))); + }) + .scopes((scopes) -> scopes.addAll(client.getScope())) + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) + .tokenSettings(TokenSettings.builder() + .accessTokenTimeToLive(Duration.ofSeconds(client.getAccessTokenValiditySeconds())) + .refreshTokenTimeToLive(Duration.ofSeconds(client.getRefreshTokenValiditySeconds() == null ? + 1L : + client.getRefreshTokenValiditySeconds())) + .build()); + + String redirectUri = client.getRedirectUri(); + if (redirectUri != null) { + builder.redirectUri(redirectUri); + } + + return builder.build(); + } +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/OAuth2ResourceOwnerPasswordAuthenticationConverter.java b/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/OAuth2ResourceOwnerPasswordAuthenticationConverter.java new file mode 100644 index 00000000000..befac73eea5 --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/OAuth2ResourceOwnerPasswordAuthenticationConverter.java @@ -0,0 +1,99 @@ +package ca.corefacility.bioinformatics.irida.oauth2; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * Attempts to extract an Access Token Request from {@link HttpServletRequest} for the OAuth 2.0 Resource Owner Password + * Credentials Grant and then converts it to an {@link OAuth2ResourceOwnerPasswordAuthenticationToken} used for + * authenticating the authorization grant. + */ +public class OAuth2ResourceOwnerPasswordAuthenticationConverter implements AuthenticationConverter { + + static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; + + @Override + public Authentication convert(HttpServletRequest request) { + + // grant_type (REQUIRED) + String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); + if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { + return null; + } + + MultiValueMap parameters = getParameters(request); + + // scope (OPTIONAL) + String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE); + if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE, ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + Set requestedScopes = null; + if (StringUtils.hasText(scope)) { + requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); + } + + // username (REQUIRED) + String username = parameters.getFirst(OAuth2ParameterNames.USERNAME); + if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME, ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + // password (REQUIRED) + String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD); + if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD, ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + if (clientPrincipal == null) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT, + ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + Map additionalParameters = parameters.entrySet() + .stream() + .filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) + && !e.getKey().equals(OAuth2ParameterNames.SCOPE)) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); + + return new OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType.PASSWORD, clientPrincipal, + requestedScopes, additionalParameters); + } + + static MultiValueMap getParameters(HttpServletRequest request) { + Map parameterMap = request.getParameterMap(); + MultiValueMap parameters = new LinkedMultiValueMap<>(parameterMap.size()); + parameterMap.forEach((key, values) -> { + if (values.length > 0) { + for (String value : values) { + parameters.add(key, value); + } + } + }); + return parameters; + } + + static void throwError(String errorCode, String parameterName, String errorUri) { + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri); + throw new OAuth2AuthenticationException(error); + } +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/OAuth2ResourceOwnerPasswordAuthenticationProvider.java b/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/OAuth2ResourceOwnerPasswordAuthenticationProvider.java new file mode 100644 index 00000000000..efdede49ae0 --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/OAuth2ResourceOwnerPasswordAuthenticationProvider.java @@ -0,0 +1,188 @@ +package ca.corefacility.bioinformatics.irida.oauth2; + +import java.security.Principal; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.*; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder; +import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * An {@link AuthenticationProvider} impementation for the OAuth 2.0 Resource Owner Password Credentials Grant. + * + * @see OAuth2ResourceOwnerPasswordAuthenticationToken + * @see Section 4.3 Resource Owner Password Credentials Grant + * @see Section 4.3.2 Access Token Request + */ +public class OAuth2ResourceOwnerPasswordAuthenticationProvider implements AuthenticationProvider { + + private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; + private final AuthenticationManager authenticationManager; + private final OAuth2AuthorizationService authorizationService; + private final OAuth2TokenGenerator tokenGenerator; + + /** + * Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProvider} using the provided parameters. + */ + public OAuth2ResourceOwnerPasswordAuthenticationProvider(AuthenticationManager authenticationManager, + OAuth2AuthorizationService authorizationService, + OAuth2TokenGenerator tokenGenerator) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); + this.authenticationManager = authenticationManager; + this.authorizationService = authorizationService; + this.tokenGenerator = tokenGenerator; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + OAuth2ResourceOwnerPasswordAuthenticationToken resouceOwnerPasswordAuthentication = (OAuth2ResourceOwnerPasswordAuthenticationToken) authentication; + + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient( + resouceOwnerPasswordAuthentication); + + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); + } + + Authentication usernamePasswordAuthentication = getUsernamePasswordAuthentication( + resouceOwnerPasswordAuthentication); + + Set authorizedScopes = registeredClient.getScopes(); // Default to configured scopes + Set requestedScopes = resouceOwnerPasswordAuthentication.getScopes(); + if (!CollectionUtils.isEmpty(requestedScopes)) { + Set unauthorizedScopes = requestedScopes.stream() + .filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope)) + .collect(Collectors.toSet()); + if (!CollectionUtils.isEmpty(unauthorizedScopes)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE); + } + + authorizedScopes = new LinkedHashSet<>(requestedScopes); + } + + // @formatter:off + DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(usernamePasswordAuthentication) + .providerContext(ProviderContextHolder.getProviderContext()) + .authorizedScopes(authorizedScopes) + .authorizationGrantType(AuthorizationGrantType.PASSWORD) + .authorizationGrant(resouceOwnerPasswordAuthentication); + // @formatter:on + + // ----- Access token ----- + OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); + OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); + if (generatedAccessToken == null) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "The token generator failed to generate the access token.", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), + generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); + + // ----- Refresh token ----- + OAuth2RefreshToken refreshToken = null; + if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && + // Do not issue refresh token to public client + !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { + + tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); + OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); + if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "The token generator failed to generate the refresh token.", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + refreshToken = (OAuth2RefreshToken) generatedRefreshToken; + + } + + // @formatter:off + OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) + .principalName(usernamePasswordAuthentication.getName()) + .authorizationGrantType(AuthorizationGrantType.PASSWORD) + .attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes) + .attribute(Principal.class.getName(), usernamePasswordAuthentication); + // @formatter:on + if (generatedAccessToken instanceof ClaimAccessor) { + authorizationBuilder.token(accessToken, + (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, + ((ClaimAccessor) generatedAccessToken).getClaims())); + } else { + authorizationBuilder.accessToken(accessToken); + } + + OAuth2Authorization authorization = authorizationBuilder.build(); + + this.authorizationService.save(authorization); + + Map additionalParameters = Collections.emptyMap(); + + return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, + additionalParameters); + } + + @Override + public boolean supports(Class authentication) { + boolean supports = OAuth2ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication); + return supports; + } + + private Authentication getUsernamePasswordAuthentication( + OAuth2ResourceOwnerPasswordAuthenticationToken resouceOwnerPasswordAuthentication) { + + Map additionalParameters = resouceOwnerPasswordAuthentication.getAdditionalParameters(); + + String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME); + String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD); + + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( + username, password); + + Authentication usernamePasswordAuthentication = authenticationManager + .authenticate(usernamePasswordAuthenticationToken); + return usernamePasswordAuthentication; + } + + private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient( + Authentication authentication) { + + OAuth2ClientAuthenticationToken clientPrincipal = null; + + if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) { + clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal(); + } + + if (clientPrincipal != null && clientPrincipal.isAuthenticated()) { + return clientPrincipal; + } + + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/OAuth2ResourceOwnerPasswordAuthenticationToken.java b/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/OAuth2ResourceOwnerPasswordAuthenticationToken.java new file mode 100644 index 00000000000..a22ab4a5a09 --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/oauth2/OAuth2ResourceOwnerPasswordAuthenticationToken.java @@ -0,0 +1,46 @@ +package ca.corefacility.bioinformatics.irida.oauth2; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; + +/** + * An {@link} Authentication implementation used for the OAuth 2.0 Resource Owner Password Credentials Grant. + * + * @see OAuth2ResourceOwnerPasswordAuthenticationProvider + */ +public class OAuth2ResourceOwnerPasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { + + private final Set scopes; + + /** + * Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationToken} using the provider parameters. + * + * @param authorizationGrantType the authorization grant type + * @param clientPrincipal the authenticated client principal + * @param scopes the requested scope(s) + * @param additionalParameters the additional parameters + */ + public OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType authorizationGrantType, + Authentication clientPrincipal, @Nullable Set scopes, + @Nullable Map additionalParameters) { + super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters); + this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet()); + } + + /** + * Returns the requested scope(s). + * + * @return the requested scope(s), or an empty {@code Set} if not available + */ + public Set getScopes() { + return this.scopes; + } +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/repositories/relational/auditing/UserRevListener.java b/src/main/java/ca/corefacility/bioinformatics/irida/repositories/relational/auditing/UserRevListener.java index 444a0490fbf..464cf13a2c4 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/repositories/relational/auditing/UserRevListener.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/repositories/relational/auditing/UserRevListener.java @@ -1,10 +1,5 @@ package ca.corefacility.bioinformatics.irida.repositories.relational.auditing; -import ca.corefacility.bioinformatics.irida.model.IridaClientDetails; -import ca.corefacility.bioinformatics.irida.model.user.User; -import ca.corefacility.bioinformatics.irida.repositories.IridaClientDetailsRepository; -import ca.corefacility.bioinformatics.irida.repositories.user.UserRepository; - import org.hibernate.envers.RevisionListener; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; @@ -12,14 +7,19 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import ca.corefacility.bioinformatics.irida.model.IridaClientDetails; +import ca.corefacility.bioinformatics.irida.model.user.User; +import ca.corefacility.bioinformatics.irida.repositories.IridaClientDetailsRepository; +import ca.corefacility.bioinformatics.irida.repositories.user.UserRepository; /** * */ -public class UserRevListener implements RevisionListener, ApplicationContextAware{ - private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UserRevListener.class); +public class UserRevListener implements RevisionListener, ApplicationContextAware { + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UserRevListener.class); private static ApplicationContext applicationContext; private static UserRepository urepo; private static IridaClientDetailsRepository clientRepo; @@ -27,29 +27,26 @@ public class UserRevListener implements RevisionListener, ApplicationContextAwar @Override public void newRevision(Object revisionEntity) { UserRevEntity rev = (UserRevEntity) revisionEntity; - - try{ - UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - User userByUsername = urepo.loadUserByUsername(principal.getUsername()); - - if(userByUsername != null){ + + try { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User userByUsername = urepo.loadUserByUsername(username); + + if (userByUsername != null) { rev.setUserId(userByUsername.getId()); - } - else{ + } else { throw new IllegalStateException("User could not be read by username so revision could not be created"); } - + //Add the client ID if the user is connected via OAuth2 setClientId(rev); - + logger.trace("Revision created by user " + userByUsername.getUsername()); - } - catch(NullPointerException ex){ + } catch (NullPointerException ex) { logger.error("No user is set in the session so it cannot be added to the revision."); throw new IllegalStateException("The database cannot be modified if a user is not logged in."); } - - + } @Override @@ -57,37 +54,35 @@ public void setApplicationContext(ApplicationContext applicationContext) throws UserRevListener.applicationContext = applicationContext; } - /** - * Initialize the listener by getting dependencies - */ - public void initialize(){ + /** + * Initialize the listener by getting dependencies + */ + public void initialize() { urepo = applicationContext.getBean(UserRepository.class); clientRepo = applicationContext.getBean(IridaClientDetailsRepository.class); } - - /** - * Add the OAuth2 client ID to the revision listener if the user is - * connecting via OAuth2 - * - * @param entity - * The revision entity to modify if necessary - */ - private void setClientId(UserRevEntity entity) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - // If the user is connecting via OAuth2 this object will be an - // OAuth2Authentication - if (auth instanceof OAuth2Authentication) { - try { - logger.trace("Found OAuth2Authentication in session. Storing clientId in revision."); - OAuth2Authentication oAuth = (OAuth2Authentication) auth; - String clientId = oAuth.getOAuth2Request().getClientId(); - IridaClientDetails clientDetails = clientRepo.loadClientDetailsByClientId(clientId); - entity.setClientId(clientDetails.getId()); - } catch (NullPointerException ex) { - throw new IllegalStateException( - "The OAuth2 client details are not in the session so it cannot be added to the revision."); - } - } - } - + + /** + * Add the OAuth2 client ID to the revision listener if the user is connecting via OAuth2 + * + * @param entity The revision entity to modify if necessary + */ + private void setClientId(UserRevEntity entity) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + // If the user is connecting via OAuth2 this object will be a JwtAuthenticationToken + if (auth instanceof JwtAuthenticationToken) { + try { + logger.trace("Found JwtAuthenticationToken in session. Storing clientId in revision."); + JwtAuthenticationToken oAuth = (JwtAuthenticationToken) auth; + Jwt jwt = (Jwt) oAuth.getPrincipal(); + String clientId = jwt.getAudience().get(0); + IridaClientDetails clientDetails = clientRepo.loadClientDetailsByClientId(clientId); + entity.setClientId(clientDetails.getId()); + } catch (IndexOutOfBoundsException ex) { + throw new IllegalStateException( + "The OAuth2 client details are not in the session so it cannot be added to the revision."); + } + } + } + } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/Fast5ObjectRemoteRepositoryImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/Fast5ObjectRemoteRepositoryImpl.java index 28496798892..f2b575669d1 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/Fast5ObjectRemoteRepositoryImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/Fast5ObjectRemoteRepositoryImpl.java @@ -13,10 +13,11 @@ import ca.corefacility.bioinformatics.irida.repositories.remote.Fast5ObjectRemoteRepository; import ca.corefacility.bioinformatics.irida.repositories.remote.resttemplate.OAuthTokenRestTemplate; import ca.corefacility.bioinformatics.irida.service.RemoteAPITokenService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; /** - * A repository implementaion for reading {@link Fast5Object} from remote - * locations using a {@link OAuthTokenRestTemplate} + * A repository implementaion for reading {@link Fast5Object} from remote locations using a + * {@link OAuthTokenRestTemplate} */ @Repository public class Fast5ObjectRemoteRepositoryImpl extends RemoteRepositoryImpl @@ -29,8 +30,8 @@ public class Fast5ObjectRemoteRepositoryImpl extends RemoteRepositoryImpl @@ -36,8 +38,8 @@ public class GenomeAssemblyRemoteRepositoryImpl extends RemoteRepositoryImpl i private static final String HASH_REL = RESTProjectsController.PROJECT_HASH_REL; /** - * Create a new {@link ProjectRemoteRepositoryImpl} with the given - * {@link RemoteAPITokenService} + * Create a new {@link ProjectRemoteRepositoryImpl} with the given {@link RemoteAPITokenService} * * @param tokenService the {@link RemoteAPITokenService} + * @param userService The {@link UserService} for reading users */ @Autowired - public ProjectRemoteRepositoryImpl(RemoteAPITokenService tokenService) { - super(tokenService, listTypeReference, objectTypeReference); + public ProjectRemoteRepositoryImpl(RemoteAPITokenService tokenService, UserService userService) { + super(tokenService, userService, listTypeReference, objectTypeReference); this.tokenService = tokenService; } @@ -63,7 +64,8 @@ public Integer readProjectHash(Project project) { OAuthTokenRestTemplate restTemplate = new OAuthTokenRestTemplate(tokenService, remoteAPI); Link link = project.getLink(HASH_REL).map(i -> i).orElse(null); - ResponseEntity> exchange = restTemplate.exchange(link.getHref(), HttpMethod.GET, HttpEntity.EMPTY, projectHashReference); + ResponseEntity> exchange = restTemplate.exchange(link.getHref(), + HttpMethod.GET, HttpEntity.EMPTY, projectHashReference); Integer projectHash = exchange.getBody().getResource().getProjectHash(); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/RemoteRepositoryImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/RemoteRepositoryImpl.java index 39117e10721..fddc053866c 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/RemoteRepositoryImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/RemoteRepositoryImpl.java @@ -7,7 +7,10 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import ca.corefacility.bioinformatics.irida.model.IridaRepresentationModel; import ca.corefacility.bioinformatics.irida.model.RemoteAPI; @@ -18,21 +21,20 @@ import ca.corefacility.bioinformatics.irida.repositories.remote.RemoteRepository; import ca.corefacility.bioinformatics.irida.repositories.remote.resttemplate.OAuthTokenRestTemplate; import ca.corefacility.bioinformatics.irida.service.RemoteAPITokenService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; /** - * Remote repository to request from remote IRIDA instances using - * {@link OAuthTokenRestTemplate}s + * Remote repository to request from remote IRIDA instances using {@link OAuthTokenRestTemplate}s * - * - * @param - * The type of object to be stored in this repository (extends - * {@link IridaRepresentationModel}) + * @param The type of object to be stored in this repository (extends {@link IridaRepresentationModel}) */ public abstract class RemoteRepositoryImpl implements RemoteRepository { // service storing api tokens for communication with the remote services private RemoteAPITokenService tokenService; + private UserService userService; + // type references for the resources being read by this repository final protected ParameterizedTypeReference> listTypeReference; final protected ParameterizedTypeReference> objectTypeReference; @@ -40,20 +42,17 @@ public abstract class RemoteRepositoryImpl> listTypeReference, ParameterizedTypeReference> objectTypeReference) { this.tokenService = tokenService; + this.userService = userService; this.listTypeReference = listTypeReference; this.objectTypeReference = objectTypeReference; } @@ -111,12 +110,14 @@ public boolean getServiceStatus(RemoteAPI remoteAPI) { protected T setRemoteStatus(T entity, RemoteAPI api) { String selfHref = entity.getSelfHref(); - // Get the logged in user and set them in the remote status object - Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); RemoteStatus remoteStatus = new RemoteStatus(selfHref, api); - if (principal instanceof User) { - remoteStatus.setReadBy((User) principal); + if (authentication instanceof UsernamePasswordAuthenticationToken) { + remoteStatus.setReadBy((User) authentication.getPrincipal()); + } else if (authentication instanceof JwtAuthenticationToken) { + User user = userService.getUserByUsername(authentication.getName()); + remoteStatus.setReadBy(user); } remoteStatus.setRemoteHashCode(entity.hashCode()); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/SampleRemoteRepositoryImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/SampleRemoteRepositoryImpl.java index 435f99038b5..75753e6c985 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/SampleRemoteRepositoryImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/SampleRemoteRepositoryImpl.java @@ -24,12 +24,11 @@ import ca.corefacility.bioinformatics.irida.repositories.remote.SampleRemoteRepository; import ca.corefacility.bioinformatics.irida.repositories.remote.resttemplate.OAuthTokenRestTemplate; import ca.corefacility.bioinformatics.irida.service.RemoteAPITokenService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; import ca.corefacility.bioinformatics.irida.web.controller.api.samples.RESTSampleMetadataController; /** * An implementation of {@link SampleRemoteRepository} - * - * */ @Repository public class SampleRemoteRepositoryImpl extends RemoteRepositoryImpl implements SampleRemoteRepository { @@ -49,8 +48,8 @@ public class SampleRemoteRepositoryImpl extends RemoteRepositoryImpl imp private RemoteAPITokenService tokenService; @Autowired - public SampleRemoteRepositoryImpl(RemoteAPITokenService tokenService) { - super(tokenService, listTypeReference, objectTypeReference); + public SampleRemoteRepositoryImpl(RemoteAPITokenService tokenService, UserService userService) { + super(tokenService, userService, listTypeReference, objectTypeReference); this.tokenService = tokenService; } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/SequenceFilePairRemoteRepositoryImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/SequenceFilePairRemoteRepositoryImpl.java index 1af4daca80e..5c4139f6f2e 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/SequenceFilePairRemoteRepositoryImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/SequenceFilePairRemoteRepositoryImpl.java @@ -11,10 +11,10 @@ import ca.corefacility.bioinformatics.irida.model.sequenceFile.SequenceFilePair; import ca.corefacility.bioinformatics.irida.repositories.remote.SequenceFilePairRemoteRepository; import ca.corefacility.bioinformatics.irida.service.RemoteAPITokenService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; /** - * {@link RemoteRepositoryImpl} for reading and listing {@link SequenceFilePair} - * objects. + * {@link RemoteRepositoryImpl} for reading and listing {@link SequenceFilePair} objects. */ @Repository public class SequenceFilePairRemoteRepositoryImpl extends RemoteRepositoryImpl @@ -25,8 +25,8 @@ public class SequenceFilePairRemoteRepositoryImpl extends RemoteRepositoryImpl implements - SequenceFileRemoteRepository { +public class SequenceFileRemoteRepositoryImpl extends RemoteRepositoryImpl + implements SequenceFileRemoteRepository { public static final MediaType DEFAULT_DOWNLOAD_MEDIA_TYPE = new MediaType("application", "fastq"); private static final ParameterizedTypeReference> listTypeReference = new ParameterizedTypeReference<>() { }; @@ -40,12 +38,12 @@ public class SequenceFileRemoteRepositoryImpl extends RemoteRepositoryImpl implements - SingleEndSequenceFileRemoteRepository { +public class SingleEndSequenceFileRemoteRepositoryImpl extends RemoteRepositoryImpl + implements SingleEndSequenceFileRemoteRepository { private static final ParameterizedTypeReference> listTypeReference = new ParameterizedTypeReference>() { }; @@ -26,10 +26,10 @@ public class SingleEndSequenceFileRemoteRepositoryImpl extends RemoteRepositoryI }; @Autowired - public SingleEndSequenceFileRemoteRepositoryImpl(RemoteAPITokenService tokenService) { - super(tokenService, listTypeReference, objectTypeReference); + public SingleEndSequenceFileRemoteRepositoryImpl(RemoteAPITokenService tokenService, UserService userService) { + super(tokenService, userService, listTypeReference, objectTypeReference); } - + @Override protected T setRemoteStatus(T entity, RemoteAPI api) { entity = super.setRemoteStatus(entity, api); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/config/UserSecurityInterceptor.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/config/UserSecurityInterceptor.java index ac7bdaed554..50a282a6e24 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/config/UserSecurityInterceptor.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/config/UserSecurityInterceptor.java @@ -4,8 +4,11 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.ui.Model; @@ -13,7 +16,7 @@ import org.springframework.web.servlet.ModelAndView; import ca.corefacility.bioinformatics.irida.model.user.Role; -import ca.corefacility.bioinformatics.irida.model.user.User; +import ca.corefacility.bioinformatics.irida.repositories.user.UserRepository; /** * Interceptor Adaptor to add the user to the {@link Model} each server call. Also ensures {@link Role#ROLE_SEQUENCER} @@ -22,6 +25,10 @@ public class UserSecurityInterceptor implements AsyncHandlerInterceptor { public static final String CURRENT_USER_DETAILS = "CURRENT_USER_DETAILS"; + @Lazy + @Autowired + private UserRepository userRepository; + /** * {@inheritDoc} */ @@ -30,11 +37,11 @@ public void postHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView modelAndView) throws Exception { HttpSession session = request.getSession(); - User userDetails = (User) session.getAttribute(CURRENT_USER_DETAILS); - if (userDetails == null && isAuthenticated()) { - UserDetails currentUserDetails = (UserDetails) SecurityContextHolder.getContext() - .getAuthentication() - .getPrincipal(); + if (isAuthenticated()) { + Authentication currentAuthentication = (Authentication) SecurityContextHolder.getContext() + .getAuthentication(); + UserDetails currentUserDetails = (UserDetails) userRepository + .loadUserByUsername(currentAuthentication.getName()); // Disallow SEQUENCER role from doing anything in the IRIDA UI if (currentUserDetails.getAuthorities().contains(Role.ROLE_SEQUENCER)) { diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/security/LoginSuccessHandler.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/security/LoginSuccessHandler.java index 0e334090187..d66186ef158 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/security/LoginSuccessHandler.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/security/LoginSuccessHandler.java @@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.web.servlet.LocaleResolver; @@ -53,7 +54,8 @@ public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpS super.onAuthenticationSuccess(httpServletRequest, httpServletResponse, authentication); HttpSession session = httpServletRequest.getSession(); - User user = (User) authentication.getPrincipal(); + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userRepository.loadUserByUsername(username); //set the user's selected locale try { diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/ClientsAjaxController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/ClientsAjaxController.java index 793112c0c25..5ad3dc15d91 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/ClientsAjaxController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/ClientsAjaxController.java @@ -7,9 +7,9 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.web.bind.annotation.*; +import ca.corefacility.bioinformatics.irida.exceptions.EntityNotFoundException; import ca.corefacility.bioinformatics.irida.ria.web.ajax.dto.ajax.AjaxErrorResponse; import ca.corefacility.bioinformatics.irida.ria.web.ajax.dto.ajax.AjaxResponse; import ca.corefacility.bioinformatics.irida.ria.web.ajax.dto.ajax.AjaxSuccessResponse; @@ -71,7 +71,7 @@ public ResponseEntity validateClientName(@RequestParam String clientId, service.validateClientId(clientId); return ResponseEntity.status(HttpStatus.CONFLICT) .body(messageSource.getMessage("server.AddClientForm.error", new Object[] { clientId }, locale)); - } catch (NoSuchClientException e) { + } catch (EntityNotFoundException e) { return ResponseEntity.ok(""); } } @@ -110,7 +110,7 @@ public ResponseEntity updateClient(@RequestBody CreateUpdateClient } catch (Exception exception) { return ResponseEntity.status(HttpStatus.CONFLICT) .body(new AjaxErrorResponse(messageSource.getMessage("server.UpdateClientForm.error", - new Object[]{request.getClientId()}, locale))); + new Object[] { request.getClientId() }, locale))); } } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/RemoteAPIAjaxController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/RemoteAPIAjaxController.java index 021d9082015..d82a48923cc 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/RemoteAPIAjaxController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/ajax/RemoteAPIAjaxController.java @@ -21,6 +21,8 @@ import ca.corefacility.bioinformatics.irida.ria.web.rempoteapi.dto.RemoteAPITableModel; import ca.corefacility.bioinformatics.irida.ria.web.services.UIRemoteAPIService; import ca.corefacility.bioinformatics.irida.service.RemoteAPIService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; + import com.google.common.collect.ImmutableMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; @@ -50,6 +52,7 @@ public class RemoteAPIAjaxController extends BaseController { private final RemoteAPIService remoteAPIService; private final UIRemoteAPIService service; private final MessageSource messageSource; + private final UserService userService; // Map storing the message names for the // getErrorsFromDataIntegrityViolationException method @@ -57,13 +60,13 @@ public class RemoteAPIAjaxController extends BaseController { RemoteAPI.SERVICE_URI_CONSTRAINT_NAME, new ExceptionPropertyAndMessage("serviceURI", "remoteapi.create.serviceURIConflict")); - @Autowired public RemoteAPIAjaxController(RemoteAPIService remoteAPIService, UIRemoteAPIService service, - MessageSource messageSource) { + MessageSource messageSource, UserService userService) { this.remoteAPIService = remoteAPIService; this.service = service; this.messageSource = messageSource; + this.userService = userService; } /** @@ -78,9 +81,8 @@ public TableResponse getAjaxAPIList(@RequestBody TableReque RemoteAPISpecification.searchRemoteAPI(tableRequest.getSearch()), tableRequest.getCurrent(), tableRequest.getPageSize(), tableRequest.getSortDirection(), tableRequest.getSortColumn()); - Authentication authentication = SecurityContextHolder.getContext() - .getAuthentication(); - User user = (User) authentication.getPrincipal(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + User user = userService.getUserByUsername(authentication.getName()); boolean isAdmin = user.getSystemRole().equals(Role.ROLE_ADMIN); List apiData = search.getContent() @@ -91,8 +93,7 @@ public TableResponse getAjaxAPIList(@RequestBody TableReque } /** - * Check the currently logged in user's OAuth2 connection status to a given - * API + * Check the currently logged in user's OAuth2 connection status to a given API * * @param apiId The ID of the api * @return "valid" or "invalid_token" message @@ -102,9 +103,8 @@ public ResponseEntity checkAPIStatus(@PathVariable Long apiId) { try { return ResponseEntity.ok(service.checkAPIStatus(apiId)); } catch (IridaOAuthException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(null); - }catch (EntityNotFoundException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null); + } catch (EntityNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); } } @@ -152,11 +152,9 @@ public ResponseEntity createSynchronizedProject(@RequestBody Creat try { return ResponseEntity.ok(service.createSynchronizedProject(request)); } catch (IridaOAuthException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(new AjaxErrorResponse(e.getMessage())); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new AjaxErrorResponse(e.getMessage())); } catch (EntityNotFoundException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(new AjaxErrorResponse(e.getMessage())); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new AjaxErrorResponse(e.getMessage())); } } @@ -179,7 +177,6 @@ public ResponseEntity postCreateRemoteAPI(RemoteAPI client, Locale } catch (DataIntegrityViolationException e) { errors = getErrorsFromDataIntegrityViolationException(e, errorMessages, messageSource, locale); } - return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) - .body(new AjaxFormErrorResponse(errors)); + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(new AjaxFormErrorResponse(errors)); } } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/analysis/AnalysesTableAjaxController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/analysis/AnalysesTableAjaxController.java index 3aa0be2b246..6a5b49672db 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/analysis/AnalysesTableAjaxController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/analysis/AnalysesTableAjaxController.java @@ -37,6 +37,7 @@ import ca.corefacility.bioinformatics.irida.service.AnalysisSubmissionService; import ca.corefacility.bioinformatics.irida.service.AnalysisTypesService; import ca.corefacility.bioinformatics.irida.service.ProjectService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; import ca.corefacility.bioinformatics.irida.service.workflow.IridaWorkflowsService; import com.google.common.base.Strings; @@ -54,12 +55,14 @@ public class AnalysesTableAjaxController { private MessageSource messageSource; private UpdateAnalysisSubmissionPermission updateAnalysisSubmissionPermission; private AnalysisAudit analysisAudit; + private UserService userService; @Autowired public AnalysesTableAjaxController(AnalysisSubmissionService analysisSubmissionService, AnalysisTypesService analysisTypesService, ProjectService projectService, IridaWorkflowsService iridaWorkflowsService, MessageSource messageSource, - UpdateAnalysisSubmissionPermission updateAnalysisSubmissionPermission, AnalysisAudit analysisAudit) { + UpdateAnalysisSubmissionPermission updateAnalysisSubmissionPermission, AnalysisAudit analysisAudit, + UserService userService) { this.analysisSubmissionService = analysisSubmissionService; this.analysisTypesService = analysisTypesService; this.projectService = projectService; @@ -67,6 +70,7 @@ public AnalysesTableAjaxController(AnalysisSubmissionService analysisSubmissionS this.messageSource = messageSource; this.updateAnalysisSubmissionPermission = updateAnalysisSubmissionPermission; this.analysisAudit = analysisAudit; + this.userService = userService; } /** @@ -103,7 +107,7 @@ public List getWorkflowTypes(Locale locale) { /** * Returns a list of analyses based on paging, sorting and filter requirements sent in {@link AnalysesListRequest} * - * @param analysesListRequest description of the paging requirements. Includes sorting, filtering, and paging + * @param analysesListRequest description of the paging requirements. Includes sorting, filtering, and paging * @param admin {@link Boolean} whether this is from the admin page. * @param projectId {@link Long} if the request is from a specific project * @param locale of the current user @@ -115,35 +119,28 @@ public TableResponse getPagedAnalyses(@RequestBody AnalysesListRe @RequestParam(required = false, defaultValue = "false") Boolean admin, @RequestParam(required = false) Long projectId, Locale locale) throws IridaWorkflowNotFoundException { - Authentication authentication = SecurityContextHolder.getContext() - .getAuthentication(); - User user = (User) authentication.getPrincipal(); - + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + User user = userService.getUserByUsername(authentication.getName()); + /* Check to see if there is a name filter */ String nameFilter = null; - if (!Strings.isNullOrEmpty(analysesListRequest.getFilters() - .getName())) { - nameFilter = analysesListRequest.getFilters() - .getName(); + if (!Strings.isNullOrEmpty(analysesListRequest.getFilters().getName())) { + nameFilter = analysesListRequest.getFilters().getName(); } /* Check to see if we are filtering by workflow type */ Set workflowIds = new HashSet<>(); - if (analysesListRequest.getFilters() - .getType() - .size() > 0) { - List workflowTypesFilter = analysesListRequest.getFilters() - .getType(); + if (analysesListRequest.getFilters().getType().size() > 0) { + List workflowTypesFilter = analysesListRequest.getFilters().getType(); for (String type : workflowTypesFilter) { AnalysisType workflowType = analysisTypesService.fromString(type); Set workflows = iridaWorkflowsService.getAllWorkflowsByType(workflowType); - workflowIds.addAll(workflows.stream() - .map(IridaWorkflow::getWorkflowIdentifier) - .collect(Collectors.toSet())); + workflowIds.addAll( + workflows.stream().map(IridaWorkflow::getWorkflowIdentifier).collect(Collectors.toSet())); } } @@ -151,11 +148,8 @@ public TableResponse getPagedAnalyses(@RequestBody AnalysesListRe Check to see if the filter includes the state. This can be any number of states. */ Set stateFilters = new HashSet<>(); - if (analysesListRequest.getFilters() - .getState() - .size() > 0) { - List states = analysesListRequest.getFilters() - .getState(); + if (analysesListRequest.getFilters().getState().size() > 0) { + List states = analysesListRequest.getFilters().getState(); stateFilters.addAll(states); } @@ -167,8 +161,7 @@ public TableResponse getPagedAnalyses(@RequestBody AnalysesListRe Project project = projectService.read(projectId); page = analysisSubmissionService.listSubmissionsForProject(analysesListRequest.getSearch(), nameFilter, stateFilters, workflowIds, project, pageRequest); - } else if (admin && user.getSystemRole() - .equals(Role.ROLE_ADMIN)) { + } else if (admin && user.getSystemRole().equals(Role.ROLE_ADMIN)) { // User is an admin and requesting the listing of all pages. page = analysisSubmissionService.listAllSubmissions(analysesListRequest.getSearch(), nameFilter, stateFilters, workflowIds, pageRequest); @@ -214,14 +207,13 @@ public void deleteAnalysisSubmissions(@RequestParam List ids, HttpServletR private AnalysisModel createAnalysisModel(AnalysisSubmission submission, Locale locale) { AnalysisState analysisState = submission.getAnalysisState(); IridaWorkflow iridaWorkflow = iridaWorkflowsService.getIridaWorkflowOrUnknown(submission); - String workflowType = iridaWorkflow.getWorkflowDescription() - .getAnalysisType() - .getType(); + String workflowType = iridaWorkflow.getWorkflowDescription().getAnalysisType().getType(); String stateString = messageSource.getMessage("analysis.state." + analysisState.toString(), null, locale); AnalysisStateModel state = new AnalysisStateModel(stateString, analysisState.toString()); String workflow = messageSource.getMessage("workflow." + workflowType + ".title", null, workflowType, locale); Long duration; - if(submission.getAnalysisState() != AnalysisState.COMPLETED && submission.getAnalysisState() != AnalysisState.ERROR) { + if (submission.getAnalysisState() != AnalysisState.COMPLETED + && submission.getAnalysisState() != AnalysisState.ERROR) { Date currentDate = new Date(); duration = DateUtilities.getDurationInMilliseconds(submission.getCreatedDate(), currentDate); } else { @@ -231,8 +223,7 @@ private AnalysisModel createAnalysisModel(AnalysisSubmission submission, Locale /* Check to see if the user has authority to update (delete) this particular submission. */ - Authentication authentication = SecurityContextHolder.getContext() - .getAuthentication(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); boolean updatePermission = this.updateAnalysisSubmissionPermission.isAllowed(authentication, submission); return new AnalysisModel(submission, state, duration, workflow, updatePermission); @@ -255,6 +246,7 @@ public void downloadAnalysis(@PathVariable Long id, HttpServletResponse response /** * Fetch the current status of the analysis server. + * * @return {@link Map} of the running and queued counts. */ @RequestMapping("/queue") @@ -266,11 +258,12 @@ public AnalysisSubmissionService.AnalysisServiceStatus fetchAnalysesQueueCounts( * Get the updated state and duration of an analysis * * @param submissionId The analysis submission id - * @param locale {@link Locale} Users locale + * @param locale {@link Locale} Users locale * @return dto which contains the updated analysis state and duration */ @RequestMapping(value = "/{submissionId}/updated-table-progress") - public ResponseEntity getUpdatedProgress(@PathVariable Long submissionId, Locale locale) { + public ResponseEntity getUpdatedProgress(@PathVariable Long submissionId, + Locale locale) { AnalysisSubmission submission = analysisSubmissionService.read(submissionId); AnalysisState analysisState = submission.getAnalysisState(); @@ -280,7 +273,8 @@ public ResponseEntity getUpdatedProgress(@PathVari // Get the run time of the analysis runtime using the analysis Long duration; - if(submission.getAnalysisState() != AnalysisState.COMPLETED && submission.getAnalysisState() != AnalysisState.ERROR) { + if (submission.getAnalysisState() != AnalysisState.COMPLETED + && submission.getAnalysisState() != AnalysisState.ERROR) { Date currentDate = new Date(); duration = DateUtilities.getDurationInMilliseconds(submission.getCreatedDate(), currentDate); } else { diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/OltuAuthorizationController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/OltuAuthorizationController.java index 3a6d6a2af29..ba8a11fa186 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/OltuAuthorizationController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/OltuAuthorizationController.java @@ -1,9 +1,13 @@ package ca.corefacility.bioinformatics.irida.ria.web.oauth; import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import javax.ws.rs.core.UriBuilder; import org.apache.oltu.oauth2.client.request.OAuthClientRequest; @@ -45,30 +49,32 @@ public OltuAuthorizationController(RemoteAPITokenService tokenService, RemoteAPI } /** - * Begin authentication procedure by redirecting to remote authorization - * location + * Begin authentication procedure by redirecting to remote authorization location * * @param remoteAPI The API we need to authenticate with - * @param redirect The location to redirect back to after authentication is - * complete + * @param redirect The location to redirect back to after authentication is complete * @return A ModelAndView beginning the authentication procedure * @throws OAuthSystemException if we can't read from the authorization server. */ - public String authenticate(RemoteAPI remoteAPI, String redirect) throws OAuthSystemException { + public String authenticate(HttpSession session, RemoteAPI remoteAPI, String redirect) throws OAuthSystemException { // get the URI for the remote service we'll be requesting from String serviceURI = remoteAPI.getServiceURI(); // build the authorization path - URI serviceAuthLocation = UriBuilder.fromUri(serviceURI) - .path("oauth") - .path("authorize") - .build(); + URI serviceAuthLocation = UriBuilder.fromUri(serviceURI).path("oauth").path("authorize").build(); logger.debug("Authenticating for service: " + remoteAPI); logger.debug("Redirect after authentication: " + redirect); // build a redirect URI to redirect to after auth flow is completed - String tokenRedirect = buildRedirectURI(remoteAPI.getId(), redirect); + String tokenRedirect = buildRedirectURI(); + + // build state object which is used to extract the authCode to the correct remoteAPI + String stateUuid = UUID.randomUUID().toString(); + Map stateMap = new HashMap(); + stateMap.put("apiId", remoteAPI.getId().toString()); + stateMap.put("redirect", redirect); + session.setAttribute(stateUuid, stateMap); // build the redirect query to request an authorization code from the // remote API @@ -77,6 +83,7 @@ public String authenticate(RemoteAPI remoteAPI, String redirect) throws OAuthSys .setRedirectURI(tokenRedirect) .setResponseType(ResponseType.CODE.toString()) .setScope("read") + .setState(stateUuid) .buildQueryMessage(); String locURI = request.getLocationUri(); @@ -90,25 +97,29 @@ public String authenticate(RemoteAPI remoteAPI, String redirect) throws OAuthSys * * @param request The incoming request * @param response The response to redirect - * @param apiId the Long ID of the API we're requesting from - * @param redirect The URL location to redirect to after completion - * @return A ModelAndView redirecting back to the resource that was - * requested + * @param state The state param which contains a map including apiId and redirect + * @return A ModelAndView redirecting back to the resource that was requested * @throws OAuthSystemException if we can't get an access token for the current request. * @throws OAuthProblemException if we can't get a response from the authorization server */ @RequestMapping(TOKEN_ENDPOINT) public String getTokenFromAuthCode(HttpServletRequest request, HttpServletResponse response, - @RequestParam("apiId") Long apiId, @RequestParam("redirect") String redirect) - throws OAuthSystemException, OAuthProblemException { + @RequestParam("state") String state) throws OAuthSystemException, OAuthProblemException { // Get the OAuth2 auth code OAuthAuthzResponse oar = OAuthAuthzResponse.oauthCodeAuthzResponse(request); String code = oar.getCode(); logger.trace("Received auth code: " + code); + HttpSession session = request.getSession(); + + Map stateMap = (Map) session.getAttribute(state); + + Long apiId = Long.parseLong(stateMap.get("apiId")); + String redirect = stateMap.get("redirect"); + // Build the redirect URI to request a token from - String tokenRedirect = buildRedirectURI(apiId, redirect); + String tokenRedirect = buildRedirectURI(); // Read the RemoteAPI from the RemoteAPIService and get the base URI RemoteAPI remoteAPI = remoteAPIService.read(apiId); @@ -122,18 +133,11 @@ public String getTokenFromAuthCode(HttpServletRequest request, HttpServletRespon /** * Build the redirect URI for the token page with the API and resource page * - * @param apiId the Long ID of the API to request from - * @param redirectPage the resource page to redirect to once the authorizatino is - * complete * @return */ - private String buildRedirectURI(Long apiId, String redirectPage) { + private String buildRedirectURI() { - URI build = UriBuilder.fromUri(serverBase) - .path(TOKEN_ENDPOINT) - .queryParam("apiId", apiId) - .queryParam("redirect", redirectPage) - .build(); + URI build = UriBuilder.fromUri(serverBase).path(TOKEN_ENDPOINT).build(); return build.toString(); } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteAPIController.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteAPIController.java index 29addf1cf0c..2403e36bb6e 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteAPIController.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteAPIController.java @@ -1,12 +1,10 @@ package ca.corefacility.bioinformatics.irida.ria.web.oauth; -import ca.corefacility.bioinformatics.irida.exceptions.IridaOAuthException; -import ca.corefacility.bioinformatics.irida.model.RemoteAPI; -import ca.corefacility.bioinformatics.irida.ria.utilities.ExceptionPropertyAndMessage; -import ca.corefacility.bioinformatics.irida.ria.web.BaseController; -import ca.corefacility.bioinformatics.irida.service.RemoteAPIService; -import ca.corefacility.bioinformatics.irida.service.remote.ProjectRemoteService; -import com.google.common.collect.ImmutableMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,12 +16,17 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.HandlerMapping; -import javax.servlet.http.HttpServletRequest; -import java.util.Map; +import ca.corefacility.bioinformatics.irida.exceptions.IridaOAuthException; +import ca.corefacility.bioinformatics.irida.model.RemoteAPI; +import ca.corefacility.bioinformatics.irida.ria.utilities.ExceptionPropertyAndMessage; +import ca.corefacility.bioinformatics.irida.ria.web.BaseController; +import ca.corefacility.bioinformatics.irida.service.RemoteAPIService; +import ca.corefacility.bioinformatics.irida.service.remote.ProjectRemoteService; + +import com.google.common.collect.ImmutableMap; /** - * Controller handling basic operations for listing, viewing, adding, and - * removing {@link RemoteAPI}s + * Controller handling basic operations for listing, viewing, adding, and removing {@link RemoteAPI}s */ @Controller @RequestMapping("/remote_api") @@ -40,11 +43,12 @@ public class RemoteAPIController extends BaseController { // Map storing the message names for the // getErrorsFromDataIntegrityViolationException method private final Map errorMessages = ImmutableMap.of( - RemoteAPI.SERVICE_URI_CONSTRAINT_NAME, new ExceptionPropertyAndMessage("serviceURI", - "remoteapi.create.serviceURIConflict")); + RemoteAPI.SERVICE_URI_CONSTRAINT_NAME, + new ExceptionPropertyAndMessage("serviceURI", "remoteapi.create.serviceURIConflict")); @Autowired - public RemoteAPIController(RemoteAPIService remoteAPIService, ProjectRemoteService projectRemoteService, OltuAuthorizationController authController) { + public RemoteAPIController(RemoteAPIService remoteAPIService, ProjectRemoteService projectRemoteService, + OltuAuthorizationController authController) { this.remoteAPIService = remoteAPIService; this.projectRemoteService = projectRemoteService; this.authController = authController; @@ -61,10 +65,8 @@ public String list() { } /** - * Initiate a token request on a remote api if one does not yet exist. Works - * with - * {@link #handleOAuthException(HttpServletRequest, IridaOAuthException)} to - * initiate the request. + * Initiate a token request on a remote api if one does not yet exist. Works with + * {@link #handleOAuthException(HttpServletRequest, IridaOAuthException)} to initiate the request. * * @param apiId the ID of the api to connect to * @param model the model to add attributes to. @@ -84,16 +86,16 @@ public String connectToAPI(@PathVariable Long apiId, Model model) { * * @param request The incoming request method * @param ex The thrown exception - * @return A redirect to the {@link OltuAuthorizationController}'s - * authentication + * @return A redirect to the {@link OltuAuthorizationController}'s authentication * @throws OAuthSystemException if the request cannot be authenticated. */ @ExceptionHandler(IridaOAuthException.class) public String handleOAuthException(HttpServletRequest request, IridaOAuthException ex) throws OAuthSystemException { logger.debug("Caught IridaOAuthException. Beginning OAuth2 authentication token flow."); String requestURI = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + HttpSession session = request.getSession(); - return authController.authenticate(ex.getRemoteAPI(), requestURI); + return authController.authenticate(session, ex.getRemoteAPI(), requestURI); } } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAssociatedProjectsService.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAssociatedProjectsService.java index 61189169aed..11d205e3b85 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAssociatedProjectsService.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIAssociatedProjectsService.java @@ -24,6 +24,7 @@ import ca.corefacility.bioinformatics.irida.ria.web.projects.settings.dto.AssociatedProject; import ca.corefacility.bioinformatics.irida.security.permissions.project.ProjectOwnerPermission; import ca.corefacility.bioinformatics.irida.service.ProjectService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; /** * Service for handling associated projects @@ -33,13 +34,15 @@ public class UIAssociatedProjectsService { private final ProjectService projectService; private final ProjectOwnerPermission projectOwnerPermission; private final MessageSource messageSource; + private final UserService userService; @Autowired public UIAssociatedProjectsService(ProjectService projectService, ProjectOwnerPermission projectOwnerPermission, - MessageSource messageSource) { + MessageSource messageSource, UserService userService) { this.projectService = projectService; this.projectOwnerPermission = projectOwnerPermission; this.messageSource = messageSource; + this.userService = userService; } /** @@ -66,7 +69,7 @@ public List getAssociatedProjectsForProject(Long projectId) { public List getAssociatedProjects(Long projectId) { Project project = projectService.read(projectId); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - User user = (User) authentication.getPrincipal(); + User user = userService.getUserByUsername(authentication.getName()); boolean hasPermission = user.getSystemRole().equals(Role.ROLE_ADMIN) || projectOwnerPermission.isAllowed(authentication, project); List relatedProjectJoins = projectService.getRelatedProjects(project); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIClientService.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIClientService.java index 8bb7509c60b..5519c85d512 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIClientService.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIClientService.java @@ -13,10 +13,10 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.Specification; -import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.stereotype.Component; import ca.corefacility.bioinformatics.irida.exceptions.EntityExistsException; +import ca.corefacility.bioinformatics.irida.exceptions.EntityNotFoundException; import ca.corefacility.bioinformatics.irida.model.IridaClientDetails; import ca.corefacility.bioinformatics.irida.repositories.specification.IridaClientDetailsSpecification; import ca.corefacility.bioinformatics.irida.ria.web.ajax.dto.clients.ClientTableModel; @@ -35,7 +35,6 @@ public class UIClientService { private final IridaClientDetailsService clientDetailsService; private final MessageSource messageSource; - private final String AUTO_APPROVE = "auto"; private final String SCOPE_READ = "read"; private final String SCOPE_WRITE = "write"; private final String GRANT_TYPE_AUTH_CODE = "authorization_code"; @@ -52,8 +51,8 @@ public UIClientService(IridaClientDetailsService clientDetailsService, MessageSo * @return Current status of the table */ public TableResponse getClientList(ClientTableRequest tableRequest) { - Specification specification = IridaClientDetailsSpecification.searchClient( - tableRequest.getSearch()); + Specification specification = IridaClientDetailsSpecification + .searchClient(tableRequest.getSearch()); Page page = clientDetailsService.search(specification, PageRequest.of(tableRequest.getCurrent(), tableRequest.getPageSize(), tableRequest.getSort())); @@ -79,9 +78,9 @@ public void deleteClientTokens(Long id) { * Validate a client identifier for a new client * * @param clientId Identifier to check to see if it exists - * @throws NoSuchClientException thrown if a client does not exist with the given client id. + * @throws EntityNotFoundException thrown if a client does not exist with the given client id. */ - public void validateClientId(String clientId) throws NoSuchClientException { + public void validateClientId(String clientId) throws EntityNotFoundException { clientDetailsService.loadClientByClientId(clientId); } @@ -112,32 +111,19 @@ public String createOrUpdateClient(CreateUpdateClientDetails request, Locale loc // Let's set up the scopes for this client Set scopes = new HashSet<>(); - Set autoScopes = new HashSet<>(); // 1. Read scope - if (request.getRead() - .equals(SCOPE_READ)) { + if (request.getRead().equals(SCOPE_READ)) { scopes.add(SCOPE_READ); - } else if (request.getRead() - .equals(AUTO_APPROVE)) { - scopes.add(SCOPE_READ); - autoScopes.add(SCOPE_READ); } // 2. Write scope - if (request.getWrite() - .equals(SCOPE_WRITE)) { - scopes.add(SCOPE_WRITE); - } else if (request.getWrite() - .equals(AUTO_APPROVE)) { + if (request.getWrite().equals(SCOPE_WRITE)) { scopes.add(SCOPE_WRITE); - autoScopes.add(SCOPE_WRITE); } client.setScope(scopes); - client.setAutoApprovableScopes(autoScopes); // Set the grant type client.setAuthorizedGrantTypes(Sets.newHashSet(request.getGrantType())); - if (request.getGrantType() - .equals(GRANT_TYPE_AUTH_CODE)) { + if (request.getGrantType().equals(GRANT_TYPE_AUTH_CODE)) { client.setRegisteredRedirectUri(request.getRedirectURI()); } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIProjectsService.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIProjectsService.java index 6fbb2c2d345..50b76f3ec14 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIProjectsService.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIProjectsService.java @@ -39,6 +39,7 @@ import ca.corefacility.bioinformatics.irida.service.ProjectService; import ca.corefacility.bioinformatics.irida.service.sample.MetadataTemplateService; import ca.corefacility.bioinformatics.irida.service.sample.SampleService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; @@ -54,18 +55,20 @@ public class UIProjectsService { private final ProjectOwnerPermission projectOwnerPermission; private final ManageLocalProjectSettingsPermission projectMembersPermission; private final MetadataTemplateService metadataTemplateService; - + private final UserService userService; @Autowired public UIProjectsService(ProjectService projectService, SampleService sampleService, MessageSource messageSource, ProjectOwnerPermission projectOwnerPermission, - ManageLocalProjectSettingsPermission projectMembersPermission, MetadataTemplateService metadataTemplateService) { + ManageLocalProjectSettingsPermission projectMembersPermission, + MetadataTemplateService metadataTemplateService, UserService userService) { this.projectService = projectService; this.sampleService = sampleService; this.messageSource = messageSource; this.projectOwnerPermission = projectOwnerPermission; this.projectMembersPermission = projectMembersPermission; this.metadataTemplateService = metadataTemplateService; + this.userService = userService; } /** @@ -167,8 +170,8 @@ private Page getPagedProjectsForUser(TableRequest tableRequest) { public ProjectDetailsResponse getProjectInfo(Long projectId, Locale locale) throws AjaxItemNotFoundException { try { Project project = projectService.read(projectId); - - User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userService.getUserByUsername(username); boolean isAdmin = user.getSystemRole() .equals(ca.corefacility.bioinformatics.irida.model.user.Role.ROLE_ADMIN); @@ -180,7 +183,8 @@ public ProjectDetailsResponse getProjectInfo(Long projectId, Locale locale) thro MetadataTemplate defaultTemplateForProject = metadataTemplateService.getDefaultTemplateForProject(project); - return new ProjectDetailsResponse(project, isAdmin || isOwner, isAdmin || isOwnerAllowRemote, defaultTemplateForProject); + return new ProjectDetailsResponse(project, isAdmin || isOwner, isAdmin || isOwnerAllowRemote, + defaultTemplateForProject); } catch (EntityNotFoundException e) { throw new AjaxItemNotFoundException( messageSource.getMessage("server.ProjectDetails.project-not-found", new Object[] {}, locale)); @@ -211,9 +215,8 @@ public String updateProjectDetails(Long projectId, UpdateProjectAttributeRequest project.setOrganism(request.getValue()); break; default: - throw new UpdateException( - messageSource.getMessage("server.ProjectDetails.error", new Object[] { request.getField() }, - locale)); + throw new UpdateException(messageSource.getMessage("server.ProjectDetails.error", + new Object[] { request.getField() }, locale)); } try { diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIUserGroupsService.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIUserGroupsService.java index 1eb65252325..2c78a7fb676 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIUserGroupsService.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIUserGroupsService.java @@ -59,11 +59,9 @@ public TableResponse getUserGroups(TableRequest request) { Page pagedGroups = userGroupService.search( UserGroupSpecification.searchUserGroup(request.getSearch()), PageRequest.of(request.getCurrent(), request.getPageSize(), request.getSort())); - User user = (User) SecurityContextHolder.getContext() - .getAuthentication() - .getPrincipal(); - boolean isAdmin = user.getSystemRole() - .equals(Role.ROLE_ADMIN); + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userService.getUserByUsername(username); + boolean isAdmin = user.getSystemRole().equals(Role.ROLE_ADMIN); List groups = pagedGroups.getContent() .stream() .map(group -> new UserGroupTableModel(group, isAdmin || isGroupOwner(user, group))) @@ -93,18 +91,14 @@ public String deleteUserGroup(Long id, Locale locale) { public UserGroupDetails getUserGroupDetails(Long groupId) { UserGroup group = userGroupService.read(groupId); Collection groupUsers = userGroupService.getUsersForGroup(group); - List members = groupUsers.stream() - .map(UserGroupMember::new) - .collect(Collectors.toList()); + List members = groupUsers.stream().map(UserGroupMember::new).collect(Collectors.toList()); /* Determine if the current user can manage this group */ - User user = (User) SecurityContextHolder.getContext() - .getAuthentication() - .getPrincipal(); - boolean isAdmin = user.getSystemRole() - .equals(Role.ROLE_ADMIN); + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userService.getUserByUsername(username); + boolean isAdmin = user.getSystemRole().equals(Role.ROLE_ADMIN); return new UserGroupDetails(group, members, isAdmin || isGroupOwner(user, group)); } @@ -138,7 +132,8 @@ public void updateUserGroupDetails(Long groupId, FieldUpdate update) { public List getUserGroupRoles(Locale locale) { final String OWNER = UserGroupJoin.UserGroupRole.GROUP_OWNER.toString(); final String MEMBER = UserGroupJoin.UserGroupRole.GROUP_MEMBER.toString(); - return ImmutableList.of(new UserGroupRole(OWNER, + return ImmutableList.of( + new UserGroupRole(OWNER, messageSource.getMessage("server.usergroups.GROUP_OWNER", new Object[] {}, locale)), new UserGroupRole(MEMBER, messageSource.getMessage("server.usergroups.GROUP_MEMBER", new Object[] {}, locale))); @@ -160,8 +155,8 @@ public List getAvailableUsersForUserGroup(Long groupId, String query) { * Add a new member to the user group * * @param groupId identifier for the {@link UserGroup} - * @param userId identifier for the {@link User} - * @param role role to assign to the user + * @param userId identifier for the {@link User} + * @param role role to assign to the user * @param locale current users {@link Locale} * @return message to the user about the status of this request */ @@ -182,7 +177,8 @@ public String addMemberToUserGroup(Long groupId, Long userId, String role, Local * @param role role to update the user to * @param locale Current users {@link Locale} * @return Message to user about the result of the update - * @throws UserGroupWithoutOwnerException thrown if changing the users role would result in the user group not having an owner + * @throws UserGroupWithoutOwnerException thrown if changing the users role would result in the user group not + * having an owner */ public String updateUserRoleOnUserGroup(Long groupId, Long userId, String role, Locale locale) throws UserGroupWithoutOwnerException { @@ -193,8 +189,8 @@ public String updateUserRoleOnUserGroup(Long groupId, Long userId, String role, try { userGroupService.changeUserGroupRole(user, group, userGroupRole); - return messageSource.getMessage("server.usergroups.role-success", new Object[] { user.getLabel(), roleTranslated }, - locale); + return messageSource.getMessage("server.usergroups.role-success", + new Object[] { user.getLabel(), roleTranslated }, locale); } catch (UserGroupWithoutOwnerException e) { throw new UserGroupWithoutOwnerException( messageSource.getMessage("server.usergroups.role-error", new Object[] { user.getLabel() }, locale)); @@ -208,7 +204,8 @@ public String updateUserRoleOnUserGroup(Long groupId, Long userId, String role, * @param userId identifier for a {@link User} * @param locale current users {@link Locale} * @return Message to user about the result of removing the user - * @throws UserGroupWithoutOwnerException thrown if removing the user would result in the user group not having an owner + * @throws UserGroupWithoutOwnerException thrown if removing the user would result in the user group not having an + * owner */ public String removeMemberFromUserGroup(Long groupId, Long userId, Locale locale) throws UserGroupWithoutOwnerException { @@ -219,9 +216,8 @@ public String removeMemberFromUserGroup(Long groupId, Long userId, Locale locale return messageSource.getMessage("server.usergroups.remove-member.success", new Object[] { user.getLabel() }, locale); } catch (UserGroupWithoutOwnerException e) { - throw new UserGroupWithoutOwnerException( - messageSource.getMessage("server.usergroups.remove-member.error", new Object[] { user.getLabel() }, - locale)); + throw new UserGroupWithoutOwnerException(messageSource.getMessage("server.usergroups.remove-member.error", + new Object[] { user.getLabel() }, locale)); } } @@ -235,13 +231,11 @@ public String removeMemberFromUserGroup(Long groupId, Long userId, Locale locale public List getProjectsForUserGroup(Long groupId, Locale locale) { UserGroup group = userGroupService.read(groupId); Collection joins = userGroupService.getProjectsWithUserGroup(group); - return joins.stream() - .map(join -> { - ProjectRole role = join.getProjectRole(); - return new UserGroupProjectTableModel(join, - messageSource.getMessage("projectRole." + role.toString(), new Object[] {}, locale)); - }) - .collect(Collectors.toList()); + return joins.stream().map(join -> { + ProjectRole role = join.getProjectRole(); + return new UserGroupProjectTableModel(join, + messageSource.getMessage("projectRole." + role.toString(), new Object[] {}, locale)); + }).collect(Collectors.toList()); } /** @@ -257,9 +251,8 @@ public Long createNewUserGroup(UserGroup userGroup, Locale locale) throws UICons UserGroup group = userGroupService.create(userGroup); return group.getId(); } catch (Exception e) { - throw new UIConstraintViolationException(ImmutableMap.of("name", - messageSource.getMessage("server.usergroups.create.constraint.name", - new Object[] { userGroup.getLabel() }, locale))); + throw new UIConstraintViolationException(ImmutableMap.of("name", messageSource.getMessage( + "server.usergroups.create.constraint.name", new Object[] { userGroup.getLabel() }, locale))); } } @@ -273,13 +266,11 @@ public Long createNewUserGroup(UserGroup userGroup, Locale locale) throws UICons private boolean isGroupOwner(User user, UserGroup group) { Collection groupUsers = userGroupService.getUsersForGroup(group); UserGroupJoin currentUserJoin = groupUsers.stream() - .filter(x -> x.getSubject() - .equals(user)) + .filter(x -> x.getSubject().equals(user)) .findAny() .orElse(null); if (currentUserJoin != null) { - return currentUserJoin.getRole() - .equals(UserGroupJoin.UserGroupRole.GROUP_OWNER); + return currentUserJoin.getRole().equals(UserGroupJoin.UserGroupRole.GROUP_OWNER); } return false; } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIUsersService.java b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIUsersService.java index 8b4e32e1976..4f5d58ead0b 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIUsersService.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/ria/web/services/UIUsersService.java @@ -19,6 +19,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.Specification; import org.springframework.mail.MailSendException; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @@ -338,7 +339,7 @@ public AjaxSuccessResponse changeUserPassword(Long userId, String oldPassword, S // If the user is updating their account make sure you update it in the session variable if (updatedUser != null && usersEqual) { HttpSession session = request.getSession(); - session.setAttribute(UserSecurityInterceptor.CURRENT_USER_DETAILS, updatedUser); + session.setAttribute(UserSecurityInterceptor.CURRENT_USER_DETAILS, (UserDetails) updatedUser); } } catch (ConstraintViolationException | DataIntegrityViolationException | PasswordReusedException ex) { errors = handleCreateUpdateException(ex, request.getLocale()); @@ -572,13 +573,7 @@ private void updateUser(Long userId, Principal principal, HttpServletRequest req Map updatedValues) throws UIUserFormException { Map errors; try { - User user = userService.updateFields(userId, updatedValues); - - // If the user is updating their account make sure you update it in the session variable - if (user != null && principal.getName().equals(user.getUsername())) { - HttpSession session = request.getSession(); - session.setAttribute(UserSecurityInterceptor.CURRENT_USER_DETAILS, user); - } + userService.updateFields(userId, updatedValues); } catch (ConstraintViolationException | DataIntegrityViolationException | PasswordReusedException ex) { errors = handleCreateUpdateException(ex, request.getLocale()); throw new UIUserFormException(errors); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/service/IridaClientDetailsService.java b/src/main/java/ca/corefacility/bioinformatics/irida/service/IridaClientDetailsService.java index c462caeddf6..4c0082073e9 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/service/IridaClientDetailsService.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/service/IridaClientDetailsService.java @@ -1,27 +1,24 @@ package ca.corefacility.bioinformatics.irida.service; import ca.corefacility.bioinformatics.irida.model.IridaClientDetails; -import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.security.oauth2.provider.ClientDetailsService; -import org.springframework.security.oauth2.provider.NoSuchClientException; /** * Service for storing and reading {@link IridaClientDetails} objects - * - * */ -public interface IridaClientDetailsService extends ClientDetailsService, CRUDService { +public interface IridaClientDetailsService extends CRUDService { /** - * {@inheritDoc} + * Get the {@link IridaClientDetails} by clientDetails + * + * @param clientId The client_id of the client to fetch. + * @return The client */ - public ClientDetails loadClientByClientId(String clientId) throws NoSuchClientException; + public IridaClientDetails loadClientByClientId(String clientId); /** * Get the number of tokens issued for a given {@link IridaClientDetails} * - * @param client - * Client to count tokens for + * @param client Client to count tokens for * @return Number of tokens issued for the given client */ public int countTokensForClient(IridaClientDetails client); @@ -29,17 +26,14 @@ public interface IridaClientDetailsService extends ClientDetailsService, CRUDSer /** * Revoke all OAuth2 tokens for a given {@link IridaClientDetails} * - * @param client - * The client to revoke tokens for + * @param client The client to revoke tokens for */ public void revokeTokensForClient(IridaClientDetails client); /** - * Get the number of all tokens defined for a given - * {@link IridaClientDetails} that are valid and not expired. + * Get the number of all tokens defined for a given {@link IridaClientDetails} that are valid and not expired. * - * @param client - * the {@link IridaClientDetails} to get tokens for + * @param client the {@link IridaClientDetails} to get tokens for * @return number of tokens defined for the client. */ public int countActiveTokensForClient(IridaClientDetails client); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/IridaClientDetailsServiceImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/IridaClientDetailsServiceImpl.java index 6f0077cf5de..c47b6149650 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/IridaClientDetailsServiceImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/IridaClientDetailsServiceImpl.java @@ -10,24 +10,19 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.security.oauth2.provider.ClientDetailsService; -import org.springframework.security.oauth2.provider.NoSuchClientException; -import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.stereotype.Service; import ca.corefacility.bioinformatics.irida.exceptions.EntityExistsException; import ca.corefacility.bioinformatics.irida.exceptions.EntityNotFoundException; import ca.corefacility.bioinformatics.irida.model.IridaClientDetails; +import ca.corefacility.bioinformatics.irida.oauth2.IridaOAuth2AuthorizationService; import ca.corefacility.bioinformatics.irida.repositories.IridaClientDetailsRepository; import ca.corefacility.bioinformatics.irida.service.IridaClientDetailsService; /** - * Service for storing and retrieving {@link IridaClientDetails} object. - * Implements {@link ClientDetailsService} for use with OAuth approvals. - * - * + * Service for storing and retrieving {@link IridaClientDetails} object. Implements for use with OAuth approvals. */ @Service("clientDetails") @PreAuthorize("hasRole('ROLE_ADMIN')") @@ -35,14 +30,14 @@ public class IridaClientDetailsServiceImpl extends CRUDServiceImpl search(Specification specifi */ @Override @PreAuthorize("permitAll()") - public ClientDetails loadClientByClientId(String clientId) throws NoSuchClientException { + public IridaClientDetails loadClientByClientId(String clientId) throws EntityNotFoundException { IridaClientDetails client = clientDetailsRepository.loadClientDetailsByClientId(clientId); if (client == null) { - throw new NoSuchClientException("Client with this clientId does not exist: " + clientId); + throw new EntityNotFoundException("Client with this clientId does not exist: " + clientId); } return client; } @@ -101,9 +96,10 @@ public void delete(Long id) throws EntityNotFoundException { * {@inheritDoc} */ public int countActiveTokensForClient(IridaClientDetails client) { - Collection findTokensByClientId = tokenStore.findTokensByClientId(client.getClientId()); - int active = findTokensByClientId.stream().mapToInt((t) -> { - return t.isExpired() ? 0 : 1; + Collection accessTokenAuthorizations = ((IridaOAuth2AuthorizationService) authorizationService) + .findAccessTokensByRegisteredClientId(client.getId().toString()); + int active = accessTokenAuthorizations.stream().mapToInt((a) -> { + return a.getAccessToken().isActive() ? 1 : 0; }).sum(); return active; } @@ -112,19 +108,19 @@ public int countActiveTokensForClient(IridaClientDetails client) { * {@inheritDoc} */ public int countTokensForClient(IridaClientDetails client) { - return tokenStore.findTokensByClientId(client.getClientId()).size(); + return ((IridaOAuth2AuthorizationService) authorizationService) + .findAccessTokensByRegisteredClientId(client.getId().toString()) + .size(); } /** * {@inheritDoc} */ public void revokeTokensForClient(IridaClientDetails client) { - Collection findTokensByClientId = tokenStore.findTokensByClientId(client.getClientId()); - for (OAuth2AccessToken token : findTokensByClientId) { - tokenStore.removeAccessToken(token); - if (token.getRefreshToken() != null) { - tokenStore.removeRefreshToken(token.getRefreshToken()); - } + Collection accessTokenAuthorizations = ((IridaOAuth2AuthorizationService) authorizationService) + .findAccessTokensByRegisteredClientId(client.getId().toString()); + for (OAuth2Authorization authorization : accessTokenAuthorizations) { + authorizationService.remove(authorization); } } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/ProjectServiceImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/ProjectServiceImpl.java index 0b1d5ccbf06..99a984ff0e4 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/ProjectServiceImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/ProjectServiceImpl.java @@ -179,8 +179,8 @@ public Project read(Long id) { @PreAuthorize("hasAnyRole('ROLE_USER')") public Project create(Project p) { Project project = super.create(p); - UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - User user = userRepository.loadUserByUsername(userDetails.getUsername()); + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userRepository.loadUserByUsername(username); addUserToProject(project, user, ProjectRole.PROJECT_OWNER, ProjectMetadataRole.LEVEL_4); return project; } @@ -239,7 +239,8 @@ public void delete(final Long id) { @Transactional @LaunchesProjectEvent(UserRoleSetProjectEvent.class) @PreAuthorize("hasPermission(#project, 'canManageLocalProjectSettings')") - public Join addUserToProject(Project project, User user, ProjectRole role, ProjectMetadataRole metadataRole) { + public Join addUserToProject(Project project, User user, ProjectRole role, + ProjectMetadataRole metadataRole) { try { ProjectUserJoin join = pujRepository.save(new ProjectUserJoin(project, user, role, metadataRole)); projectSubscriptionService.addProjectSubscriptionForProjectAndUser(project, user); @@ -316,7 +317,8 @@ public Join updateUserProjectRole(Project project, User user, Pro @Override @Transactional @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#project,'canManageLocalProjectSettings')") - public Join updateUserProjectMetadataRole(Project project, User user, ProjectMetadataRole metadataRole) { + public Join updateUserProjectMetadataRole(Project project, User user, + ProjectMetadataRole metadataRole) { ProjectUserJoin projectJoinForUser = pujRepository.getProjectJoinForUser(project, user); if (projectJoinForUser == null) { throw new EntityNotFoundException( @@ -335,7 +337,8 @@ public Join updateUserProjectMetadataRole(Project project, User u @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#project, 'canManageLocalProjectSettings')") public Join updateUserGroupProjectRole(Project project, UserGroup userGroup, ProjectRole projectRole, ProjectMetadataRole metadataRole) throws ProjectWithoutOwnerException { - final UserGroupProjectJoin projectJoinForUserGroup = ugpjRepository.findByProjectAndUserGroup(project, userGroup); + final UserGroupProjectJoin projectJoinForUserGroup = ugpjRepository.findByProjectAndUserGroup(project, + userGroup); if (projectJoinForUserGroup == null) { throw new EntityNotFoundException( "Join between this project and group does not exist. Group: " + userGroup + " Project: " + project); @@ -355,8 +358,9 @@ public Join updateUserGroupProjectRole(Project project, User @Transactional @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#project, 'canManageLocalProjectSettings')") public Join updateUserGroupProjectMetadataRole(Project project, UserGroup userGroup, - ProjectMetadataRole metadataRole) { - final UserGroupProjectJoin projectJoinForUserGroup = ugpjRepository.findByProjectAndUserGroup(project, userGroup); + ProjectMetadataRole metadataRole) { + final UserGroupProjectJoin projectJoinForUserGroup = ugpjRepository.findByProjectAndUserGroup(project, + userGroup); if (projectJoinForUserGroup == null) { throw new EntityNotFoundException( "Join between this project and group does not exist. Group: " + userGroup + " Project: " + project); @@ -407,9 +411,8 @@ public ProjectSampleJoin addSampleToProject(Project project, Sample sample, bool // Check to ensure a sample with this sample name doesn't exist in this // project already if (sampleRepository.getSampleBySampleName(project, sample.getSampleName()) != null) { - throw new ExistingSampleNameException( - "Sample with the name '" + sample.getSampleName() + "' already exists in project " - + project.getId(), sample); + throw new ExistingSampleNameException("Sample with the name '" + sample.getSampleName() + + "' already exists in project " + project.getId(), sample); } // the sample hasn't been persisted before, persist it before calling @@ -441,8 +444,7 @@ public ProjectSampleJoin addSampleToProject(Project project, Sample sample, bool @Override @Transactional @LaunchesProjectEvent(SampleAddedProjectEvent.class) - @PreAuthorize( - "hasRole('ROLE_ADMIN') or ( hasPermission(#source, 'isProjectOwner') and hasPermission(#destination, 'isProjectOwner'))") + @PreAuthorize("hasRole('ROLE_ADMIN') or ( hasPermission(#source, 'isProjectOwner') and hasPermission(#destination, 'isProjectOwner'))") public ProjectSampleJoin moveSampleBetweenProjects(Project source, Project destination, Sample sample) { //read the existing ProjectSampleJoin to see if we're the owner ProjectSampleJoin projectSampleJoin = psjRepository.readSampleForProject(source, sample); @@ -559,7 +561,10 @@ public List> getProjectsForUser(User user) { @Transactional(readOnly = true) @PreAuthorize("hasRole('ROLE_USER')") public List getProjectsForUserUnique(User user) { - final List projectListUser = pujRepository.getProjectsForUser(user).stream().map(u -> u.getSubject()).collect(Collectors.toList()); + final List projectListUser = pujRepository.getProjectsForUser(user) + .stream() + .map(u -> u.getSubject()) + .collect(Collectors.toList()); final List projectListUserGroup = ugpjRepository.findProjectsByUser(user) .stream() @@ -567,9 +572,7 @@ public List getProjectsForUserUnique(User user) { .map(j -> j.getSubject()) .collect(Collectors.toList()); - return new ImmutableList.Builder().addAll(projectListUser) - .addAll(projectListUserGroup) - .build(); + return new ImmutableList.Builder().addAll(projectListUser).addAll(projectListUserGroup).build(); } /** @@ -605,16 +608,15 @@ public Collection getUserGroupProjectJoins(User user, Proj * {@inheritDoc} */ @Override - @PreAuthorize( - "hasRole('ROLE_ADMIN') or hasPermission(#subject,'isProjectOwner') and hasPermission(#relatedProject,'canReadProject')") + @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#subject,'isProjectOwner') and hasPermission(#relatedProject,'canReadProject')") public RelatedProjectJoin addRelatedProject(Project subject, Project relatedProject) { if (subject.equals(relatedProject)) { throw new IllegalArgumentException("Project cannot be related to itself"); } try { - RelatedProjectJoin relation = relatedProjectRepository.save( - new RelatedProjectJoin(subject, relatedProject)); + RelatedProjectJoin relation = relatedProjectRepository + .save(new RelatedProjectJoin(subject, relatedProject)); return relation; } catch (DataIntegrityViolationException e) { throw new EntityExistsException( @@ -690,8 +692,8 @@ public Join addReferenceFileToProject(Project project, R @Override @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#project, 'isProjectOwner')") public void removeReferenceFileFromProject(Project project, ReferenceFile file) { - List> referenceFilesForProject = prfjRepository.findReferenceFilesForProject( - project); + List> referenceFilesForProject = prfjRepository + .findReferenceFilesForProject(project); Join specificJoin = null; for (Join join : referenceFilesForProject) { if (join.getObject().equals(file)) { @@ -702,9 +704,8 @@ public void removeReferenceFileFromProject(Project project, ReferenceFile file) if (specificJoin != null) { prfjRepository.delete((ProjectReferenceFileJoin) specificJoin); } else { - throw new EntityNotFoundException( - "Cannot find a join for project [" + project.getName() + "] and reference file [" + file.getLabel() - + "]."); + throw new EntityNotFoundException("Cannot find a join for project [" + project.getName() + + "] and reference file [" + file.getLabel() + "]."); } } @@ -715,11 +716,8 @@ public void removeReferenceFileFromProject(Project project, ReferenceFile file) @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#p, 'canManageLocalProjectSettings')") public Page getUnassociatedProjects(final Project p, final String searchName, final Integer page, final Integer count, final Direction sortDirection, final String... sortedBy) { - - final UserDetails loggedInDetails = (UserDetails) SecurityContextHolder.getContext() - .getAuthentication() - .getPrincipal(); - final User loggedIn = userRepository.loadUserByUsername(loggedInDetails.getUsername()); + final String username = SecurityContextHolder.getContext().getAuthentication().getName(); + final User loggedIn = userRepository.loadUserByUsername(username); final PageRequest pr = PageRequest.of(page, count, sortDirection, getOrDefaultSortProperties(sortedBy)); if (loggedIn.getSystemRole().equals(Role.ROLE_ADMIN)) { return projectRepository.findAllProjectsByNameExcludingProject(searchName, p, pr); @@ -735,10 +733,8 @@ public Page getUnassociatedProjects(final Project p, final String searc @PreAuthorize("hasRole('ROLE_USER')") public Page findProjectsForUser(final String search, final Integer page, final Integer count, final Sort sort) { - final UserDetails loggedInDetails = (UserDetails) SecurityContextHolder.getContext() - .getAuthentication() - .getPrincipal(); - final User loggedIn = userRepository.loadUserByUsername(loggedInDetails.getUsername()); + final String username = SecurityContextHolder.getContext().getAuthentication().getName(); + final User loggedIn = userRepository.loadUserByUsername(username); final PageRequest pr = PageRequest.of(page, count, getOrDefaultSort(sort)); return projectRepository.findAll(searchForProjects(search, null, null, loggedIn), pr); } @@ -853,8 +849,8 @@ public Project createProjectWithSamples(Project project, List sampleIds, b @PostFilter("hasPermission(filterObject, 'canReadProject')") @Override public List getProjectsUsedInAnalysisSubmission(AnalysisSubmission submission) { - Set findSequencingObjectsForAnalysisSubmission = sequencingObjectRepository.findSequencingObjectsForAnalysisSubmission( - submission); + Set findSequencingObjectsForAnalysisSubmission = sequencingObjectRepository + .findSequencingObjectsForAnalysisSubmission(submission); // get available projects Set projectsInAnalysis = getProjectsForSequencingObjects(findSequencingObjectsForAnalysisSubmission); @@ -895,7 +891,7 @@ private static final Sort getOrDefaultSort(Sort sort) { * @param projectRole The {@link ProjectRole} to search for. * @param user The user to search * @return a {@link Specification} to search for {@link Project} where the specified {@link User} has a certain - * {@link ProjectRole}. + * {@link ProjectRole}. */ private static final Specification getProjectJoinsWithRole(User user, ProjectRole projectRole) { return new Specification() { @@ -920,15 +916,11 @@ private static final Specification searchForProjects(final String allFi return new Specification() { /** - * This {@link Predicate} considers *all* fields on a - * {@link Project} with an OR filter. + * This {@link Predicate} considers *all* fields on a {@link Project} with an OR filter. * - * @param root - * the root of the query - * @param query - * the query - * @param cb - * the builder + * @param root the root of the query + * @param query the query + * @param cb the builder * @return a {@link Predicate} that covers all fields with OR. */ private Predicate allFieldsPredicate(final Root root, final CriteriaQuery query, @@ -941,17 +933,13 @@ private Predicate allFieldsPredicate(final Root root, final CriteriaQue } /** - * This {@link Predicate} considers each specific field on - * {@link Project} separately and joins them with an AND filter. + * This {@link Predicate} considers each specific field on {@link Project} separately and joins them with an + * AND filter. * - * @param root - * the root of the query - * @param query - * the query - * @param cb - * the builder - * @return a {@link Predicate} that covers individual fields with an - * AND. + * @param root the root of the query + * @param query the query + * @param cb the builder + * @return a {@link Predicate} that covers individual fields with an AND. */ private Predicate specificFiltersPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { @@ -967,17 +955,13 @@ private Predicate specificFiltersPredicate(final Root root, final Crite } /** - * This {@link Predicate} filters out {@link Project}s for the - * specific user where they are assigned individually as a member. + * This {@link Predicate} filters out {@link Project}s for the specific user where they are assigned + * individually as a member. * - * @param root - * the root of the query - * @param query - * the query - * @param cb - * the builder - * @return a {@link Predicate} that filters {@link Project}s where - * users are individually assigned. + * @param root the root of the query + * @param query the query + * @param cb the builder + * @return a {@link Predicate} that filters {@link Project}s where users are individually assigned. */ private Predicate individualProjectMembership(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { @@ -989,19 +973,14 @@ private Predicate individualProjectMembership(final Root root, final Cr } /** - * This {@link Predicate} filters out {@link Project}s for the - * specific user where they are assigned transitively through a - * {@link UserGroup}. + * This {@link Predicate} filters out {@link Project}s for the specific user where they are assigned + * transitively through a {@link UserGroup}. * - * @param root - * the root of the query - * @param query - * the query - * @param cb - * the builder - * @return a {@link Predicate} that filters {@link Project}s where - * users are assigned transitively through {@link UserGroup} - * . + * @param root the root of the query + * @param query the query + * @param cb the builder + * @return a {@link Predicate} that filters {@link Project}s where users are assigned transitively through + * {@link UserGroup} . */ private Predicate groupProjectMembership(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { @@ -1041,7 +1020,6 @@ public Long getProjectsCreated(Date createdDate) { return projectsCount; } - /** * {@inheritDoc} */ diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/RemoteAPITokenServiceImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/RemoteAPITokenServiceImpl.java index 5c4264afac4..d9d0dc7acd9 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/RemoteAPITokenServiceImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/RemoteAPITokenServiceImpl.java @@ -17,6 +17,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -104,8 +105,11 @@ public RemoteAPIToken updateTokenFromRefreshToken(RemoteAPI api) { URI serviceTokenLocation = UriBuilder.fromUri(api.getServiceURI()).path("oauth").path("token").build(); OAuthClientRequest tokenRequest = OAuthClientRequest.tokenLocation(serviceTokenLocation.toString()) - .setClientId(api.getClientId()).setClientSecret(api.getClientSecret()) - .setRefreshToken(refreshToken).setGrantType(GrantType.REFRESH_TOKEN).buildBodyMessage(); + .setClientId(api.getClientId()) + .setClientSecret(api.getClientSecret()) + .setRefreshToken(refreshToken) + .setGrantType(GrantType.REFRESH_TOKEN) + .buildBodyMessage(); OAuthJSONAccessTokenResponse accessToken = oauthClient.accessToken(tokenRequest); @@ -129,11 +133,12 @@ public RemoteAPIToken updateTokenFromRefreshToken(RemoteAPI api) { /** * Get a new token from the given auth code + * * @param authcode the auth code to create a token for * @param remoteAPI the remote api to get a token for * @param tokenRedirect a redirect url to get the token from * @return a new token - * @throws OAuthSystemException If building the token request fails + * @throws OAuthSystemException If building the token request fails * @throws OAuthProblemException If the token request fails */ @Transactional @@ -147,8 +152,11 @@ public RemoteAPIToken createTokenFromAuthCode(String authcode, RemoteAPI remoteA // Create the token request form the given auth code OAuthClientRequest tokenRequest = OAuthClientRequest.tokenLocation(serviceTokenLocation.toString()) - .setClientId(remoteAPI.getClientId()).setClientSecret(remoteAPI.getClientSecret()) - .setRedirectURI(tokenRedirect).setCode(authcode).setGrantType(GrantType.AUTHORIZATION_CODE) + .setClientId(remoteAPI.getClientId()) + .setClientSecret(remoteAPI.getClientSecret()) + .setRedirectURI(tokenRedirect) + .setCode(authcode) + .setGrantType(GrantType.AUTHORIZATION_CODE) .buildBodyMessage(); // execute the request @@ -168,7 +176,7 @@ public RemoteAPIToken createTokenFromAuthCode(String authcode, RemoteAPI remoteA // create the OAuth2 token and store it RemoteAPIToken token = new RemoteAPIToken(accessToken, refreshToken, remoteAPI, expiry); - + return create(token); } @@ -179,9 +187,9 @@ public RemoteAPIToken createTokenFromAuthCode(String authcode, RemoteAPI remoteA */ private String getUserName() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.getPrincipal() instanceof UserDetails) { - UserDetails details = (UserDetails) authentication.getPrincipal(); - String username = details.getUsername(); + if (authentication != null && (authentication.getPrincipal() instanceof UserDetails + || authentication instanceof JwtAuthenticationToken)) { + String username = authentication.getName(); return username; } @@ -192,8 +200,7 @@ private String getUserName() { /** * Remove any old token for this user from the database * - * @param apiToken - * the api token to remove. + * @param apiToken the api token to remove. * @return the token that was found. */ protected RemoteAPIToken getOldTokenId(RemoteAPIToken apiToken) { diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/analysis/submission/AnalysisSubmissionServiceImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/analysis/submission/AnalysisSubmissionServiceImpl.java index 5dcd30fda77..b7334fd320b 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/analysis/submission/AnalysisSubmissionServiceImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/analysis/submission/AnalysisSubmissionServiceImpl.java @@ -21,7 +21,6 @@ import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import ca.corefacility.bioinformatics.irida.exceptions.EntityExistsException; @@ -392,8 +391,8 @@ public List getAllAutomatedAnalysisOutputInfoFo @PreAuthorize("hasRole('ROLE_USER')") public AnalysisSubmission create(AnalysisSubmission analysisSubmission) throws ConstraintViolationException, EntityExistsException { - UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - User user = userRepository.loadUserByUsername(userDetails.getUsername()); + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userRepository.loadUserByUsername(username); analysisSubmission.setSubmitter(user); return super.create(analysisSubmission); @@ -417,8 +416,8 @@ public Set getAnalysisSubmissionsForUser(User user) { @Override @PreAuthorize("hasRole('ROLE_USER')") public Set getAnalysisSubmissionsForCurrentUser() { - UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - User user = userRepository.loadUserByUsername(userDetails.getUsername()); + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userRepository.loadUserByUsername(username); return getAnalysisSubmissionsForUser(user); } @@ -480,8 +479,8 @@ public AnalysisSubmissionTemplate createSingleSampleSubmissionTemplate(IridaWork template.setStatusMessage(statusMessage); - UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - User user = userRepository.loadUserByUsername(userDetails.getUsername()); + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + User user = userRepository.loadUserByUsername(username); template.setSubmitter(user); template = analysisTemplateRepository.save(template); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/sample/MetadataTemplateServiceImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/sample/MetadataTemplateServiceImpl.java index 99d4295662e..83127ccce3c 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/sample/MetadataTemplateServiceImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/sample/MetadataTemplateServiceImpl.java @@ -9,7 +9,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import ca.corefacility.bioinformatics.irida.exceptions.EntityNotFoundException; @@ -113,8 +112,7 @@ public List getMetadataTemplatesForProject(Project project) { @PreAuthorize("permitAll()") @Override public MetadataTemplateField readMetadataField(Long id) { - return fieldRepository.findById(id) - .orElse(null); + return fieldRepository.findById(id).orElse(null); } /** @@ -134,8 +132,7 @@ public MetadataTemplateField readMetadataFieldByKey(String key) { return fieldRepository.findMetadataFieldByStaticId(stripped); } else { String stripped = key.replaceFirst(MetadataTemplateField.DYNAMIC_FIELD_PREFIX, ""); - return fieldRepository.findById(Long.parseLong(stripped)) - .orElse(null); + return fieldRepository.findById(Long.parseLong(stripped)).orElse(null); } } @@ -173,23 +170,22 @@ public List getAllMetadataFieldsByQueryString(String quer public Set convertMetadataStringsToSet(Map metadataMap) { Set metadata = new HashSet<>(); - metadataMap.entrySet() - .forEach(e -> { - MetadataTemplateField field = readMetadataFieldByLabel(e.getKey()); + metadataMap.entrySet().forEach(e -> { + MetadataTemplateField field = readMetadataFieldByLabel(e.getKey()); - // if not, create a new one - if (field == null) { - field = new MetadataTemplateField(e.getKey(), "text"); - field = saveMetadataField(field); - } + // if not, create a new one + if (field == null) { + field = new MetadataTemplateField(e.getKey(), "text"); + field = saveMetadataField(field); + } - MetadataEntry entry = e.getValue(); + MetadataEntry entry = e.getValue(); - entry.setField(field); + entry.setField(field); - metadata.add(entry); + metadata.add(entry); - }); + }); return metadata; } @@ -219,7 +215,8 @@ public List getMetadataRestrictionsForProject(Project proje @PreAuthorize("hasPermission(#project, 'isProjectOwner')") @Override @Transactional - public MetadataRestriction setMetadataRestriction(Project project, MetadataTemplateField field, ProjectMetadataRole role) { + public MetadataRestriction setMetadataRestriction(Project project, MetadataTemplateField field, + ProjectMetadataRole role) { MetadataRestriction metadataRestrictionForFieldAndProject = getMetadataRestrictionForFieldAndProject(project, field); @@ -271,35 +268,33 @@ private List getPermittedFieldsForRole(Project project, P } //get all restrictions for the project - List restrictionForProject = metadataRestrictionRepository.getRestrictionForProject( - project); + List restrictionForProject = metadataRestrictionRepository + .getRestrictionForProject(project); //collect the fields into a map Map restrictionMap = restrictionForProject.stream() .collect(Collectors.toMap(MetadataRestriction::getField, field -> field)); //for each field to check - List filteredFields = metadataFieldsForProject.stream() - .filter(field -> { - //if the restriction map contains the field - if (restrictionMap.containsKey(field)) { - MetadataRestriction metadataRestriction = restrictionMap.get(field); - ProjectMetadataRole restrictionRole = metadataRestriction.getLevel(); - - //compare the restriction level to the given role. If it's greater or equal, we're good - if (role.getLevel() >= restrictionRole.getLevel()) { - return true; - } - - //if it's less, filter out the field - return false; - } else { - //if there's no restriction set for the field, all users can view - return true; - } + List filteredFields = metadataFieldsForProject.stream().filter(field -> { + //if the restriction map contains the field + if (restrictionMap.containsKey(field)) { + MetadataRestriction metadataRestriction = restrictionMap.get(field); + ProjectMetadataRole restrictionRole = metadataRestriction.getLevel(); + + //compare the restriction level to the given role. If it's greater or equal, we're good + if (role.getLevel() >= restrictionRole.getLevel()) { + return true; + } - }) - .collect(Collectors.toList()); + //if it's less, filter out the field + return false; + } else { + //if there's no restriction set for the field, all users can view + return true; + } + + }).collect(Collectors.toList()); return filteredFields; } @@ -311,22 +306,19 @@ private List getPermittedFieldsForRole(Project project, P @Override public List getPermittedFieldsForCurrentUser(Project project, boolean includeTemplateFields) { - final UserDetails loggedInDetails = (UserDetails) SecurityContextHolder.getContext() - .getAuthentication() - .getPrincipal(); + final String username = SecurityContextHolder.getContext().getAuthentication().getName(); + final User loggedInUser = userRepository.loadUserByUsername(username); - User loggedInUser = userRepository.loadUserByUsername(loggedInDetails.getUsername()); ProjectUserJoin projectJoinForUser = pujRepository.getProjectJoinForUser(project, loggedInUser); - List groupsForProjectAndUser = userGroupProjectJoinRepository.findGroupsForProjectAndUser( - project, loggedInUser); + List groupsForProjectAndUser = userGroupProjectJoinRepository + .findGroupsForProjectAndUser(project, loggedInUser); ProjectMetadataRole metadataRole = ProjectMetadataRole.getMaxRoleForProjectAndGroups(projectJoinForUser, groupsForProjectAndUser); //if the user isn't on the project and the user is an admin, give them project owner powers - if (metadataRole == null && loggedInUser.getSystemRole() - .equals(Role.ROLE_ADMIN)) { + if (metadataRole == null && loggedInUser.getSystemRole().equals(Role.ROLE_ADMIN)) { metadataRole = ProjectMetadataRole.LEVEL_4; } @@ -342,15 +334,15 @@ public List getPermittedFieldsForCurrentUser(Project proj @PreAuthorize("hasPermission(#project, 'canReadProject')") @Override public MetadataTemplate getDefaultTemplateForProject(Project project) { - List metadataTemplatesForProject = metadataTemplateRepository.getMetadataTemplatesForProject( - project); + List metadataTemplatesForProject = metadataTemplateRepository + .getMetadataTemplatesForProject(project); Optional templateOptional = metadataTemplatesForProject.stream() .filter(MetadataTemplate::isProjectDefault) .findFirst(); MetadataTemplate template = null; - if(templateOptional.isPresent()){ + if (templateOptional.isPresent()) { template = templateOptional.get(); } @@ -366,7 +358,7 @@ public MetadataTemplate getDefaultTemplateForProject(Project project) { public MetadataTemplate updateDefaultMetadataTemplateForProject(Project project, MetadataTemplate template) { MetadataTemplate originalDefault = getDefaultTemplateForProject(project); - if(originalDefault != null) { + if (originalDefault != null) { originalDefault.setProjectDefault(false); metadataTemplateRepository.save(originalDefault); } @@ -385,7 +377,7 @@ public MetadataTemplate updateDefaultMetadataTemplateForProject(Project project, @Transactional public void removeDefaultMetadataTemplateForProject(Project project) { MetadataTemplate originalDefault = getDefaultTemplateForProject(project); - if(originalDefault != null) { + if (originalDefault != null) { originalDefault.setProjectDefault(false); metadataTemplateRepository.save(originalDefault); } diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/sample/SampleServiceImpl.java b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/sample/SampleServiceImpl.java index 7669f922f88..2e082b7af48 100644 --- a/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/sample/SampleServiceImpl.java +++ b/src/main/java/ca/corefacility/bioinformatics/irida/service/impl/sample/SampleServiceImpl.java @@ -21,7 +21,6 @@ import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -210,9 +209,11 @@ public List getLockedSamplesInProject(Project project) { @Override @PreAuthorize("hasPermission(#project, 'canReadProject')") @PostAuthorize("hasPermission(returnObject,'readProjectMetadataResponse')") - public ProjectMetadataResponse getMetadataForProjectSamples(Project project, List sampleIds, List fields) { + public ProjectMetadataResponse getMetadataForProjectSamples(Project project, List sampleIds, + List fields) { checkArgument(!fields.isEmpty(), "fields must not be empty"); - Map> metadataForProjectSamples = metadataEntryRepository.getMetadataForProjectSamples(project, sampleIds, fields); + Map> metadataForProjectSamples = metadataEntryRepository + .getMetadataForProjectSamples(project, sampleIds, fields); return new ProjectMetadataResponse(project, metadataForProjectSamples); } @@ -394,9 +395,8 @@ public Sample mergeSamples(Project project, Sample mergeInto, Collection // confirm that all samples are part of the same project: confirmProjectSampleJoin(project, mergeInto); - logger.debug( - "Merging samples " + toMerge.stream().map(Sample::getId).collect(Collectors.toList()) + " into sample [" - + mergeInto.getId() + "]"); + logger.debug("Merging samples " + toMerge.stream().map(Sample::getId).collect(Collectors.toList()) + + " into sample [" + mergeInto.getId() + "]"); for (Sample s : toMerge) { confirmProjectSampleJoin(project, s); @@ -666,10 +666,8 @@ public List updateMultiple(Collection objects) { @Override public Page searchSamplesForUser(String query, final Integer page, final Integer count, final Sort sort) { - final UserDetails loggedInDetails = (UserDetails) SecurityContextHolder.getContext() - .getAuthentication() - .getPrincipal(); - final User loggedIn = userRepository.loadUserByUsername(loggedInDetails.getUsername()); + final String username = SecurityContextHolder.getContext().getAuthentication().getName(); + final User loggedIn = userRepository.loadUserByUsername(username); final PageRequest pr = PageRequest.of(page, count, sort); diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/web/controller/api/RESTOAuthAccessController.java b/src/main/java/ca/corefacility/bioinformatics/irida/web/controller/api/RESTOAuthAccessController.java deleted file mode 100644 index 2e4002425f6..00000000000 --- a/src/main/java/ca/corefacility/bioinformatics/irida/web/controller/api/RESTOAuthAccessController.java +++ /dev/null @@ -1,50 +0,0 @@ -package ca.corefacility.bioinformatics.irida.web.controller.api; - -import java.security.Principal; -import java.util.Map; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.oauth2.provider.AuthorizationRequest; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.SessionAttributes; -import org.springframework.web.servlet.ModelAndView; - -import com.google.common.base.Joiner; - -/** - * Controller class for serving custom OAuth2 confirmation pages - */ -@Controller -@SessionAttributes("authorizationRequest") -public class RESTOAuthAccessController { - private static final Logger logger = LoggerFactory.getLogger(RESTOAuthAccessController.class); - - /** - * Basic access confirmation controller for OAuth2 - * - * @param model Model objects to be passed to the view - * @param principal The principal user making the auth request - * @return A ModelAndView for the access_confirmation page - */ - @RequestMapping("/api/oauth/confirm_access") - public ModelAndView getAccessConfirmation(Map model, Principal principal) { - //get the authorization request from the model - AuthorizationRequest clientAuth = (AuthorizationRequest) model.remove("authorizationRequest"); - - logger.trace("Token request recieved from " + clientAuth.getClientId() + " for " + principal.getName()); - //get a list of the scopes from the request - Set scopes = clientAuth.getScope(); - String join = Joiner.on(" & ") - .join(scopes); - - //add necessary information to the model - model.put("auth_request", clientAuth); - model.put("scopes", join); - model.put("principal", principal); - - return new ModelAndView("oauth/access_confirmation", model); - } -} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/web/controller/api/RESTOAuthAuthorizationConsentController.java b/src/main/java/ca/corefacility/bioinformatics/irida/web/controller/api/RESTOAuthAuthorizationConsentController.java new file mode 100644 index 00000000000..4108dc6b89d --- /dev/null +++ b/src/main/java/ca/corefacility/bioinformatics/irida/web/controller/api/RESTOAuthAuthorizationConsentController.java @@ -0,0 +1,47 @@ +package ca.corefacility.bioinformatics.irida.web.controller.api; + +import java.security.Principal; +import java.util.Map; + +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +import com.google.common.base.Joiner; + +/** + * Controller class for serving custom OAuth2 authorization consent pages + */ +@Controller +public class RESTOAuthAuthorizationConsentController { + + /** + * Basic authorization consent controller for OAuth2 + * + * @param principal The princiapl user making the auth request + * @param model Model objects to be passed to the view + * @param clientId The clientId + * @param scope The requested scopes + * @param state The state object from the auth request + * @return + */ + @GetMapping("/api/oauth/consent") + public ModelAndView consent(Principal principal, Map model, + @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId, + @RequestParam(OAuth2ParameterNames.SCOPE) String scope, + @RequestParam(OAuth2ParameterNames.STATE) String state) { + + String[] requestedScopes = StringUtils.delimitedListToStringArray(scope, " "); + + model.put("clientId", clientId); + model.put("state", state); + model.put("requestedScopes", Joiner.on(" & ").join(requestedScopes)); + model.put("scopes", requestedScopes); + model.put("principalName", principal.getName()); + + return new ModelAndView("oauth/authorization_consent", model); + } +} diff --git a/src/main/java/ca/corefacility/bioinformatics/irida/web/controller/api/exception/CustomOAuth2ExceptionTranslator.java b/src/main/java/ca/corefacility/bioinformatics/irida/web/controller/api/exception/CustomOAuth2ExceptionTranslator.java deleted file mode 100644 index 77e2aab5a8e..00000000000 --- a/src/main/java/ca/corefacility/bioinformatics/irida/web/controller/api/exception/CustomOAuth2ExceptionTranslator.java +++ /dev/null @@ -1,65 +0,0 @@ -package ca.corefacility.bioinformatics.irida.web.controller.api.exception; - -import java.util.Map; -import java.util.Optional; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; -import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; -import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator; -import org.springframework.security.web.util.ThrowableAnalyzer; - -import com.google.common.collect.ImmutableMap; - -/** - * Produces a more helpful error message than - * "An Authentication object was not found in the SecurityContext" when a client - * attempts to connect without a token. - * - * - */ -public class CustomOAuth2ExceptionTranslator extends DefaultWebResponseExceptionTranslator { - private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2ExceptionTranslator.class); - - /** map of exception types to a human-readble message for the client */ - private static final Map, String> EXCEPTION_MESSAGES = ImmutableMap - .of(AuthenticationException.class, - "No client credentials were provided. You must get an access token from the oauth provider at /api/oauth/token.", - InvalidGrantException.class, - "The credentials you provided were invalid. Please provide valid credentials and try again."); - - private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); - - @Override - public ResponseEntity translate(Exception ex) throws Exception { - final Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); - - final Optional, String>> exceptionHandled = EXCEPTION_MESSAGES.entrySet() - .stream().filter(c -> throwableAnalyzer.getFirstThrowableOfType(c.getKey(), causeChain) != null) - .findAny(); - - // defer to the default behaviour when dealing with anything - // except for an unauthorized response. - if (!exceptionHandled.isPresent()) { - logger.debug("Passing exception " + ex + " to super to handle."); - return super.translate(ex); - } - - // handle the unauthorized request. - final OAuth2Exception e = new OAuth2Exception(exceptionHandled.get().getValue(), ex); - - final HttpHeaders headers = new HttpHeaders(); - headers.set("Cache-Control", "no-store"); - headers.set("Pragma", "no-cache"); - headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary())); - - return new ResponseEntity(e, headers, HttpStatus.valueOf(401)); - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4e016e27c6e..788601d4593 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -25,6 +25,10 @@ spring.jpa.properties.hibernate.export.envers=true spring.jpa.open-in-view=false +# Default keystore for holding public/private keys required for OAuth2 JWK Access Token encryption/decryption +oauth2.jwk.key-store=classpath:jwk-key-store.jks +oauth2.jwk.key-store-password=SECRET + locales.default=en locales.enabled=en diff --git a/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/all-changes.xml b/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/all-changes.xml index 9af985dbee2..899aa4c8031 100644 --- a/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/all-changes.xml +++ b/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/all-changes.xml @@ -1,8 +1,9 @@ - - + + + + \ No newline at end of file diff --git a/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/oauth2-authorization-tables.xml b/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/oauth2-authorization-tables.xml new file mode 100644 index 00000000000..996890989c0 --- /dev/null +++ b/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/oauth2-authorization-tables.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/refactor-client-details.xml b/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/refactor-client-details.xml new file mode 100644 index 00000000000..d7b7ebb2fd3 --- /dev/null +++ b/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/refactor-client-details.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/update-remote-api-token-token-types.xml b/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/update-remote-api-token-token-types.xml new file mode 100644 index 00000000000..3eb23f86f49 --- /dev/null +++ b/src/main/resources/ca/corefacility/bioinformatics/irida/database/changesets/unreleased/update-remote-api-token-token-types.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/ca/corefacility/bioinformatics/irida/sql/oauth-token.sql b/src/main/resources/ca/corefacility/bioinformatics/irida/sql/oauth-token.sql index 8208396d808..2d168a727bb 100644 --- a/src/main/resources/ca/corefacility/bioinformatics/irida/sql/oauth-token.sql +++ b/src/main/resources/ca/corefacility/bioinformatics/irida/sql/oauth-token.sql @@ -1,11 +1,8 @@ -DROP TABLE IF EXISTS oauth_access_token; -DROP TABLE IF EXISTS oauth_refresh_token; +DROP TABLE IF EXISTS `oauth2_authorization`; +DROP TABLE IF EXISTS `oauth2_authorization_consent`; --- Data from client details must be manually removed due to FK constraints with oauth_access_token -DELETE FROM client_details_authorities; DELETE FROM client_details_scope; DELETE FROM client_details_grant_types; -DELETE FROM client_details_resource_ids; DELETE FROM client_details; -- client details @@ -13,15 +10,7 @@ insert into client_details (id, clientId, clientSecret, token_validity, createdD insert into client_details (id, clientId, clientSecret, token_validity, createdDate) values (2, "linker", "ZG5K1AFVSycE25ooxgcBRGCWFzSTfDnJ1DxSkdEmEho", 43200,now()); insert into client_details (id, clientId, clientSecret, token_validity, createdDate) values (3, "pythonLinker", "bySZBP5jNO9pSZTz3omFRtJs3XFAvshxGgvXIlZ2zjk", 43200,now()); insert into client_details (id, clientId, clientSecret, token_validity, createdDate) values (4, "testClient", "testClientSecret", 43200,now()); -insert into client_details (id, clientId, clientSecret, redirect_uri, token_validity, createdDate) values (5, "webClient", "webClientSecret", "http://localhost:8080/api/oauth/authorization/token", 43200,now()); - -insert into client_role (name, description) values ("ROLE_CLIENT","A basic OAuth2 client"); - -insert into client_details_authorities (client_details_id,authority_name) values (1,"ROLE_CLIENT"); -insert into client_details_authorities (client_details_id,authority_name) values (2,"ROLE_CLIENT"); -insert into client_details_authorities (client_details_id,authority_name) values (3,"ROLE_CLIENT"); -insert into client_details_authorities (client_details_id,authority_name) values (4,"ROLE_CLIENT"); -insert into client_details_authorities (client_details_id,authority_name) values (5,"ROLE_CLIENT"); +insert into client_details (id, clientId, clientSecret, redirect_uri, token_validity, createdDate) values (5, "webClient", "webClientSecret", "http://127.0.0.1:8080/api/oauth/authorization/token", 43200,now()); insert into client_details_scope (client_details_id,scope) values (1,"read"), (1,"write"); insert into client_details_scope (client_details_id,scope) values (2,"read"); @@ -35,12 +24,6 @@ insert into client_details_grant_types (client_details_id,grant_value) values (3 insert into client_details_grant_types (client_details_id,grant_value) values (4,"password"); insert into client_details_grant_types (client_details_id,grant_value) values (5,"authorization_code"); -insert into client_details_resource_ids (client_details_id,resource_id) values (1,"NmlIrida"); -insert into client_details_resource_ids (client_details_id,resource_id) values (2,"NmlIrida"); -insert into client_details_resource_ids (client_details_id,resource_id) values (3,"NmlIrida"); -insert into client_details_resource_ids (client_details_id,resource_id) values (4,"NmlIrida"); -insert into client_details_resource_ids (client_details_id,resource_id) values (5,"NmlIrida"); - -CREATE TABLE oauth_access_token (token_id VARCHAR(255), token LONGBLOB NOT NULL, authentication_id VARCHAR(255) NOT NULL, user_name VARCHAR(255) NOT NULL, client_id VARCHAR(255) NOT NULL, authentication LONGBLOB NOT NULL, refresh_token VARCHAR(255), PRIMARY KEY(token_id), CONSTRAINT `FK_OAUTH_TOKEN_CLIENT_DETAILS` FOREIGN KEY (`client_id`) REFERENCES `client_details` (`clientId`) ON DELETE CASCADE, UNIQUE (`authentication_id`)); +CREATE TABLE `oauth2_authorization` ( `id` varchar(100) NOT NULL, `registered_client_id` varchar(100) NOT NULL, `principal_name` varchar(200) NOT NULL, `authorization_grant_type` varchar(100) NOT NULL, `attributes` varchar(4000) DEFAULT NULL, `state` varchar(500) DEFAULT NULL, `authorization_code_value` blob DEFAULT NULL, `authorization_code_issued_at` timestamp NULL DEFAULT NULL, `authorization_code_expires_at` timestamp NULL DEFAULT NULL, `authorization_code_metadata` varchar(2000) DEFAULT NULL, `access_token_value` blob DEFAULT NULL, `access_token_issued_at` timestamp NULL DEFAULT NULL, `access_token_expires_at` timestamp NULL DEFAULT NULL, `access_token_metadata` varchar(2000) DEFAULT NULL, `access_token_type` varchar(100) DEFAULT NULL, `access_token_scopes` varchar(1000) DEFAULT NULL, `oidc_id_token_value` blob DEFAULT NULL, `oidc_id_token_issued_at` timestamp NULL DEFAULT NULL, `oidc_id_token_expires_at` timestamp NULL DEFAULT NULL, `oidc_id_token_metadata` varchar(2000) DEFAULT NULL, `refresh_token_value` blob DEFAULT NULL, `refresh_token_issued_at` timestamp NULL DEFAULT NULL, `refresh_token_expires_at` timestamp NULL DEFAULT NULL, `refresh_token_metadata` varchar(2000) DEFAULT NULL, PRIMARY KEY (`id`)); -CREATE TABLE oauth_refresh_token (token_id VARCHAR(255), token LONGBLOB NOT NULL, authentication LONGBLOB NOT NULL, PRIMARY KEY(token_id)); \ No newline at end of file +CREATE TABLE `oauth2_authorization_consent` ( `registered_client_id` varchar(100) NOT NULL, `principal_name` varchar(200) NOT NULL, `authorities` varchar(1000) NOT NULL, PRIMARY KEY (`registered_client_id`, `principal_name`)); \ No newline at end of file diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index b21a9da6de6..8e6db7fe9f7 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -1306,7 +1306,6 @@ AddClientForm.readScope=Read Scope AddClientForm.writeScope=Write Scope AddClientForm.scopeNotAllowed=Not Allowed AddClientForm.scopeAllowed=Allowed -AddClientForm.scopeAllowedAutoApprove=Allowed and Auto Approve server.UpdateClientForm.error=A client with the client id "{0}" already exists server.UpdateClientForm.success=Successfully updated {0} server.AddClientForm.success=Successfully added {0} @@ -1349,7 +1348,6 @@ client.remove.confirm=Remove client {0}? client.revoke.confirm=Revoke all tokens for client {0}? client.scope.write=Write client.scope.read=Read -client.scope.autoApprove=Auto approve? client.edit.title=Edit Client {0} client.edit.new-secret=Generate new client secret client.edit.button=Update Client diff --git a/src/main/resources/scripts/oauthclients/1-beforeUpdate.pl b/src/main/resources/scripts/oauthclients/1-beforeUpdate.pl deleted file mode 100644 index 12daf337dc9..00000000000 --- a/src/main/resources/scripts/oauthclients/1-beforeUpdate.pl +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/perl - -use DBI; -use Getopt::Long; -use strict; -use warnings; - -my $username = "test"; -my $password = "test"; -my $db = "irida_test"; -my $host = "localhost"; - -GetOptions( - 'u|username=s'=>\$username, - 'p|password=s'=>\$password, - 'd|db=s'=>\$db, - 'h|host=s'=>\$host -); - -my $dbstring = "dbi:mysql:$db:$host"; - -my $dbh = DBI->connect($dbstring,$username,$password, {RaiseError=>1,AutoCommit=>0}) or die "Cannot connect to database: $DBI::errstr"; - -my $sql = "SELECT id,clientId from Revisions"; -my $sth = $dbh->prepare($sql); - -my $rv = $sth->execute(); - -my ($rid,$clientId); - -$sth->bind_columns(undef,\$rid,\$clientId); -while($sth->fetch()){ - print "$rid,$clientId\n"; -} - -$dbh->disconnect(); diff --git a/src/main/resources/scripts/oauthclients/2-addClients.pl b/src/main/resources/scripts/oauthclients/2-addClients.pl deleted file mode 100644 index ae16580ac7f..00000000000 --- a/src/main/resources/scripts/oauthclients/2-addClients.pl +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/perl - -use DBI; -use Getopt::Long; -use strict; -use warnings; - -my $username = "test"; -my $password = "test"; -my $db = "irida_test"; -my $host = "localhost"; - -GetOptions( - 'u|username=s'=>\$username, - 'p|password=s'=>\$password, - 'd|db=s'=>\$db, - 'h|host=s'=>\$host -); - -my $dbstring = "dbi:mysql:$db:$host"; - -my @client_details = ( - {id=>1,clientId=>"sequencer", clientSecret=>"N9Ywc6GKWWZotzsJGutj3BZXJDRn65fXJqjrk29yTjI",token_validity=>43200}, - {id=>2,clientId=>"linker", clientSecret=>"ZG5K1AFVSycE25ooxgcBRGCWFzSTfDnJ1DxSkdEmEho",token_validity=>43200}, - {id=>3,clientId=>"pythonLinker", clientSecret=>"bySZBP5jNO9pSZTz3omFRtJs3XFAvshxGgvXIlZ2zjk",token_validity=>43200} -); - -my @client_details_authorities =( - {client_details_id=>1, authority_name=>"ROLE_CLIENT"}, - {client_details_id=>3, authority_name=>"ROLE_CLIENT"}, - {client_details_id=>2, authority_name=>"ROLE_CLIENT"} -); - -my @client_details_scope =( - {client_details_id=>1, scope=>"read"}, - {client_details_id=>1, scope=>"write"}, - {client_details_id=>2, scope=>"read"}, - {client_details_id=>3, scope=>"read"} -); - -my @client_details_grant_types =( - {client_details_id=>1, grant_value=>"password"}, - {client_details_id=>2, grant_value=>"password"}, - {client_details_id=>3, grant_value=>"password"} -); - -my @client_details_resource_ids =( - {client_details_id=>1, resource_id=>"NmlIrida"}, - {client_details_id=>2, resource_id=>"NmlIrida"}, - {client_details_id=>3, resource_id=>"NmlIrida"} -); - -my $dbh = DBI->connect($dbstring,$username,$password, {RaiseError=>1,AutoCommit=>0}) or die "Cannot connect to database: $DBI::errstr"; - -my $sql = "INSERT INTO client_details (id,clientId,clientSecret,token_validity,createdDate) VALUES (?,?,?,?,now())"; -my $sth = $dbh->prepare($sql); -foreach my $client(@client_details){ - my $rv = $sth->execute($client->{id},$client->{clientId},$client->{clientSecret},$client->{token_validity}); - if($rv){ - print "Added client '$client->{clientId}'\n"; - } - else{ - die "Couldn't add client $client->{clientId}"; - } -} - -$sql = "insert into client_details_authorities (client_details_id,authority_name) values (?,?)"; -$sth = $dbh->prepare($sql); -foreach my $client(@client_details_authorities){ - my $rv = $sth->execute($client->{client_details_id},$client->{authority_name}); - if(!$rv){ - die "Couldn't add client authorities"; - } -} - -$sql = "insert into client_details_scope (client_details_id,scope) values (?,?)"; -$sth = $dbh->prepare($sql); -foreach my $client(@client_details_scope){ - my $rv = $sth->execute($client->{client_details_id},$client->{scope}); - if(!$rv){ - die "Couldn't add client scope"; - } -} - -$sql = "insert into client_details_grant_types (client_details_id,grant_value) values (?,?)"; -$sth = $dbh->prepare($sql); -foreach my $client(@client_details_grant_types){ - my $rv = $sth->execute($client->{client_details_id},$client->{grant_value}); - if(!$rv){ - die "Couldn't add client grant type"; - } -} - -$sql = "insert into client_details_resource_ids (client_details_id,resource_id) values (?,?)"; -$sth = $dbh->prepare($sql); -foreach my $client(@client_details_resource_ids){ - my $rv = $sth->execute($client->{client_details_id},$client->{resource_id}); - if(!$rv){ - die "Couldn't add client resource id"; - } -} - -$dbh->commit(); - -$dbh->disconnect(); diff --git a/src/main/resources/scripts/oauthclients/3-afterUpdate.pl b/src/main/resources/scripts/oauthclients/3-afterUpdate.pl deleted file mode 100644 index ac3875ff344..00000000000 --- a/src/main/resources/scripts/oauthclients/3-afterUpdate.pl +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/perl - -use DBI; -use Getopt::Long; -use strict; -use warnings; - -my $username = "test"; -my $password = "test"; -my $db = "irida_test"; -my $host = "localhost"; -my $file; - -GetOptions( - 'u|username=s'=>\$username, - 'p|password=s'=>\$password, - 'd|db=s'=>\$db, - 'h|host=s'=>\$host, - 'i|input=s'=>\$file -); - -my $dbstring = "dbi:mysql:$db:$host"; - -open(FILE,$file) or die "Can't open data file"; - -my $dbh = DBI->connect($dbstring,$username,$password, {RaiseError=>1,AutoCommit=>0}) or die "Cannot connect to database: $DBI::errstr"; - -my $sql = "UPDATE Revisions SET client_id=(SELECT id FROM client_details WHERE clientId=?) WHERE id=?"; -my $sth = $dbh->prepare($sql); - -while(my $line = ){ - chomp $line; - my ($rid,$clientId) = split(/,/,$line); - my $rv = $sth->execute($clientId,$rid); - if($rv){ - print "Updated revision $rid\n"; - } - else{ - $dbh->rollback(); - die "ERROR: Couldn't update revision $rv"; - } -} - -$dbh->commit(); - -close(FILE); - -$dbh->disconnect(); diff --git a/src/main/webapp/pages/oauth/access_confirmation.html b/src/main/webapp/pages/oauth/access_confirmation.html deleted file mode 100644 index e4700031edc..00000000000 --- a/src/main/webapp/pages/oauth/access_confirmation.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - IRIDA OAuth2 Approval Page - - - - -

- -
-
- - - -
-
- - - -
-
- - diff --git a/src/main/webapp/pages/oauth/authorization_consent.html b/src/main/webapp/pages/oauth/authorization_consent.html new file mode 100644 index 00000000000..783bd406a82 --- /dev/null +++ b/src/main/webapp/pages/oauth/authorization_consent.html @@ -0,0 +1,92 @@ + + + + IRIDA OAuth2 Approval Page + + + + + + + + +
+ +
+ + diff --git a/src/main/webapp/pages/oauth/login-api.html b/src/main/webapp/pages/oauth/login-api.html deleted file mode 100644 index 30258edec54..00000000000 --- a/src/main/webapp/pages/oauth/login-api.html +++ /dev/null @@ -1,27 +0,0 @@ - - - -

_Login to IRIDA REST API_

-
- - - - - - - - - - - - - - - -
_User name:_
_Password:_
 
- - -
-
- - \ No newline at end of file diff --git a/src/main/webapp/resources/js/pages/admin/components/clients/add/AddClientModal.jsx b/src/main/webapp/resources/js/pages/admin/components/clients/add/AddClientModal.jsx index b5d8cce1893..75a2bc46c7e 100644 --- a/src/main/webapp/resources/js/pages/admin/components/clients/add/AddClientModal.jsx +++ b/src/main/webapp/resources/js/pages/admin/components/clients/add/AddClientModal.jsx @@ -49,17 +49,9 @@ export function AddClientModal({ children, onComplete, existing = null }) { const radioStyle = { display: "block", lineHeight: `35px` }; if (existing !== null) { - let read = existing.scope.includes("read") - ? "read" - : existing.autoApprovableScopes.includes("read") - ? "auto" - : "no"; + let read = existing.scope.includes("read") ? "read" : "no"; - let write = existing.scope.includes("write") - ? "write" - : existing.autoApprovableScopes.includes("write") - ? "auto" - : "no"; + let write = existing.scope.includes("write") ? "write" : "no"; const refreshToken = existing.authorizedGrantTypes.includes("refresh_token") ? existing.refreshTokenValiditySeconds @@ -292,11 +284,6 @@ export function AddClientModal({ children, onComplete, existing = null }) { {i18n("AddClientForm.scopeAllowed")} - - - {i18n("AddClientForm.scopeAllowedAutoApprove")} - - @@ -311,11 +298,6 @@ export function AddClientModal({ children, onComplete, existing = null }) { {i18n("AddClientForm.scopeAllowed")} - - - {i18n("AddClientForm.scopeAllowedAutoApprove")} - - diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/annotation/GalaxyIntegrationTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/annotation/GalaxyIntegrationTest.java index df2660edd12..88973c6ff8c 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/annotation/GalaxyIntegrationTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/annotation/GalaxyIntegrationTest.java @@ -18,9 +18,8 @@ import ca.corefacility.bioinformatics.irida.config.services.IridaApiServicesConfig; /** - * Annotation that is to be specified on Galaxy integration tests. Simplifies - * the configuration of tests by automatically adding a number of necessary - * annotations. + * Annotation that is to be specified on Galaxy integration tests. Simplifies the configuration of tests by + * automatically adding a number of necessary annotations. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @@ -30,8 +29,12 @@ @Tag("IntegrationTest") @Tag("Galaxy") @ActiveProfiles("test") -@SpringBootTest(classes = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, - IridaApiServicesConfig.class, IridaApiTestFilesystemConfig.class, IridaApiGalaxyTestConfig.class }) +@SpringBootTest(classes = { + DataSourceAutoConfiguration.class, + HibernateJpaAutoConfiguration.class, + IridaApiTestFilesystemConfig.class, + IridaApiServicesConfig.class, + IridaApiGalaxyTestConfig.class }) public @interface GalaxyIntegrationTest { } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/config/IridaApiGalaxyTestConfig.java b/src/test/java/ca/corefacility/bioinformatics/irida/config/IridaApiGalaxyTestConfig.java index 4ad440b5809..389706e495e 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/config/IridaApiGalaxyTestConfig.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/config/IridaApiGalaxyTestConfig.java @@ -14,20 +14,24 @@ import org.springframework.context.annotation.Import; import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.jaxb.Jaxb2Marshaller; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import java.util.concurrent.Executor; /** - * Configuration for any integration tests requiring the use of Galaxy. Used to - * make sure the configuration is the same for every test requiring Galaxy to - * avoid duplicate Galaxy beans being created. - * - * + * Configuration for any integration tests requiring the use of Galaxy. Used to make sure the configuration is the same + * for every test requiring Galaxy to avoid duplicate Galaxy beans being created. */ @TestConfiguration -@Import({ GalaxyExecutionTestConfig.class, NonWindowsLocalGalaxyConfig.class, WindowsLocalGalaxyConfig.class, - AnalysisExecutionServiceTestConfig.class, IridaWorkflowsTestConfig.class, - IridaWorkflowsGalaxyIntegrationTestConfig.class, IridaDbUnitConfig.class }) +@Import({ + GalaxyExecutionTestConfig.class, + NonWindowsLocalGalaxyConfig.class, + WindowsLocalGalaxyConfig.class, + AnalysisExecutionServiceTestConfig.class, + IridaWorkflowsTestConfig.class, + IridaWorkflowsGalaxyIntegrationTestConfig.class, + IridaDbUnitConfig.class }) public class IridaApiGalaxyTestConfig { /** @@ -43,11 +47,19 @@ public Unmarshaller workflowDescriptionUnmarshaller() { } /** - * @return An ExecutorService executing code in the same thread for testing - * purposes. + * @return An ExecutorService executing code in the same thread for testing purposes. */ @Bean public Executor uploadExecutor() { return MoreExecutors.directExecutor(); } + + /** + * @return An InMemoryOAuth2AuthorizationService so that ClientDetailsService does not have bean errors during + * Galaxy integration testing. + */ + @Bean + public OAuth2AuthorizationService authorizationService() { + return new InMemoryOAuth2AuthorizationService(); + } } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/config/IridaIntegrationTestUriConfig.java b/src/test/java/ca/corefacility/bioinformatics/irida/config/IridaIntegrationTestUriConfig.java index 951134d4748..64370fd854c 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/config/IridaIntegrationTestUriConfig.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/config/IridaIntegrationTestUriConfig.java @@ -17,14 +17,10 @@ import io.restassured.http.ContentType; /** - * Configuration to be loaded in IntegrationTest via {@Link Import} annotation. - * This waits for the servlet container initialized event and uses - * {@Link LocalHostUriTemplateHandler} to retrieve the current URL to the servlet - * container and set it up within the specific testing utilities. - * - * Also provides a {@Link LocalHostUriTemplateHandler} {@Link Bean} which can be used - * in tests to access the servlet containers root uri. - * + * Configuration to be loaded in IntegrationTest via {@Link Import} annotation. This waits for the servlet container + * initialized event and uses {@Link LocalHostUriTemplateHandler} to retrieve the current URL to the servlet container + * and set it up within the specific testing utilities. Also provides a {@Link LocalHostUriTemplateHandler} {@Link Bean} + * which can be used in tests to access the servlet containers root uri. */ @TestConfiguration @Profile("it") @@ -38,17 +34,16 @@ public class IridaIntegrationTestUriConfig { @EventListener public void onServletContainerInitialized(WebServerInitializedEvent event) { uriTemplateHandler = new LocalHostUriTemplateHandler(event.getApplicationContext().getEnvironment()); - String baseUrl = uriTemplateHandler.getRootUri() + "/"; + String baseUrl = uriTemplateHandler.getRootUri().replace("localhost", "127.0.0.1") + "/"; AbstractPage.setBaseUrl(baseUrl); RemoteApiUtilities.setBaseUrl(baseUrl); oltuAuthorizationController.setServerBase(baseUrl); // Setup RestAssured - RestAssured.requestSpecification = new RequestSpecBuilder() - .setContentType(ContentType.JSON) - .setAccept(ContentType.JSON) + RestAssured.requestSpecification = new RequestSpecBuilder().setContentType(ContentType.JSON) + .setAccept(ContentType.JSON) .setBaseUri(uriTemplateHandler.getRootUri()) - .build(); + .build(); } @Lazy @@ -56,5 +51,5 @@ public void onServletContainerInitialized(WebServerInitializedEvent event) { public LocalHostUriTemplateHandler uriTemplateHandler() { return uriTemplateHandler; } - + } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/ProjectRemoteRepositoryImplTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/ProjectRemoteRepositoryImplTest.java index 3f3245612e7..5d783dbd838 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/ProjectRemoteRepositoryImplTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/repositories/remote/impl/ProjectRemoteRepositoryImplTest.java @@ -9,6 +9,7 @@ import ca.corefacility.bioinformatics.irida.model.project.Project; import ca.corefacility.bioinformatics.irida.repositories.remote.ProjectRemoteRepository; import ca.corefacility.bioinformatics.irida.service.RemoteAPITokenService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; import ca.corefacility.bioinformatics.irida.web.controller.api.projects.RESTProjectsController; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -19,12 +20,14 @@ public class ProjectRemoteRepositoryImplTest { private ProjectRemoteRepository projectRemoteRepository; private RemoteAPITokenService tokenService; + private UserService userService; private static final String PROJECT_HASH_REL = RESTProjectsController.PROJECT_HASH_REL; @BeforeEach public void setUp() { tokenService = mock(RemoteAPITokenService.class); - projectRemoteRepository = new ProjectRemoteRepositoryImpl(tokenService); + userService = mock(UserService.class); + projectRemoteRepository = new ProjectRemoteRepositoryImpl(tokenService, userService); } @Test diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pages/remoteapi/RemoteAPIDetailsPage.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pages/remoteapi/RemoteAPIDetailsPage.java index 302ded95a83..c0afba3ae93 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pages/remoteapi/RemoteAPIDetailsPage.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/integration/pages/remoteapi/RemoteAPIDetailsPage.java @@ -75,8 +75,8 @@ public void clickDeleteButton() { public void confirmDelete() { logger.debug("clicking confirm-delete button"); - WebElement confirmButton = (new WebDriverWait(driver, 10)).until(ExpectedConditions.elementToBeClickable(By - .className("t-confirm-delete"))); + WebElement confirmButton = (new WebDriverWait(driver, 10)) + .until(ExpectedConditions.elementToBeClickable(By.className("t-confirm-delete"))); confirmButton.click(); } @@ -106,17 +106,17 @@ public void clickAuthorize() { driver.switchTo().window(subWindowHandler); // switch to popup window // Now you are in the popup window, perform necessary actions here - WebElement authorizeButton = driver.findElement(By.id("authorize-btn")); authorizeButton.click(); - - driver.switchTo().window(parentWindowHandler); // switch back to parent window + driver.switchTo().window(parentWindowHandler); // switch back to parent window waitForTime(8000); } public enum ApiStatus { - CONNECTED, INVALID, ERROR + CONNECTED, + INVALID, + ERROR } } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/OltuAuthorizationControllerTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/OltuAuthorizationControllerTest.java index 637867c2a08..ba1f02afb4c 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/OltuAuthorizationControllerTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/OltuAuthorizationControllerTest.java @@ -1,23 +1,17 @@ package ca.corefacility.bioinformatics.irida.ria.unit.web.oauth; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.net.URLDecoder; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; -import org.apache.oltu.oauth2.client.OAuthClient; import org.apache.oltu.oauth2.common.exception.OAuthProblemException; import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.junit.jupiter.api.BeforeEach; @@ -29,14 +23,16 @@ import ca.corefacility.bioinformatics.irida.service.RemoteAPIService; import ca.corefacility.bioinformatics.irida.service.RemoteAPITokenService; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + public class OltuAuthorizationControllerTest { private OltuAuthorizationController controller; - private RemoteAPIService apiService; - private RemoteAPITokenService tokenService; - - OAuthClient oauthClient; + private HttpSession session; private final String serverBase = "http://localserver"; @@ -44,37 +40,43 @@ public class OltuAuthorizationControllerTest { public void setUp() { apiService = mock(RemoteAPIService.class); tokenService = mock(RemoteAPITokenService.class); - oauthClient = mock(OAuthClient.class); + session = mock(HttpSession.class); controller = new OltuAuthorizationController(tokenService, apiService); controller.setServerBase(serverBase); } @Test - public void testAuthenticate() throws OAuthSystemException, UnsupportedEncodingException { + public void testAuthenticate() throws IOException, OAuthSystemException, UnsupportedEncodingException { RemoteAPI remoteAPI = new RemoteAPI("name", "http://uri", "a description", "id", "secret"); remoteAPI.setId(1L); String redirect = "http://base"; - String authenticate = controller.authenticate(remoteAPI, redirect); + String authenticate = controller.authenticate(session, remoteAPI, redirect); // need to decode the escaped characters String decoded = URLDecoder.decode(authenticate, "UTF-8"); assertTrue(decoded.startsWith("redirect:")); - assertTrue(decoded.contains(redirect)); assertTrue(decoded.contains(serverBase)); } @Test - public void testGetTokenFromAuthCode() throws IOException, OAuthSystemException, OAuthProblemException, - URISyntaxException { + public void testGetTokenFromAuthCode() + throws IOException, OAuthSystemException, OAuthProblemException, URISyntaxException { Long apiId = 1L; RemoteAPI remoteAPI = new RemoteAPI("name", "http://remoteLocation", "a description", "id", "secret"); remoteAPI.setId(apiId); String code = "code"; String redirect = "http://originalPage"; + String stateUuid = UUID.randomUUID().toString(); + Map stateMap = new HashMap(); + stateMap.put("apiId", apiId.toString()); + stateMap.put("redirect", redirect); + + when(session.getAttribute(stateUuid)).thenReturn(stateMap); + when(apiService.read(apiId)).thenReturn(remoteAPI); HttpServletRequest request = mock(HttpServletRequest.class); @@ -83,28 +85,35 @@ public void testGetTokenFromAuthCode() throws IOException, OAuthSystemException, requestParams.put("code", new String[] { code }); when(request.getParameterMap()).thenReturn(requestParams); + when(request.getSession()).thenReturn(session); - controller.getTokenFromAuthCode(request, response, apiId, redirect); + controller.getTokenFromAuthCode(request, response, stateUuid); verify(apiService).read(apiId); ArgumentCaptor redirectArg = ArgumentCaptor.forClass(String.class); verify(tokenService).createTokenFromAuthCode(eq(code), eq(remoteAPI), redirectArg.capture()); - + String capturedRedirect = redirectArg.getValue(); - assertTrue(capturedRedirect.contains(redirect)); assertTrue(capturedRedirect.contains(serverBase)); } @Test - public void testGetTokenFromAuthCodeExtraSlash() throws IOException, OAuthSystemException, OAuthProblemException, - URISyntaxException { + public void testGetTokenFromAuthCodeExtraSlash() + throws IOException, OAuthSystemException, OAuthProblemException, URISyntaxException { Long apiId = 1L; RemoteAPI remoteAPI = new RemoteAPI("name", "http://remoteLocation", "a description", "id", "secret"); remoteAPI.setId(apiId); String code = "code"; String redirect = "http://originalPage"; + String stateUuid = UUID.randomUUID().toString(); + Map stateMap = new HashMap(); + stateMap.put("apiId", apiId.toString()); + stateMap.put("redirect", redirect); + + when(session.getAttribute(stateUuid)).thenReturn(stateMap); + //adding a trailing slash to the serverbase to try to confuse the redirect URI controller.setServerBase(serverBase + "/"); @@ -116,8 +125,9 @@ public void testGetTokenFromAuthCodeExtraSlash() throws IOException, OAuthSystem requestParams.put("code", new String[] { code }); when(request.getParameterMap()).thenReturn(requestParams); + when(request.getSession()).thenReturn(session); - controller.getTokenFromAuthCode(request, response, apiId, redirect); + controller.getTokenFromAuthCode(request, response, stateUuid); verify(apiService).read(apiId); @@ -125,7 +135,6 @@ public void testGetTokenFromAuthCodeExtraSlash() throws IOException, OAuthSystem verify(tokenService).createTokenFromAuthCode(eq(code), eq(remoteAPI), redirectArg.capture()); String capturedRedirect = redirectArg.getValue(); - assertTrue(capturedRedirect.contains(redirect)); assertTrue(capturedRedirect.contains(serverBase)); assertFalse(capturedRedirect.contains(serverBase + "//")); } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIAjaxControllerTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIAjaxControllerTest.java index c252b00d417..bdc9febe1f6 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIAjaxControllerTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIAjaxControllerTest.java @@ -23,6 +23,7 @@ import ca.corefacility.bioinformatics.irida.ria.web.rempoteapi.dto.RemoteAPITableModel; import ca.corefacility.bioinformatics.irida.ria.web.services.UIRemoteAPIService; import ca.corefacility.bioinformatics.irida.service.RemoteAPIService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -35,6 +36,7 @@ public class RemoteAPIAjaxControllerTest { private RemoteAPIService remoteAPIService; private UIRemoteAPIService uiRemoteAPIService; + private UserService userService; private RemoteAPIAjaxController controller; private final RemoteAPI REMOTE_API_01 = new RemoteAPI("Toronto", "http://toronto.nowhere", "", "toronto", "123456"); @@ -46,8 +48,9 @@ public class RemoteAPIAjaxControllerTest { public void init() { remoteAPIService = mock(RemoteAPIService.class); uiRemoteAPIService = mock(UIRemoteAPIService.class); + userService = mock(UserService.class); MessageSource messageSource = mock(MessageSource.class); - controller = new RemoteAPIAjaxController(remoteAPIService, uiRemoteAPIService, messageSource); + controller = new RemoteAPIAjaxController(remoteAPIService, uiRemoteAPIService, messageSource, userService); Page remoteAPIPage = new Page<>() { @Override @@ -148,6 +151,7 @@ public void getAjaxAPIListTest() { Long userId = 1L; User puser = new User(userId, USER_NAME, null, null, null, null, null); puser.setSystemRole(Role.ROLE_USER); + when(userService.getUserByUsername(puser.getUsername())).thenReturn(puser); Authentication auth = new UsernamePasswordAuthenticationToken(puser, null); SecurityContextHolder.getContext().setAuthentication(auth); diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIControllerTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIControllerTest.java index 6414222aaad..bf3cd2d286b 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIControllerTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/oauth/RemoteAPIControllerTest.java @@ -1,18 +1,21 @@ package ca.corefacility.bioinformatics.irida.ria.unit.web.oauth; -import ca.corefacility.bioinformatics.irida.exceptions.IridaOAuthException; -import ca.corefacility.bioinformatics.irida.model.RemoteAPI; -import ca.corefacility.bioinformatics.irida.ria.web.oauth.OltuAuthorizationController; -import ca.corefacility.bioinformatics.irida.ria.web.oauth.RemoteAPIController; -import ca.corefacility.bioinformatics.irida.service.RemoteAPIService; -import ca.corefacility.bioinformatics.irida.service.remote.ProjectRemoteService; +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; + import org.apache.oltu.oauth2.common.exception.OAuthSystemException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.ui.ExtendedModelMap; import org.springframework.web.servlet.HandlerMapping; -import javax.servlet.http.HttpServletRequest; +import ca.corefacility.bioinformatics.irida.exceptions.IridaOAuthException; +import ca.corefacility.bioinformatics.irida.model.RemoteAPI; +import ca.corefacility.bioinformatics.irida.ria.web.oauth.OltuAuthorizationController; +import ca.corefacility.bioinformatics.irida.ria.web.oauth.RemoteAPIController; +import ca.corefacility.bioinformatics.irida.service.RemoteAPIService; +import ca.corefacility.bioinformatics.irida.service.remote.ProjectRemoteService; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -29,8 +32,7 @@ public void setUp() { remoteAPIService = mock(RemoteAPIService.class); projectRemoteService = mock(ProjectRemoteService.class); authController = mock(OltuAuthorizationController.class); - remoteAPIController = new RemoteAPIController(remoteAPIService, projectRemoteService, - authController); + remoteAPIController = new RemoteAPIController(remoteAPIService, projectRemoteService, authController); } @@ -62,7 +64,7 @@ public void testConnectToAPIActiveToken() { } @Test - public void testHandleOAuthException() throws OAuthSystemException { + public void testHandleOAuthException() throws IOException, OAuthSystemException { HttpServletRequest request = mock(HttpServletRequest.class); String redirect = "http://request"; when(request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).thenReturn(redirect); @@ -71,6 +73,6 @@ public void testHandleOAuthException() throws OAuthSystemException { IridaOAuthException ex = new IridaOAuthException("msg", client); remoteAPIController.handleOAuthException(request, ex); - verify(authController).authenticate(client, redirect); + verify(authController).authenticate(request.getSession(), client, redirect); } } \ No newline at end of file diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIAssociatedProjectsServiceTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIAssociatedProjectsServiceTest.java index 253472e2b2b..52a08d0d419 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIAssociatedProjectsServiceTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIAssociatedProjectsServiceTest.java @@ -16,6 +16,7 @@ import ca.corefacility.bioinformatics.irida.ria.web.services.UIAssociatedProjectsService; import ca.corefacility.bioinformatics.irida.security.permissions.project.ProjectOwnerPermission; import ca.corefacility.bioinformatics.irida.service.ProjectService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; import com.google.common.collect.ImmutableList; @@ -26,6 +27,7 @@ public class UIAssociatedProjectsServiceTest { private UIAssociatedProjectsService service; private ProjectService projectService; + private UserService userService; // DATA private final Long PROJECT_1_ID = 1L; @@ -39,9 +41,10 @@ public class UIAssociatedProjectsServiceTest { @BeforeEach public void setUp() { projectService = mock(ProjectService.class); + userService = mock(UserService.class); ProjectOwnerPermission projectOwnerPermission = mock(ProjectOwnerPermission.class); MessageSource messageSource = mock(MessageSource.class); - service = new UIAssociatedProjectsService(projectService, projectOwnerPermission, messageSource); + service = new UIAssociatedProjectsService(projectService, projectOwnerPermission, messageSource, userService); PROJECT_1.setId(PROJECT_1_ID); PROJECT_2.setId(PROJECT_2_ID); diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIProjectServiceTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIProjectServiceTest.java index f371cc212bf..d8af6222be3 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIProjectServiceTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIProjectServiceTest.java @@ -16,6 +16,7 @@ import ca.corefacility.bioinformatics.irida.service.ProjectService; import ca.corefacility.bioinformatics.irida.service.sample.MetadataTemplateService; import ca.corefacility.bioinformatics.irida.service.sample.SampleService; +import ca.corefacility.bioinformatics.irida.service.user.UserService; import com.google.common.collect.ImmutableList; @@ -28,25 +29,27 @@ public class UIProjectServiceTest { private UIProjectsService service; private ProjectService projectService; private SampleService sampleService; + private UserService userService; @BeforeEach public void setUp() { projectService = mock(ProjectService.class); sampleService = mock(SampleService.class); + userService = mock(UserService.class); MessageSource messageSource = mock(MessageSource.class); ProjectOwnerPermission projectOwnerPermission = mock(ProjectOwnerPermission.class); ManageLocalProjectSettingsPermission manageLocalProjectSettingsPermission = mock( ManageLocalProjectSettingsPermission.class); MetadataTemplateService metadataTemplateService = mock(MetadataTemplateService.class); service = new UIProjectsService(projectService, sampleService, messageSource, projectOwnerPermission, - manageLocalProjectSettingsPermission, metadataTemplateService); + manageLocalProjectSettingsPermission, metadataTemplateService, userService); // Set up the project PROJECT_01.setId(PROJECT_01_ID); PROJECT_01.setName("Test Project"); when(projectService.create(any(Project.class))).thenReturn(PROJECT_01); - when(projectService.createProjectWithSamples(any(Project.class), anyList(), anyBoolean())).thenReturn( - PROJECT_01); + when(projectService.createProjectWithSamples(any(Project.class), anyList(), anyBoolean())) + .thenReturn(PROJECT_01); } @Test @@ -55,7 +58,7 @@ public void testCreateProject() { request.setName("Test Project"); request.setLock(false); request.setMetadataRestrictions(ImmutableList.of()); - + service.createProject(request); verify(projectService, times(1)).create(any(Project.class)); diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIUserGroupsServiceTest.java b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIUserGroupsServiceTest.java index b9ceba04015..c54a89e66a7 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIUserGroupsServiceTest.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/ria/unit/web/services/UIUserGroupsServiceTest.java @@ -30,7 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString;import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; public class UIUserGroupsServiceTest { @@ -62,9 +62,8 @@ public void setUp() { SecurityContext securityContext = mock(SecurityContext.class); when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext); - when(SecurityContextHolder.getContext() - .getAuthentication() - .getPrincipal()).thenReturn(USER_1); + when(SecurityContextHolder.getContext().getAuthentication().getName()).thenReturn(USER_1.getUsername()); + when(userService.getUserByUsername(USER_1.getUsername())).thenReturn(USER_1); when(userGroupService.search(any(), any())).thenReturn(getPagedUserGroups()); when(userGroupService.read(GROUP_1.getId())).thenReturn(GROUP_1); diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/security/annotations/WithMockOAuth2Client.java b/src/test/java/ca/corefacility/bioinformatics/irida/security/annotations/WithMockOAuth2Client.java index 8bbdc885f14..929baf67f33 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/security/annotations/WithMockOAuth2Client.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/security/annotations/WithMockOAuth2Client.java @@ -7,15 +7,11 @@ import java.lang.annotation.Target; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.test.context.support.WithSecurityContext; /** - * Annotation for use with {@link WithMockOAuth2SecurityContextFactory} to add a - * {@link OAuth2Authentication} object into the security context for junit - * tests. - * - * + * Annotation for use with {@link WithMockOAuth2SecurityContextFactory} to add a {@link OAuth2Authentication} object + * into the security context for junit tests. */ @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @@ -23,19 +19,17 @@ @WithSecurityContext(factory = WithMockOAuth2SecurityContextFactory.class) public @interface WithMockOAuth2Client { /** - * The username to be used. Note that {@link #value()} is a synonym for - * {@link #username()}, but if {@link #username()} is specified it will take - * precedence. + * The username to be used. Note that {@link #value()} is a synonym for {@link #username()}, but if + * {@link #username()} is specified it will take precedence. * * @return */ String username() default ""; /** - * The roles to use. The default is "USER". A {@link GrantedAuthority} will - * be created for each value within roles. Each value in roles will - * automatically be prefixed with "ROLE_". For example, the default will - * result in "ROLE_USER" being used. + * The roles to use. The default is "USER". A {@link GrantedAuthority} will be created for each value within roles. + * Each value in roles will automatically be prefixed with "ROLE_". For example, the default will result in + * "ROLE_USER" being used. * * @return */ diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/security/annotations/WithMockOAuth2SecurityContextFactory.java b/src/test/java/ca/corefacility/bioinformatics/irida/security/annotations/WithMockOAuth2SecurityContextFactory.java index 75629b1b410..dad5ee5b753 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/security/annotations/WithMockOAuth2SecurityContextFactory.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/security/annotations/WithMockOAuth2SecurityContextFactory.java @@ -1,28 +1,22 @@ package ca.corefacility.bioinformatics.irida.security.annotations; +import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Set; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; -import org.springframework.security.oauth2.provider.OAuth2Authentication; -import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.test.context.support.WithSecurityContextFactory; -import com.google.common.collect.Sets; - /** - * Security context factory listening for {@link WithMockOAuth2Client} - * annotations on junit tests. Adds a OAuth2Authentication object into the - * security context. - * - * + * Security context factory listening for {@link WithMockOAuth2Client} annotations on junit tests. Adds a + * OAuth2Authentication object into the security context. */ public class WithMockOAuth2SecurityContextFactory implements WithSecurityContextFactory { @@ -48,19 +42,22 @@ public SecurityContext createSecurityContext(WithMockOAuth2Client withClient) { // Get the client id String clientId = withClient.clientId(); - // get the oauth scopes - String[] scopes = withClient.scope(); - Set scopeCollection = Sets.newHashSet(scopes); // Create the UsernamePasswordAuthenticationToken User principal = new User(username, withClient.password(), true, true, true, true, authorities); - Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), - principal.getAuthorities()); - // Create the authorization request and OAuth2Authentication object - OAuth2Request authRequest = new OAuth2Request(null, clientId, null, true, scopeCollection, null, null, null, - null); - OAuth2Authentication oAuth = new OAuth2Authentication(authRequest, authentication); + // Create the JwtAuthenticationToken + Jwt jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .subject(principal.getUsername()) + .audience(Collections.singletonList(clientId)) + .issuedAt(Instant.MIN) + .expiresAt(Instant.MAX) + .issuer("https://issuer.example.org") + .jti("jti") + .notBefore(Instant.MIN) + .build(); + JwtAuthenticationToken oAuth = new JwtAuthenticationToken(jwt, authorities); // Add the OAuth2Authentication object to the security context SecurityContext context = SecurityContextHolder.createEmptyContext(); diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/service/impl/integration/IridaClientDetailsServiceImplIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/service/impl/integration/IridaClientDetailsServiceImplIT.java index 223cc379c57..34e4dfa96c3 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/service/impl/integration/IridaClientDetailsServiceImplIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/service/impl/integration/IridaClientDetailsServiceImplIT.java @@ -1,21 +1,19 @@ package ca.corefacility.bioinformatics.irida.service.impl.integration; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.security.test.context.support.WithMockUser; import ca.corefacility.bioinformatics.irida.annotation.ServiceIntegrationTest; +import ca.corefacility.bioinformatics.irida.exceptions.EntityNotFoundException; +import ca.corefacility.bioinformatics.irida.model.IridaClientDetails; import ca.corefacility.bioinformatics.irida.service.impl.IridaClientDetailsServiceImpl; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DatabaseTearDown; +import static org.junit.jupiter.api.Assertions.*; + @ServiceIntegrationTest @DatabaseSetup("/ca/corefacility/bioinformatics/irida/service/impl/IridaClientDetailsServiceImplIT.xml") @DatabaseTearDown("/ca/corefacility/bioinformatics/irida/test/integration/TableReset.xml") @@ -26,7 +24,7 @@ public class IridaClientDetailsServiceImplIT { @Test @WithMockUser(username = "anonymous", roles = "ANONYMOUS") public void testReadClientDetailsAnonymous() { - ClientDetails loadClientByClientId = clientDetailsService.loadClientByClientId("testClient"); + IridaClientDetails loadClientByClientId = clientDetailsService.loadClientByClientId("testClient"); assertNotNull(loadClientByClientId); assertEquals(loadClientByClientId.getClientId(), "testClient"); } @@ -34,7 +32,7 @@ public void testReadClientDetailsAnonymous() { @Test @WithMockUser(username = "anonymous", roles = "ANONYMOUS") public void testClientNotExists() { - assertThrows(NoSuchClientException.class, () -> { + assertThrows(EntityNotFoundException.class, () -> { clientDetailsService.loadClientByClientId("badClient"); }); } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/web/controller/test/integration/security/SecurityIT.java b/src/test/java/ca/corefacility/bioinformatics/irida/web/controller/test/integration/security/SecurityIT.java index d2ff05514ff..53a58da96e0 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/web/controller/test/integration/security/SecurityIT.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/web/controller/test/integration/security/SecurityIT.java @@ -1,29 +1,24 @@ package ca.corefacility.bioinformatics.irida.web.controller.test.integration.security; -import static io.restassured.RestAssured.expect; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; - import org.apache.http.HttpStatus; import org.junit.jupiter.api.Test; import ca.corefacility.bioinformatics.irida.annotation.RestIntegrationTest; +import static io.restassured.RestAssured.expect; + /** * General tests relating to security for the REST API. - * */ @RestIntegrationTest public class SecurityIT { /** - * Test that we get the right type of response when we don't have valid - * credentials. + * Test that we get the right type of response when we don't have valid credentials. */ @Test public void testAccessWithoutAuthentication() { - expect().body("error", is("invalid_request")).and() - .body("error_description", containsString("No client credentials were provided")) - .statusCode(HttpStatus.SC_UNAUTHORIZED).when().get("/api"); + + expect().statusCode(HttpStatus.SC_UNAUTHORIZED).header("WWW-Authenticate", "Bearer").when().get("/api"); } } diff --git a/src/test/java/ca/corefacility/bioinformatics/irida/web/controller/test/integration/util/ITestAuthUtils.java b/src/test/java/ca/corefacility/bioinformatics/irida/web/controller/test/integration/util/ITestAuthUtils.java index 7e405a342dc..c2a24f2c0df 100644 --- a/src/test/java/ca/corefacility/bioinformatics/irida/web/controller/test/integration/util/ITestAuthUtils.java +++ b/src/test/java/ca/corefacility/bioinformatics/irida/web/controller/test/integration/util/ITestAuthUtils.java @@ -6,13 +6,12 @@ import java.util.HashMap; import java.util.Map; +import io.restassured.path.json.exception.JsonPathException; import io.restassured.response.Response; import io.restassured.specification.RequestSpecification; /** * Utilities for doing web requests as certain types of user roles. - * - * */ public class ITestAuthUtils { @@ -25,11 +24,11 @@ public class ITestAuthUtils { private static final String ROLE_OTHER_USER = "other"; public static final String BAD_USERNAME = "bad_username"; public static final String BAD_PASSWORD = "bad_password"; - + private static final String CLIENT_ID = "testClient"; private static final String CLIENT_SECRET = "testClientSecret"; private static final String OAUTH_ENDPOINT = "/api/oauth/token"; - + static { ROLE_TO_USER = new HashMap<>(); ROLE_TO_USER.put(ROLE_ADMIN, new AuthenticationHolder("admin", "password1")); @@ -43,30 +42,35 @@ public class ITestAuthUtils { public static RequestSpecification asRole(String role) { AuthenticationHolder pair = ROLE_TO_USER.get(role); - String oAuthToken = getOAuthToken(pair.username,pair.password); + String oAuthToken = getOAuthToken(pair.username, pair.password); pair.setToken(oAuthToken); String authString = "Bearer " + oAuthToken; - return given().header("Authorization", authString); + return given().header("Authorization", authString); } - + public static String getTokenForRole(final String role) { - AuthenticationHolder pair = ROLE_TO_USER.get(role); + AuthenticationHolder pair = ROLE_TO_USER.get(role); if (pair.token == null) { asRole(role); } return pair.token; } - - private static String getOAuthToken(String username, String password){ - Response response = given().param("grant_type", "password") - .param("client_id", CLIENT_ID) - .param("client_secret", CLIENT_SECRET) - .param("username", username) - .param("password", password) - .get(OAUTH_ENDPOINT); - - String token = from(response.getBody().asString()).getString("access_token"); - return token; + + private static String getOAuthToken(String username, String password) { + Response response = given().auth() + .preemptive() + .basic(CLIENT_ID, CLIENT_SECRET) + .contentType("application/x-www-form-urlencoded") + .formParam("grant_type", "password") + .formParam("username", username) + .formParam("password", password) + .post(OAUTH_ENDPOINT); + try { + String token = from(response.getBody().asString()).getString("access_token"); + return token; + } catch (JsonPathException ex) { + return ""; + } } /** @@ -77,7 +81,7 @@ private static String getOAuthToken(String username, String password){ public static RequestSpecification asUser() { return asRole(ROLE_USER); } - + /** * Execute an HTTP request as a *different* user. * @@ -104,7 +108,7 @@ public static RequestSpecification asManager() { public static RequestSpecification asAdmin() { return asRole(ROLE_ADMIN); } - + /** * Execute an HTTP request as a sequencer. * @@ -123,7 +127,7 @@ public AuthenticationHolder(String username, String password) { this.username = username; this.password = password; } - + public void setToken(final String token) { this.token = token; } diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/IridaClientDetailsServiceImplIT.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/IridaClientDetailsServiceImplIT.xml index d21481284a6..b2ab4d4de79 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/IridaClientDetailsServiceImplIT.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/IridaClientDetailsServiceImplIT.xml @@ -1,33 +1,13 @@ - + - + + - - - - - - - - - - - + + - + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml index 8d09e5a4b37..676a15cea9c 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/ProjectsPageIT.xml @@ -1,218 +1,119 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/oauth/CreateRemoteApisIT.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/oauth/CreateRemoteApisIT.xml index b1eaf01af0f..334d5e35fa6 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/oauth/CreateRemoteApisIT.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/oauth/CreateRemoteApisIT.xml @@ -1,13 +1,6 @@ - + - - - - + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteApisIT.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteApisIT.xml index 155448664c4..d7afc9e920b 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteApisIT.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/oauth/RemoteApisIT.xml @@ -1,30 +1,10 @@ - - - - + + + + - - - - - + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/pipelines/AssemblyPipelinePageIT.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/pipelines/AssemblyPipelinePageIT.xml index 1ee79870bcf..4eff6361b06 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/pipelines/AssemblyPipelinePageIT.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/pipelines/AssemblyPipelinePageIT.xml @@ -1,85 +1,41 @@ - - - + + + - - - - - - + + + - + - - - + + + - - + + - - + + - - - - + + + + - - + + - - - + + + - - - + + + - - - + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/pipelines/PipelinePhylogenomicsView.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/pipelines/PipelinePhylogenomicsView.xml index 2773b4e49f6..812410b63a1 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/pipelines/PipelinePhylogenomicsView.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/pipelines/PipelinePhylogenomicsView.xml @@ -1,109 +1,50 @@ - - - - + + + + - - - - - - + + + - - + + - - - - - - + + + + + + - - + + - - - + + + - - - + + + - - + + - - - + + + - + - - + + - - + + - - - + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/AssociatedProjectsPageIT.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/AssociatedProjectsPageIT.xml index a87c263f728..8609f14df0f 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/AssociatedProjectsPageIT.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/AssociatedProjectsPageIT.xml @@ -1,54 +1,30 @@ - + - - - - - - + + + - - - - - - + + + + + + - - - - - + + + + + - - - + + + - + - - - + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectLineListCreateTemplatePage.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectLineListCreateTemplatePage.xml index c03eebf95f3..5297b7456ed 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectLineListCreateTemplatePage.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectLineListCreateTemplatePage.xml @@ -1,162 +1,92 @@ - - - - + + + + - - - - - - + + + + + + + + + - - - - - - + + + + + + + + - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + + + + - - - - - - - - - - - + + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectSampleMetadataView.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectSampleMetadataView.xml index c03eebf95f3..5297b7456ed 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectSampleMetadataView.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectSampleMetadataView.xml @@ -1,162 +1,92 @@ - - - - + + + + - - - - - - + + + + + + + + + - - - - - - + + + + + + + + - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + + + + - - - - - - - - - - - + + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectSamplesView.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectSamplesView.xml index 090bf95e254..4cab60503ed 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectSamplesView.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/ria/web/projects/ProjectSamplesView.xml @@ -1,205 +1,114 @@ - - - - + + + + - - - - - - + + + - - - - + + + + - - - - - - - - + + + + + + + + - + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - + + + + + + - - - - + + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/test/integration/TableReset.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/test/integration/TableReset.xml index f5b46ce1d20..6d97954755a 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/test/integration/TableReset.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/test/integration/TableReset.xml @@ -6,22 +6,18 @@ - + - - + + - + - + @@ -35,63 +31,60 @@ - - - - - - - - + + + + + - + - - + + - + - + - + - - + + - + - + - - - - - - + + + + + + @@ -99,11 +92,11 @@ - + - - - + + + @@ -133,9 +126,6 @@ - - - @@ -144,24 +134,24 @@ - - - + + + - - + + - - - - - - - - - + + + + + + + + + - + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/analysis/RESTAnalysisSubmissionControllerIT.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/analysis/RESTAnalysisSubmissionControllerIT.xml index a1564c0f35d..26b48ee3cca 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/analysis/RESTAnalysisSubmissionControllerIT.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/analysis/RESTAnalysisSubmissionControllerIT.xml @@ -1,88 +1,37 @@ - - - + + + - - - - - - - + + + + - + - + - + - - - - + + + + - + - - + + - - - + + + - + - + - - + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectIntegrationTest.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectIntegrationTest.xml index 5d5d840074b..6f091139c44 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectIntegrationTest.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectIntegrationTest.xml @@ -1,52 +1,21 @@ - - - - - + + + + + - + - + - - + + - - - - - - - - + + + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectSamplesIntegrationTest.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectSamplesIntegrationTest.xml index b6a0020a364..83da5d3a9d1 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectSamplesIntegrationTest.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectSamplesIntegrationTest.xml @@ -1,65 +1,29 @@ - - - - + + + + - - - + + + - - - + + + - - - - + + + + - - - + + + - - - - - - - - + + + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectUsersIntegrationTest.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectUsersIntegrationTest.xml index d0705551421..6984f6f2088 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectUsersIntegrationTest.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/ProjectUsersIntegrationTest.xml @@ -1,53 +1,20 @@ - - - - - + + + + + - - + + - - - + + + - - - - - - - - + + + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/RESTProjectAnalysisControllerIT.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/RESTProjectAnalysisControllerIT.xml index 6f400313684..0e183052d77 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/RESTProjectAnalysisControllerIT.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/project/RESTProjectAnalysisControllerIT.xml @@ -1,68 +1,26 @@ - - - + + + - + - + - + - - - - + + + + - - - - - - - + + + + - - - - + + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/RESTSampleAssemblyControllerIT.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/RESTSampleAssemblyControllerIT.xml index 3ce84dbdab0..14241ea0710 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/RESTSampleAssemblyControllerIT.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/RESTSampleAssemblyControllerIT.xml @@ -1,60 +1,29 @@ - - - + + + - + - + - - + + - - - - + + + + - + - - - - - - - + + + + - - - - - + + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/RESTSampleMetadataControllerIT.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/RESTSampleMetadataControllerIT.xml index ddc6a74a898..94796c7b656 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/RESTSampleMetadataControllerIT.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/RESTSampleMetadataControllerIT.xml @@ -1,53 +1,30 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/SampleSequenceFilesIntegrationTest.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/SampleSequenceFilesIntegrationTest.xml index fbd7a1bad02..02a4e224678 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/SampleSequenceFilesIntegrationTest.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sample/SampleSequenceFilesIntegrationTest.xml @@ -1,105 +1,54 @@ - - - + + + - + - + - - + + - + - + - - - - - - - + + + + - - - - - - - + + + + + + + - - - - + + + + - - - - + + + + - - - + + + - - - + + + - - - + + + - - + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sequencingrun/SequencingRunIntegrationTest.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sequencingrun/SequencingRunIntegrationTest.xml index b802bb36eee..dced0a7f84c 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sequencingrun/SequencingRunIntegrationTest.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/sequencingrun/SequencingRunIntegrationTest.xml @@ -1,49 +1,20 @@ - - - - + + + + - - - + + + - - - + + + - - - + - - - + + \ No newline at end of file diff --git a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/user/UserIntegrationTest.xml b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/user/UserIntegrationTest.xml index 18649d8bb64..7211fc18ef4 100644 --- a/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/user/UserIntegrationTest.xml +++ b/src/test/resources/ca/corefacility/bioinformatics/irida/web/controller/test/integration/user/UserIntegrationTest.xml @@ -1,30 +1,11 @@ - - - + + + - - - + - - - + + \ No newline at end of file