Commit 198ff6ff authored by Ashutosh Shimpi's avatar Ashutosh Shimpi
Browse files

Changes related to GA4GH integration with ega-data-api

parent 99df448f
Pipeline #123068 failed with stages
in 20 minutes and 51 seconds
......@@ -74,6 +74,18 @@
<scope>test</scope>
</dependency> -->
<!-- JWT Dependencies -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>4.3.1</version>
</dependency>
<dependency>
<groupId>net.minidev</groupId>
<artifactId>json-smart</artifactId>
<version>2.3</version>
</dependency>
</dependencies>
<dependencyManagement>
......
......@@ -15,19 +15,25 @@ public class Constants {
public static String KEYS_SERVICE;
public static String DATA_SERVICE;
@Value("${ega.internal.filedatabase.url}")
public void setFileDatabaseService(String filedburl) {
FILEDATABASE_SERVICE = filedburl;
}
@Value("${ega.internal.res.url}")
public void setKeyService(String resurl) {
RES_SERVICE = resurl;
}
@Value("${ega.internal.key.url}")
public void setResService(String keyurl) {
KEYS_SERVICE = keyurl;
}
}
@Value("${ega.internal.data.url}")
public void setDataService(final String dataServiceURL) {
DATA_SERVICE = dataServiceURL;
}
}
......@@ -15,6 +15,14 @@
*/
package eu.elixir.ega.ebi.commons.config;
import com.nimbusds.jwt.SignedJWT;
import eu.elixir.ega.ebi.commons.shared.service.FileDatasetService;
import eu.elixir.ega.ebi.commons.shared.service.Ga4ghService;
import eu.elixir.ega.ebi.commons.shared.service.JWTService;
import eu.elixir.ega.ebi.commons.shared.service.UserDetailsService;
import net.minidev.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
......@@ -26,23 +34,60 @@ import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import java.net.URI;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static java.lang.System.currentTimeMillis;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static java.util.stream.Collectors.toMap;
/**
* @author asenf
*/
public class MyAccessTokenConverter implements AccessTokenConverter {
private UserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
private static final Logger LOGGER = LoggerFactory.getLogger(MyAccessTokenConverter.class);
private UserAuthenticationConverter userTokenConverter;
private boolean includeGrantType;
private final JWTService jwtService;
private final Ga4ghService ga4ghService;
private final FileDatasetService fileDatasetService;
private final UserDetailsService userDetailsService;
public MyAccessTokenConverter(final JWTService jwtService,
final Ga4ghService ga4ghService,
final FileDatasetService fileDatasetService,
final UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.ga4ghService = ga4ghService;
this.fileDatasetService = fileDatasetService;
this.userTokenConverter = new DefaultUserAuthenticationConverter();
this.userDetailsService = userDetailsService;
}
public MyAccessTokenConverter(final JWTService jwtService,
final Ga4ghService ga4ghService,
final FileDatasetService fileDatasetService,
final UserAuthenticationConverter userTokenConverter,
final UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.ga4ghService = ga4ghService;
this.fileDatasetService = fileDatasetService;
this.userTokenConverter = userTokenConverter;
this.userDetailsService = userDetailsService;
}
/**
* Converter for the part of the data in the token representing a user.
......@@ -69,6 +114,7 @@ public class MyAccessTokenConverter implements AccessTokenConverter {
*
* @param token The authentication token to convert
* @param authentication Authentication object to extract information from.
*
* @return A {@link HashMap} of authentication tokens
*/
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
......@@ -113,8 +159,9 @@ public class MyAccessTokenConverter implements AccessTokenConverter {
* information from 'map'.
*
* @param value Value to set in the return authentication token
* @param map An authentication map, such as returned from
* {@link convertAccessToken}
* @param map An authentication map, such as returned from
* {@link AccessTokenConverter}
*
* @return Access token containing the combined information
*/
public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
......@@ -140,18 +187,17 @@ public class MyAccessTokenConverter implements AccessTokenConverter {
* authentication request.
*
* @param map An authentication map, such as returned from
* {@link convertAccessToken}
* {@link AccessTokenConverter}
*
* @return An OAuth2 authentication object
*/
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
final List<String> ga4ghDatasets = ga4ghService.getDatasets(getEGAAccountId(map));
final Map<String, List<String>> datasetFileMap = getDatasetFileMap(ga4ghDatasets);
// Add Dataset Permissions as Roles
Map<String, Object> info = new HashMap<>(map);
if (info.containsKey("Permissions")) {
Object get = info.get("Permissions");
info.put(AUTHORITIES, get); // OK - Permissions from AAI Response
}
final Map<String, Object> info = new HashMap<>(map);
info.put(AUTHORITIES, datasetFileMap);
Map<String, String> parameters = new HashMap<>();
Set<String> scope = extractScope(map);
......@@ -174,12 +220,60 @@ public class MyAccessTokenConverter implements AccessTokenConverter {
return new OAuth2Authentication(request, user);
}
private String getEGAAccountId(final Map<String, ?> map) {
return userDetailsService
.getEGAAccountId((String) map.get("user_id"))
.orElseGet(() -> userDetailsService
.getEGAAccountIdForElixirId((String) map.get("sub"))
.orElse(""));
}
private boolean isValidToken(final SignedJWT signedJWT) {
try {
return new Date(currentTimeMillis())
.before(signedJWT.getJWTClaimsSet().getExpirationTime());
} catch (ParseException e) {
LOGGER.error("Unable to get JWT token expiration time from=" + e.getMessage(), e);
}
return false;
}
private Optional<String> getDatasetId(final SignedJWT signedJWT) {
try {
final JSONObject jsonObject = signedJWT.getJWTClaimsSet().toJSONObject();
final JSONObject jsonObjectGa4ghVisa = (JSONObject) jsonObject.get("ga4gh_visa_v1");
final String type = jsonObjectGa4ghVisa.getAsString("type");
if ("ControlledAccessGrants".equals(type)) {
final String uri = URI.create(jsonObjectGa4ghVisa.getAsString("value")).getPath();
return of(uri.substring(uri.lastIndexOf("/") + 1));
}
} catch (ParseException e) {
LOGGER.error("Unable to get JWT claims as JSON object=" + e.getMessage(), e);
}
return empty();
}
private Map<String, List<String>> getDatasetFileMap(final List<String> ga4ghDatasets) {
return ga4ghDatasets
.parallelStream()
.map(jwtService::parse)
.filter(Optional::isPresent)
.map(Optional::get)
.filter(jwtService::isValidSignature)
.filter(this::isValidToken)
.map(this::getDatasetId)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(toMap(datasetId -> datasetId, fileDatasetService::getFileIds));
}
/**
* Extracts the authentication audience (held in the 'AUD' key) from the
* given authentication map.
*
* @param map An authentication map, such as returned from
* {@link convertAccessToken}
* {@link AccessTokenConverter}
*
* @return The extracted authentication audience
*/
private Collection<String> getAudience(Map<String, ?> map) {
......@@ -197,7 +291,8 @@ public class MyAccessTokenConverter implements AccessTokenConverter {
* given authentication map.
*
* @param map An authentication map, such as returned from
* {@link convertAccessToken}
* {@link AccessTokenConverter}
*
* @return The extracted authentication scope
*/
private Set<String> extractScope(Map<String, ?> map) {
......@@ -214,5 +309,4 @@ public class MyAccessTokenConverter implements AccessTokenConverter {
}
return scope;
}
}
......@@ -106,13 +106,16 @@ public class MyConfiguration {
.build());
GuavaCache datasetFile = new GuavaCache("datasetFile", CacheBuilder.newBuilder()
.expireAfterWrite(24, TimeUnit.HOURS)
.build());
.build());
GuavaCache indexFile = new GuavaCache("indexFile", CacheBuilder.newBuilder()
.expireAfterWrite(24, TimeUnit.HOURS)
.build());
.build());
GuavaCache datasetsGa4gh = new GuavaCache("datasetsGa4gh", CacheBuilder.newBuilder()
.expireAfterWrite(50, TimeUnit.MINUTES)
.build());
simpleCacheManager.setCaches(Arrays.asList(tokens, access, reqFile, index, fileHead, headerFile, fileSize,
fileFile, fileDatasetFile, datasetFile, indexFile));
fileFile, fileDatasetFile, datasetFile, indexFile, datasetsGa4gh));
return simpleCacheManager;
}
......
......@@ -24,15 +24,16 @@ import org.springframework.security.oauth2.provider.token.UserAuthenticationConv
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static java.util.Collections.emptyMap;
/**
* @author asenf
* <p>
* Changed field from USERNAME to 'user_id' - looking for a fix in MiterID directly!
* <p>
* Changed field from USERNAME to 'user_id' - looking for a fix in MiterID directly!
*/
public class MyUserAuthenticationConverter implements UserAuthenticationConverter {
......@@ -73,48 +74,40 @@ public class MyUserAuthenticationConverter implements UserAuthenticationConverte
return response;
}
@SuppressWarnings("unchecked")
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Object principal = map.get(USERNAME);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
Map<String, List<String>> datasetFileMapping = getdatasetFileMapping(map, authorities);
Map<String, List<String>> datasetFileMapping;
Collection<? extends GrantedAuthority> authorities;
if (map.containsKey(AUTHORITIES)) {
datasetFileMapping = (Map<String, List<String>>) map.get(AUTHORITIES);
authorities = getAuthorities(datasetFileMapping.keySet());
} else {
authorities = defaultAuthorities;
datasetFileMapping = emptyMap();
}
if (userDetailsService != null) {
UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
authorities = user.getAuthorities();
principal = user;
}
return new CustomUsernamePasswordAuthenticationToken(principal, "N/A", authorities, datasetFileMapping);
return new CustomUsernamePasswordAuthenticationToken(
principal,
"N/A",
authorities,
datasetFileMapping
);
}
return null;
}
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
if (!map.containsKey(AUTHORITIES)) {
return defaultAuthorities;
}
Object authorities = map.get(AUTHORITIES);
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
}
if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
.collectionToCommaDelimitedString((Collection<?>) authorities));
}
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}
private Map<String, List<String>> getdatasetFileMapping(Map<String, ?> map,
Collection<? extends GrantedAuthority> authorities) {
Map<String, List<String>> datasetFileMappings = new HashMap<>();
if (authorities != null && authorities.size() > 0) {
for (GrantedAuthority grantedAuthority : authorities) {
String dataSetId = grantedAuthority.getAuthority();
if (map.containsKey(dataSetId)) {
datasetFileMappings.put(dataSetId, (List<String>) map.get(dataSetId));
}
}
}
return datasetFileMappings;
private Collection<? extends GrantedAuthority> getAuthorities(Collection<?> authorities) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
.collectionToCommaDelimitedString(authorities));
}
}
/*
*
* Copyright 2021 EMBL - European Bioinformatics Institute
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package eu.elixir.ega.ebi.commons.shared.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class JWTTokenDTO {
private List<String> userClaimsTokens;
public JWTTokenDTO() {
}
public List<String> getUserClaimsTokens() {
return userClaimsTokens;
}
@JsonProperty("ga4gh_passport_v1")
public void setUserClaimsTokens(List<String> userClaimsTokens) {
this.userClaimsTokens = userClaimsTokens;
}
}
/*
*
* Copyright 2021 EMBL - European Bioinformatics Institute
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package eu.elixir.ega.ebi.commons.shared.service;
import java.util.List;
public interface FileDatasetService {
List<String> getFileIds(String datasetId);
}
/*
*
* Copyright 2021 EMBL - European Bioinformatics Institute
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package eu.elixir.ega.ebi.commons.shared.service;
import java.util.List;
public interface Ga4ghService {
List<String> getDatasets(String userId);
}
/*
*
* Copyright 2021 EMBL - European Bioinformatics Institute
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package eu.elixir.ega.ebi.commons.shared.service;
import com.nimbusds.jwt.SignedJWT;
import java.util.Optional;
public interface JWTService {
Optional<SignedJWT> parse(String signedJWTString);
boolean isValidSignature(SignedJWT signedJWT);
}
/*
*
* Copyright 2021 EMBL - European Bioinformatics Institute
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package eu.elixir.ega.ebi.commons.shared.service;
import java.util.Optional;
public interface UserDetailsService {
Optional<String> getEGAAccountId(String userEmail);
Optional<String> getEGAAccountIdForElixirId(String elixirId);
}
/*
*
* Copyright 2021 EMBL - European Bioinformatics Institute
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package eu.elixir.ega.ebi.commons.shared.service.internal;
import eu.elixir.ega.ebi.commons.shared.service.FileDatasetService;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.util.List;
import static eu.elixir.ega.ebi.commons.config.Constants.DATA_SERVICE;
import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl;
public class FileDatasetServiceImpl implements FileDatasetService {
private final RestTemplate restTemplate;
public FileDatasetServiceImpl(final RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public List<String> getFileIds(final String datasetId) {
final URI requestURI = fromHttpUrl(DATA_SERVICE)
.path("/datasets/{datasetId}/file-ids")
.buildAndExpand(datasetId)
.toUri();
return restTemplate.exchange(
requestURI,
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<String>>() {
}
).getBody();
}
}